diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30bb4e0b027a..011e1288b2d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create matrix id: platforms @@ -88,7 +88,7 @@ jobs: fi - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.ARTIFACT_NAME }} path: /tmp/out/* @@ -143,7 +143,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Create matrix id: platforms diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4ba63f3f030c..5b9b92bbb80f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 2 # CodeQL 2.16.4's auto-build added support for multi-module repositories, @@ -61,19 +61,19 @@ jobs: ln -s vendor.sum go.sum - name: Update Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: "1.24.5" + go-version: "1.25.5" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:go" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 87433ec00704..bd6ce12287f9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,14 +37,14 @@ jobs: - alpine - debian engine-version: - - 28 # latest - - 27 # latest - 1 - - 26 # github actions default - - 23 # mirantis lts + - rc # latest rc + - 29 # latest + - 28 # latest - 1 + - 25 # mirantis lts steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Update daemon.json run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b84754248c7..ca7380205d71 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,24 +53,25 @@ jobs: fail-fast: false matrix: os: - - macos-13 # macOS 13 on Intel - - macos-14 # macOS 14 on arm64 (Apple Silicon M1) + - macos-14 # macOS 14 on arm64 (Apple Silicon M1) + - macos-15-intel # macOS 15 on Intel + - 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 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: path: ${{ env.GOPATH }}/src/github.com/docker/cli - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: "1.24.5" + go-version: "1.25.5" - name: Test run: | - go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/') + go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') go tool cover -func=/tmp/coverage.txt working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli shell: bash diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 815cbae0f84f..6a04aca0521e 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -11,18 +11,23 @@ permissions: on: pull_request: - types: [opened, edited, labeled, unlabeled] + types: [opened, edited, labeled, unlabeled, synchronize] jobs: - check-area-label: + check-labels: runs-on: ubuntu-24.04 timeout-minutes: 120 # guardrails timeout for the whole job steps: - name: Missing `area/` label - if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/') + if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/') run: | echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label" 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 run: exit 0 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9bd443c13d77..e6508d8fc783 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Generate shell: 'script --return --quiet --command "bash {0}"' @@ -74,7 +74,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Run shell: 'script --return --quiet --command "bash {0}"' diff --git a/.golangci.yml b/.golangci.yml index 0df356b2309d..20d042cbc8d0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,7 +5,7 @@ run: # which causes it to fallback to go1.17 semantics. # # TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24 - go: "1.24.5" + go: "1.25.5" timeout: 5m @@ -86,12 +86,16 @@ linters: 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: forbid: @@ -124,10 +128,9 @@ linters: no-unaliased: true alias: - # Enforce alias to prevent it accidentally being used instead of our - # own errdefs package (or vice-versa). - - pkg: github.com/containerd/errdefs - alias: cerrdefs + # 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" @@ -153,6 +156,7 @@ linters: 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 usetesting: os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478 @@ -221,6 +225,14 @@ linters: linters: - staticcheck + # Ignore deprecation linting for cli/command/stack/*. + # + # FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389 + - text: '^(SA1019): ' + path: "cli/command/stack" + linters: + - staticcheck + # Log a warning if an exclusion rule is unused. # Default: false warn-unused: true diff --git a/.mailmap b/.mailmap index 43a28a2bc29f..67f2f90896ac 100644 --- a/.mailmap +++ b/.mailmap @@ -64,11 +64,14 @@ Arko Dasgupta Arko Dasgupta Arnaud Porterie Arnaud Porterie +Arthur Flageul +Arthur Flageul Arthur Gautier Arthur Peka -Austin Vazquez -Austin Vazquez <55906459+austinvazquez@users.noreply.github.com> -Austin Vazquez +Austin Vazquez +Austin Vazquez <55906459+austinvazquez@users.noreply.github.com> +Austin Vazquez +Austin Vazquez Avi Miller Ben Bonnefoy Ben Golub @@ -150,6 +153,8 @@ Dave Henderson Dave Tucker David Alvarez David Alvarez +David Dooling +David Dooling David Karlsson David Karlsson <35727626+dvdksn@users.noreply.github.com> David M. Karr diff --git a/AUTHORS b/AUTHORS index c5a480b5e5d2..57af08b20444 100644 --- a/AUTHORS +++ b/AUTHORS @@ -63,6 +63,7 @@ Andreas Köhler Andres G. Aragoneses Andres Leon Rangel Andrew France +Andrew He Andrew Hsu Andrew Macpherson Andrew McDonnell @@ -86,11 +87,12 @@ Archimedes Trajano Arko Dasgupta Arnaud Porterie Arnaud Rebillout +Arthur Flageul Arthur Peka Ashly Mathew Ashwini Oruganti Aslam Ahemad -Austin Vazquez +Austin Vazquez Azat Khuyiyakhmetov Bardia Keyoumarsi Barnaby Gray @@ -135,10 +137,12 @@ Cao Weiwei Carlo Mion Carlos Alexandro Becker Carlos de Paula +carsontham Carston Schilds Casey Korver Ce Gao Cedric Davies +Cesar Talledo Cezar Sa Espinola Chad Faragher Chao Wang @@ -220,7 +224,7 @@ David Alvarez David Beitey David Calavera David Cramer -David Dooling +David Dooling David Gageot David Karlsson David le Blanc @@ -265,6 +269,7 @@ Eli Uriegas Eli Uriegas Elias Faxö Elliot Luo <956941328@qq.com> +Eng Zer Jun Eric Bode Eric Curtin Eric Engestrom @@ -345,6 +350,7 @@ Henning Sprang Henry N Hernan Garcia Hongbin Lu +Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com> Hu Keping Huayi Zhang Hugo Chastel @@ -595,6 +601,7 @@ Michael Prokop Michael Scharf Michael Spetsiotis Michael Steinert +Michael Tews Michael West Michal Minář Michał Czeraszkiewicz @@ -896,6 +903,7 @@ Wenlong Zhang Wenzhi Liang Wes Morgan Wewang Xiaorenfine +Will Wang William Henry Xianglin Gao Xiaodong Liu diff --git a/Dockerfile b/Dockerfile index 6ea150890011..f1b3bb233b77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,33 +5,39 @@ ARG BASE_VARIANT=alpine # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # It must be a supported tag in the docker.io/library/alpine image repository # that's also available as alpine image variant for the Golang version used. -ARG ALPINE_VERSION=3.21 +ARG ALPINE_VERSION=3.22 ARG BASE_DEBIAN_DISTRO=bookworm -ARG GO_VERSION=1.24.5 -ARG XX_VERSION=1.6.1 -ARG GOVERSIONINFO_VERSION=v1.4.1 +ARG GO_VERSION=1.25.5 + +# XX_VERSION specifies the version of the xx utility to use. +# It must be a valid tag in the docker.io/tonistiigi/xx image repository. +ARG XX_VERSION=1.7.0 + +# 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.12.3 +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.25.0 +ARG BUILDX_VERSION=0.29.1 # COMPOSE_VERSION is the version of compose to install in the dev container. # It must be a tag in the docker.io/docker/compose-bin image repository # on Docker Hub. -ARG COMPOSE_VERSION=v2.38.2 +ARG COMPOSE_VERSION=v2.40.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine ENV GOTOOLCHAIN=local COPY --link --from=xx / / -RUN apk add --no-cache bash clang lld llvm file git +RUN apk add --no-cache bash clang lld llvm file git git-daemon WORKDIR /go/src/github.com/docker/cli FROM build-base-alpine AS build-alpine @@ -91,7 +97,7 @@ ENV GO111MODULE=auto RUN --mount=type=bind,target=.,rw \ --mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/go/pkg/mod \ - gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/') + gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') FROM scratch AS test-coverage COPY --from=test /tmp/coverage.txt /coverage.txt @@ -116,10 +122,6 @@ FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM docker/compose-bin:${COMPOSE_VERSION} AS compose 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=build /out ./build/ COPY --link --from=build-plugins /out ./build/ diff --git a/Makefile b/Makefile index 0ec30ba6683b..93c080ee4bb0 100644 --- a/Makefile +++ b/Makefile @@ -34,12 +34,12 @@ test: test-unit ## run tests .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 - gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS) + gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS) .PHONY: test-coverage test-coverage: ## run test coverage mkdir -p $(CURDIR)/build/coverage - gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt + gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt .PHONY: lint lint: ## run all the lint tools @@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation .PHONY: fmt fmt: ## run gofumpt (if present) or gofmt @if command -v gofumpt > /dev/null; then \ - gofumpt -w -d -lang=1.23 . ; \ + gofumpt -w -d -lang=1.24 . ; \ else \ go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \ fi @@ -69,6 +69,15 @@ dynbinary: ## build dynamically linked binary plugins: ## build example CLI 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 vendor: ## update vendor with go modules rm -rf vendor diff --git a/VERSION b/VERSION index af15bc84df1f..0d2de8f9bf0a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -28.3.0-dev +29.0.0-dev diff --git a/cli-plugins/examples/helloworld/main.go b/cli-plugins/examples/helloworld/main.go index fecb18325ccd..874c0a942857 100644 --- a/cli-plugins/examples/helloworld/main.go +++ b/cli-plugins/examples/helloworld/main.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -25,7 +26,7 @@ func main() { Short: "Print the API version of the server", RunE: func(_ *cobra.Command, _ []string) error { apiClient := dockerCLI.Client() - ping, err := apiClient.Ping(context.Background()) + ping, err := apiClient.Ping(context.Background(), client.PingOptions{}) if err != nil { return err } diff --git a/cli-plugins/manager/annotations.go b/cli-plugins/manager/annotations.go deleted file mode 100644 index 8fc76b73c9bc..000000000000 --- a/cli-plugins/manager/annotations.go +++ /dev/null @@ -1,30 +0,0 @@ -package manager - -import "github.com/docker/cli/cli-plugins/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 = metadata.CommandAnnotationPlugin - - // CommandAnnotationPluginVendor is added to every stub command - // added by AddPluginCommandStubs and contains the vendor of - // that plugin. - CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor - - // CommandAnnotationPluginVersion is added to every stub command - // added by AddPluginCommandStubs and contains the version of - // that plugin. - CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion - - // 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 = metadata.CommandAnnotationPluginInvalid - - // CommandAnnotationPluginCommandPath is added to overwrite the - // command path for a plugin invocation. - CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath -) diff --git a/cli-plugins/manager/candidate.go b/cli-plugins/manager/candidate.go index d809926cf62b..a4253968dcca 100644 --- a/cli-plugins/manager/candidate.go +++ b/cli-plugins/manager/candidate.go @@ -6,12 +6,6 @@ import ( "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 { path string } diff --git a/cli-plugins/manager/candidate_test.go b/cli-plugins/manager/candidate_test.go index e8df42909949..a0dbb0dd7b56 100644 --- a/cli-plugins/manager/candidate_test.go +++ b/cli-plugins/manager/candidate_test.go @@ -32,14 +32,12 @@ func (c *fakeCandidate) Metadata() ([]byte, error) { func TestValidateCandidate(t *testing.T) { const ( goodPluginName = metadata.NamePrefix + "goodplugin" + builtinName = metadata.NamePrefix + "builtin" + builtinAlias = metadata.NamePrefix + "alias" - builtinName = metadata.NamePrefix + "builtin" - builtinAlias = metadata.NamePrefix + "alias" - - 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}` + badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" + badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" + goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName ) fakeroot := &cobra.Command{Use: "docker"} @@ -51,31 +49,103 @@ func TestValidateCandidate(t *testing.T) { }) for _, tc := range []struct { - name string - c *fakeCandidate + name string + plugin *fakeCandidate // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). err string invalid string + expVer string }{ - /* Each failing one of the tests */ - {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"}, - {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)}, - {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"}, - {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: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"}, - {name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`}, - {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"}, - // This one should work - {name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}}, - {name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}}, + // Invalid cases. + { + name: "empty path", + plugin: &fakeCandidate{path: ""}, + err: "plugin candidate path cannot be empty", + }, + { + name: "bad prefix", + plugin: &fakeCandidate{path: badPrefixPath}, + err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix), + }, + { + name: "bad path", + plugin: &fakeCandidate{path: badNamePath}, + invalid: "did not match", + }, + { + 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 ..`, + }, + { + 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) { - p, err := newPlugin(tc.c, fakeroot.Commands()) + p, err := newPlugin(tc.plugin, fakeroot.Commands()) switch { case tc.err != "": assert.ErrorContains(t, err, tc.err) @@ -86,7 +156,7 @@ func TestValidateCandidate(t *testing.T) { default: assert.NilError(t, err) assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) - assert.Equal(t, p.SchemaVersion, "0.1.0") + assert.Equal(t, p.SchemaVersion, tc.expVer) assert.Equal(t, p.Vendor, "e2e-testing") } }) diff --git a/cli-plugins/manager/cobra.go b/cli-plugins/manager/cobra.go index ddf067be1dbd..57ec4a7909ec 100644 --- a/cli-plugins/manager/cobra.go +++ b/cli-plugins/manager/cobra.go @@ -38,6 +38,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e rootCmd.AddCommand(&cobra.Command{ Use: p.Name, Short: p.ShortDescription, + Hidden: p.Hidden, Run: func(_ *cobra.Command, _ []string) {}, Annotations: annotations, DisableFlagParsing: true, diff --git a/cli-plugins/manager/error.go b/cli-plugins/manager/error.go index 1b5f4f60e54e..bb4135d70c2e 100644 --- a/cli-plugins/manager/error.go +++ b/cli-plugins/manager/error.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package manager @@ -23,11 +23,6 @@ func (e *pluginError) Error() string { 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. func (e *pluginError) Unwrap() error { return e.cause @@ -41,14 +36,11 @@ func (e *pluginError) MarshalText() (text []byte, err error) { // wrapAsPluginError wraps an error in a pluginError with an // additional message. func wrapAsPluginError(err error, msg string) error { - if err == nil { - return nil - } return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)} } -// NewPluginError creates a new pluginError, analogous to +// newPluginError creates a new pluginError, analogous to // errors.Errorf. -func NewPluginError(msg string, args ...any) error { +func newPluginError(msg string, args ...any) error { return &pluginError{cause: fmt.Errorf(msg, args...)} } diff --git a/cli-plugins/manager/error_test.go b/cli-plugins/manager/error_test.go index c4cb19bd55d5..2e92001c20ed 100644 --- a/cli-plugins/manager/error_test.go +++ b/cli-plugins/manager/error_test.go @@ -10,7 +10,7 @@ import ( ) func TestPluginError(t *testing.T) { - err := NewPluginError("new error") + err := newPluginError("new error") assert.Check(t, is.Error(err, "new error")) inner := errors.New("testing") @@ -21,4 +21,7 @@ func TestPluginError(t *testing.T) { actual, err := json.Marshal(err) assert.Check(t, err) assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual))) + + err = wrapAsPluginError(nil, "wrapping") + assert.Check(t, is.Error(err, "wrapping: %!w()")) } diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index 25515bccb868..48a0db34173d 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -2,6 +2,7 @@ package manager import ( "context" + "errors" "os" "os/exec" "path/filepath" @@ -9,28 +10,16 @@ import ( "strings" "sync" + "github.com/containerd/errdefs" "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/debug" "github.com/fvbommel/sortorder" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" ) -const ( - // 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 = metadata.ReexecEnvvar - - // ResourceAttributesEnvvar is the name of the envvar that includes additional - // resource attributes for OTEL. - // - // Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release. - ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" -) - // errPluginNotFound is the error returned when a plugin could not be found. type errPluginNotFound string @@ -40,17 +29,6 @@ func (e errPluginNotFound) Error() string { return "Error: No such CLI plugin: " + string(e) } -type notFound interface{ NotFound() } - -// IsNotFound is true if the given error is due to a plugin not being found. -func IsNotFound(err error) bool { - if e, ok := err.(*pluginError); ok { - err = e.Cause() - } - _, ok := err.(notFound) - return ok -} - // getPluginDirs returns the platform-specific locations to search for plugins // in order of preference. // @@ -81,9 +59,17 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) { return } for _, dentry := range dentries { - switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list - case 0, os.ModeSymlink: - // Regular file or symlink, keep going + switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list + case os.ModeSymlink: + if !debug.IsEnabled() { + // 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: // Something else, ignore. continue @@ -127,7 +113,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi if err != nil { return nil, err } - if !IsNotFound(p.Err) { + if !errdefs.IsNotFound(p.Err) { p.ShadowedPaths = paths[1:] } return &p, nil @@ -164,7 +150,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e if err != nil { return err } - if !IsNotFound(p.Err) { + if !errdefs.IsNotFound(p.Err) { p.ShadowedPaths = paths[1:] mu.Lock() defer mu.Unlock() @@ -185,15 +171,15 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e return plugins, nil } -// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. +// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin. // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts. -// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow. +// The error returned satisfies the [errdefs.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) { // This uses the full original args, not the args which may // have been provided by cobra to our caller. This is because // they lack e.g. global options which we must propagate here. args := os.Args[1:] - if !pluginNameRe.MatchString(name) { + if !isValidPluginName(name) { // We treat this as "not found" so that callers will // fallback to their "invalid" command path. return nil, errPluginNotFound(name) diff --git a/cli-plugins/manager/manager_test.go b/cli-plugins/manager/manager_test.go index 64609fbbad39..16c43b29d338 100644 --- a/cli-plugins/manager/manager_test.go +++ b/cli-plugins/manager/manager_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" @@ -37,7 +38,7 @@ func TestListPluginCandidates(t *testing.T) { "plugins3-target", // Will be referenced as a symlink from below fs.WithFile("docker-plugin1", ""), fs.WithDir("ignored3"), - fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later) + fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ... fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should. ), @@ -71,9 +72,6 @@ func TestListPluginCandidates(t *testing.T) { "hardlink2": { dir.Join("plugins2", "docker-hardlink2"), }, - "brokensymlink": { - dir.Join("plugins3", "docker-brokensymlink"), - }, "symlinked": { dir.Join("plugins3", "docker-symlinked"), }, @@ -131,7 +129,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)), _, err = GetPlugin("ccc", cli, &cobra.Command{}) assert.Error(t, err, "Error: No such CLI plugin: ccc") - assert.Assert(t, IsNotFound(err)) + assert.Assert(t, errdefs.IsNotFound(err)) } func TestListPluginsIsSorted(t *testing.T) { @@ -166,8 +164,8 @@ func TestErrPluginNotFound(t *testing.T) { var err error = errPluginNotFound("test") err.(errPluginNotFound).NotFound() assert.Error(t, err, "Error: No such CLI plugin: test") - assert.Assert(t, IsNotFound(err)) - assert.Assert(t, !IsNotFound(nil)) + assert.Assert(t, errdefs.IsNotFound(err)) + assert.Assert(t, !errdefs.IsNotFound(nil)) } func TestGetPluginDirs(t *testing.T) { diff --git a/cli-plugins/manager/metadata.go b/cli-plugins/manager/metadata.go deleted file mode 100644 index 9bddb121422d..000000000000 --- a/cli-plugins/manager/metadata.go +++ /dev/null @@ -1,23 +0,0 @@ -package manager - -import ( - "github.com/docker/cli/cli-plugins/metadata" -) - -const ( - // NamePrefix is the prefix required on all plugin binary names - NamePrefix = metadata.NamePrefix - - // MetadataSubcommandName is the name of the plugin subcommand - // which must be supported by every plugin and returns the - // plugin metadata. - MetadataSubcommandName = metadata.MetadataSubcommandName - - // HookSubcommandName is the name of the plugin subcommand - // which must be implemented by plugins declaring support - // for hooks in their metadata. - HookSubcommandName = metadata.HookSubcommandName -) - -// Metadata provided by the plugin. -type Metadata = metadata.Metadata diff --git a/cli-plugins/manager/plugin.go b/cli-plugins/manager/plugin.go index fa846452b548..824d82d12f1f 100644 --- a/cli-plugins/manager/plugin.go +++ b/cli-plugins/manager/plugin.go @@ -2,21 +2,20 @@ package manager import ( "context" + "encoding" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" + "strconv" "strings" "github.com/docker/cli/cli-plugins/metadata" - "github.com/docker/cli/internal/lazyregexp" "github.com/spf13/cobra" ) -var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$") - // Plugin represents a potential plugin with all it's metadata. type Plugin struct { metadata.Metadata @@ -31,12 +30,34 @@ type Plugin struct { 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 // Plugin. If the candidate fails one of the tests then `Plugin.Err` // is set, and is always a `pluginError`, but the `Plugin` is still // returned with no error. An error is only returned due to a // non-recoverable error. -func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { +func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { path := c.Path() if path == "" { return Plugin{}, errors.New("plugin candidate path cannot be empty") @@ -62,8 +83,8 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { } // Now apply the candidate tests, so these update p.Err. - if !pluginNameRe.MatchString(p.Name) { - p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String()) + if !isValidPluginName(p.Name) { + p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat) return p, nil } @@ -75,11 +96,11 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { continue } 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 } 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 } } @@ -95,17 +116,42 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { p.Err = wrapAsPluginError(err, "invalid metadata") return p, nil } - if p.Metadata.SchemaVersion != "0.1.0" { - p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion) + if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil { + p.Err = &pluginError{cause: err} return p, nil } 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 } +// 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 ..", 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 HookPluginData) ([]byte, error) { @@ -124,3 +170,26 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, return hookCmdOutput, 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 +} diff --git a/cli-plugins/manager/plugin_test.go b/cli-plugins/manager/plugin_test.go new file mode 100644 index 000000000000..ace51c12c630 --- /dev/null +++ b/cli-plugins/manager/plugin_test.go @@ -0,0 +1,43 @@ +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)) + }) + } +} diff --git a/cli-plugins/metadata/metadata.go b/cli-plugins/metadata/metadata.go index 9d408c00b3dd..7061486a7056 100644 --- a/cli-plugins/metadata/metadata.go +++ b/cli-plugins/metadata/metadata.go @@ -33,4 +33,6 @@ type Metadata struct { 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"` } diff --git a/cli-plugins/plugin/plugin.go b/cli-plugins/plugin/plugin.go index 39d6d8694342..58ee3b876287 100644 --- a/cli-plugins/plugin/plugin.go +++ b/cli-plugins/plugin/plugin.go @@ -14,7 +14,7 @@ import ( "github.com/docker/cli/cli/command" "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" "go.opentelemetry.io/otel" ) @@ -80,19 +80,23 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat return cmd.Execute() } -// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. -func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) { +// Run is the top-level entry point to the CLI plugin framework. It should +// be called from the plugin's "main()" function. It initializes a new +// [command.DockerCli] instance with the given options before calling +// 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() + dockerCLI, err := command.NewDockerCli(ops...) if err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) 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 { var stErr cli.StatusError if errors.As(err, &stErr) { // StatusError should only be used for errors, and all errors should @@ -100,10 +104,10 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) { if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. stErr.StatusCode = 1 } - _, _ = fmt.Fprintln(dockerCli.Err(), stErr) + _, _ = fmt.Fprintln(dockerCLI.Err(), stErr) os.Exit(stErr.StatusCode) } - _, _ = fmt.Fprintln(dockerCli.Err(), err) + _, _ = fmt.Fprintln(dockerCLI.Err(), err) os.Exit(1) } } @@ -135,7 +139,7 @@ func withPluginClientConn(name string) command.CLIOption { if err != nil { return err } - apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer)) + apiClient, err := client.New(client.WithDialContext(helper.Dialer)) if err != nil { return err } @@ -164,6 +168,11 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "", }, } + + // Disable file-completion by default. Most commands and flags should not + // complete with filenames. + cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp) + opts, _ := cli.SetupPluginRootCommand(cmd) cmd.SetIn(dockerCli.In()) @@ -175,11 +184,24 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta newMetadataSubcommand(plugin, meta), ) - cli.DisableFlagsInUseLine(cmd) + visitAll(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()) } +// visitAll traverses all commands from the root. +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 == "" { meta.ShortDescription = plugin.Short diff --git a/cli-plugins/plugin/plugin_test.go b/cli-plugins/plugin/plugin_test.go new file mode 100644 index 000000000000..8b2b299fb1c0 --- /dev/null +++ b/cli-plugins/plugin/plugin_test.go @@ -0,0 +1,28 @@ +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) + } +} diff --git a/cli/cobra.go b/cli/cobra.go index 7a14b6f483b9..4ec721f88772 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -12,7 +12,6 @@ import ( "github.com/fvbommel/sortorder" "github.com/moby/term" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -167,35 +166,6 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error { 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{ Use: "help [command]", Short: "Help about the command", @@ -204,7 +174,7 @@ var helpCommand = &cobra.Command{ RunE: func(c *cobra.Command, args []string) error { cmd, args, e := c.Root().Find(args) if cmd == nil || e != nil || len(args) > 0 { - return errors.Errorf("unknown help topic: %v", strings.Join(args, " ")) + return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) } helpFunc := cmd.HelpFunc() helpFunc(cmd, args) @@ -280,11 +250,12 @@ func commandAliases(cmd *cobra.Command) string { if cmd.HasParent() { parentPath = cmd.Parent().CommandPath() + " " } - aliases := cmd.CommandPath() + var aliases strings.Builder + aliases.WriteString(cmd.CommandPath()) for _, alias := range cmd.Aliases { - aliases += ", " + parentPath + alias + aliases.WriteString(", " + parentPath + alias) } - return aliases + return aliases.String() } func topCommands(cmd *cobra.Command) []*cobra.Command { @@ -380,13 +351,10 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command { func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command { cmds := []*cobra.Command{} for _, sub := range cmd.Commands() { - if isPlugin(sub) { - if invalidPluginReason(sub) == "" { - cmds = append(cmds, sub) - } + if invalidPluginReason(sub) != "" { continue } - if sub.IsAvailableCommand() && sub.HasSubCommands() { + if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) { cmds = append(cmds, sub) } } diff --git a/cli/cobra_test.go b/cli/cobra_test.go index e1d4bcb2d25f..eb87c75d4184 100644 --- a/cli/cobra_test.go +++ b/cli/cobra_test.go @@ -10,28 +10,6 @@ import ( 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) { // Non plugin. assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "") @@ -78,6 +56,33 @@ func TestInvalidPlugin(t *testing.T) { 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) { root := &cobra.Command{Use: "root"} sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}} diff --git a/cli/command/builder/client_test.go b/cli/command/builder/client_test.go index e1cdedd133c9..f48914bdb76c 100644 --- a/cli/command/builder/client_test.go +++ b/cli/command/builder/client_test.go @@ -3,18 +3,17 @@ package builder import ( "context" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) + builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) } -func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) { +func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) { if c.builderPruneFunc != nil { return c.builderPruneFunc(ctx, opts) } - return nil, nil + return client.BuildCachePruneResult{}, nil } diff --git a/cli/command/builder/cmd.go b/cli/command/builder/cmd.go index ba2c069e8998..8aa693e5b53c 100644 --- a/cli/command/builder/cmd.go +++ b/cli/command/builder/cmd.go @@ -6,29 +6,41 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/internal/commands" ) -// NewBuilderCommand returns a cobra command for `builder` subcommands -func NewBuilderCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newBuilderCommand) + 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{ Use: "builder", Short: "Manage builds", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.31"}, + + DisableFlagsInUseLine: true, } cmd.AddCommand( - NewPruneCommand(dockerCli), - image.NewBuildCommand(dockerCli), + newPruneCommand(dockerCLI), + // we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function. + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) + image.NewBuildCommand(dockerCLI), ) return cmd } -// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand. +// 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 { +func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command { return &cobra.Command{ Use: "bake [OPTIONS] [TARGET...]", Short: "Build from a file", @@ -40,5 +52,6 @@ func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command { "aliases": "docker buildx bake", "version": "1.31", }, + DisableFlagsInUseLine: true, } } diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 7a323a393941..a7c88dcc42ac 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -8,23 +8,30 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/build" "github.com/docker/go-units" + "github.com/moby/moby/client" "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 { - force bool - all bool - filter opts.FilterOpt - keepStorage opts.MemBytes + force bool + all bool + filter opts.FilterOpt + reservedSpace opts.MemBytes } -// NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// newPruneCommand returns a new cobra prune command for images +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -32,25 +39,26 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove build cache", Args: cli.NoArgs, 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 { return err } 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 }, - Annotations: map[string]string{"version": "1.39"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.39"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() 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.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`) - flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache") + flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache") return cmd } @@ -61,8 +69,7 @@ const ( ) func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := options.filter.Value() - pruneFilters = command.PruneFilters(dockerCli, pruneFilters) + pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) warning := normalWarning if options.all { @@ -78,15 +85,15 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{ - All: options.all, - KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720 - Filters: pruneFilters, + resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{ + All: options.all, + ReservedSpace: options.reservedSpace.Value(), + Filters: pruneFilters, }) if err != nil { return 0, "", err } - + report := resp.Report if len(report.CachesDeleted) > 0 { var sb strings.Builder sb.WriteString("Deleted build cache objects:\n") @@ -104,7 +111,22 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// CachePrune executes a prune command for build cache -func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - 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, + }) } diff --git a/cli/command/builder/prune_test.go b/cli/command/builder/prune_test.go index e0c3097b07e2..884523197e2b 100644 --- a/cli/command/builder/prune_test.go +++ b/cli/command/builder/prune_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/build" + "github.com/moby/moby/client" ) func TestBuilderPromptTermination(t *testing.T) { @@ -15,11 +15,11 @@ func TestBuilderPromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - builderPruneFunc: func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) { - return nil, errors.New("fakeClient builderPruneFunc should not be called") + 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 := newPruneCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) diff --git a/cli/command/checkpoint/client_test.go b/cli/command/checkpoint/client_test.go index 3d4e0d79111e..2be1189fd832 100644 --- a/cli/command/checkpoint/client_test.go +++ b/cli/command/checkpoint/client_test.go @@ -3,34 +3,33 @@ package checkpoint import ( "context" - "github.com/docker/docker/api/types/checkpoint" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error - checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error - checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) + checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) + checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) + checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) } -func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error { +func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { if cli.checkpointCreateFunc != nil { return cli.checkpointCreateFunc(container, options) } - return nil + return client.CheckpointCreateResult{}, nil } -func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error { +func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { if cli.checkpointDeleteFunc != nil { return cli.checkpointDeleteFunc(container, options) } - return nil + return client.CheckpointRemoveResult{}, nil } -func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) { +func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { if cli.checkpointListFunc != nil { return cli.checkpointListFunc(container, options) } - return []checkpoint.Summary{}, nil + return client.CheckpointListResult{}, nil } diff --git a/cli/command/checkpoint/cmd.go b/cli/command/checkpoint/cmd.go index 2a698e74e5e2..fd33bd8a8da8 100644 --- a/cli/command/checkpoint/cmd.go +++ b/cli/command/checkpoint/cmd.go @@ -3,26 +3,32 @@ package checkpoint import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) -func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newCheckpointCommand) +} + +// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental) +func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "checkpoint", Short: "Manage checkpoints", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "experimental": "", "ostype": "linux", "version": "1.25", }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), + newCreateCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), ) return cmd } diff --git a/cli/command/checkpoint/create.go b/cli/command/checkpoint/create.go index 8455e979e64f..d361d78aa0fa 100644 --- a/cli/command/checkpoint/create.go +++ b/cli/command/checkpoint/create.go @@ -6,8 +6,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,7 +17,7 @@ type createOptions struct { leaveRunning bool } -func newCreateCommand(dockerCli command.Cli) *cobra.Command { +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { var opts createOptions cmd := &cobra.Command{ @@ -28,9 +27,10 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] opts.checkpoint = args[1] - return runCreate(cmd.Context(), dockerCli, opts) + return runCreate(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -41,7 +41,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { } func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error { - err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{ + _, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{ CheckpointID: opts.checkpoint, CheckpointDir: opts.checkpointDir, Exit: !opts.leaveRunning, diff --git a/cli/command/checkpoint/create_test.go b/cli/command/checkpoint/create_test.go index 5e40eca09723..5f91c0493a5d 100644 --- a/cli/command/checkpoint/create_test.go +++ b/cli/command/checkpoint/create_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -16,7 +16,7 @@ import ( func TestCheckpointCreateErrors(t *testing.T) { testCases := []struct { args []string - checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error + checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) expectedError string }{ { @@ -29,8 +29,8 @@ func TestCheckpointCreateErrors(t *testing.T) { }, { args: []string{"foo", "bar"}, - checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error { - return errors.New("error creating checkpoint for container foo") + checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { + return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo") }, expectedError: "error creating checkpoint for container foo", }, @@ -59,12 +59,12 @@ func TestCheckpointCreateWithOptions(t *testing.T) { leaveRunning := strconv.FormatBool(tc) t.Run("leave-running="+leaveRunning, func(t *testing.T) { var actualContainerName string - var actualOptions checkpoint.CreateOptions + var actualOptions client.CheckpointCreateOptions cli := test.NewFakeCli(&fakeClient{ - checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error { + checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { actualContainerName = container actualOptions = options - return nil + return client.CheckpointCreateResult{}, nil }, }) cmd := newCreateCommand(cli) @@ -75,7 +75,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) { assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir)) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal(actualContainerName, containerName)) - expected := checkpoint.CreateOptions{ + expected := client.CheckpointCreateOptions{ CheckpointID: checkpointName, CheckpointDir: checkpointDir, Exit: !tc, diff --git a/cli/command/checkpoint/formatter.go b/cli/command/checkpoint/formatter.go index 47ee77635b71..915ce1832dc0 100644 --- a/cli/command/checkpoint/formatter.go +++ b/cli/command/checkpoint/formatter.go @@ -2,7 +2,7 @@ package checkpoint import ( "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/api/types/checkpoint" ) const ( @@ -10,25 +10,31 @@ const ( checkpointNameHeader = "CHECKPOINT NAME" ) -// NewFormat returns a format for use with a checkpoint Context -func NewFormat(source string) formatter.Format { +// newFormat returns a format for use with a checkpointContext. +func newFormat(source string) formatter.Format { if source == formatter.TableFormatKey { return defaultCheckpointFormat } return formatter.Format(source) } -// FormatWrite writes formatted checkpoints using the Context -func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error { - render := func(format func(subContext formatter.SubContext) error) error { +// formatWrite writes formatted checkpoints using the Context +func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error { + cpContext := &checkpointContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Name": checkpointNameHeader, + }, + }, + } + return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error { for _, cp := range checkpoints { if err := format(&checkpointContext{c: cp}); err != nil { return err } } return nil - } - return ctx.Write(newCheckpointContext(), render) + }) } type checkpointContext struct { @@ -36,14 +42,6 @@ type checkpointContext struct { c checkpoint.Summary } -func newCheckpointContext() *checkpointContext { - cpCtx := checkpointContext{} - cpCtx.Header = formatter.SubHeaderContext{ - "Name": checkpointNameHeader, - } - return &cpCtx -} - func (c *checkpointContext) MarshalJSON() ([]byte, error) { return formatter.MarshalJSON(c) } diff --git a/cli/command/checkpoint/formatter_test.go b/cli/command/checkpoint/formatter_test.go index 7f265294bab7..5704fa45b9ce 100644 --- a/cli/command/checkpoint/formatter_test.go +++ b/cli/command/checkpoint/formatter_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/api/types/checkpoint" "gotest.tools/v3/assert" ) @@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) { expected string }{ { - formatter.Context{Format: NewFormat(defaultCheckpointFormat)}, + formatter.Context{Format: newFormat(defaultCheckpointFormat)}, `CHECKPOINT NAME checkpoint-1 checkpoint-2 @@ -23,14 +23,14 @@ checkpoint-3 `, }, { - formatter.Context{Format: NewFormat("{{.Name}}")}, + formatter.Context{Format: newFormat("{{.Name}}")}, `checkpoint-1 checkpoint-2 checkpoint-3 `, }, { - formatter.Context{Format: NewFormat("{{.Name}}:")}, + formatter.Context{Format: newFormat("{{.Name}}:")}, `checkpoint-1: checkpoint-2: checkpoint-3: @@ -41,7 +41,7 @@ checkpoint-3: for _, testcase := range cases { out := bytes.NewBufferString("") testcase.context.Output = out - err := FormatWrite(testcase.context, []checkpoint.Summary{ + err := formatWrite(testcase.context, []checkpoint.Summary{ {Name: "checkpoint-1"}, {Name: "checkpoint-2"}, {Name: "checkpoint-3"}, diff --git a/cli/command/checkpoint/list.go b/cli/command/checkpoint/list.go index 55344e08c9d5..6ee0ed8f738b 100644 --- a/cli/command/checkpoint/list.go +++ b/cli/command/checkpoint/list.go @@ -7,7 +7,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -15,7 +15,7 @@ type listOptions struct { checkpointDir string } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { var opts listOptions cmd := &cobra.Command{ @@ -24,9 +24,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List checkpoints for a container", Args: cli.ExactArgs(1), 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() @@ -35,8 +36,8 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error { - checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{ +func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error { + checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{ CheckpointDir: opts.checkpointDir, }) if err != nil { @@ -44,8 +45,8 @@ func runList(ctx context.Context, dockerCli command.Cli, container string, opts } cpCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(formatter.TableFormatKey), + Output: dockerCLI.Out(), + Format: newFormat(formatter.TableFormatKey), } - return FormatWrite(cpCtx, checkpoints) + return formatWrite(cpCtx, checkpoints.Items) } diff --git a/cli/command/checkpoint/list_test.go b/cli/command/checkpoint/list_test.go index 33d2e6379c5f..26e9e5511984 100644 --- a/cli/command/checkpoint/list_test.go +++ b/cli/command/checkpoint/list_test.go @@ -6,7 +6,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/api/types/checkpoint" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -15,7 +16,7 @@ import ( func TestCheckpointListErrors(t *testing.T) { testCases := []struct { args []string - checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) + checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) expectedError string }{ { @@ -28,8 +29,8 @@ func TestCheckpointListErrors(t *testing.T) { }, { args: []string{"foo"}, - checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) { - return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo") + checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { + return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo") }, expectedError: "error getting checkpoints for container foo", }, @@ -50,17 +51,19 @@ func TestCheckpointListErrors(t *testing.T) { func TestCheckpointListWithOptions(t *testing.T) { var containerID, checkpointDir string cli := test.NewFakeCli(&fakeClient{ - checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) { + checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { containerID = container checkpointDir = options.CheckpointDir - return []checkpoint.Summary{ - {Name: "checkpoint-foo"}, + return client.CheckpointListResult{ + Items: []checkpoint.Summary{ + {Name: "checkpoint-foo"}, + }, }, nil }, }) cmd := newListCommand(cli) cmd.SetArgs([]string{"container-foo"}) - cmd.Flags().Set("checkpoint-dir", "/dir/foo") + assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("/dir/foo", checkpointDir)) diff --git a/cli/command/checkpoint/remove.go b/cli/command/checkpoint/remove.go index 7308948b39bf..773a69407fcb 100644 --- a/cli/command/checkpoint/remove.go +++ b/cli/command/checkpoint/remove.go @@ -1,11 +1,9 @@ package checkpoint import ( - "context" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -13,7 +11,7 @@ type removeOptions struct { checkpointDir string } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -22,8 +20,14 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove a checkpoint", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts) + containerID, checkpointID := args[0], args[1] + _, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{ + CheckpointID: checkpointID, + CheckpointDir: opts.checkpointDir, + }) + return err }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -31,10 +35,3 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { 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, - }) -} diff --git a/cli/command/checkpoint/remove_test.go b/cli/command/checkpoint/remove_test.go index 7064fbf1ee67..30777da8cff9 100644 --- a/cli/command/checkpoint/remove_test.go +++ b/cli/command/checkpoint/remove_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/checkpoint" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -14,7 +14,7 @@ import ( func TestCheckpointRemoveErrors(t *testing.T) { testCases := []struct { args []string - checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error + checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) expectedError string }{ { @@ -27,8 +27,8 @@ func TestCheckpointRemoveErrors(t *testing.T) { }, { args: []string{"foo", "bar"}, - checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error { - return errors.New("error deleting checkpoint") + checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { + return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint") }, expectedError: "error deleting checkpoint", }, @@ -49,16 +49,16 @@ func TestCheckpointRemoveErrors(t *testing.T) { func TestCheckpointRemoveWithOptions(t *testing.T) { var containerID, checkpointID, checkpointDir string cli := test.NewFakeCli(&fakeClient{ - checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error { + checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { containerID = container checkpointID = options.CheckpointID checkpointDir = options.CheckpointDir - return nil + return client.CheckpointRemoveResult{}, nil }, }) cmd := newRemoveCommand(cli) cmd.SetArgs([]string{"container-foo", "checkpoint-bar"}) - cmd.Flags().Set("checkpoint-dir", "/dir/foo") + assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("checkpoint-bar", checkpointID)) diff --git a/cli/command/cli.go b/cli/command/cli.go index 1e042ec0e23b..b1e9453343a1 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -1,10 +1,11 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command import ( "context" + "errors" "fmt" "io" "os" @@ -23,11 +24,8 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/version" dopts "github.com/docker/cli/opts" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/build" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -45,12 +43,9 @@ type Cli interface { Client() client.APIClient Streams SetIn(in *streams.In) - Apply(ops ...CLIOption) error config.Provider ServerInfo() ServerInfo - DefaultVersion() string CurrentVersion() string - ContentTrustEnabled() bool BuildKitEnabled() (bool, error) ContextStore() store.Store CurrentContext() string @@ -70,7 +65,6 @@ type DockerCli struct { err *streams.Out client client.APIClient serverInfo ServerInfo - contentTrust bool contextStore store.Store currentContext string init sync.Once @@ -78,6 +72,7 @@ type DockerCli struct { dockerEndpoint docker.Endpoint contextStoreConfig *store.Config initTimeout time.Duration + userAgent string res telemetryResource // baseCtx is the base context used for internal operations. In the future @@ -88,17 +83,12 @@ type DockerCli struct { enableGlobalMeter, enableGlobalTracer bool } -// DefaultVersion returns [api.DefaultVersion]. -func (*DockerCli) DefaultVersion() string { - return api.DefaultVersion -} - // CurrentVersion returns the API version currently negotiated, or the default // version otherwise. func (cli *DockerCli) CurrentVersion() string { _ = cli.initialize() if cli.client == nil { - return api.DefaultVersion + return client.MaxAPIVersion } return cli.client.ClientVersion() } @@ -157,19 +147,13 @@ func (cli *DockerCli) ServerInfo() 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. func (cli *DockerCli) BuildKitEnabled() (bool, error) { // use DOCKER_BUILDKIT env var value if set and not empty if v := os.Getenv("DOCKER_BUILDKIT"); v != "" { enabled, err := strconv.ParseBool(v) if err != nil { - return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value") + return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err) } return enabled, nil } @@ -269,7 +253,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) cli.contextStore = &ContextStoreWithDefault{ Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(cli.options, *cli.contextStoreConfig) + return resolveDefaultContext(cli.options, *cli.contextStoreConfig) }, } @@ -282,6 +266,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) } 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 } @@ -295,17 +290,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile. contextStore := &ContextStoreWithDefault{ Store: store.New(config.ContextStoreDir(), storeConfig), Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(opts, storeConfig) + return resolveDefaultContext(opts, storeConfig) }, } endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile)) if err != nil { - return nil, errors.Wrap(err, "unable to resolve docker endpoint") + return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err) } - return newAPIClientFromEndpoint(endpoint, configFile) + return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent())) } -func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) { +func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) { opts, err := ep.ClientOpts() if err != nil { return nil, err @@ -313,8 +308,15 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF if len(configFile.HTTPHeaders) > 0 { opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) } - opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent())) - return client.NewClientWithOpts(opts...) + withCustomHeaders, err := withCustomHeadersFromEnv() + if err != nil { + return nil, err + } + if withCustomHeaders != nil { + opts = append(opts, withCustomHeaders) + } + opts = append(opts, extraOpts...) + return client.New(opts...) } func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { @@ -375,24 +377,21 @@ func (cli *DockerCli) initializeFromClient() { ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout()) defer cancel() - ping, err := cli.client.Ping(ctx) + ping, err := cli.client.Ping(ctx, client.PingOptions{ + NegotiateAPIVersion: true, + ForceNegotiate: true, + }) if err != nil { // Default to true if we fail to connect to daemon cli.serverInfo = ServerInfo{HasExperimental: true} - - if ping.APIVersion != "" { - cli.client.NegotiateAPIVersionPing(ping) - } return } - cli.serverInfo = ServerInfo{ HasExperimental: ping.Experimental, OSType: ping.OSType, BuildkitVersion: ping.BuilderVersion, SwarmStatus: ping.SwarmStatus, } - cli.client.NegotiateAPIVersionPing(ping) } // ContextStore returns the ContextStore @@ -475,15 +474,67 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) { 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 { cli.init.Do(func() { cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() if cli.initErr != nil { - cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint") + cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr) return } if cli.client == nil { - if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil { + ops := []client.Opt{client.WithUserAgent(cli.userAgent)} + if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil { return } } @@ -495,16 +546,6 @@ func (cli *DockerCli) initialize() error { 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 // server type ServerInfo struct { @@ -519,7 +560,7 @@ type ServerInfo struct { // 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 // endpoint. - SwarmStatus *swarm.Status + SwarmStatus *client.SwarmStatus } // NewDockerCli returns a DockerCli instance with all operators applied on it. @@ -527,15 +568,17 @@ type ServerInfo struct { // environment. func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { defaultOps := []CLIOption{ - WithContentTrustFromEnv(), WithDefaultContextStoreConfig(), WithStandardStreams(), + WithUserAgent(UserAgent()), } ops = append(defaultOps, ops...) cli := &DockerCli{baseCtx: context.Background()} - if err := cli.Apply(ops...); err != nil { - return nil, err + for _, op := range ops { + if err := op(cli); err != nil { + return nil, err + } } return cli, nil } @@ -547,11 +590,11 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) { case 1: return dopts.ParseHost(defaultToTLS, hosts[0]) default: - return "", errors.New("Specify only one -H") + return "", errors.New("specify only one -H") } } -// UserAgent returns the user agent string used for making API requests +// UserAgent returns the default user agent string used for making API requests. func UserAgent() string { return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index dd3c9473369d..f787956c5b7e 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -3,16 +3,16 @@ package command import ( "context" "encoding/csv" + "errors" + "fmt" "io" "net/http" "os" - "strconv" "strings" "github.com/docker/cli/cli/streams" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/moby/term" - "github.com/pkg/errors" ) // CLIOption is a functional argument to apply options to a [DockerCli]. These @@ -75,28 +75,6 @@ func WithErrorStream(err io.Writer) CLIOption { } } -// 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 - } -} - // WithDefaultContextStoreConfig configures the cli to use the default context store configuration. func WithDefaultContextStoreConfig() CLIOption { return func(cli *DockerCli) error { @@ -180,61 +158,70 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS" // 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 { - return func(apiClient *client.Client) error { - value := os.Getenv(envOverrideHTTPHeaders) - if value == "" { - return nil - } - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return invalidParameter(errors.Errorf( - "failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", - envOverrideHTTPHeaders, +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, )) } - if len(fields) == 0 { - return 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 invalidParameter(errors.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 invalidParameter(errors.Errorf( - `failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, - envOverrideHTTPHeaders, kv, - )) - } - - env[http.CanonicalHeaderKey(k)] = v + // 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, + )) } - 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 - } + env[http.CanonicalHeaderKey(k)] = v + } - // 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)(apiClient) + 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.userAgent = userAgent + return nil } } diff --git a/cli/command/cli_options_test.go b/cli/command/cli_options_test.go deleted file mode 100644 index 45ac1d8a5773..000000000000 --- a/cli/command/cli_options_test.go +++ /dev/null @@ -1,28 +0,0 @@ -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)) -} diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index ea67d403632c..27eb103a1fea 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -18,10 +18,9 @@ import ( "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -34,7 +33,7 @@ func TestNewAPIClientFromFlags(t *testing.T) { apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) - assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) + assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) } func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { @@ -47,7 +46,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), slug+host) - assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) + assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) } func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { @@ -71,7 +70,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { apiClient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) - assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) + assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) // 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"}) @@ -80,7 +79,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { "My-Header": "Custom-Value", "User-Agent": UserAgent(), } - _, err = apiClient.Ping(context.Background()) + _, err = apiClient.Ping(context.TODO(), client.PingOptions{}) assert.NilError(t, err) assert.DeepEqual(t, received, expectedHeaders) } @@ -106,7 +105,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { apiClient, err := NewAPIClientFromFlags(opts, configFile) assert.NilError(t, err) assert.Equal(t, apiClient.DaemonHost(), host) - assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) + assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) expectedHeaders := http.Header{ "One": []string{"one-value"}, @@ -115,14 +114,14 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) { "Four": []string{"four-value-override"}, "User-Agent": []string{UserAgent()}, } - _, err = apiClient.Ping(context.Background()) + _, err = apiClient.Ping(context.TODO(), client.PingOptions{}) assert.NilError(t, err) assert.DeepEqual(t, received, expectedHeaders) } func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { - const customVersion = "v3.3.3" - const expectedVersion = "3.3.3" + const customVersion = "v3.3" + const expectedVersion = "3.3" t.Setenv("DOCKER_API_VERSION", customVersion) t.Setenv("DOCKER_HOST", ":2375") @@ -135,51 +134,55 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { type fakeClient struct { client.Client - pingFunc func() (types.Ping, error) + pingFunc func() (client.PingResult, error) version string negotiated bool } -func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) { - return c.pingFunc() +func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) { + res, err := c.pingFunc() + if options.NegotiateAPIVersion { + if res.APIVersion != "" { + if c.negotiated || options.ForceNegotiate { + c.negotiated = true + } + } + } + return res, err } func (c *fakeClient) ClientVersion() string { return c.version } -func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) { - c.negotiated = true -} - func TestInitializeFromClient(t *testing.T) { const defaultVersion = "v1.55" testcases := []struct { doc string - pingFunc func() (types.Ping, error) + pingFunc func() (client.PingResult, error) expectedServer ServerInfo negotiated bool }{ { doc: "successful ping", - pingFunc: func() (types.Ping, error) { - return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil + pingFunc: func() (client.PingResult, error) { + return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil }, expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"}, negotiated: true, }, { doc: "failed ping, no API version", - pingFunc: func() (types.Ping, error) { - return types.Ping{}, errors.New("failed") + pingFunc: func() (client.PingResult, error) { + return client.PingResult{}, errors.New("failed") }, expectedServer: ServerInfo{HasExperimental: true}, }, { doc: "failed ping, with API version", - pingFunc: func() (types.Ping, error) { - return types.Ping{APIVersion: "v1.33"}, errors.New("failed") + pingFunc: func() (client.PingResult, error) { + return client.PingResult{APIVersion: "v1.44"}, errors.New("failed") }, expectedServer: ServerInfo{HasExperimental: true}, negotiated: true, @@ -188,16 +191,16 @@ func TestInitializeFromClient(t *testing.T) { for _, tc := range testcases { t.Run(tc.doc, func(t *testing.T) { - apiclient := &fakeClient{ + apiClient := &fakeClient{ pingFunc: tc.pingFunc, version: defaultVersion, } - cli := &DockerCli{client: apiclient} + cli := &DockerCli{client: apiClient} err := cli.Initialize(flags.NewClientOptions()) assert.NilError(t, err) assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) - assert.Equal(t, apiclient.negotiated, tc.negotiated) + assert.Equal(t, apiClient.negotiated, tc.negotiated) }) } } @@ -211,7 +214,7 @@ func TestInitializeFromClientHangs(t *testing.T) { assert.NilError(t, err) receiveReqCh := make(chan bool) - timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) + timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second) defer cancel() // Simulate a server that hangs on connections. @@ -255,8 +258,14 @@ func TestInitializeFromClientHangs(t *testing.T) { } func TestNewDockerCliAndOperators(t *testing.T) { - // Test default operations and also overriding default ones - cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input")))) + outbuf := bytes.NewBuffer(nil) + errbuf := bytes.NewBuffer(nil) + + cli, err := NewDockerCli( + WithInputStream(io.NopCloser(strings.NewReader("some input"))), + WithOutputStream(outbuf), + WithErrorStream(errbuf), + ) assert.NilError(t, err) // Check streams are initialized assert.Check(t, cli.In() != nil) @@ -266,19 +275,6 @@ func TestNewDockerCliAndOperators(t *testing.T) { assert.NilError(t, err) assert.Equal(t, string(inputStream), "some input") - // Apply can modify a dockerCli after construction - outbuf := bytes.NewBuffer(nil) - errbuf := bytes.NewBuffer(nil) - err = cli.Apply( - WithInputStream(io.NopCloser(strings.NewReader("input"))), - WithOutputStream(outbuf), - WithErrorStream(errbuf), - ) - assert.NilError(t, err) - // Check input stream - inputStream, err = io.ReadAll(cli.In()) - assert.NilError(t, err) - assert.Equal(t, string(inputStream), "input") // Check output stream _, err = fmt.Fprint(cli.Out(), "output") assert.NilError(t, err) @@ -296,9 +292,9 @@ func TestNewDockerCliAndOperators(t *testing.T) { func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) { cli, err := NewDockerCli() assert.NilError(t, err) - assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) { - return client.NewClientWithOpts() - }))) + apiClient, err := client.New() + assert.NilError(t, err) + assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient))) assert.Check(t, cli.ContextStore() != nil) } @@ -353,3 +349,46 @@ func TestHooksEnabled(t *testing.T) { 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) { + 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") +} diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index d3929293999d..2b375938c9ec 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -1,110 +1,30 @@ package commands import ( - "os" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/builder" - "github.com/docker/cli/cli/command/checkpoint" - "github.com/docker/cli/cli/command/config" - "github.com/docker/cli/cli/command/container" - "github.com/docker/cli/cli/command/context" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/command/manifest" - "github.com/docker/cli/cli/command/network" - "github.com/docker/cli/cli/command/node" - "github.com/docker/cli/cli/command/plugin" - "github.com/docker/cli/cli/command/registry" - "github.com/docker/cli/cli/command/secret" - "github.com/docker/cli/cli/command/service" - "github.com/docker/cli/cli/command/stack" - "github.com/docker/cli/cli/command/swarm" - "github.com/docker/cli/cli/command/system" - "github.com/docker/cli/cli/command/trust" - "github.com/docker/cli/cli/command/volume" + _ "github.com/docker/cli/cli/command/builder" + _ "github.com/docker/cli/cli/command/checkpoint" + _ "github.com/docker/cli/cli/command/config" + _ "github.com/docker/cli/cli/command/container" + _ "github.com/docker/cli/cli/command/context" + _ "github.com/docker/cli/cli/command/image" + _ "github.com/docker/cli/cli/command/manifest" + _ "github.com/docker/cli/cli/command/network" + _ "github.com/docker/cli/cli/command/node" + _ "github.com/docker/cli/cli/command/plugin" + _ "github.com/docker/cli/cli/command/registry" + _ "github.com/docker/cli/cli/command/secret" + _ "github.com/docker/cli/cli/command/service" + _ "github.com/docker/cli/cli/command/stack" + _ "github.com/docker/cli/cli/command/swarm" + _ "github.com/docker/cli/cli/command/system" + _ "github.com/docker/cli/cli/command/volume" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// AddCommands adds all the commands from cli/command to the root command -func AddCommands(cmd *cobra.Command, dockerCli command.Cli) { - 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.NewBakeStubCommand(dockerCli), - 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 +func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) { + for _, c := range commands.Commands() { + cmd.AddCommand(c(dockerCLI)) } - cmdCopy := *cmd - cmdCopy.Hidden = true - cmdCopy.Aliases = []string{} - return &cmdCopy } diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index 41ebebf642c7..f461d5befcdc 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -4,21 +4,14 @@ import ( "os" "strings" + "github.com/distribution/reference" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion. -// -// Deprecated: use [cobra.CompletionFunc]. -type ValidArgsFn = cobra.CompletionFunc - -// APIClientProvider provides a method to get an [client.APIClient], initializing +// APIClientProvider provides a method to get a [client.APIClient], initializing // it if needed. // // It's a smaller interface than [command.Cli], and used in situations where an @@ -34,24 +27,57 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc { if limit > 0 && len(args) >= limit { return nil, cobra.ShellCompDirectiveNoFileComp } - list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{}) + res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string - for _, img := range list { + for _, img := range res.Items { names = append(names, img.RepoTags...) } return names, cobra.ShellCompDirectiveNoFileComp } } +// ImageNamesWithBase offers completion for images present within the local store, +// including both full image names with tags and base image names (repository names only) +// when multiple tags exist for the same base name +func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + 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 // By default, only names are returned. // Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs. func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{ + res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{ All: all, }) if err != nil { @@ -61,7 +87,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes" var names []string - for _, ctr := range list { + for _, ctr := range res.Items { skip := false for _, fn := range filters { if fn != nil && !fn(ctr) { @@ -84,12 +110,12 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta // VolumeNames offers completion for volumes func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{}) + res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string - for _, vol := range list.Volumes { + for _, vol := range res.Items { names = append(names, vol.Name) } return names, cobra.ShellCompDirectiveNoFileComp @@ -99,12 +125,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc { // NetworkNames offers completion for networks func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{}) + res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string - for _, nw := range list { + for _, nw := range res.Items { names = append(names, nw.Name) } return names, cobra.ShellCompDirectiveNoFileComp @@ -124,14 +150,16 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc { // export MY_VAR=hello // docker run --rm --env MY_VAR alpine printenv MY_VAR // hello -func EnvVarNames(_ *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) +func EnvVarNames() cobra.CompletionFunc { + return 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 } - return names, cobra.ShellCompDirectiveNoFileComp } // FromList offers completion for the given list of options. @@ -142,13 +170,10 @@ func FromList(options ...string) cobra.CompletionFunc { // 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.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveDefault -} - -// NoComplete is used for commands where there's no relevant completion -func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp +func FileNames() cobra.CompletionFunc { + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveDefault + } } var commonPlatforms = []string{ @@ -188,6 +213,8 @@ var commonPlatforms = []string{ // - 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.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) { - return commonPlatforms, cobra.ShellCompDirectiveNoFileComp +func Platforms() cobra.CompletionFunc { + return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return commonPlatforms, cobra.ShellCompDirectiveNoFileComp + } } diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go index 12cd0ce425ee..27b52cc16bdd 100644 --- a/cli/command/completion/functions_test.go +++ b/cli/command/completion/functions_test.go @@ -6,13 +6,11 @@ import ( "sort" "testing" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" - "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" @@ -30,38 +28,38 @@ func (c fakeCLI) Client() client.APIClient { type fakeClient struct { client.Client - containerListFunc func(options container.ListOptions) ([]container.Summary, error) - imageListFunc func(options image.ListOptions) ([]image.Summary, error) - networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) - volumeListFunc func(filter filters.Args) (volume.ListResponse, error) + 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(_ context.Context, options container.ListOptions) ([]container.Summary, error) { +func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) { if c.containerListFunc != nil { - return c.containerListFunc(options) + return c.containerListFunc(ctx, options) } - return []container.Summary{}, nil + return client.ContainerListResult{}, nil } -func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) { +func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) { if c.imageListFunc != nil { - return c.imageListFunc(options) + return c.imageListFunc(ctx, options) } - return []image.Summary{}, nil + return client.ImageListResult{}, nil } -func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { +func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { if c.networkListFunc != nil { return c.networkListFunc(ctx, options) } - return []network.Inspect{}, nil + return client.NetworkListResult{}, nil } -func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) { +func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) { if c.volumeListFunc != nil { - return c.volumeListFunc(options.Filters) + return c.volumeListFunc(ctx, options) } - return volume.ListResponse{}, nil + return client.VolumeListResult{}, nil } func TestCompleteContainerNames(t *testing.T) { @@ -71,7 +69,7 @@ func TestCompleteContainerNames(t *testing.T) { filters []func(container.Summary) bool containers []container.Summary expOut []string - expOpts container.ListOptions + expOpts client.ContainerListOptions expDirective cobra.ShellCompDirective }{ { @@ -87,7 +85,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"}, - expOpts: container.ListOptions{All: true}, + expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, { @@ -100,7 +98,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"}, - expOpts: container.ListOptions{All: true}, + expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, { @@ -124,7 +122,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, expOut: []string{"container-b"}, - expOpts: container.ListOptions{All: true}, + expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, { @@ -140,7 +138,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}}, }, expOut: []string{"container-a"}, - expOpts: container.ListOptions{All: true}, + expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, { @@ -155,12 +153,12 @@ func TestCompleteContainerNames(t *testing.T) { t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes") } comp := ContainerNames(fakeCLI{&fakeClient{ - containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) { - assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{}))) + containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) { + assert.Check(t, is.DeepEqual(opts, tc.expOpts)) if tc.expDirective == cobra.ShellCompDirectiveError { - return nil, errors.New("some error occurred") + return client.ContainerListResult{}, errors.New("some error occurred") } - return tc.containers, nil + return client.ContainerListResult{Items: tc.containers}, nil }, }}, tc.showAll, tc.filters...) @@ -176,7 +174,7 @@ func TestCompleteEnvVarNames(t *testing.T) { "ENV_A": "hello-a", "ENV_B": "hello-b", }) - values, directives := EnvVarNames(nil, nil, "") + values, directives := EnvVarNames()(nil, nil, "") assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") sort.Strings(values) @@ -185,7 +183,7 @@ func TestCompleteEnvVarNames(t *testing.T) { } func TestCompleteFileNames(t *testing.T) { - values, directives := FileNames(nil, nil, "") + values, directives := FileNames()(nil, nil, "") assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault)) assert.Check(t, is.Len(values, 0)) } @@ -228,11 +226,11 @@ func TestCompleteImageNames(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { comp := ImageNames(fakeCLI{&fakeClient{ - imageListFunc: func(options image.ListOptions) ([]image.Summary, error) { + imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) { if tc.expDirective == cobra.ShellCompDirectiveError { - return nil, errors.New("some error occurred") + return client.ImageListResult{}, errors.New("some error occurred") } - return tc.images, nil + return client.ImageListResult{Items: tc.images}, nil }, }}, -1) @@ -257,9 +255,24 @@ func TestCompleteNetworkNames(t *testing.T) { { doc: "with results", networks: []network.Summary{ - {ID: "nw-c", Name: "network-c"}, - {ID: "nw-b", Name: "network-b"}, - {ID: "nw-a", Name: "network-a"}, + { + 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, @@ -273,11 +286,11 @@ func TestCompleteNetworkNames(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { comp := NetworkNames(fakeCLI{&fakeClient{ - networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { + networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) { if tc.expDirective == cobra.ShellCompDirectiveError { - return nil, errors.New("some error occurred") + return client.NetworkListResult{}, errors.New("some error occurred") } - return tc.networks, nil + return client.NetworkListResult{Items: tc.networks}, nil }, }}) @@ -288,14 +301,8 @@ func TestCompleteNetworkNames(t *testing.T) { } } -func TestCompleteNoComplete(t *testing.T) { - values, directives := NoComplete(nil, nil, "") - assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp)) - assert.Check(t, is.Len(values, 0)) -} - func TestCompletePlatforms(t *testing.T) { - values, directives := Platforms(nil, nil, "") + 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)) } @@ -303,7 +310,7 @@ func TestCompletePlatforms(t *testing.T) { func TestCompleteVolumeNames(t *testing.T) { tests := []struct { doc string - volumes []*volume.Volume + volumes []volume.Volume expOut []string expDirective cobra.ShellCompDirective }{ @@ -313,7 +320,7 @@ func TestCompleteVolumeNames(t *testing.T) { }, { doc: "with results", - volumes: []*volume.Volume{ + volumes: []volume.Volume{ {Name: "volume-c"}, {Name: "volume-b"}, {Name: "volume-a"}, @@ -330,11 +337,11 @@ func TestCompleteVolumeNames(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { comp := VolumeNames(fakeCLI{&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { + volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) { if tc.expDirective == cobra.ShellCompDirectiveError { - return volume.ListResponse{}, errors.New("some error occurred") + return client.VolumeListResult{}, errors.New("some error occurred") } - return volume.ListResponse{Volumes: tc.volumes}, nil + return client.VolumeListResult{Items: tc.volumes}, nil }, }}) diff --git a/cli/command/config/client_test.go b/cli/command/config/client_test.go index 14c68d504d49..ad71a68e2454 100644 --- a/cli/command/config/client_test.go +++ b/cli/command/config/client_test.go @@ -3,42 +3,41 @@ package config import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) - configInspectFunc func(context.Context, string) (swarm.Config, []byte, error) - configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error) - configRemoveFunc func(string) error + configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) + configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) + configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) + configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) } -func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { +func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { if c.configCreateFunc != nil { - return c.configCreateFunc(ctx, spec) + return c.configCreateFunc(ctx, options) } - return swarm.ConfigCreateResponse{}, nil + return client.ConfigCreateResult{}, nil } -func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) { +func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) { if c.configInspectFunc != nil { - return c.configInspectFunc(ctx, id) + return c.configInspectFunc(ctx, id, options) } - return swarm.Config{}, nil, nil + return client.ConfigInspectResult{}, nil } -func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { +func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { if c.configListFunc != nil { return c.configListFunc(ctx, options) } - return []swarm.Config{}, nil + return client.ConfigListResult{}, nil } -func (c *fakeClient) ConfigRemove(_ context.Context, name string) error { +func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { if c.configRemoveFunc != nil { - return c.configRemoveFunc(name) + return c.configRemoveFunc(ctx, name, options) } - return nil + return client.ConfigRemoveResult{}, nil } diff --git a/cli/command/config/cmd.go b/cli/command/config/cmd.go index d83e33b2057b..588f100fb7d0 100644 --- a/cli/command/config/cmd.go +++ b/cli/command/config/cmd.go @@ -4,27 +4,33 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" + "github.com/docker/cli/internal/commands" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// NewConfigCommand returns a cobra command for `config` subcommands -func NewConfigCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newConfigCommand) +} + +// newConfigCommand returns a cobra command for `config` subcommands +func newConfigCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Manage Swarm configs", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.30", "swarm": "manager", }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newConfigListCommand(dockerCli), - newConfigCreateCommand(dockerCli), - newConfigInspectCommand(dockerCli), - newConfigRemoveCommand(dockerCli), + newConfigListCommand(dockerCLI), + newConfigCreateCommand(dockerCLI), + newConfigInspectCommand(dockerCLI), + newConfigRemoveCommand(dockerCLI), ) return cmd } @@ -32,12 +38,12 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command { // completeNames offers completion for swarm configs func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{}) + res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string - for _, config := range list { + for _, config := range res.Items { names = append(names, config.ID) } return names, cobra.ShellCompDirectiveNoFileComp diff --git a/cli/command/config/create.go b/cli/command/config/create.go index 8fefd19d5926..d81011675fa6 100644 --- a/cli/command/config/create.go +++ b/cli/command/config/create.go @@ -2,30 +2,30 @@ package config import ( "context" + "errors" "fmt" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/moby/sys/sequential" - "github.com/pkg/errors" "github.com/spf13/cobra" ) -// CreateOptions specifies some options that are used when creating a config. -type CreateOptions struct { - Name string - TemplateDriver string - File string - Labels opts.ListOpts +// createOptions specifies some options that are used when creating a config. +type createOptions struct { + name string + templateDriver string + file string + labels opts.ListOpts } -func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command { - createOpts := CreateOptions{ - Labels: opts.NewListOpts(opts.ValidateLabel), +func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command { + createOpts := createOptions{ + labels: opts.NewListOpts(opts.ValidateLabel), } cmd := &cobra.Command{ @@ -33,42 +33,61 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command { Short: "Create a config from a file or STDIN", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - createOpts.Name = args[0] - createOpts.File = args[1] - return RunConfigCreate(cmd.Context(), dockerCli, createOpts) + createOpts.name = args[0] + createOpts.file = args[1] + return runCreate(cmd.Context(), dockerCLI, createOpts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + 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.VarP(&createOpts.Labels, "label", "l", "Config labels") - flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver") - flags.SetAnnotation("template-driver", "version", []string{"1.37"}) + flags.VarP(&createOpts.labels, "label", "l", "Config labels") + flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver") + _ = flags.SetAnnotation("template-driver", "version", []string{"1.37"}) return cmd } -// RunConfigCreate creates a config with the given options. -func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error { +// runCreate creates a config with the given options. +func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { apiClient := dockerCLI.Client() - configData, err := readConfigData(dockerCLI.In(), options.File) + configData, err := readConfigData(dockerCLI.In(), options.file) if err != nil { - return errors.Errorf("Error reading content from %q: %v", options.File, err) + return fmt.Errorf("error reading content from %q: %v", options.file, err) } spec := swarm.ConfigSpec{ Annotations: swarm.Annotations{ - Name: options.Name, - Labels: opts.ConvertKVStringsToMap(options.Labels.GetSlice()), + Name: options.name, + Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), }, Data: configData, } - if options.TemplateDriver != "" { + if options.templateDriver != "" { spec.Templating = &swarm.Driver{ - Name: options.TemplateDriver, + Name: options.templateDriver, } } - r, err := apiClient.ConfigCreate(ctx, spec) + r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{ + Spec: spec, + }) if err != nil { return err } diff --git a/cli/command/config/create_test.go b/cli/command/config/create_test.go index f5d6727bfce2..54068423dfd7 100644 --- a/cli/command/config/create_test.go +++ b/cli/command/config/create_test.go @@ -12,7 +12,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -23,7 +24,7 @@ const configDataFile = "config-create-with-name.golden" func TestConfigCreateErrors(t *testing.T) { testCases := []struct { args []string - configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) + configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) expectedError string }{ { @@ -36,8 +37,8 @@ func TestConfigCreateErrors(t *testing.T) { }, { args: []string{"name", filepath.Join("testdata", configDataFile)}, - configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - return swarm.ConfigCreateResponse{}, errors.New("error creating config") + configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { + return client.ConfigCreateResult{}, errors.New("error creating config") }, expectedError: "error creating config", }, @@ -61,15 +62,15 @@ func TestConfigCreateWithName(t *testing.T) { const name = "config-with-name" var actual []byte cli := test.NewFakeCli(&fakeClient{ - configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - if spec.Name != name { - return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) + configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { + if options.Spec.Name != name { + return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) } - actual = spec.Data + actual = options.Spec.Data - return swarm.ConfigCreateResponse{ - ID: "ID-" + spec.Name, + return client.ConfigCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) @@ -100,13 +101,13 @@ func TestConfigCreateWithLabels(t *testing.T) { } cli := test.NewFakeCli(&fakeClient{ - configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - if !reflect.DeepEqual(spec, expected) { - return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec) + configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { + if !reflect.DeepEqual(options.Spec, expected) { + return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec) } - return swarm.ConfigCreateResponse{ - ID: "ID-" + spec.Name, + return client.ConfigCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) @@ -126,17 +127,17 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) { const name = "config-with-template-driver" cli := test.NewFakeCli(&fakeClient{ - configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - if spec.Name != name { - return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) + configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { + if options.Spec.Name != name { + return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) } - if spec.Templating.Name != expectedDriver.Name { - return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels) + if options.Spec.Templating.Name != expectedDriver.Name { + return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels) } - return swarm.ConfigCreateResponse{ - ID: "ID-" + spec.Name, + return client.ConfigCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) diff --git a/cli/command/config/formatter.go b/cli/command/config/formatter.go index f2defa721b80..09b264337ad0 100644 --- a/cli/command/config/formatter.go +++ b/cli/command/config/formatter.go @@ -7,8 +7,9 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-units" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -29,8 +30,8 @@ Data: {{.Data}}` ) -// NewFormat returns a Format for rendering using a config Context -func NewFormat(source string, quiet bool) formatter.Format { +// newFormat returns a Format for rendering using a configContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return configInspectPrettyTemplate @@ -43,31 +44,28 @@ func NewFormat(source string, quiet bool) formatter.Format { return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, configs []swarm.Config) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, config := range configs { +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error { + cCtx := &configContext{ + HeaderContext: formatter.HeaderContext{ + 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} if err := format(configCtx); err != nil { return err } } 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 { @@ -114,12 +112,12 @@ func (c *configContext) Label(name string) string { return c.c.Spec.Annotations.Labels[name] } -// InspectFormatWrite renders the context for a list of configs -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != configInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// inspectFormatWrite renders the context for a list of configs +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != configInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } - render := func(format func(subContext formatter.SubContext) error) error { + return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { configI, _, err := getRef(ref) if err != nil { @@ -134,8 +132,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } } return nil - } - return ctx.Write(&configInspectContext{}, render) + }) } type configInspectContext struct { diff --git a/cli/command/config/formatter_test.go b/cli/command/config/formatter_test.go index 8736a876a48b..7675b285c388 100644 --- a/cli/command/config/formatter_test.go +++ b/cli/command/config/formatter_test.go @@ -6,7 +6,8 @@ import ( "time" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -27,44 +28,46 @@ func TestConfigContextFormatWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `ID NAME CREATED UPDATED 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 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME passwords id_rsa `, }, { - formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)}, `1-passwords 2-id_rsa `, }, } - configs := []swarm.Config{ - { - ID: "1", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}, - }, - { - ID: "2", - Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, - Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}, + res := client.ConfigListResult{ + Items: []swarm.Config{ + { + ID: "1", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}, + }, + { + ID: "2", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}, + }, }, } for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, configs); err != nil { + if err := formatWrite(tc.context, res); err != nil { assert.ErrorContains(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) diff --git a/cli/command/config/inspect.go b/cli/command/config/inspect.go index 1983857eb578..e423a4891e32 100644 --- a/cli/command/config/inspect.go +++ b/cli/command/config/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package config @@ -12,61 +12,61 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// InspectOptions contains options for the docker config inspect command. -type InspectOptions struct { - Names []string - Format string - Pretty bool +// inspectOptions contains options for the docker config inspect command. +type inspectOptions struct { + names []string + format string + pretty bool } -func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command { - opts := InspectOptions{} +func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command { + opts := inspectOptions{} cmd := &cobra.Command{ Use: "inspect [OPTIONS] CONFIG [CONFIG...]", Short: "Display detailed information on one or more configs", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Names = args - return RunConfigInspect(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + opts.names = args + return runInspect(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } - 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().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) + cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } -// RunConfigInspect inspects the given Swarm config. -func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error { +// runInspect inspects the given Swarm config. +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { apiClient := dockerCLI.Client() - if opts.Pretty { - opts.Format = "pretty" + if opts.pretty { + opts.format = "pretty" } getRef := func(id string) (any, []byte, error) { - return apiClient.ConfigInspectWithRaw(ctx, id) + res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{}) + return res.Config, res.Raw, err } - f := opts.Format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } configCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewFormat(f, false), + Format: newFormat(opts.format, 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 nil diff --git a/cli/command/config/inspect_test.go b/cli/command/config/inspect_test.go index a2a8e648baf2..fd02924fa59e 100644 --- a/cli/command/config/inspect_test.go +++ b/cli/command/config/inspect_test.go @@ -10,7 +10,7 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error) + configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) expectedError string }{ { @@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) { }, { args: []string{"foo"}, - configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) { - return swarm.Config{}, nil, errors.New("error while inspecting the config") + configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) { + return client.ConfigInspectResult{}, errors.New("error while inspecting the config") }, expectedError: "error while inspecting the config", }, @@ -41,11 +41,13 @@ func TestConfigInspectErrors(t *testing.T) { }, { args: []string{"foo", "bar"}, - configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) { + configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { if configID == "foo" { - return *builders.Config(builders.ConfigName("foo")), nil, nil + return client.ConfigInspectResult{ + Config: *builders.Config(builders.ConfigName("foo")), + }, nil } - return swarm.Config{}, nil, errors.New("error while inspecting the config") + return client.ConfigInspectResult{}, errors.New("error while inspecting the config") }, expectedError: "error while inspecting the config", }, @@ -70,25 +72,34 @@ func TestConfigInspectWithoutFormat(t *testing.T) { testCases := []struct { name string args []string - configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error) + configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) }{ { name: "single-config", args: []string{"foo"}, - configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) { + configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { if name != "foo" { - return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) + return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) } - return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil + return client.ConfigInspectResult{ + Config: *builders.Config( + builders.ConfigID("ID-foo"), + builders.ConfigName("foo"), + ), + }, nil }, }, { name: "multiple-configs-with-labels", args: []string{"foo", "bar"}, - configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) { - return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil + configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { + return client.ConfigInspectResult{ + Config: *builders.Config( + builders.ConfigID("ID-"+name), + builders.ConfigName(name), + builders.ConfigLabels(map[string]string{"label1": "label-foo"}), + ), + }, nil }, }, } @@ -102,16 +113,19 @@ func TestConfigInspectWithoutFormat(t *testing.T) { } func TestConfigInspectWithFormat(t *testing.T) { - configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) { - return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil + configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { + return client.ConfigInspectResult{ + Config: *builders.Config( + builders.ConfigName("foo"), + builders.ConfigLabels(map[string]string{"label1": "label-foo"}), + ), + }, nil } testCases := []struct { name string format string args []string - configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error) + configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) }{ { name: "simple-template", @@ -141,21 +155,23 @@ func TestConfigInspectWithFormat(t *testing.T) { func TestConfigInspectPretty(t *testing.T) { testCases := []struct { name string - configInspectFunc func(context.Context, string) (swarm.Config, []byte, error) + configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) }{ { name: "simple", - configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) { - return *builders.Config( - builders.ConfigLabels(map[string]string{ - "lbl1": "value1", - }), - builders.ConfigID("configID"), - builders.ConfigName("configName"), - builders.ConfigCreatedAt(time.Time{}), - builders.ConfigUpdatedAt(time.Time{}), - builders.ConfigData([]byte("payload here")), - ), []byte{}, nil + configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { + return client.ConfigInspectResult{ + Config: *builders.Config( + builders.ConfigLabels(map[string]string{ + "lbl1": "value1", + }), + builders.ConfigID("configID"), + builders.ConfigName("configName"), + builders.ConfigCreatedAt(time.Time{}), + builders.ConfigUpdatedAt(time.Time{}), + builders.ConfigData([]byte("payload here")), + ), + }, nil }, }, } diff --git a/cli/command/config/ls.go b/cli/command/config/ls.go index f6e68cfaa2ef..cbed96200ee9 100644 --- a/cli/command/config/ls.go +++ b/cli/command/config/ls.go @@ -6,24 +6,23 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// ListOptions contains options for the docker config ls command. -type ListOptions struct { - Quiet bool - Format string - Filter opts.FilterOpt +// listOptions contains options for the docker config ls command. +type listOptions struct { + quiet bool + format string + filter opts.FilterOpt } -func newConfigListCommand(dockerCli command.Cli) *cobra.Command { - listOpts := ListOptions{Filter: opts.NewFilterOpt()} +func newConfigListCommand(dockerCLI command.Cli) *cobra.Command { + listOpts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -31,44 +30,45 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command { Short: "List configs", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return RunConfigList(cmd.Context(), dockerCli, listOpts) + return runList(cmd.Context(), dockerCLI, listOpts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp) - flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp) + flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } -// RunConfigList lists Swarm configs. -func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error { +// runList lists Swarm configs. +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { apiClient := dockerCLI.Client() - configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.Filter.Value()}) + res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()}) if err != nil { return err } - format := options.Format + format := options.format if len(format) == 0 { - if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet { + if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.quiet { format = dockerCLI.ConfigFile().ConfigFormat } else { format = formatter.TableFormatKey } } - sort.Slice(configs, func(i, j int) bool { - return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name) + sort.Slice(res.Items, func(i, j int) bool { + return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name) }) configCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewFormat(format, options.Quiet), + Format: newFormat(format, options.quiet), } - return FormatWrite(configCtx, configs) + return formatWrite(configCtx, res) } diff --git a/cli/command/config/ls_test.go b/cli/command/config/ls_test.go index 6bc712aaf070..9f6d97eefdf0 100644 --- a/cli/command/config/ls_test.go +++ b/cli/command/config/ls_test.go @@ -10,16 +10,16 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" ) func TestConfigListErrors(t *testing.T) { testCases := []struct { args []string - configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error) + configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) expectedError string }{ { @@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) { expectedError: "accepts no argument", }, { - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - return []swarm.Config{}, errors.New("error listing configs") + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + return client.ConfigListResult{}, errors.New("error listing configs") }, expectedError: "error listing configs", }, @@ -48,26 +48,28 @@ func TestConfigListErrors(t *testing.T) { func TestConfigList(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - return []swarm.Config{ - *builders.Config(builders.ConfigID("ID-1-foo"), - builders.ConfigName("1-foo"), - builders.ConfigVersion(swarm.Version{Index: 10}), - builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), - builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *builders.Config(builders.ConfigID("ID-10-foo"), - builders.ConfigName("10-foo"), - builders.ConfigVersion(swarm.Version{Index: 11}), - builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), - builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *builders.Config(builders.ConfigID("ID-2-foo"), - builders.ConfigName("2-foo"), - builders.ConfigVersion(swarm.Version{Index: 11}), - builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), - builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), - ), + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + return client.ConfigListResult{ + Items: []swarm.Config{ + *builders.Config(builders.ConfigID("ID-1-foo"), + builders.ConfigName("1-foo"), + builders.ConfigVersion(swarm.Version{Index: 10}), + builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), + builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *builders.Config(builders.ConfigID("ID-10-foo"), + builders.ConfigName("10-foo"), + builders.ConfigVersion(swarm.Version{Index: 11}), + builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), + builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *builders.Config(builders.ConfigID("ID-2-foo"), + builders.ConfigName("2-foo"), + builders.ConfigVersion(swarm.Version{Index: 11}), + builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), + builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + }, }, nil }, }) @@ -78,12 +80,14 @@ func TestConfigList(t *testing.T) { func TestConfigListWithQuietOption(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - return []swarm.Config{ - *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), - *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ - "label": "label-bar", - })), + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + return client.ConfigListResult{ + Items: []swarm.Config{ + *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), + *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -95,12 +99,14 @@ func TestConfigListWithQuietOption(t *testing.T) { func TestConfigListWithConfigFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - return []swarm.Config{ - *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), - *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ - "label": "label-bar", - })), + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + return client.ConfigListResult{ + Items: []swarm.Config{ + *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), + *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -114,12 +120,14 @@ func TestConfigListWithConfigFormat(t *testing.T) { func TestConfigListWithFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - return []swarm.Config{ - *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), - *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ - "label": "label-bar", - })), + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + return client.ConfigListResult{ + Items: []swarm.Config{ + *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), + *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -131,22 +139,24 @@ func TestConfigListWithFormat(t *testing.T) { func TestConfigListWithFilter(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0])) - assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0])) - return []swarm.Config{ - *builders.Config(builders.ConfigID("ID-foo"), - builders.ConfigName("foo"), - builders.ConfigVersion(swarm.Version{Index: 10}), - builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), - builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *builders.Config(builders.ConfigID("ID-bar"), - builders.ConfigName("bar"), - builders.ConfigVersion(swarm.Version{Index: 11}), - builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), - builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), - ), + configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { + assert.Check(t, options.Filters["name"]["foo"]) + assert.Check(t, options.Filters["label"]["lbl1=Label-bar"]) + return client.ConfigListResult{ + Items: []swarm.Config{ + *builders.Config(builders.ConfigID("ID-foo"), + builders.ConfigName("foo"), + builders.ConfigVersion(swarm.Version{Index: 10}), + builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), + builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *builders.Config(builders.ConfigID("ID-bar"), + builders.ConfigName("bar"), + builders.ConfigVersion(swarm.Version{Index: 11}), + builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), + builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + }, }, nil }, }) diff --git a/cli/command/config/remove.go b/cli/command/config/remove.go index 01cbe331c135..0b3462404c81 100644 --- a/cli/command/config/remove.go +++ b/cli/command/config/remove.go @@ -7,39 +7,31 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// RemoveOptions contains options for the docker config rm command. -type RemoveOptions struct { - Names []string -} - -func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "rm CONFIG [CONFIG...]", Aliases: []string{"remove"}, Short: "Remove one or more configs", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts := RemoveOptions{ - Names: args, - } - return RunConfigRemove(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runRemove(cmd.Context(), dockerCLI, args) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } } -// RunConfigRemove removes the given Swarm configs. -func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error { +// runRemove removes the given Swarm configs. +func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error { apiClient := dockerCLI.Client() var errs []error - for _, name := range opts.Names { - if err := apiClient.ConfigRemove(ctx, name); err != nil { + for _, name := range names { + if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil { errs = append(errs, err) continue } diff --git a/cli/command/config/remove_test.go b/cli/command/config/remove_test.go index 9213fcc11f71..dbe8ed8b9e0c 100644 --- a/cli/command/config/remove_test.go +++ b/cli/command/config/remove_test.go @@ -1,12 +1,14 @@ package config import ( + "context" "errors" "io" "strings" "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -14,7 +16,7 @@ import ( func TestConfigRemoveErrors(t *testing.T) { testCases := []struct { args []string - configRemoveFunc func(string) error + configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) expectedError string }{ { @@ -23,8 +25,8 @@ func TestConfigRemoveErrors(t *testing.T) { }, { args: []string{"foo"}, - configRemoveFunc: func(name string) error { - return errors.New("error removing config") + configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { + return client.ConfigRemoveResult{}, errors.New("error removing config") }, expectedError: "error removing config", }, @@ -46,9 +48,9 @@ func TestConfigRemoveWithName(t *testing.T) { names := []string{"foo", "bar"} var removedConfigs []string cli := test.NewFakeCli(&fakeClient{ - configRemoveFunc: func(name string) error { + configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { removedConfigs = append(removedConfigs, name) - return nil + return client.ConfigRemoveResult{}, nil }, }) cmd := newConfigRemoveCommand(cli) @@ -63,12 +65,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) { var removedConfigs []string cli := test.NewFakeCli(&fakeClient{ - configRemoveFunc: func(name string) error { + configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { removedConfigs = append(removedConfigs, name) if name == "foo" { - return errors.New("error removing config: " + name) + return client.ConfigRemoveResult{}, errors.New("error removing config: " + name) } - return nil + return client.ConfigRemoveResult{}, nil }, }) diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go index f5ee5cf31115..8316c56fb0ce 100644 --- a/cli/command/container/attach.go +++ b/cli/command/container/attach.go @@ -2,15 +2,15 @@ package container import ( "context" + "errors" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/moby/sys/signal" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -23,25 +23,25 @@ type AttachOptions struct { } func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) { - c, err := apiClient.ContainerInspect(ctx, args) + c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{}) if err != nil { return nil, err } - if !c.State.Running { - return nil, errors.New("You cannot attach to a stopped container, start it first") + if !c.Container.State.Running { + return nil, errors.New("cannot attach to a stopped container, start it first") } - if c.State.Paused { - return nil, errors.New("You cannot attach to a paused container, unpause it first") + if c.Container.State.Paused { + return nil, errors.New("cannot attach to a paused container, unpause it first") } - if c.State.Restarting { - return nil, errors.New("You cannot attach to a restarting container, wait until it is running") + if c.Container.State.Restarting { + return nil, errors.New("cannot attach to a restarting container, wait until it is running") } - return &c, nil + return &c.Container, nil } -// NewAttachCommand creates a new cobra.Command for `docker attach` -func NewAttachCommand(dockerCLI command.Cli) *cobra.Command { +// newAttachCommand creates a new cobra.Command for `docker attach` +func newAttachCommand(dockerCLI command.Cli) *cobra.Command { var opts AttachOptions cmd := &cobra.Command{ @@ -58,6 +58,7 @@ func NewAttachCommand(dockerCLI command.Cli) *cobra.Command { ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State != container.StatePaused }), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -73,7 +74,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o // request channel to wait for client waitCtx := context.WithoutCancel(ctx) - resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "") + waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{}) c, err := inspectContainerAndCheckState(ctx, apiClient, containerID) if err != nil { @@ -89,7 +90,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o detachKeys = opts.DetachKeys } - options := container.AttachOptions{ + options := client.ContainerAttachOptions{ Stream: true, Stdin: !opts.NoStdin && c.Config.OpenStdin, Stdout: true, @@ -113,11 +114,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o defer signal.StopCatch(sigc) } - resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options) - if errAttach != nil { - return errAttach + res, err := apiClient.ContainerAttach(ctx, containerID, options) + if err != nil { + return err } - defer resp.Close() + defer res.HijackedResponse.Close() // If use docker attach command to attach to a stop container, it will return // "You cannot attach to a stopped container" error, it's ok, but when @@ -141,7 +142,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o inputStream: in, outputStream: dockerCLI.Out(), errorStream: dockerCLI.Err(), - resp: resp, + resp: res.HijackedResponse, tty: c.Config.Tty, detachKeys: options.DetachKeys, } @@ -151,19 +152,19 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o return err } - return getExitStatus(errC, resultC) + return getExitStatus(waitRes) } -func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error { +func getExitStatus(waitRes client.ContainerWaitResult) error { select { - case result := <-resultC: + case result := <-waitRes.Result: if result.Error != nil { return errors.New(result.Error.Message) } if result.StatusCode != 0 { return cli.StatusError{StatusCode: int(result.StatusCode)} } - case err := <-errC: + case err := <-waitRes.Error: return err } @@ -176,7 +177,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) { // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially // resize it, then go back to normal. Without this, every attach after the first will // require the user to manually resize or hit enter. - resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false) + resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false) // After the above resizing occurs, the call to MonitorTtySize below will handle resetting back // to the actual size. diff --git a/cli/command/container/attach_test.go b/cli/command/container/attach_test.go index b4ae6e9f45a0..60b9c51ed1e3 100644 --- a/cli/command/container/attach_test.go +++ b/cli/command/container/attach_test.go @@ -7,7 +7,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -16,23 +17,23 @@ func TestNewAttachCommandErrors(t *testing.T) { name string args []string expectedError string - containerInspectFunc func(img string) (container.InspectResponse, error) + containerInspectFunc func(img string) (client.ContainerInspectResult, error) }{ { name: "client-error", args: []string{"5cb5bb5e4a3b"}, expectedError: "something went wrong", - containerInspectFunc: func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{}, errors.New("something went wrong") + containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{}, errors.New("something went wrong") }, }, { name: "client-stopped", args: []string{"5cb5bb5e4a3b"}, - expectedError: "You cannot attach to a stopped container", - containerInspectFunc: func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ + expectedError: "cannot attach to a stopped container", + containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{ + Container: container.InspectResponse{ State: &container.State{ Running: false, }, @@ -43,10 +44,10 @@ func TestNewAttachCommandErrors(t *testing.T) { { name: "client-paused", args: []string{"5cb5bb5e4a3b"}, - expectedError: "You cannot attach to a paused container", - containerInspectFunc: func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ + expectedError: "cannot attach to a paused container", + containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{ + Container: container.InspectResponse{ State: &container.State{ Running: true, Paused: true, @@ -58,10 +59,10 @@ func TestNewAttachCommandErrors(t *testing.T) { { name: "client-restarting", args: []string{"5cb5bb5e4a3b"}, - expectedError: "You cannot attach to a restarting container", - containerInspectFunc: func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{ - ContainerJSONBase: &container.ContainerJSONBase{ + expectedError: "cannot attach to a restarting container", + containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{ + Container: container.InspectResponse{ State: &container.State{ Running: true, Paused: false, @@ -74,7 +75,7 @@ func TestNewAttachCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) + cmd := newAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -124,7 +125,10 @@ func TestGetExitStatus(t *testing.T) { resultC <- *testcase.result } - err := getExitStatus(errC, resultC) + err := getExitStatus(client.ContainerWaitResult{ + Result: resultC, + Error: errC, + }) if testcase.expectedError == nil { assert.NilError(t, err) diff --git a/cli/command/container/client_test.go b/cli/command/container/client_test.go index 369c845e2110..9b66f713cb59 100644 --- a/cli/command/container/client_test.go +++ b/cli/command/container/client_test.go @@ -3,232 +3,236 @@ package container import ( "context" "io" + "net/http" + "strings" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/moby/moby/client" ) +func mockContainerExportResult(content string) client.ContainerExportResult { + return io.NopCloser(strings.NewReader(content)) +} + +func mockContainerLogsResult(content string) client.ContainerLogsResult { + return io.NopCloser(strings.NewReader(content)) +} + +type fakeStreamResult struct { + io.ReadCloser + client.ImagePushResponse // same interface as [client.ImagePushResponse] +} + +func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) } +func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() } + type fakeClient struct { client.Client - inspectFunc func(string) (container.InspectResponse, error) - execInspectFunc func(execID string) (container.ExecInspect, error) - execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error) - createContainerFunc func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string) (container.CreateResponse, error) - containerStartFunc func(containerID string, options container.StartOptions) error - imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) - infoFunc func() (system.Info, error) - containerStatPathFunc func(containerID, path string) (container.PathStat, error) - containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error) - logFunc func(string, container.LogsOptions) (io.ReadCloser, error) - waitFunc func(string) (<-chan container.WaitResponse, <-chan error) - containerListFunc func(container.ListOptions) ([]container.Summary, error) - containerExportFunc func(string) (io.ReadCloser, error) - containerExecResizeFunc func(id string, options container.ResizeOptions) error - containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error - containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error - containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error - containerKillFunc func(ctx context.Context, containerID, signal string) error - containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) - containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) - containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error) + inspectFunc func(string) (client.ContainerInspectResult, error) + execInspectFunc func(execID string) (client.ExecInspectResult, error) + execCreateFunc func(containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error) + createContainerFunc func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) + containerStartFunc func(containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) + imagePullFunc func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) + infoFunc func() (client.SystemInfoResult, error) + containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error) + containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error) + logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error) + waitFunc func(string) client.ContainerWaitResult + containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error) + containerExportFunc func(string) (client.ContainerExportResult, error) + containerExecResizeFunc func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) + containerRemoveFunc func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) + containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) + containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) + containerKillFunc func(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) + containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) + containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) + containerDiffFunc func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) containerRenameFunc func(ctx context.Context, oldName, newName string) error - containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error) - containerPauseFunc func(ctx context.Context, container string) error + containerCommitFunc func(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) + containerPauseFunc func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) Version string } -func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) { +func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) { if f.containerListFunc != nil { return f.containerListFunc(options) } - return []container.Summary{}, nil + return client.ContainerListResult{}, nil } -func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) { +func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) { if f.inspectFunc != nil { return f.inspectFunc(containerID) } - return container.InspectResponse{}, nil + return client.ContainerInspectResult{}, nil } -func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) { +func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) { if f.execCreateFunc != nil { return f.execCreateFunc(containerID, config) } - return container.ExecCreateResponse{}, nil + return client.ExecCreateResult{}, nil } -func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) { +func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) { if f.execInspectFunc != nil { return f.execInspectFunc(execID) } - return container.ExecInspect{}, nil + return client.ExecInspectResult{}, nil } -func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error { - return nil +func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) { + return client.ExecStartResult{}, nil } -func (f *fakeClient) ContainerCreate( - _ context.Context, - config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, -) (container.CreateResponse, error) { +func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { if f.createContainerFunc != nil { - return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName) + return f.createContainerFunc(options) } - return container.CreateResponse{}, nil + return client.ContainerCreateResult{}, nil } -func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error { +func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) { if f.containerRemoveFunc != nil { return f.containerRemoveFunc(ctx, containerID, options) } - return nil + return client.ContainerRemoveResult{}, nil } -func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { - if f.imageCreateFunc != nil { - return f.imageCreateFunc(ctx, parentReference, options) +func (f *fakeClient) ImagePull(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) { + if f.imagePullFunc != nil { + return f.imagePullFunc(ctx, parentReference, options) } - return nil, nil + return fakeStreamResult{}, nil } -func (f *fakeClient) Info(_ context.Context) (system.Info, error) { +func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { if f.infoFunc != nil { return f.infoFunc() } - return system.Info{}, nil + return client.SystemInfoResult{}, nil } -func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) { +func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) { if f.containerStatPathFunc != nil { - return f.containerStatPathFunc(containerID, path) + return f.containerStatPathFunc(containerID, options.Path) } - return container.PathStat{}, nil + return client.ContainerStatPathResult{}, nil } -func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) { +func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) { if f.containerCopyFromFunc != nil { - return f.containerCopyFromFunc(containerID, srcPath) + return f.containerCopyFromFunc(containerID, options.SourcePath) } - return nil, container.PathStat{}, nil + return client.CopyFromContainerResult{}, nil } -func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) { +func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) { if f.logFunc != nil { return f.logFunc(containerID, options) } - return nil, nil + return http.NoBody, nil } func (f *fakeClient) ClientVersion() string { return f.Version } -func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { +func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult { if f.waitFunc != nil { return f.waitFunc(containerID) } - return nil, nil + return client.ContainerWaitResult{} } -func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error { +func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) { if f.containerStartFunc != nil { return f.containerStartFunc(containerID, options) } - return nil + return client.ContainerStartResult{}, nil } -func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) { +func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) { if f.containerExportFunc != nil { return f.containerExportFunc(containerID) } - return nil, nil + return http.NoBody, nil } -func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error { +func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) { if f.containerExecResizeFunc != nil { return f.containerExecResizeFunc(id, options) } - return nil + return client.ExecResizeResult{}, nil } -func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error { +func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { if f.containerKillFunc != nil { - return f.containerKillFunc(ctx, containerID, signal) + return f.containerKillFunc(ctx, containerID, options) } - return nil + return client.ContainerKillResult{}, nil } -func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { +func (f *fakeClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) { if f.containerPruneFunc != nil { - return f.containerPruneFunc(ctx, pruneFilters) + return f.containerPruneFunc(ctx, options) } - return container.PruneReport{}, nil + return client.ContainerPruneResult{}, nil } -func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error { +func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) { if f.containerRestartFunc != nil { return f.containerRestartFunc(ctx, containerID, options) } - return nil + return client.ContainerRestartResult{}, nil } -func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { +func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) { if f.containerStopFunc != nil { return f.containerStopFunc(ctx, containerID, options) } - return nil + return client.ContainerStopResult{}, nil } -func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { +func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) { if f.containerAttachFunc != nil { return f.containerAttachFunc(ctx, containerID, options) } - return types.HijackedResponse{}, nil + return client.ContainerAttachResult{}, nil } -func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) { +func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) { if f.containerDiffFunc != nil { return f.containerDiffFunc(ctx, containerID) } - return []container.FilesystemChange{}, nil + return client.ContainerDiffResult{}, nil } -func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error { +func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) { if f.containerRenameFunc != nil { - return f.containerRenameFunc(ctx, oldName, newName) + return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName) } - return nil + return client.ContainerRenameResult{}, nil } -func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) { +func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) { if f.containerCommitFunc != nil { return f.containerCommitFunc(ctx, containerID, options) } - return container.CommitResponse{}, nil + return client.ContainerCommitResult{}, nil } -func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error { +func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) { if f.containerPauseFunc != nil { - return f.containerPauseFunc(ctx, containerID) + return f.containerPauseFunc(ctx, containerID, options) } - return nil + return client.ContainerPauseResult{}, nil +} + +func (*fakeClient) Ping(_ context.Context, _ client.PingOptions) (client.PingResult, error) { + return client.PingResult{}, nil } diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go index 4ff00e74b591..4a6174d50d25 100644 --- a/cli/command/container/cmd.go +++ b/cli/command/container/cmd.go @@ -3,43 +3,73 @@ package container import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewContainerCommand returns a cobra command for `container` subcommands -func NewContainerCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newRunCommand) + commands.Register(newExecCommand) + commands.Register(newPsCommand) + commands.Register(newContainerCommand) + commands.RegisterLegacy(newAttachCommand) + commands.RegisterLegacy(newCommitCommand) + commands.RegisterLegacy(newCopyCommand) + commands.RegisterLegacy(newCreateCommand) + commands.RegisterLegacy(newDiffCommand) + commands.RegisterLegacy(newExportCommand) + commands.RegisterLegacy(newKillCommand) + commands.RegisterLegacy(newLogsCommand) + commands.RegisterLegacy(newPauseCommand) + commands.RegisterLegacy(newPortCommand) + commands.RegisterLegacy(newRenameCommand) + commands.RegisterLegacy(newRestartCommand) + commands.RegisterLegacy(newRmCommand) + commands.RegisterLegacy(newStartCommand) + commands.RegisterLegacy(newStatsCommand) + commands.RegisterLegacy(newStopCommand) + commands.RegisterLegacy(newTopCommand) + commands.RegisterLegacy(newUnpauseCommand) + commands.RegisterLegacy(newUpdateCommand) + commands.RegisterLegacy(newWaitCommand) +} + +// newContainerCommand returns a cobra command for `container` subcommands +func newContainerCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "container", Short: "Manage containers", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, } cmd.AddCommand( - NewAttachCommand(dockerCli), - NewCommitCommand(dockerCli), - NewCopyCommand(dockerCli), - NewCreateCommand(dockerCli), - NewDiffCommand(dockerCli), - NewExecCommand(dockerCli), - NewExportCommand(dockerCli), - NewKillCommand(dockerCli), - NewLogsCommand(dockerCli), - NewPauseCommand(dockerCli), - NewPortCommand(dockerCli), - NewRenameCommand(dockerCli), - NewRestartCommand(dockerCli), - newRemoveCommand(dockerCli), - NewRunCommand(dockerCli), - NewStartCommand(dockerCli), - NewStatsCommand(dockerCli), - NewStopCommand(dockerCli), - NewTopCommand(dockerCli), - NewUnpauseCommand(dockerCli), - NewUpdateCommand(dockerCli), - NewWaitCommand(dockerCli), - newListCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), + newAttachCommand(dockerCLI), + newCommitCommand(dockerCLI), + newCopyCommand(dockerCLI), + newCreateCommand(dockerCLI), + newDiffCommand(dockerCLI), + newExecCommand(dockerCLI), + newExportCommand(dockerCLI), + newKillCommand(dockerCLI), + newLogsCommand(dockerCLI), + newPauseCommand(dockerCLI), + newPortCommand(dockerCLI), + newRenameCommand(dockerCLI), + newRestartCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newRunCommand(dockerCLI), + newStartCommand(dockerCLI), + newStatsCommand(dockerCLI), + newStopCommand(dockerCLI), + newTopCommand(dockerCLI), + newUnpauseCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newWaitCommand(dockerCLI), + newListCommand(dockerCLI), + newInspectCommand(dockerCLI), + newPruneCommand(dockerCLI), ) return cmd } diff --git a/cli/command/container/commit.go b/cli/command/container/commit.go index 8c8c798f68bc..9d5c2ea0a99c 100644 --- a/cli/command/container/commit.go +++ b/cli/command/container/commit.go @@ -2,13 +2,14 @@ package container import ( "context" + "errors" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,13 +18,14 @@ type commitOptions struct { reference string pause bool + noPause bool comment string author string changes opts.ListOpts } -// NewCommitCommand creates a new cobra.Command for `docker commit` -func NewCommitCommand(dockerCli command.Cli) *cobra.Command { +// newCommitCommand creates a new cobra.Command for `docker commit` +func newCommitCommand(dockerCLI command.Cli) *cobra.Command { var options commitOptions cmd := &cobra.Command{ @@ -35,18 +37,29 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { options.reference = args[1] } - return runCommit(cmd.Context(), dockerCli, &options) + if cmd.Flag("pause").Changed { + if cmd.Flag("no-pause").Changed { + return errors.New("conflicting options: --no-pause and --pause cannot be used together") + } + options.noPause = !options.pause + } + return runCommit(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker container commit, docker commit", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.SetInterspersed(false) - flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit") + // TODO(thaJeztah): Deprecated: the --pause flag was deprecated in v29 and can be removed in v30. + flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit (deprecated: use --no-pause instead)") + _ = flags.MarkDeprecated("pause", "and enabled by default. Use --no-pause to disable pausing during commit.") + + flags.BoolVar(&options.noPause, "no-pause", false, "Disable pausing container during commit") flags.StringVarP(&options.comment, "message", "m", "", "Commit message") flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith ")`) @@ -57,17 +70,17 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command { } func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error { - response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{ + response, err := dockerCli.Client().ContainerCommit(ctx, options.container, client.ContainerCommitOptions{ Reference: options.reference, Comment: options.comment, Author: options.author, Changes: options.changes.GetSlice(), - Pause: options.pause, + NoPause: options.noPause, }) if err != nil { return err } - fmt.Fprintln(dockerCli.Out(), response.ID) + _, _ = fmt.Fprintln(dockerCli.Out(), response.ID) return nil } diff --git a/cli/command/container/commit_test.go b/cli/command/container/commit_test.go index f1a62571fc7b..fa8164d0291d 100644 --- a/cli/command/container/commit_test.go +++ b/cli/command/container/commit_test.go @@ -7,36 +7,32 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestRunCommit(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerCommitFunc: func( - ctx context.Context, - ctr string, - options container.CommitOptions, - ) (container.CommitResponse, error) { + containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) { assert.Check(t, is.Equal(options.Author, "Author Name ")) assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"})) assert.Check(t, is.Equal(options.Comment, "commit message")) - assert.Check(t, is.Equal(options.Pause, false)) + assert.Check(t, is.Equal(options.NoPause, true)) assert.Check(t, is.Equal(ctr, "container-id")) - return container.CommitResponse{ID: "image-id"}, nil + return client.ContainerCommitResult{ID: "image-id"}, nil }, }) - cmd := NewCommitCommand(cli) + cmd := newCommitCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs( []string{ "--author", "Author Name ", "--change", "EXPOSE 80", "--message", "commit message", - "--pause=false", + "--no-pause", "container-id", }, ) @@ -51,16 +47,12 @@ func TestRunCommitClientError(t *testing.T) { clientError := errors.New("client error") cli := test.NewFakeCli(&fakeClient{ - containerCommitFunc: func( - ctx context.Context, - ctr string, - options container.CommitOptions, - ) (container.CommitResponse, error) { - return container.CommitResponse{}, clientError + containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) { + return client.ContainerCommitResult{}, clientError }, }) - cmd := NewCommitCommand(cli) + cmd := newCommitCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"container-id"}) diff --git a/cli/command/container/completion.go b/cli/command/container/completion.go index 05e53739ee2f..73ae61fa1b20 100644 --- a/cli/command/container/completion.go +++ b/cli/command/container/completion.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package container @@ -8,7 +8,8 @@ import ( "sync" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/moby/sys/capability" "github.com/moby/sys/signal" "github.com/spf13/cobra" @@ -122,15 +123,15 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames) _ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames) _ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns()) - _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) - _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames()) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames()) _ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt) _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI)) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever)) _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) _ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt) @@ -186,11 +187,11 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { // of the build-in log drivers. func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - info, err := dockerCLI.Client().Info(cmd.Context()) + res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{}) if err != nil { return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp } - drivers := info.Plugins.Log + drivers := res.Info.Plugins.Log return drivers, cobra.ShellCompDirectiveNoFileComp } } @@ -279,12 +280,12 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She // completeVolumeDriver contacts the API to get the built-in and installed volume drivers. func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - info, err := dockerCLI.Client().Info(cmd.Context()) + res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{}) if err != nil { // fallback: the built-in drivers return []string{"local"}, cobra.ShellCompDirectiveNoFileComp } - drivers := info.Plugins.Volume + drivers := res.Info.Plugins.Volume return drivers, cobra.ShellCompDirectiveNoFileComp } } diff --git a/cli/command/container/completion_test.go b/cli/command/container/completion_test.go index 0977fdc5192b..21b20211d232 100644 --- a/cli/command/container/completion_test.go +++ b/cli/command/container/completion_test.go @@ -6,7 +6,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/moby/sys/signal" "github.com/spf13/cobra" "gotest.tools/v3/assert" @@ -26,7 +27,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) { func TestCompletePid(t *testing.T) { tests := []struct { - containerListFunc func(container.ListOptions) ([]container.Summary, error) + containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error) toComplete string expectedCompletions []string expectedDirective cobra.ShellCompDirective @@ -42,10 +43,12 @@ func TestCompletePid(t *testing.T) { expectedDirective: cobra.ShellCompDirectiveNoSpace, }, { - containerListFunc: func(container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1"), - *builders.Container("c2"), + containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1"), + *builders.Container("c2"), + }, }, nil }, toComplete: "container:", @@ -59,7 +62,7 @@ func TestCompletePid(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ containerListFunc: tc.containerListFunc, }) - completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete) + completions, directive := completePid(cli)(newRunCommand(cli), nil, tc.toComplete) assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions)) assert.Check(t, is.Equal(directive, tc.expectedDirective)) }) diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index 40b038458703..5121cb88a594 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -3,6 +3,7 @@ package container import ( "bytes" "context" + "errors" "fmt" "io" "os" @@ -15,11 +16,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams" - "github.com/docker/docker/api/types/container" "github.com/docker/go-units" "github.com/moby/go-archive" + "github.com/moby/moby/client" "github.com/morikuni/aec" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -121,8 +121,8 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6 return restore, done } -// NewCopyCommand creates a new `docker cp` command -func NewCopyCommand(dockerCli command.Cli) *cobra.Command { +// newCopyCommand creates a new `docker cp` command +func newCopyCommand(dockerCLI command.Cli) *cobra.Command { var opts copyOptions cmd := &cobra.Command{ @@ -147,13 +147,14 @@ container source to stdout.`, opts.destination = args[1] if !cmd.Flag("quiet").Changed { // User did not specify "quiet" flag; suppress output if no terminal is attached - opts.quiet = !dockerCli.Out().IsTerminal() + opts.quiet = !dockerCLI.Out().IsTerminal() } - return runCopy(cmd.Context(), dockerCli, opts) + return runCopy(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker container cp, docker cp", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -229,11 +230,13 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp // if client requests to follow symbol link, then must decide target file to be copied var rebaseName string if copyConfig.followLink { - srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath) + src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{ + Path: srcPath, + }) // If the destination is a symbolic link, we should follow it. - if err == nil && srcStat.Mode&os.ModeSymlink != 0 { - linkTarget := srcStat.LinkTarget + if err == nil && src.Stat.Mode&os.ModeSymlink != 0 { + linkTarget := src.Stat.LinkTarget if !isAbs(linkTarget) { // Join with the parent directory. srcParent, _ := archive.SplitPathDirEntry(srcPath) @@ -248,11 +251,14 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) defer cancel() - content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath) + cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{ + SourcePath: srcPath, + }) if err != nil { return err } - defer content.Close() + content := cpRes.Content + defer func() { _ = content.Close() }() if dstPath == "-" { _, err = io.Copy(dockerCLI.Out(), content) @@ -262,7 +268,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp srcInfo := archive.CopyInfo{ Path: srcPath, Exists: true, - IsDir: stat.Mode.IsDir(), + IsDir: cpRes.Stat.Mode.IsDir(), RebaseName: rebaseName, } @@ -298,50 +304,50 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp // about both the source and destination. The API is a simple tar // archive/extract API but we can use the stat info header about the // destination to be more informed about exactly what the destination is. -func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) { +func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error { srcPath := copyConfig.sourcePath dstPath := copyConfig.destPath if srcPath != "-" { // Get an absolute source path. - srcPath, err = resolveLocalPath(srcPath) + p, err := resolveLocalPath(srcPath) if err != nil { return err } + srcPath = p } apiClient := dockerCLI.Client() // Prepare destination copy info by stat-ing the container path. dstInfo := archive.CopyInfo{Path: dstPath} - dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath) - - // If the destination is a symbolic link, we should evaluate it. - if err == nil && dstStat.Mode&os.ModeSymlink != 0 { - linkTarget := dstStat.LinkTarget - if !isAbs(linkTarget) { - // Join with the parent directory. - dstParent, _ := archive.SplitPathDirEntry(dstPath) - linkTarget = filepath.Join(dstParent, linkTarget) - } - - dstInfo.Path = linkTarget - dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget) - // FIXME(thaJeztah): unhandled error (should this return?) - } + if dst, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: dstPath}); err == nil { + // If the destination is a symbolic link, we should evaluate it. + if dst.Stat.Mode&os.ModeSymlink != 0 { + linkTarget := dst.Stat.LinkTarget + if !isAbs(linkTarget) { + // Join with the parent directory. + dstParent, _ := archive.SplitPathDirEntry(dstPath) + linkTarget = filepath.Join(dstParent, linkTarget) + } - // Validate the destination path - if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil { - return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath) - } + dstInfo.Path = linkTarget + dst, err = apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: linkTarget}) + } + // Validate the destination path + if err == nil { + if err := command.ValidateOutputPathFileMode(dst.Stat.Mode); err != nil { + return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, copyConfig.container, dstPath, err) + } + dstInfo.Exists, dstInfo.IsDir = true, dst.Stat.Mode.IsDir() + } - // Ignore any error and assume that the parent directory of the destination - // path exists, in which case the copy may still succeed. If there is any - // type of conflict (e.g., non-directory overwriting an existing directory - // or vice versa) the extraction will fail. If the destination simply did - // not exist, but the parent directory does, the extraction will still - // succeed. - if err == nil { - dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() + // Ignore any error and assume that the parent directory of the destination + // path exists, in which case the copy may still succeed. If there is any + // type of conflict (e.g., non-directory overwriting an existing directory + // or vice versa) the extraction will fail. If the destination simply did + // not exist, but the parent directory does, the extraction will still + // succeed. + _ = err // Intentionally ignore stat errors (see above) } var ( @@ -354,7 +360,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo content = os.Stdin resolvedDstPath = dstInfo.Path if !dstInfo.IsDir { - return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath) + return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath) } } else { // Prepare source copy info. @@ -397,23 +403,27 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo } } - options := container.CopyToContainerOptions{ - CopyUIDGID: copyConfig.copyUIDGID, + options := client.CopyToContainerOptions{ + DestinationPath: resolvedDstPath, + Content: content, + CopyUIDGID: copyConfig.copyUIDGID, } if copyConfig.quiet { - return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) + _, err := apiClient.CopyToContainer(ctx, copyConfig.container, options) + return err } ctx, cancel := signal.NotifyContext(ctx, os.Interrupt) restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize) - res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) + // TODO(thaJeztah): error-handling looks odd here; should it be handled differently? + _, err := apiClient.CopyToContainer(ctx, copyConfig.container, options) cancel() <-done restore() - fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path) + _, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path) - return res + return err } // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be diff --git a/cli/command/container/cp_test.go b/cli/command/container/cp_test.go index ac4acbeda7f4..cb724f9d9823 100644 --- a/cli/command/container/cp_test.go +++ b/cli/command/container/cp_test.go @@ -9,9 +9,9 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -52,9 +52,11 @@ func TestRunCopyFromContainerToStdout(t *testing.T) { tarContent := "the tar content" cli := test.NewFakeCli(&fakeClient{ - containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) { + containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) { assert.Check(t, is.Equal("container", ctr)) - return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil + return client.CopyFromContainerResult{ + Content: io.NopCloser(strings.NewReader(tarContent)), + }, nil }, }) err := runCopy(context.TODO(), cli, copyOptions{ @@ -73,10 +75,12 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) { destDir := fs.NewDir(t, "cp-test") cli := test.NewFakeCli(&fakeClient{ - containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) { + containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) { assert.Check(t, is.Equal("container", ctr)) readCloser, err := archive.Tar(srcDir.Path(), compression.None) - return readCloser, container.PathStat{}, err + return client.CopyFromContainerResult{ + Content: readCloser, + }, err }, }) err := runCopy(context.TODO(), cli, copyOptions{ @@ -99,10 +103,12 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing. defer destDir.Remove() cli := test.NewFakeCli(&fakeClient{ - containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) { + containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) { assert.Check(t, is.Equal("container", ctr)) readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{}) - return readCloser, container.PathStat{}, err + return client.CopyFromContainerResult{ + Content: readCloser, + }, err }, }) err := runCopy(context.TODO(), cli, copyOptions{ diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 88d274dfbf3b..36c6cfe6f8d3 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -4,33 +4,27 @@ import ( "archive/tar" "bytes" "context" + "errors" "fmt" "io" - "net/netip" "os" "path" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/streams" - "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" - imagetypes "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/client" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -45,14 +39,13 @@ const ( type createOptions struct { name string platform string - untrusted bool pull string // always, missing, never quiet bool useAPISocket bool } -// NewCreateCommand creates a new cobra.Command for `docker create` -func NewCreateCommand(dockerCli command.Cli) *cobra.Command { +// newCreateCommand creates a new cobra.Command for `docker create` +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { var options createOptions var copts *containerOptions @@ -65,12 +58,13 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { copts.Args = args[1:] } - return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts) + return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), &options, copts) }, Annotations: map[string]string{ "aliases": "docker container create, docker create", }, - ValidArgsFunction: completion.ImageNames(dockerCli, -1), + ValidArgsFunction: completion.ImageNames(dockerCLI, -1), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -80,24 +74,23 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`) flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output") flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth") - flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now. + _ = flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Mark flag as experimental for now. // Add an explicit help that doesn't have a `-h` to prevent the conflict // with hostname flags.Bool("help", false, "Print usage") - command.AddPlatformFlag(flags, &options.platform) - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) - copts = addFlags(flags) + // TODO(thaJeztah): consider adding platform as "image create option" on containerOptions + flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) - addCompletions(cmd, dockerCli) + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") + copts = addFlags(flags) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) + addCompletions(cmd, dockerCLI) return cmd } @@ -110,7 +103,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, } } proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice())) - newEnv := []string{} + newEnv := make([]string, 0, len(proxyConfig)) for k, v := range proxyConfig { if v == nil { newEnv = append(newEnv, k) @@ -119,7 +112,12 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, } } copts.env = *opts.NewListOptsRef(&newEnv, nil) - containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType) + serverInfo, err := dockerCli.Client().Ping(ctx, client.PingOptions{}) + if err != nil { + return err + } + + containerCfg, err := parse(flags, copts, serverInfo.OSType) if err != nil { return cli.StatusError{ Status: withHelp(err, "create").Error(), @@ -141,20 +139,27 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options * return err } - responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{ + var ociPlatforms []ocispec.Platform + if options.platform != "" { + // Already validated. + ociPlatforms = append(ociPlatforms, platforms.MustParse(options.platform)) + } + resp, err := dockerCli.Client().ImagePull(ctx, img, client.ImagePullOptions{ RegistryAuth: encodedAuth, - Platform: options.platform, + Platforms: ociPlatforms, }) if err != nil { return err } - defer responseBody.Close() + defer func() { + _ = resp.Close() + }() out := dockerCli.Err() if options.quiet { out = streams.NewOut(io.Discard) } - return jsonstream.Display(ctx, responseBody, out) + return jsonstream.Display(ctx, resp, out) } type cidFile struct { @@ -167,13 +172,13 @@ func (cid *cidFile) Close() error { if cid.file == nil { return nil } - cid.file.Close() + _ = cid.file.Close() if cid.written { return nil } if err := os.Remove(cid.path); err != nil { - return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path) + return fmt.Errorf("failed to remove the CID file '%s': %w", cid.path, err) } return nil @@ -184,7 +189,7 @@ func (cid *cidFile) Write(id string) error { return nil } if _, err := cid.file.Write([]byte(id)); err != nil { - return errors.Wrap(err, "failed to write the container ID to the file") + return fmt.Errorf("failed to write the container ID to the file: %w", err) } cid.written = true return nil @@ -195,12 +200,12 @@ func newCIDFile(cidPath string) (*cidFile, error) { return &cidFile{}, nil } if _, err := os.Stat(cidPath); err == nil { - return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath) + return nil, errors.New("container ID file found, make sure the other container isn't running or delete " + cidPath) } f, err := os.Create(cidPath) if err != nil { - return nil, errors.Wrap(err, "failed to create the container ID file") + return nil, fmt.Errorf("failed to create the container ID file: %w", err) } return &cidFile{path: cidPath, file: f}, nil @@ -212,16 +217,23 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c hostConfig := containerCfg.HostConfig networkingConfig := containerCfg.NetworkingConfig - var ( - trustedRef reference.Canonical - namedRef reference.Named - ) + var namedRef reference.Named + + // TODO(thaJeztah): add a platform option-type / flag-type. + if options.platform != "" { + _, err = platforms.Parse(options.platform) + if err != nil { + return "", err + } + } containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile) if err != nil { return "", err } - defer containerIDFile.Close() + defer func() { + _ = containerIDFile.Close() + }() ref, err := reference.ParseAnyReference(config.Image) if err != nil { @@ -229,15 +241,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c } if named, ok := ref.(reference.Named); ok { namedRef = reference.TagNameOnly(named) - - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted { - var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef) - if err != nil { - return "", err - } - config.Image = reference.FamiliarString(trustedRef) - } } const dockerConfigPathInContainer = "/run/secrets/docker/config.json" @@ -308,14 +311,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c } var platform *ocispec.Platform - // Engine API version 1.41 first introduced the option to specify platform on - // create. It will produce an error if you try to set a platform on older API - // versions, so check the API version here to maintain backwards - // compatibility for CLI users. - if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") { + if options.platform != "" { p, err := platforms.Parse(options.platform) if err != nil { - return "", errors.Wrap(invalidParameter(err), "error parsing specified platform") + return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err)) } platform = &p } @@ -324,9 +323,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c if err := pullImage(ctx, dockerCli, config.Image, options); err != nil { return err } - if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { - return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef) - } return nil } @@ -338,10 +334,17 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() - response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name) + response, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{ + Name: options.name, + // Image: config.Image, // TODO(thaJeztah): pass image-ref separate + Platform: platform, + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + }) if err != nil { // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior. - if cerrdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing { + if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing { if !options.quiet { // we don't want to write to stdout anything apart from container.ID _, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) @@ -352,7 +355,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c } var retryErr error - response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name) + response, retryErr = dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{ + Name: options.name, + // Image: config.Image, // TODO(thaJeztah): pass image-ref separate + Platform: platform, + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + }) if retryErr != nil { return "", retryErr } @@ -361,10 +371,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c } } - if warn := localhostDNSWarning(*hostConfig); warn != "" { - response.Warnings = append(response.Warnings, warn) - } - containerID = response.ID for _, w := range response.Warnings { _, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w) @@ -385,19 +391,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c return containerID, err } -// check the DNS settings passed via --dns against localhost regexp to warn if -// they are trying to set a DNS to a localhost address. -// -// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode). -func localhostDNSWarning(hostConfig container.HostConfig) string { - for _, dnsIP := range hostConfig.DNS { - if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() { - return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr) - } - } - return "" -} - func validatePullOpt(val string) error { switch val { case PullImageAlways, PullImageMissing, PullImageNever, "": @@ -419,7 +412,7 @@ func validatePullOpt(val string) error { // // The path should be an absolute path in the container, commonly // /root/.docker/config.json. -func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error { +func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error { var configBuf bytes.Buffer if err := config.SaveToWriter(&configBuf); err != nil { return fmt.Errorf("saving creds: %w", err) @@ -428,7 +421,7 @@ func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClie // We don't need to get super fancy with the tar creation. var tarBuf bytes.Buffer tarWriter := tar.NewWriter(&tarBuf) - tarWriter.WriteHeader(&tar.Header{ + _ = tarWriter.WriteHeader(&tar.Header{ Name: configPath, Size: int64(configBuf.Len()), Mode: 0o600, @@ -442,8 +435,11 @@ func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClie return fmt.Errorf("closing tar for config copy failed: %w", err) } - if err := dockerAPI.CopyToContainer(ctx, containerID, "/", - &tarBuf, container.CopyToContainerOptions{}); err != nil { + _, err := apiClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{ + DestinationPath: "/", + Content: &tarBuf, + }) + if err != nil { return fmt.Errorf("copying config.json into container failed: %w", err) } diff --git a/cli/command/container/create_test.go b/cli/command/container/create_test.go index fd94b624c822..597218cf7f13 100644 --- a/cli/command/container/create_test.go +++ b/cli/command/container/create_test.go @@ -13,13 +13,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/system" "github.com/google/go-cmp/cmp" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "github.com/spf13/pflag" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -116,36 +113,31 @@ func TestCreateContainerImagePullPolicy(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) { pullCounter := 0 - client := &fakeClient{ - createContainerFunc: func( - config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, - ) (container.CreateResponse, error) { + apiClient := &fakeClient{ + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { defer func() { tc.ResponseCounter++ }() switch tc.ResponseCounter { case 0: - return container.CreateResponse{}, fakeNotFound{} + return client.ContainerCreateResult{}, fakeNotFound{} default: - return container.CreateResponse{ID: containerID}, nil + return client.ContainerCreateResult{ID: containerID}, nil } }, - imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { + imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) { defer func() { pullCounter++ }() - return io.NopCloser(strings.NewReader("")), nil + return fakeStreamResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil }, - infoFunc: func() (system.Info, error) { - return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{IndexServerAddress: "https://indexserver.example.com"}, + }, nil }, } - fakeCLI := test.NewFakeCli(client) + fakeCLI := test.NewFakeCli(apiClient) id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{ - name: "name", - platform: runtime.GOOS, - untrusted: true, - pull: tc.PullPolicy, + name: "name", + platform: runtime.GOOS, + pull: tc.PullPolicy, }) if tc.ExpectedErrMsg != "" { @@ -206,7 +198,7 @@ func TestCreateContainerValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{})) + cmd := newCreateCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -221,55 +213,6 @@ func TestCreateContainerValidateFlags(t *testing.T) { } } -func TestNewCreateCommandWithContentTrustErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - notaryFunc test.NotaryClientFuncType - }{ - { - name: "offline-notary-server", - notaryFunc: notary.GetOfflineNotaryRepository, - expectedError: "client is offline", - args: []string{"image:tag"}, - }, - { - name: "uninitialized-notary-server", - notaryFunc: notary.GetUninitializedNotaryRepository, - expectedError: "remote trust data does not exist", - args: []string{"image:tag"}, - }, - { - name: "empty-notary-server", - notaryFunc: notary.GetEmptyTargetsNotaryRepository, - expectedError: "No valid trust data for tag", - args: []string{"image:tag"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, - ) (container.CreateResponse, error) { - return container.CreateResponse{}, errors.New("shouldn't try to pull image") - }, - }, test.EnableContentTrust) - fakeCLI.SetNotaryClient(tc.notaryFunc) - cmd := NewCreateCommand(fakeCLI) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} - func TestNewCreateCommandWithWarnings(t *testing.T) { testCases := []struct { name string @@ -291,30 +234,15 @@ func TestNewCreateCommandWithWarnings(t *testing.T) { args: []string{"image:tag"}, warnings: []string{"warning from daemon", "another warning from daemon"}, }, - { - name: "container-create-localhost-dns", - args: []string{"--dns=127.0.0.11", "image:tag"}, - warning: true, - }, - { - name: "container-create-localhost-dns-ipv6", - args: []string{"--dns=::1", "image:tag"}, - warning: true, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, - ) (container.CreateResponse, error) { - return container.CreateResponse{Warnings: tc.warnings}, nil + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + return client.ContainerCreateResult{Warnings: tc.warnings}, nil }, }) - cmd := NewCreateCommand(fakeCLI) + cmd := newCreateCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() @@ -344,15 +272,10 @@ func TestCreateContainerWithProxyConfig(t *testing.T) { sort.Strings(expected) fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, - ) (container.CreateResponse, error) { - sort.Strings(config.Env) - assert.DeepEqual(t, config.Env, expected) - return container.CreateResponse{}, nil + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + sort.Strings(options.Config.Env) + assert.DeepEqual(t, options.Config.Env, expected) + return client.ContainerCreateResult{}, nil }, }) fakeCLI.SetConfigFile(&configfile.ConfigFile{ @@ -366,7 +289,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) { }, }, }) - cmd := NewCreateCommand(fakeCLI) + cmd := newCreateCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"image:tag"}) err := cmd.Execute() diff --git a/cli/command/container/diff.go b/cli/command/container/diff.go index 93791fbd094e..feb0f843e80f 100644 --- a/cli/command/container/diff.go +++ b/cli/command/container/diff.go @@ -7,33 +7,35 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// NewDiffCommand creates a new cobra.Command for `docker diff` -func NewDiffCommand(dockerCli command.Cli) *cobra.Command { +// newDiffCommand creates a new cobra.Command for `docker diff` +func newDiffCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "diff CONTAINER", Short: "Inspect changes to files or directories on a container's filesystem", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDiff(cmd.Context(), dockerCli, args[0]) + return runDiff(cmd.Context(), dockerCLI, args[0]) }, Annotations: map[string]string{ "aliases": "docker container diff, docker diff", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } } func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error { - changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID) + res, err := dockerCLI.Client().ContainerDiff(ctx, containerID, client.ContainerDiffOptions{}) if err != nil { return err } diffCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewDiffFormat("{{.Type}} {{.Path}}"), + Format: newDiffFormat("{{.Type}} {{.Path}}"), } - return DiffFormatWrite(diffCtx, changes) + return diffFormatWrite(diffCtx, res) } diff --git a/cli/command/container/diff_test.go b/cli/command/container/diff_test.go index 685a95d125ba..1e941d974569 100644 --- a/cli/command/container/diff_test.go +++ b/cli/command/container/diff_test.go @@ -8,35 +8,35 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestRunDiff(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerDiffFunc: func( - ctx context.Context, - containerID string, - ) ([]container.FilesystemChange, error) { - return []container.FilesystemChange{ - { - Kind: container.ChangeModify, - Path: "/path/to/file0", - }, - { - Kind: container.ChangeAdd, - Path: "/path/to/file1", - }, - { - Kind: container.ChangeDelete, - Path: "/path/to/file2", + containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) { + return client.ContainerDiffResult{ + Changes: []container.FilesystemChange{ + { + Kind: container.ChangeModify, + Path: "/path/to/file0", + }, + { + Kind: container.ChangeAdd, + Path: "/path/to/file1", + }, + { + Kind: container.ChangeDelete, + Path: "/path/to/file2", + }, }, }, nil }, }) - cmd := NewDiffCommand(cli) + cmd := newDiffCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"container-id"}) @@ -60,15 +60,12 @@ func TestRunDiffClientError(t *testing.T) { clientError := errors.New("client error") cli := test.NewFakeCli(&fakeClient{ - containerDiffFunc: func( - ctx context.Context, - containerID string, - ) ([]container.FilesystemChange, error) { - return nil, clientError + containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) { + return client.ContainerDiffResult{}, clientError }, }) - cmd := NewDiffCommand(cli) + cmd := newDiffCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/errors.go b/cli/command/container/errors.go index 957aa25fa6c5..d0d6f7055329 100644 --- a/cli/command/container/errors.go +++ b/cli/command/container/errors.go @@ -1,9 +1,9 @@ package container -import cerrdefs "github.com/containerd/errdefs" +import "github.com/containerd/errdefs" func invalidParameter(err error) error { - if err == nil || cerrdefs.IsInvalidArgument(err) { + if err == nil || errdefs.IsInvalidArgument(err) { return err } return invalidParameterErr{err} @@ -17,7 +17,7 @@ func (e invalidParameterErr) Unwrap() error { } func notFound(err error) error { - if err == nil || cerrdefs.IsNotFound(err) { + if err == nil || errdefs.IsNotFound(err) { return err } return notFoundErr{err} diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index b491e24a1b8e..02510d94bced 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -2,6 +2,7 @@ package container import ( "context" + "errors" "fmt" "io" @@ -10,9 +11,8 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -39,8 +39,8 @@ func NewExecOptions() ExecOptions { } } -// NewExecCommand creates a new cobra.Command for `docker exec` -func NewExecCommand(dockerCli command.Cli) *cobra.Command { +// newExecCommand creates a new cobra.Command for "docker exec". +func newExecCommand(dockerCLI command.Cli) *cobra.Command { options := NewExecOptions() cmd := &cobra.Command{ @@ -50,15 +50,16 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { containerIDorName := args[0] options.Command = args[1:] - return RunExec(cmd.Context(), dockerCli, containerIDorName, options) + return RunExec(cmd.Context(), dockerCLI, containerIDorName, options) }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State != container.StatePaused }), Annotations: map[string]string{ "category-top": "2", "aliases": "docker container exec, docker exec", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -71,14 +72,14 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "[:]")`) flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command") flags.VarP(&options.Env, "env", "e", "Set environment variables") - flags.SetAnnotation("env", "version", []string{"1.25"}) + _ = flags.SetAnnotation("env", "version", []string{"1.25"}) flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables") - flags.SetAnnotation("env-file", "version", []string{"1.25"}) + _ = flags.SetAnnotation("env-file", "version", []string{"1.25"}) flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container") - flags.SetAnnotation("workdir", "version", []string{"1.35"}) + _ = flags.SetAnnotation("workdir", "version", []string{"1.35"}) - _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) - _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames()) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames()) return cmd } @@ -96,18 +97,18 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin // otherwise if we error out we will leak execIDs on the server (and // there's no easy way to clean those up). But also in order to make "not // exist" errors take precedence we do a dummy inspect first. - if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil { + if _, err := apiClient.ContainerInspect(ctx, containerIDorName, client.ContainerInspectOptions{}); err != nil { return err } if !options.Detach { - if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil { + if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.TTY); err != nil { return err } } fillConsoleSize(execOptions, dockerCLI) - response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions) + response, err := apiClient.ExecCreate(ctx, containerIDorName, *execOptions) if err != nil { return err } @@ -118,23 +119,24 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin } if options.Detach { - return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{ + _, err := apiClient.ExecStart(ctx, execID, client.ExecStartOptions{ Detach: options.Detach, - Tty: execOptions.Tty, - ConsoleSize: execOptions.ConsoleSize, + TTY: execOptions.TTY, + ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width}, }) + return err } return interactiveExec(ctx, dockerCLI, execOptions, execID) } -func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) { - if execOptions.Tty { +func fillConsoleSize(execOptions *client.ExecCreateOptions, dockerCli command.Cli) { + if execOptions.TTY { height, width := dockerCli.Out().GetTtySize() - execOptions.ConsoleSize = &[2]uint{height, width} + execOptions.ConsoleSize = client.ConsoleSize{Height: height, Width: width} } } -func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error { +func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *client.ExecCreateOptions, execID string) error { // Interactive exec requested. var ( out, stderr io.Writer @@ -148,7 +150,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co out = dockerCli.Out() } if execOptions.AttachStderr { - if execOptions.Tty { + if execOptions.TTY { stderr = dockerCli.Out() } else { stderr = dockerCli.Err() @@ -157,9 +159,9 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co fillConsoleSize(execOptions, dockerCli) apiClient := dockerCli.Client() - resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{ - Tty: execOptions.Tty, - ConsoleSize: execOptions.ConsoleSize, + resp, err := apiClient.ExecAttach(ctx, execID, client.ExecAttachOptions{ + TTY: execOptions.TTY, + ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width}, }) if err != nil { return err @@ -176,8 +178,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co inputStream: in, outputStream: out, errorStream: stderr, - resp: resp, - tty: execOptions.Tty, + resp: resp.HijackedResponse, + tty: execOptions.TTY, detachKeys: execOptions.DetachKeys, } @@ -185,7 +187,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co }() }() - if execOptions.Tty && dockerCli.In().IsTerminal() { + if execOptions.TTY && dockerCli.In().IsTerminal() { if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { _, _ = fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) } @@ -199,8 +201,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *co return getExecExitStatus(ctx, apiClient, execID) } -func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error { - resp, err := apiClient.ContainerExecInspect(ctx, execID) +func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, execID string) error { + resp, err := apiClient.ExecInspect(ctx, execID, client.ExecInspectOptions{}) if err != nil { // If we can't connect, then the daemon probably died. if !client.IsErrConnectionFailed(err) { @@ -217,11 +219,11 @@ func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, // parseExec parses the specified args for the specified command and generates // an ExecConfig from it. -func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) { - execOptions := &container.ExecOptions{ +func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client.ExecCreateOptions, error) { + execOptions := &client.ExecCreateOptions{ User: execOpts.User, Privileged: execOpts.Privileged, - Tty: execOpts.TTY, + TTY: execOpts.TTY, Cmd: execOpts.Command, WorkingDir: execOpts.Workdir, } diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go index 9690d091c4d3..0b2d0d043a36 100644 --- a/cli/command/container/exec_test.go +++ b/cli/command/container/exec_test.go @@ -12,7 +12,7 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -38,10 +38,10 @@ TWO=2 testcases := []struct { options ExecOptions configFile configfile.ConfigFile - expected container.ExecOptions + expected client.ExecCreateOptions }{ { - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, AttachStdout: true, AttachStderr: true, @@ -49,7 +49,7 @@ TWO=2 options: withDefaultOpts(ExecOptions{}), }, { - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command1", "command2"}, AttachStdout: true, AttachStderr: true, @@ -64,18 +64,18 @@ TWO=2 TTY: true, User: "uid", }), - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ User: "uid", AttachStdin: true, AttachStdout: true, AttachStderr: true, - Tty: true, + TTY: true, Cmd: []string{"command"}, }, }, { options: withDefaultOpts(ExecOptions{Detach: true}), - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, }, }, @@ -85,15 +85,15 @@ TWO=2 Interactive: true, Detach: true, }), - expected: container.ExecOptions{ - Tty: true, + expected: client.ExecCreateOptions{ + TTY: true, Cmd: []string{"command"}, }, }, { options: withDefaultOpts(ExecOptions{Detach: true}), configFile: configfile.ConfigFile{DetachKeys: "de"}, - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, DetachKeys: "de", }, @@ -104,13 +104,13 @@ TWO=2 DetachKeys: "ab", }), configFile: configfile.ConfigFile{DetachKeys: "de"}, - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, DetachKeys: "ab", }, }, { - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, AttachStdout: true, AttachStderr: true, @@ -118,12 +118,12 @@ TWO=2 }, options: func() ExecOptions { o := withDefaultOpts(ExecOptions{}) - o.EnvFile.Set(tmpFile.Path()) + _ = o.EnvFile.Set(tmpFile.Path()) return o }(), }, { - expected: container.ExecOptions{ + expected: client.ExecCreateOptions{ Cmd: []string{"command"}, AttachStdout: true, AttachStderr: true, @@ -131,8 +131,8 @@ TWO=2 }, options: func() ExecOptions { o := withDefaultOpts(ExecOptions{}) - o.EnvFile.Set(tmpFile.Path()) - o.Env.Set("ONE=override") + _ = o.EnvFile.Set(tmpFile.Path()) + _ = o.Env.Set("ONE=override") return o }(), }, @@ -149,7 +149,7 @@ TWO=2 func TestParseExecNoSuchFile(t *testing.T) { execOpts := withDefaultOpts(ExecOptions{}) - execOpts.EnvFile.Set("no-such-env-file") + assert.Check(t, execOpts.EnvFile.Set("no-such-env-file")) execConfig, err := parseExec(execOpts, &configfile.ConfigFile{}) assert.ErrorContains(t, err, "no-such-env-file") assert.Check(t, os.IsNotExist(err)) @@ -176,8 +176,8 @@ func TestRunExec(t *testing.T) { doc: "inspect error", options: NewExecOptions(), client: &fakeClient{ - inspectFunc: func(string) (container.InspectResponse, error) { - return container.InspectResponse{}, errors.New("failed inspect") + inspectFunc: func(string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{}, errors.New("failed inspect") }, }, expectedError: "failed inspect", @@ -194,7 +194,7 @@ func TestRunExec(t *testing.T) { t.Run(testcase.doc, func(t *testing.T) { fakeCLI := test.NewFakeCli(testcase.client) - err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options) + err := RunExec(context.TODO(), fakeCLI, "the-container", testcase.options) if testcase.expectedError != "" { assert.ErrorContains(t, err, testcase.expectedError) } else if !assert.Check(t, err) { @@ -206,8 +206,8 @@ func TestRunExec(t *testing.T) { } } -func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) { - return container.ExecCreateResponse{ID: "execid"}, nil +func execCreateWithID(_ string, _ client.ExecCreateOptions) (client.ExecCreateResult, error) { + return client.ExecCreateResult{ID: "exec-id"}, nil } func TestGetExecExitStatus(t *testing.T) { @@ -234,13 +234,13 @@ func TestGetExecExitStatus(t *testing.T) { } for _, testcase := range testcases { - client := &fakeClient{ - execInspectFunc: func(id string) (container.ExecInspect, error) { + apiClient := &fakeClient{ + execInspectFunc: func(id string) (client.ExecInspectResult, error) { assert.Check(t, is.Equal(execID, id)) - return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError + return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError }, } - err := getExecExitStatus(context.Background(), client, execID) + err := getExecExitStatus(context.Background(), apiClient, execID) assert.Check(t, is.Equal(testcase.expectedError, err)) } } @@ -250,20 +250,20 @@ func TestNewExecCommandErrors(t *testing.T) { name string args []string expectedError string - containerInspectFunc func(img string) (container.InspectResponse, error) + containerInspectFunc func(img string) (client.ContainerInspectResult, error) }{ { name: "client-error", args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"}, expectedError: "something went wrong", - containerInspectFunc: func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{}, errors.New("something went wrong") + containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{}, errors.New("something went wrong") }, }, } for _, tc := range testCases { fakeCLI := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}) - cmd := NewExecCommand(fakeCLI) + cmd := newExecCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) diff --git a/cli/command/container/export.go b/cli/command/container/export.go index 990c2e66f8c8..0c0f3410b5ed 100644 --- a/cli/command/container/export.go +++ b/cli/command/container/export.go @@ -2,13 +2,15 @@ package container import ( "context" + "errors" + "fmt" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/moby/sys/atomicwriter" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -17,8 +19,8 @@ type exportOptions struct { output string } -// NewExportCommand creates a new `docker export` command -func NewExportCommand(dockerCli command.Cli) *cobra.Command { +// newExportCommand creates a new "docker container export" command. +func newExportCommand(dockerCLI command.Cli) *cobra.Command { var opts exportOptions cmd := &cobra.Command{ @@ -27,12 +29,13 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] - return runExport(cmd.Context(), dockerCli, opts) + return runExport(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker container export, docker export", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -52,13 +55,13 @@ func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) e } else { writer, err := atomicwriter.New(opts.output, 0o600) if err != nil { - return errors.Wrap(err, "failed to export container") + return fmt.Errorf("failed to export container: %w", err) } defer writer.Close() output = writer } - responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container) + responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container, client.ContainerExportOptions{}) if err != nil { return err } diff --git a/cli/command/container/export_test.go b/cli/command/container/export_test.go index 182713ab993a..8fe6be06b9ff 100644 --- a/cli/command/container/export_test.go +++ b/cli/command/container/export_test.go @@ -2,10 +2,10 @@ package container import ( "io" - "strings" "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/fs" ) @@ -15,11 +15,12 @@ func TestContainerExportOutputToFile(t *testing.T) { defer dir.Remove() cli := test.NewFakeCli(&fakeClient{ - containerExportFunc: func(container string) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("bar")), nil + containerExportFunc: func(container string) (client.ContainerExportResult, error) { + // FIXME(thaJeztah): how to mock this? + return mockContainerExportResult("bar"), nil }, }) - cmd := NewExportCommand(cli) + cmd := newExportCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"-o", dir.Join("foo"), "container"}) assert.NilError(t, cmd.Execute()) @@ -33,11 +34,12 @@ func TestContainerExportOutputToFile(t *testing.T) { func TestContainerExportOutputToIrregularFile(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerExportFunc: func(container string) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("foo")), nil + containerExportFunc: func(container string) (client.ContainerExportResult, error) { + // FIXME(thaJeztah): how to mock this? + return mockContainerExportResult("foo"), nil }, }) - cmd := NewExportCommand(cli) + cmd := newExportCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"-o", "/dev/random", "container"}) diff --git a/cli/command/container/formatter_diff.go b/cli/command/container/formatter_diff.go index 822e1eef51b0..35c48d5d1bda 100644 --- a/cli/command/container/formatter_diff.go +++ b/cli/command/container/formatter_diff.go @@ -2,7 +2,8 @@ package container import ( "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" ) const ( @@ -12,25 +13,24 @@ const ( pathHeader = "PATH" ) -// NewDiffFormat returns a format for use with a diff Context -func NewDiffFormat(source string) formatter.Format { +// newDiffFormat returns a format for use with a diff [formatter.Context]. +func newDiffFormat(source string) formatter.Format { if source == formatter.TableFormatKey { return defaultDiffTableFormat } return formatter.Format(source) } -// DiffFormatWrite writes formatted diff using the Context -func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, change := range changes { +// diffFormatWrite writes formatted diff using the [formatter.Context]. +func diffFormatWrite(fmtCtx formatter.Context, changes client.ContainerDiffResult) error { + return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error { + for _, change := range changes.Changes { if err := format(&diffContext{c: change}); err != nil { return err } } return nil - } - return ctx.Write(newDiffContext(), render) + }) } type diffContext struct { @@ -39,12 +39,14 @@ type diffContext struct { } func newDiffContext() *diffContext { - diffCtx := diffContext{} - diffCtx.Header = formatter.SubHeaderContext{ - "Type": changeTypeHeader, - "Path": pathHeader, + return &diffContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Type": changeTypeHeader, + "Path": pathHeader, + }, + }, } - return &diffCtx } func (d *diffContext) MarshalJSON() ([]byte, error) { diff --git a/cli/command/container/formatter_diff_test.go b/cli/command/container/formatter_diff_test.go index e06117e84a2e..0d2849d25cfd 100644 --- a/cli/command/container/formatter_diff_test.go +++ b/cli/command/container/formatter_diff_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -16,7 +17,7 @@ func TestDiffContextFormatWrite(t *testing.T) { expected string }{ { - formatter.Context{Format: NewDiffFormat("table")}, + formatter.Context{Format: newDiffFormat("table")}, `CHANGE TYPE PATH C /var/log/app.log A /usr/app/app.js @@ -24,7 +25,7 @@ D /usr/app/old_app.js `, }, { - formatter.Context{Format: NewDiffFormat("table {{.Path}}")}, + formatter.Context{Format: newDiffFormat("table {{.Path}}")}, `PATH /var/log/app.log /usr/app/app.js @@ -32,7 +33,7 @@ D /usr/app/old_app.js `, }, { - formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, + formatter.Context{Format: newDiffFormat("{{.Type}}: {{.Path}}")}, `C: /var/log/app.log A: /usr/app/app.js D: /usr/app/old_app.js @@ -40,17 +41,19 @@ D: /usr/app/old_app.js }, } - diffs := []container.FilesystemChange{ - {Kind: container.ChangeModify, Path: "/var/log/app.log"}, - {Kind: container.ChangeAdd, Path: "/usr/app/app.js"}, - {Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"}, + diffs := client.ContainerDiffResult{ + Changes: []container.FilesystemChange{ + {Kind: container.ChangeModify, Path: "/var/log/app.log"}, + {Kind: container.ChangeAdd, Path: "/usr/app/app.js"}, + {Kind: container.ChangeDelete, Path: "/usr/app/old_app.js"}, + }, } for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { out := bytes.NewBufferString("") tc.context.Output = out - err := DiffFormatWrite(tc.context, diffs) + err := diffFormatWrite(tc.context, diffs) if err != nil { assert.Error(t, err, tc.expected) } else { diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index 48371eedc890..626c63142d91 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -167,6 +167,7 @@ func (c *statsContext) Container() string { } func (c *statsContext) Name() string { + // TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char. if len(c.s.Name) > 1 { return c.s.Name[1:] } diff --git a/cli/command/container/formatter_stats_test.go b/cli/command/container/formatter_stats_test.go index 91c69a5d7c26..59a7d70429a1 100644 --- a/cli/command/container/formatter_stats_test.go +++ b/cli/command/container/formatter_stats_test.go @@ -5,45 +5,181 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/cli/internal/test" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestContainerStatsContext(t *testing.T) { - containerID := test.RandomID() + const actorID = "c74518277ddc15a6afeaaeb06ee5f7433dcb27188224777c1efa7df1e8766d65" var ctx statsContext - tt := []struct { + tests := []struct { + name string stats StatsEntry osType string expValue string expHeader string call func() string }{ - {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, - {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, - {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage}, - {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, + { + name: "Container id", + stats: StatsEntry{ID: actorID, Container: actorID}, + expValue: actorID, + expHeader: containerHeader, + call: ctx.Container, + }, + { + name: "Container name", + stats: StatsEntry{ID: actorID, Container: "a-long-container-name"}, + expValue: "a-long-container-name", + expHeader: containerHeader, + call: ctx.Container, + }, + { + name: "ID", + stats: StatsEntry{ID: actorID}, + expValue: actorID, + expHeader: formatter.ContainerIDHeader, + call: ctx.ID, + }, + { + name: "Name", + stats: StatsEntry{Name: "/container-name"}, + expValue: "container-name", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "Name empty", + stats: StatsEntry{Name: ""}, + expValue: "--", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "Name prefix only", + stats: StatsEntry{Name: "/"}, + expValue: "--", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "CPUPerc", + stats: StatsEntry{CPUPercentage: 5.5}, + expValue: "5.50%", + expHeader: cpuPercHeader, + call: ctx.CPUPerc, + }, + { + name: "CPUPerc invalid", + stats: StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, + expValue: "--", + expHeader: cpuPercHeader, + call: ctx.CPUPerc, + }, + { + name: "NetIO", + stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, + expValue: "0.31B / 12.3B", + expHeader: netIOHeader, + call: ctx.NetIO, + }, + { + name: "NetIO invalid", + stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, + expValue: "--", + expHeader: netIOHeader, + call: ctx.NetIO, + }, + { + name: "BlockIO", + stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, + expValue: "0.1B / 2.3B", + expHeader: blockIOHeader, + call: ctx.BlockIO, + }, + { + name: "BlockIO invalid", + stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, + expValue: "--", + expHeader: blockIOHeader, + call: ctx.BlockIO, + }, + { + name: "MemPerc", + stats: StatsEntry{MemoryPercentage: 10.2}, + expValue: "10.20%", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemPerc invalid", + stats: StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, + expValue: "--", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemPerc windows", + stats: StatsEntry{MemoryPercentage: 10.2}, + osType: "windows", + expValue: "--", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemUsage", + stats: StatsEntry{Memory: 24, MemoryLimit: 30}, + expValue: "24B / 30B", + expHeader: memUseHeader, + call: ctx.MemUsage, + }, + { + name: "MemUsage invalid", + stats: StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, + expValue: "-- / --", + expHeader: memUseHeader, + call: ctx.MemUsage, + }, + { + name: "MemUsage windows", + stats: StatsEntry{Memory: 24, MemoryLimit: 30}, + osType: "windows", + expValue: "24B", + expHeader: winMemUseHeader, + call: ctx.MemUsage, + }, + { + name: "PIDs", + stats: StatsEntry{PidsCurrent: 10}, + expValue: "10", + expHeader: pidsHeader, + call: ctx.PIDs, + }, + { + name: "PIDs invalid", + stats: StatsEntry{PidsCurrent: 10, IsInvalid: true}, + expValue: "--", + expHeader: pidsHeader, + call: ctx.PIDs, + }, + { + name: "PIDs windows", + stats: StatsEntry{PidsCurrent: 10}, + osType: "windows", + expValue: "--", + expHeader: pidsHeader, + call: ctx.PIDs, + }, } - for _, te := range tt { - ctx = statsContext{s: te.stats, os: te.osType} - if v := te.call(); v != te.expValue { - t.Fatalf("Expected %q, got %q", te.expValue, v) - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx = statsContext{s: tc.stats, os: tc.osType} + if v := tc.call(); v != tc.expValue { + t.Fatalf("Expected %q, got %q", tc.expValue, v) + } + }) } } diff --git a/cli/command/container/hijack.go b/cli/command/container/hijack.go index 8e725cb44432..2a27052040fb 100644 --- a/cli/command/container/hijack.go +++ b/cli/command/container/hijack.go @@ -8,8 +8,8 @@ import ( "sync" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/client" "github.com/moby/term" "github.com/sirupsen/logrus" ) @@ -38,7 +38,7 @@ type hijackedIOStreamer struct { outputStream io.Writer errorStream io.Writer - resp types.HijackedResponse + resp client.HijackedResponse tty bool detachKeys string diff --git a/cli/command/container/inspect.go b/cli/command/container/inspect.go index 6a33549ca8e2..867fc5fd1e80 100644 --- a/cli/command/container/inspect.go +++ b/cli/command/container/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package container @@ -11,6 +11,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -21,7 +22,7 @@ type inspectOptions struct { } // newInspectCommand creates a new cobra.Command for `docker container inspect` -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -30,9 +31,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.refs = args - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -45,6 +47,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { apiClient := dockerCLI.Client() return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) { - return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size) + res, err := apiClient.ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: opts.size}) + if err != nil { + return nil, nil, err + } + return &res.Container, res.Raw, nil }) } diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go index 3d5c59941831..368c35a4de2b 100644 --- a/cli/command/container/kill.go +++ b/cli/command/container/kill.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,8 +18,8 @@ type killOptions struct { containers []string } -// NewKillCommand creates a new cobra.Command for `docker kill` -func NewKillCommand(dockerCli command.Cli) *cobra.Command { +// newKillCommand creates a new cobra.Command for "docker container kill" +func newKillCommand(dockerCLI command.Cli) *cobra.Command { var opts killOptions cmd := &cobra.Command{ @@ -27,12 +28,13 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runKill(cmd.Context(), dockerCli, &opts) + return runKill(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container kill, docker kill", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -46,7 +48,10 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command { func runKill(ctx context.Context, dockerCLI command.Cli, opts *killOptions) error { apiClient := dockerCLI.Client() errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - return apiClient.ContainerKill(ctx, container, opts.signal) + _, err := apiClient.ContainerKill(ctx, container, client.ContainerKillOptions{ + Signal: opts.signal, + }) + return err }) var errs []error diff --git a/cli/command/container/kill_test.go b/cli/command/container/kill_test.go index 7bd1f10b9272..fdaa2eb96f7c 100644 --- a/cli/command/container/kill_test.go +++ b/cli/command/container/kill_test.go @@ -8,23 +8,20 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestRunKill(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerKillFunc: func( - ctx context.Context, - container string, - signal string, - ) error { - assert.Assert(t, is.Equal(signal, "STOP")) - return nil + containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { + assert.Assert(t, is.Equal(options.Signal, "STOP")) + return client.ContainerKillResult{}, nil }, }) - cmd := NewKillCommand(cli) + cmd := newKillCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{ @@ -47,16 +44,12 @@ func TestRunKill(t *testing.T) { func TestRunKillClientError(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerKillFunc: func( - ctx context.Context, - container string, - signal string, - ) error { - return fmt.Errorf("client error for container %s", container) + containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { + return client.ContainerKillResult{}, fmt.Errorf("client error for container %s", container) }, }) - cmd := NewKillCommand(cli) + cmd := newKillCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/list.go b/cli/command/container/list.go index 4523fe51d216..eb63f6722c05 100644 --- a/cli/command/container/list.go +++ b/cli/command/container/list.go @@ -2,17 +2,16 @@ package container import ( "context" + "fmt" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/docker/cli/templates" - "github.com/docker/docker/api/types/container" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -28,8 +27,8 @@ type psOptions struct { filter opts.FilterOpt } -// NewPsCommand creates a new cobra.Command for `docker ps` -func NewPsCommand(dockerCLI command.Cli) *cobra.Command { +// newPsCommand creates a new cobra.Command for "docker container ps" +func newPsCommand(dockerCLI command.Cli) *cobra.Command { options := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -44,7 +43,8 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command { "category-top": "3", "aliases": "docker container ls, docker container list, docker container ps, docker ps", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -62,14 +62,14 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command { } func newListCommand(dockerCLI command.Cli) *cobra.Command { - cmd := *NewPsCommand(dockerCLI) + cmd := *newPsCommand(dockerCLI) cmd.Aliases = []string{"ps", "list"} cmd.Use = "ls [OPTIONS]" return &cmd } -func buildContainerListOptions(options *psOptions) (*container.ListOptions, error) { - listOptions := &container.ListOptions{ +func buildContainerListOptions(options *psOptions) (client.ContainerListOptions, error) { + listOptions := client.ContainerListOptions{ All: options.all, Limit: options.last, Size: options.size, @@ -82,9 +82,9 @@ func buildContainerListOptions(options *psOptions) (*container.ListOptions, erro // always validate template when `--format` is used, for consistency if len(options.format) > 0 { - tmpl, err := templates.NewParse("", options.format) + tmpl, err := templates.Parse(options.format) if err != nil { - return nil, errors.Wrap(err, "failed to parse template") + return client.ContainerListOptions{}, fmt.Errorf("failed to parse template: %w", err) } optionsProcessor := formatter.NewContainerContext() @@ -92,7 +92,7 @@ func buildContainerListOptions(options *psOptions) (*container.ListOptions, erro // This shouldn't error out but swallowing the error makes it harder // to track down if preProcessor issues come up. if err := tmpl.Execute(io.Discard, optionsProcessor); err != nil { - return nil, errors.Wrap(err, "failed to execute template") + return client.ContainerListOptions{}, fmt.Errorf("failed to execute template: %w", err) } // if `size` was not explicitly set to false (with `--size=false`) @@ -127,7 +127,7 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error return err } - containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions) + res, err := dockerCLI.Client().ContainerList(ctx, listOptions) if err != nil { return err } @@ -137,5 +137,5 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error Format: formatter.NewContainerFormat(options.format, options.quiet, listOptions.Size), Trunc: !options.noTrunc, } - return formatter.ContainerWrite(containerCtx, containers) + return formatter.ContainerWrite(containerCtx, res.Items) } diff --git a/cli/command/container/list_test.go b/cli/command/container/list_test.go index fac33332bad1..2f49e9721e68 100644 --- a/cli/command/container/list_test.go +++ b/cli/command/container/list_test.go @@ -9,7 +9,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -25,7 +26,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { expectedAll bool expectedSize bool expectedLimit int - expectedFilters map[string]string + expectedFilters client.Filters }{ { psOpts: &psOptions{ @@ -34,13 +35,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { last: 5, filter: filters, }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"), }, { psOpts: &psOptions{ @@ -49,10 +47,9 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { last: -1, nLatest: true, }, - expectedAll: true, - expectedSize: true, - expectedLimit: 1, - expectedFilters: make(map[string]string), + expectedAll: true, + expectedSize: true, + expectedLimit: 1, }, { psOpts: &psOptions{ @@ -63,13 +60,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { // With .Size, size should be true format: "{{.Size}}", }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"), }, { psOpts: &psOptions{ @@ -80,13 +74,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { // With .Size, size should be true format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}", }, - expectedAll: true, - expectedSize: true, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"), }, { psOpts: &psOptions{ @@ -97,13 +88,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { // Without .Size, size should be false format: "{{.CreatedAt}} {{.Networks}}", }, - expectedAll: true, - expectedSize: false, - expectedLimit: 5, - expectedFilters: map[string]string{ - "foo": "bar", - "baz": "foo", - }, + expectedAll: true, + expectedSize: false, + expectedLimit: 5, + expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"), }, } @@ -114,21 +102,14 @@ func TestContainerListBuildContainerListOptions(t *testing.T) { assert.Check(t, is.Equal(c.expectedAll, options.All)) assert.Check(t, is.Equal(c.expectedSize, options.Size)) assert.Check(t, is.Equal(c.expectedLimit, options.Limit)) - assert.Check(t, is.Equal(len(c.expectedFilters), options.Filters.Len())) - - for k, v := range c.expectedFilters { - f := options.Filters - if !f.ExactMatch(k, v) { - t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k)) - } - } + assert.Check(t, is.DeepEqual(c.expectedFilters, options.Filters)) } } func TestContainerListErrors(t *testing.T) { testCases := []struct { flags map[string]string - containerListFunc func(container.ListOptions) ([]container.Summary, error) + containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error) expectedError string }{ { @@ -144,8 +125,8 @@ func TestContainerListErrors(t *testing.T) { expectedError: `wrong number of args for join`, }, { - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return nil, errors.New("error listing containers") + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{}, errors.New("error listing containers") }, expectedError: "error listing containers", }, @@ -168,13 +149,15 @@ func TestContainerListErrors(t *testing.T) { func TestContainerListWithoutFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1"), - *builders.Container("c2", builders.WithName("foo")), - *builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)), - *builders.Container("c4", builders.WithPort(81, 81, builders.UDP)), - *builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1"), + *builders.Container("c2", builders.WithName("foo")), + *builders.Container("c3", builders.WithPort(80, 80, builders.TCP), builders.WithPort(81, 81, builders.TCP), builders.WithPort(82, 82, builders.TCP)), + *builders.Container("c4", builders.WithPort(81, 81, builders.UDP)), + *builders.Container("c5", builders.WithPort(82, 82, builders.IP("8.8.8.8"), builders.TCP)), + }, }, nil }, }) @@ -188,10 +171,12 @@ func TestContainerListWithoutFormat(t *testing.T) { func TestContainerListNoTrunc(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1"), - *builders.Container("c2", builders.WithName("foo/bar")), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1"), + *builders.Container("c2", builders.WithName("foo/bar")), + }, }, nil }, }) @@ -207,10 +192,12 @@ func TestContainerListNoTrunc(t *testing.T) { // Test for GitHub issue docker/docker#21772 func TestContainerListNamesMultipleTime(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1"), - *builders.Container("c2", builders.WithName("foo/bar")), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1"), + *builders.Container("c2", builders.WithName("foo/bar")), + }, }, nil }, }) @@ -226,10 +213,12 @@ func TestContainerListNamesMultipleTime(t *testing.T) { // Test for GitHub issue docker/docker#30291 func TestContainerListFormatTemplateWithArg(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1", builders.WithLabel("some.label", "value")), - *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1", builders.WithLabel("some.label", "value")), + *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")), + }, }, nil }, }) @@ -279,9 +268,9 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(options container.ListOptions) ([]container.Summary, error) { + containerListFunc: func(options client.ContainerListOptions) (client.ContainerListResult, error) { assert.Check(t, is.Equal(options.Size, tc.sizeExpected)) - return []container.Summary{}, nil + return client.ContainerListResult{}, nil }, }) cmd := newListCommand(cli) @@ -299,10 +288,12 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) { func TestContainerListWithConfigFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)), - *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1", builders.WithLabel("some.label", "value"), builders.WithSize(10700000)), + *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar"), builders.WithSize(3200000)), + }, }, nil }, }) @@ -319,10 +310,12 @@ func TestContainerListWithConfigFormat(t *testing.T) { func TestContainerListWithFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) { - return []container.Summary{ - *builders.Container("c1", builders.WithLabel("some.label", "value")), - *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")), + containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{ + Items: []container.Summary{ + *builders.Container("c1", builders.WithLabel("some.label", "value")), + *builders.Container("c2", builders.WithName("foo/bar"), builders.WithLabel("foo", "bar")), + }, }, nil }, }) diff --git a/cli/command/container/logs.go b/cli/command/container/logs.go index 3d536f721b73..ebdbc1f62516 100644 --- a/cli/command/container/logs.go +++ b/cli/command/container/logs.go @@ -7,8 +7,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -23,8 +23,8 @@ type logsOptions struct { container string } -// NewLogsCommand creates a new cobra.Command for `docker logs` -func NewLogsCommand(dockerCli command.Cli) *cobra.Command { +// newLogsCommand creates a new cobra.Command for "docker container logs" +func newLogsCommand(dockerCLI command.Cli) *cobra.Command { var opts logsOptions cmd := &cobra.Command{ @@ -33,12 +33,13 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] - return runLogs(cmd.Context(), dockerCli, &opts) + return runLogs(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container logs, docker logs", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -53,12 +54,12 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command { } func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error { - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) + c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{}) if err != nil { return err } - responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, container.LogsOptions{ + resp, err := dockerCli.Client().ContainerLogs(ctx, c.Container.ID, client.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, Since: opts.since, @@ -71,12 +72,12 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro if err != nil { return err } - defer responseBody.Close() + defer func() { _ = resp.Close() }() - if c.Config.Tty { - _, err = io.Copy(dockerCli.Out(), responseBody) + if c.Container.Config.Tty { + _, err = io.Copy(dockerCli.Out(), resp) } else { - _, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody) + _, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), resp) } return err } diff --git a/cli/command/container/logs_test.go b/cli/command/container/logs_test.go index 452f097b00ff..06130925455e 100644 --- a/cli/command/container/logs_test.go +++ b/cli/command/container/logs_test.go @@ -2,27 +2,22 @@ package container import ( "context" - "io" - "strings" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) -var logFn = func(expectedOut string) func(string, container.LogsOptions) (io.ReadCloser, error) { - return func(container string, opts container.LogsOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader(expectedOut)), nil - } -} - func TestRunLogs(t *testing.T) { - inspectFn := func(containerID string) (container.InspectResponse, error) { - return container.InspectResponse{ - Config: &container.Config{Tty: true}, - ContainerJSONBase: &container.ContainerJSONBase{State: &container.State{Running: false}}, + inspectFn := func(containerID string) (client.ContainerInspectResult, error) { + return client.ContainerInspectResult{ + Container: container.InspectResponse{ + Config: &container.Config{Tty: true}, + State: &container.State{Running: false}, + }, }, nil } @@ -38,7 +33,13 @@ func TestRunLogs(t *testing.T) { doc: "successful logs", expectedOut: "foo", options: &logsOptions{}, - client: &fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn}, + client: &fakeClient{ + logFunc: func(container string, opts client.ContainerLogsOptions) (client.ContainerLogsResult, error) { + // FIXME(thaJeztah): how to mock this? + return mockContainerLogsResult("foo"), nil + }, + inspectFunc: inspectFn, + }, }, } diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index 647dc5d51008..a0cc1cb8f9d3 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -1,25 +1,30 @@ +// FIXME(vvoland): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + package container import ( "bytes" "encoding/json" + "errors" "fmt" + "net" + "net/netip" "os" "path" "path/filepath" "reflect" - "strconv" + "slices" "strings" "time" - "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/internal/lazyregexp" + "github.com/docker/cli/internal/volumespec" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - networktypes "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" "github.com/spf13/pflag" cdi "tags.cncf.io/container-device-interface/pkg/parser" ) @@ -51,7 +56,7 @@ type containerOptions struct { deviceWriteBps opts.ThrottledeviceOpt links opts.ListOpts aliases opts.ListOpts - linkLocalIPs opts.ListOpts + linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly deviceReadIOps opts.ThrottledeviceOpt deviceWriteIOps opts.ThrottledeviceOpt env opts.ListOpts @@ -63,7 +68,7 @@ type containerOptions struct { sysctls *opts.MapOpts publish opts.ListOpts expose opts.ListOpts - dns opts.ListOpts + dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly dnsSearch opts.ListOpts dnsOptions opts.ListOpts extraHosts opts.ListOpts @@ -93,7 +98,6 @@ type containerOptions struct { memory opts.MemBytes memoryReservation opts.MemBytes memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes user string workingDir string cpuCount int64 @@ -112,8 +116,8 @@ type containerOptions struct { swappiness int64 netMode opts.NetworkOpt macAddress string - ipv4Address string - ipv6Address string + ipv4Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly + ipv6Address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly ipcMode string pidsLimit int64 restartPolicy string @@ -229,8 +233,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.MarkHidden("dns-opt") flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") - flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)") - flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") + flags.IPVar(&copts.ipv4Address, "ip", nil, "IPv4 address (e.g., 172.30.100.104)") + flags.IPVar(&copts.ipv6Address, "ip6", nil, "IPv6 address (e.g., 2001:db8::33)") flags.Var(&copts.links, "link", "Add link to another container") flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)") @@ -293,7 +297,6 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) - flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") flags.VarP(&copts.memory, "memory", "m", "Memory limit") flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") @@ -317,13 +320,18 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(copts.annotations, "annotation", "Add an annotation to the container (passed through to the OCI runtime)") flags.SetAnnotation("annotation", "version", []string{"1.43"}) + // TODO(thaJeztah): remove in next release (v30.0, or v29.x) + var stub opts.MemBytes + flags.Var(&stub, "kernel-memory", "Kernel memory limit (deprecated)") + _ = flags.MarkDeprecated("kernel-memory", "and no longer supported by the kernel") + return copts } type containerConfig struct { Config *container.Config HostConfig *container.HostConfig - NetworkingConfig *networktypes.NetworkingConfig + NetworkingConfig *network.NetworkingConfig } // parse parses the args for the specified command and generates a Config, @@ -340,8 +348,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // Validate the input mac address if copts.macAddress != "" { - if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { - return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) + if _, err := net.ParseMAC(strings.TrimSpace(copts.macAddress)); err != nil { + return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress) } } if copts.stdin { @@ -357,14 +365,14 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con swappiness := copts.swappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } var binds []string volumes := copts.volumes.GetMap() // add any bind targets to the list of container volumes for bind := range copts.volumes.GetMap() { - parsed, err := loader.ParseVolume(bind) + parsed, err := volumespec.Parse(bind) if err != nil { return nil, err } @@ -372,7 +380,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con if parsed.Source != "" { toBind := bind - if parsed.Type == string(mounttypes.TypeBind) { + if parsed.Type == string(mount.TypeBind) { if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok { if !filepath.IsAbs(hostPart) && strings.HasPrefix(hostPart, ".") { if absHostPart, err := filepath.Abs(hostPart); err == nil { @@ -412,45 +420,63 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con entrypoint = []string{""} } - publishOpts := copts.publish.GetSlice() - var ( - ports map[nat.Port]struct{} - portBindings map[nat.Port][]nat.PortBinding - convertedOpts []string - ) - - convertedOpts, err = convertToStandardNotation(publishOpts) + // TODO(thaJeztah): remove uses of go-connections/nat here. + convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice()) if err != nil { return nil, err } - ports, portBindings, err = nat.ParsePortSpecs(convertedOpts) + // short syntax ([ip:]public:private[/proto]) + // + // TODO(thaJeztah): we need an equivalent that handles the "ip-address" part without depending on the nat package. + ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts) if err != nil { return nil, err } + portBindings := network.PortMap{} + for port, bindings := range natPortBindings { + p, err := network.ParsePort(string(port)) + if err != nil { + return nil, err + } + portBindings[p] = []network.PortBinding{} + for _, b := range bindings { + var hostIP netip.Addr + if b.HostIP != "" { + hostIP, err = netip.ParseAddr(b.HostIP) + if err != nil { + return nil, err + } + } + portBindings[p] = append(portBindings[p], network.PortBinding{ + HostIP: hostIP, + HostPort: b.HostPort, + }) + } + } + + // Add published ports as exposed ports. + exposedPorts := network.PortSet{} + for port := range ports { + p, err := network.ParsePort(string(port)) + if err != nil { + return nil, err + } + exposedPorts[p] = struct{}{} + } // Merge in exposed ports to the map of published ports for _, e := range copts.expose.GetSlice() { - if strings.Contains(e, ":") { - return nil, errors.Errorf("invalid port format for --expose: %s", e) - } // support two formats for expose, original format /[] // or /[] - proto, port := nat.SplitProtoPort(e) - // parse the start and end port and create a sequence of ports to expose - // if expose a port, the start and end port are the same - start, end, err := nat.ParsePortRange(port) + pr, err := network.ParsePortRange(e) if err != nil { - return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) + return nil, fmt.Errorf("invalid range format for --expose: %w", err) } - for i := start; i <= end; i++ { - p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) - if err != nil { - return nil, err - } - if _, exists := ports[p]; !exists { - ports[p] = struct{}{} - } + // parse the start and end port and create a sequence of ports to expose + // if expose a port, the start and end port are the same + for p := range pr.All() { + exposedPorts[p] = struct{}{} } } @@ -458,23 +484,19 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // device path (as opposed to during flag parsing), as at the time we are // parsing flags, we haven't yet sent a _ping to the daemon to determine // what operating system it is. - deviceMappings := []container.DeviceMapping{} - var cdiDeviceNames []string - for _, device := range copts.devices.GetSlice() { - var ( - validated string - deviceMapping container.DeviceMapping - err error - ) + devices := copts.devices.GetSlice() + deviceMappings := make([]container.DeviceMapping, 0, len(devices)) + cdiDeviceNames := make([]string, 0, len(devices)) + for _, device := range devices { if cdi.IsQualifiedName(device) { cdiDeviceNames = append(cdiDeviceNames, device) continue } - validated, err = validateDevice(device, serverOS) + validated, err := validateDevice(device, serverOS) if err != nil { return nil, err } - deviceMapping, err = parseDevice(validated, serverOS) + deviceMapping, err := parseDevice(validated, serverOS) if err != nil { return nil, err } @@ -495,22 +517,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con pidMode := container.PidMode(copts.pidMode) if !pidMode.Valid() { - return nil, errors.Errorf("--pid: invalid PID mode") + return nil, errors.New("--pid: invalid PID mode") } utsMode := container.UTSMode(copts.utsMode) if !utsMode.Valid() { - return nil, errors.Errorf("--uts: invalid UTS mode") + return nil, errors.New("--uts: invalid UTS mode") } usernsMode := container.UsernsMode(copts.usernsMode) if !usernsMode.Valid() { - return nil, errors.Errorf("--userns: invalid USER mode") + return nil, errors.New("--userns: invalid USER mode") } cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode) if !cgroupnsMode.Valid() { - return nil, errors.Errorf("--cgroupns: invalid CGROUP mode") + return nil, errors.New("--cgroupns: invalid CGROUP mode") } restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy) @@ -545,7 +567,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con copts.healthStartInterval != 0 if copts.noHealthcheck { if haveHealthSettings { - return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") + return nil, errors.New("--no-healthcheck conflicts with --health-* options") } healthConfig = &container.HealthConfig{Test: []string{"NONE"}} } else if haveHealthSettings { @@ -554,13 +576,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con probe = []string{"CMD-SHELL", copts.healthCmd} } if copts.healthInterval < 0 { - return nil, errors.Errorf("--health-interval cannot be negative") + return nil, errors.New("--health-interval cannot be negative") } if copts.healthTimeout < 0 { - return nil, errors.Errorf("--health-timeout cannot be negative") + return nil, errors.New("--health-timeout cannot be negative") } if copts.healthRetries < 0 { - return nil, errors.Errorf("--health-retries cannot be negative") + return nil, errors.New("--health-retries cannot be negative") } if copts.healthStartPeriod < 0 { return nil, errors.New("--health-start-period cannot be negative") @@ -594,7 +616,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con MemoryReservation: copts.memoryReservation.Value(), MemorySwap: copts.memorySwap.Value(), MemorySwappiness: &copts.swappiness, - KernelMemory: copts.kernelMemory.Value(), OomKillDisable: &copts.oomKillDisable, NanoCPUs: copts.cpus.Value(), CPUCount: copts.cpuCount, @@ -624,7 +645,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con config := &container.Config{ Hostname: copts.hostname, Domainname: copts.domainname, - ExposedPorts: ports, + ExposedPorts: exposedPorts, User: copts.user, Tty: copts.tty, OpenStdin: copts.stdin, @@ -635,7 +656,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con Cmd: runCmd, Image: copts.Image, Volumes: volumes, - MacAddress: copts.macAddress, Entrypoint: entrypoint, WorkingDir: copts.workingDir, Labels: opts.ConvertKVStringsToMap(labels), @@ -660,7 +680,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // but pre created containers can still have those nil values. // See https://github.com/docker/docker/pull/17779 // for a more detailed explanation on why we don't want that. - DNS: copts.dns.GetAllOrEmpty(), + DNS: toNetipAddrSlice(copts.dns.GetAllOrEmpty()), DNSSearch: copts.dnsSearch.GetAllOrEmpty(), DNSOptions: copts.dnsOptions.GetAllOrEmpty(), ExtraHosts: copts.extraHosts.GetSlice(), @@ -693,7 +713,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con } if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { - return nil, errors.Errorf("conflicting options: cannot specify both --restart and --rm") + return nil, errors.New("conflicting options: cannot specify both --restart and --rm") } // only set this value if the user provided the flag, else it should default to nil @@ -706,25 +726,17 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con config.StdinOnce = true } - networkingConfig := &networktypes.NetworkingConfig{ - EndpointsConfig: make(map[string]*networktypes.EndpointSettings), - } - - networkingConfig.EndpointsConfig, err = parseNetworkOpts(copts) + epCfg, err := parseNetworkOpts(copts) if err != nil { return nil, err } - // Put the endpoint-specific MacAddress of the "main" network attachment into the container Config for backward - // compatibility with older daemons. - if nw, ok := networkingConfig.EndpointsConfig[hostConfig.NetworkMode.NetworkName()]; ok { - config.MacAddress = nw.MacAddress //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. - } - return &containerConfig{ - Config: config, - HostConfig: hostConfig, - NetworkingConfig: networkingConfig, + Config: config, + HostConfig: hostConfig, + NetworkingConfig: &network.NetworkingConfig{ + EndpointsConfig: epCfg, + }, }, nil } @@ -735,9 +747,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // this function may return _multiple_ endpoints, which is not currently supported // by the daemon, but may be in future; it's up to the daemon to produce an error // in case that is not supported. -func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.EndpointSettings, error) { +func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSettings, error) { var ( - endpoints = make(map[string]*networktypes.EndpointSettings, len(copts.netMode.Value())) + endpoints = make(map[string]*network.EndpointSettings, len(copts.netMode.Value())) hasUserDefined, hasNonUserDefined bool ) @@ -776,14 +788,14 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin return nil, err } if _, ok := endpoints[n.Target]; ok { - return nil, invalidParameter(errors.Errorf("network %q is specified multiple times", n.Target)) + return nil, invalidParameter(fmt.Errorf("network %q is specified multiple times", n.Target)) } // For backward compatibility: if no custom options are provided for the network, // and only a single network is specified, omit the endpoint-configuration // on the client (the daemon will still create it when creating the container) if i == 0 && len(copts.netMode.Value()) == 1 { - if ep == nil || reflect.DeepEqual(*ep, networktypes.EndpointSettings{}) { + if ep == nil || reflect.DeepEqual(*ep, network.EndpointSettings{}) { continue } } @@ -803,10 +815,10 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption if len(n.Links) > 0 && copts.links.Len() > 0 { return invalidParameter(errors.New("conflicting options: cannot specify both --link and per-network links")) } - if n.IPv4Address != "" && copts.ipv4Address != "" { + if n.IPv4Address.IsValid() && copts.ipv4Address != nil { return invalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address")) } - if n.IPv6Address != "" && copts.ipv6Address != "" { + if n.IPv6Address.IsValid() && copts.ipv6Address != nil { return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address")) } if n.MacAddress != "" && copts.macAddress != "" { @@ -825,23 +837,26 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption n.Links = make([]string, copts.links.Len()) copy(n.Links, copts.links.GetSlice()) } - if copts.ipv4Address != "" { - n.IPv4Address = copts.ipv4Address + if copts.ipv4Address != nil { + if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok { + n.IPv4Address = ipv4 + } } - if copts.ipv6Address != "" { - n.IPv6Address = copts.ipv6Address + if copts.ipv6Address != nil { + if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok { + n.IPv6Address = ipv6 + } } if copts.macAddress != "" { n.MacAddress = copts.macAddress } if copts.linkLocalIPs.Len() > 0 { - n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) - copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice()) + n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice()) } return nil } -func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.EndpointSettings, error) { +func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) { if strings.TrimSpace(ep.Target) == "" { return nil, errors.New("no name set for network") } @@ -854,7 +869,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End } } - epConfig := &networktypes.EndpointSettings{ + epConfig := &network.EndpointSettings{ GwPriority: ep.GwPriority, } epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...) @@ -865,18 +880,19 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End if len(ep.Links) > 0 { epConfig.Links = ep.Links } - if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 { - epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{ + if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 { + epConfig.IPAMConfig = &network.EndpointIPAMConfig{ IPv4Address: ep.IPv4Address, IPv6Address: ep.IPv6Address, LinkLocalIPs: ep.LinkLocalIPs, } } if ep.MacAddress != "" { - if _, err := opts.ValidateMACAddress(ep.MacAddress); err != nil { - return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress) + ma, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress)) + if err != nil { + return nil, fmt.Errorf("%s is not a valid mac address", ep.MacAddress) } - epConfig.MacAddress = ep.MacAddress + epConfig.MacAddress = network.HardwareAddr(ma) } return epConfig, nil } @@ -889,7 +905,7 @@ func convertToStandardNotation(ports []string) ([]string, error) { for _, param := range strings.Split(publish, ",") { k, v, ok := strings.Cut(param, "=") if !ok || k == "" { - return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param) + return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param) } params[k] = v } @@ -904,7 +920,7 @@ func convertToStandardNotation(ports []string) ([]string, error) { func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts) if loggingDriver == "none" && len(loggingOpts) > 0 { - return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver) + return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver) } return loggingOptsMap, nil } @@ -918,7 +934,7 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) { } if (!ok || v == "") && k != "no-new-privileges" { // "no-new-privileges" is the only option that does not require a value. - return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) + return securityOpts, fmt.Errorf("invalid --security-opt: %q", opt) } if k == "seccomp" { switch v { @@ -929,11 +945,11 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) { // content if it's valid JSON. f, err := os.ReadFile(v) if err != nil { - return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err) + return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err) } b := bytes.NewBuffer(nil) if err := json.Compact(b, f); err != nil { - return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", v, err) + return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err) } securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) } @@ -968,7 +984,7 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) { for _, option := range storageOpts { k, v, ok := strings.Cut(option, "=") if !ok { - return nil, errors.Errorf("invalid storage option") + return nil, errors.New("invalid storage option") } m[k] = v } @@ -983,7 +999,7 @@ func parseDevice(device, serverOS string) (container.DeviceMapping, error) { case "windows": return parseWindowsDevice(device) } - return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS) + return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS) } // parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct @@ -1007,7 +1023,7 @@ func parseLinuxDevice(device string) (container.DeviceMapping, error) { case 1: src = arr[0] default: - return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device) + return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device) } if dst == "" { @@ -1037,7 +1053,7 @@ func validateDeviceCgroupRule(val string) (string, error) { return val, nil } - return val, errors.Errorf("invalid device cgroup format '%s'", val) + return val, fmt.Errorf("invalid device cgroup format '%s'", val) } // validDeviceMode checks if the mode for device is valid or not. @@ -1069,7 +1085,7 @@ func validateDevice(val string, serverOS string) (string, error) { // Windows does validation entirely server-side return val, nil } - return "", errors.Errorf("unknown server OS: %s", serverOS) + return "", fmt.Errorf("unknown server OS: %s", serverOS) } // validateLinuxPath is the implementation of validateDevice knowing that the @@ -1084,12 +1100,12 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error) var mode string if strings.Count(val, ":") > 2 { - return val, errors.Errorf("bad format for path: %s", val) + return val, fmt.Errorf("bad format for path: %s", val) } split := strings.SplitN(val, ":", 3) if split[0] == "" { - return val, errors.Errorf("bad format for path: %s", val) + return val, fmt.Errorf("bad format for path: %s", val) } switch len(split) { case 1: @@ -1108,13 +1124,13 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error) containerPath = split[1] mode = split[2] if isValid := validator(split[2]); !isValid { - return val, errors.Errorf("bad mode specified: %s", mode) + return val, fmt.Errorf("bad mode specified: %s", mode) } val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) } if !path.IsAbs(containerPath) { - return val, errors.Errorf("%s is not an absolute path", containerPath) + return val, fmt.Errorf("%s is not an absolute path", containerPath) } return val, nil } @@ -1122,10 +1138,23 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error) // validateAttach validates that the specified string is a valid attach option. func validateAttach(val string) (string, error) { s := strings.ToLower(val) - for _, str := range []string{"stdin", "stdout", "stderr"} { - if s == str { - return s, nil + if slices.Contains([]string{"stdin", "stdout", "stderr"}, s) { + return s, nil + } + return val, errors.New("valid streams are STDIN, STDOUT and STDERR") +} + +func toNetipAddrSlice(ips []string) []netip.Addr { + if len(ips) == 0 { + return nil + } + netIPs := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue } + netIPs = append(netIPs, addr) } - return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") + return netIPs } diff --git a/cli/command/container/opts_test.go b/cli/command/container/opts_test.go index c54f3107b6d5..51a0e72b4943 100644 --- a/cli/command/container/opts_test.go +++ b/cli/command/container/opts_test.go @@ -4,21 +4,31 @@ import ( "errors" "fmt" "io" + "net" + "net/netip" "os" "runtime" "strings" "testing" "time" - "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/moby/moby/api/types/container" + networktypes "github.com/moby/moby/api/types/network" "github.com/spf13/pflag" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/skip" ) +func mustParseMAC(s string) networktypes.HardwareAddr { + mac, err := net.ParseMAC(s) + if err != nil { + panic(err) + } + return networktypes.HardwareAddr(mac) +} + func TestValidateAttach(t *testing.T) { valid := []string{ "stdin", @@ -48,7 +58,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network if err := flags.Parse(args); err != nil { return nil, nil, nil, err } - // TODO: fix tests to accept ContainerConfig + // TODO(dnephin): fix tests to accept ContainerConfig; see https://github.com/moby/moby/pull/31621 containerCfg, err := parse(flags, copts, runtime.GOOS) if err != nil { return nil, nil, nil, err @@ -350,13 +360,9 @@ func TestParseWithMacAddress(t *testing.T) { if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) } - config, hostConfig, nwConfig := mustParse(t, validMacAddress) - if config.MacAddress != "92:d0:c6:0a:29:33" { //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. - t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as container-wide MacAddress, got '%v'", - config.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. - } + _, hostConfig, nwConfig := mustParse(t, validMacAddress) defaultNw := hostConfig.NetworkMode.NetworkName() - if nwConfig.EndpointsConfig[defaultNw].MacAddress != "92:d0:c6:0a:29:33" { + if nwConfig.EndpointsConfig[defaultNw].MacAddress.String() != "92:d0:c6:0a:29:33" { t.Fatalf("Expected the default endpoint to have the MacAddress '92:d0:c6:0a:29:33' set, got '%v'", nwConfig.EndpointsConfig[defaultNw].MacAddress) } } @@ -429,56 +435,55 @@ func TestParseHostnameDomainname(t *testing.T) { } func TestParseWithExpose(t *testing.T) { - invalids := map[string]string{ - ":": "invalid port format for --expose: :", - "8080:9090": "invalid port format for --expose: 8080:9090", - "/tcp": "invalid range format for --expose: /tcp, error: empty string specified for ports", - "/udp": "invalid range format for --expose: /udp, error: empty string specified for ports", - "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, - "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, - } - valids := map[string][]nat.Port{ - "8080/tcp": {"8080/tcp"}, - "8080/udp": {"8080/udp"}, - "8080/ncp": {"8080/ncp"}, - "8080-8080/udp": {"8080/udp"}, - "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, - } - for expose, expectedError := range invalids { - if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { - t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) + t.Run("invalid", func(t *testing.T) { + tests := map[string]string{ + ":": `invalid range format for --expose: invalid start port ':': invalid syntax`, + "8080:9090": `invalid range format for --expose: invalid start port '8080:9090': invalid syntax`, + "/tcp": `invalid range format for --expose: invalid start port '': value is empty`, + "/udp": `invalid range format for --expose: invalid start port '': value is empty`, + "NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`, + "NaN-NaN/tcp": `invalid range format for --expose: invalid start port 'NaN': invalid syntax`, + "8080-NaN/tcp": `invalid range format for --expose: invalid end port 'NaN': invalid syntax`, + "1234567890-8080/tcp": `invalid range format for --expose: invalid start port '1234567890': value out of range`, } - } - for expose, exposedPorts := range valids { - config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) - if err != nil { - t.Fatal(err) + for expose, expectedError := range tests { + t.Run(expose, func(t *testing.T) { + _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) + assert.Error(t, err, expectedError) + }) } - if len(config.ExposedPorts) != len(exposedPorts) { - t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) + }) + t.Run("valid", func(t *testing.T) { + tests := map[string][]networktypes.Port{ + "8080/tcp": {networktypes.MustParsePort("8080/tcp")}, + "8080/udp": {networktypes.MustParsePort("8080/udp")}, + "8080/ncp": {networktypes.MustParsePort("8080/ncp")}, + "8080-8080/udp": {networktypes.MustParsePort("8080/udp")}, + "8080-8082/tcp": {networktypes.MustParsePort("8080/tcp"), networktypes.MustParsePort("8081/tcp"), networktypes.MustParsePort("8082/tcp")}, } - for _, port := range exposedPorts { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) - } + for expose, exposedPorts := range tests { + t.Run(expose, func(t *testing.T) { + config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) + assert.NilError(t, err) + for _, port := range exposedPorts { + _, ok := config.ExposedPorts[port] + assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port]) + } + }) } - } - // Merge with actual published port - config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) - if err != nil { - t.Fatal(err) - } - if len(config.ExposedPorts) != 2 { - t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) - } - ports := []nat.Port{"80/tcp", "81/tcp"} - for _, port := range ports { - if _, ok := config.ExposedPorts[port]; !ok { - t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) + }) + + t.Run("merge with published", func(t *testing.T) { + // Merge with actual published port + config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) + assert.NilError(t, err) + assert.Check(t, is.Len(config.ExposedPorts, 2)) + ports := []networktypes.Port{networktypes.MustParsePort("80/tcp"), networktypes.MustParsePort("81/tcp")} + for _, port := range ports { + _, ok := config.ExposedPorts[port] + assert.Check(t, ok, "missing port %q in exposed ports: %#+v", port, config.ExposedPorts[port]) } - } + }) } func TestParseDevice(t *testing.T) { @@ -576,7 +581,6 @@ func TestParseNetworkConfig(t *testing.T) { name string flags []string expected map[string]*networktypes.EndpointSettings - expectedCfg container.Config expectedHostCfg container.HostConfig expectedErr string }{ @@ -608,9 +612,9 @@ func TestParseNetworkConfig(t *testing.T) { expected: map[string]*networktypes.EndpointSettings{ "net1": { IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", - LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")}, }, Links: []string{"foo:bar", "bar:baz"}, Aliases: []string{"web1", "web2"}, @@ -638,9 +642,9 @@ func TestParseNetworkConfig(t *testing.T) { "net1": { DriverOpts: map[string]string{"field1": "value1"}, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", - LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"}, + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.2.2"), netip.MustParseAddr("fe80::169:254:2:2")}, }, Links: []string{"foo:bar", "bar:baz"}, Aliases: []string{"web1", "web2"}, @@ -649,15 +653,15 @@ func TestParseNetworkConfig(t *testing.T) { "net3": { DriverOpts: map[string]string{"field3": "value3"}, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, Aliases: []string{"web3"}, }, "net4": { - MacAddress: "02:32:1c:23:00:04", + MacAddress: mustParseMAC("02:32:1c:23:00:04"), IPAMConfig: &networktypes.EndpointIPAMConfig{ - LinkLocalIPs: []string{"169.254.169.254"}, + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")}, }, }, }, @@ -673,14 +677,13 @@ func TestParseNetworkConfig(t *testing.T) { "field2": "value2", }, IPAMConfig: &networktypes.EndpointIPAMConfig{ - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, Aliases: []string{"web1", "web2"}, - MacAddress: "02:32:1c:23:00:04", + MacAddress: mustParseMAC("02:32:1c:23:00:04"), }, }, - expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"}, }, { @@ -695,10 +698,9 @@ func TestParseNetworkConfig(t *testing.T) { expected: map[string]*networktypes.EndpointSettings{ "net1": { Aliases: []string{"foobar"}, - MacAddress: "52:0f:f3:dc:50:10", + MacAddress: mustParseMAC("52:0f:f3:dc:50:10"), }, }, - expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"}, expectedHostCfg: container.HostConfig{NetworkMode: "net1"}, }, { @@ -745,7 +747,7 @@ func TestParseNetworkConfig(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - config, hConfig, nwConfig, err := parseRun(tc.flags) + _, hConfig, nwConfig, err := parseRun(tc.flags) if tc.expectedErr != "" { assert.Error(t, err, tc.expectedErr) @@ -753,9 +755,8 @@ func TestParseNetworkConfig(t *testing.T) { } assert.NilError(t, err) - assert.DeepEqual(t, config.MacAddress, tc.expectedCfg.MacAddress) //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. assert.DeepEqual(t, hConfig.NetworkMode, tc.expectedHostCfg.NetworkMode) - assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected) + assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected, cmpopts.EquateComparable(netip.Addr{})) }) } } @@ -1017,7 +1018,7 @@ func TestParseLabelfileVariables(t *testing.T) { func TestParseEntryPoint(t *testing.T) { config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) assert.NilError(t, err) - assert.Check(t, is.DeepEqual([]string(config.Entrypoint), []string{"anything"})) + assert.Check(t, is.DeepEqual(config.Entrypoint, []string{"anything"})) } func TestValidateDevice(t *testing.T) { diff --git a/cli/command/container/pause.go b/cli/command/container/pause.go index 78dc6fe37d8d..ede873bfb1d9 100644 --- a/cli/command/container/pause.go +++ b/cli/command/container/pause.go @@ -8,7 +8,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -16,8 +17,8 @@ type pauseOptions struct { containers []string } -// NewPauseCommand creates a new cobra.Command for `docker pause` -func NewPauseCommand(dockerCli command.Cli) *cobra.Command { +// newPauseCommand creates a new cobra.Command for "docker container pause" +func newPauseCommand(dockerCLI command.Cli) *cobra.Command { var opts pauseOptions return &cobra.Command{ @@ -26,20 +27,24 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runPause(cmd.Context(), dockerCli, &opts) + return runPause(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container pause, docker pause", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State != container.StatePaused }), + DisableFlagsInUseLine: true, } } func runPause(ctx context.Context, dockerCLI command.Cli, opts *pauseOptions) error { apiClient := dockerCLI.Client() - errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerPause) + errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { + _, err := apiClient.ContainerPause(ctx, container, client.ContainerPauseOptions{}) + return err + }) var errs []error for _, ctr := range opts.containers { diff --git a/cli/command/container/pause_test.go b/cli/command/container/pause_test.go index a359797a94cf..41619c177282 100644 --- a/cli/command/container/pause_test.go +++ b/cli/command/container/pause_test.go @@ -8,20 +8,15 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestRunPause(t *testing.T) { - cli := test.NewFakeCli( - &fakeClient{ - containerPauseFunc: func(ctx context.Context, container string) error { - return nil - }, - }, - ) + cli := test.NewFakeCli(&fakeClient{}) - cmd := NewPauseCommand(cli) + cmd := newPauseCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"container-id-1", "container-id-2"}) @@ -41,13 +36,13 @@ func TestRunPause(t *testing.T) { func TestRunPauseClientError(t *testing.T) { cli := test.NewFakeCli( &fakeClient{ - containerPauseFunc: func(ctx context.Context, container string) error { - return fmt.Errorf("client error for container %s", container) + containerPauseFunc: func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) { + return client.ContainerPauseResult{}, fmt.Errorf("client error for container %s", container) }, }, ) - cmd := NewPauseCommand(cli) + cmd := newPauseCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"container-id-1", "container-id-2"}) diff --git a/cli/command/container/port.go b/cli/command/container/port.go index 3ed950f37582..534ddd1d34c0 100644 --- a/cli/command/container/port.go +++ b/cli/command/container/port.go @@ -5,15 +5,14 @@ import ( "fmt" "net" "sort" - "strconv" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/go-connections/nat" "github.com/fvbommel/sortorder" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -23,8 +22,8 @@ type portOptions struct { port string } -// NewPortCommand creates a new cobra.Command for `docker port` -func NewPortCommand(dockerCli command.Cli) *cobra.Command { +// newPortCommand creates a new cobra.Command for "docker container port". +func newPortCommand(dockerCLI command.Cli) *cobra.Command { var opts portOptions cmd := &cobra.Command{ @@ -36,12 +35,13 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { opts.port = args[1] } - return runPort(cmd.Context(), dockerCli, &opts) + return runPort(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container port, docker port", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } return cmd } @@ -53,31 +53,28 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command { // proto is specified. We should consider changing this to "any" protocol // for the given private port. func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) error { - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) + c, err := dockerCli.Client().ContainerInspect(ctx, opts.container, client.ContainerInspectOptions{}) if err != nil { return err } var out []string if opts.port != "" { - port, proto, _ := strings.Cut(opts.port, "/") - if proto == "" { - proto = "tcp" + port, err := network.ParsePort(opts.port) + if err != nil { + return err } - if _, err = strconv.ParseUint(port, 10, 16); err != nil { - return errors.Wrapf(err, "Error: invalid port (%s)", port) - } - frontends, exists := c.NetworkSettings.Ports[nat.Port(port+"/"+proto)] + frontends, exists := c.Container.NetworkSettings.Ports[port] if !exists || len(frontends) == 0 { - return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container) + return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container) } for _, frontend := range frontends { - out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort)) + out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)) } } else { - for from, frontends := range c.NetworkSettings.Ports { + for from, frontends := range c.Container.NetworkSettings.Ports { for _, frontend := range frontends { - out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort))) + out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))) } } } diff --git a/cli/command/container/port_test.go b/cli/command/container/port_test.go index 5ed907dcb2d7..8fbe37aca7d2 100644 --- a/cli/command/container/port_test.go +++ b/cli/command/container/port_test.go @@ -2,11 +2,13 @@ package container import ( "io" + "net/netip" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" - "github.com/docker/go-connections/nat" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -14,59 +16,59 @@ import ( func TestNewPortCommandOutput(t *testing.T) { testCases := []struct { name string - ips []string + ips []netip.Addr port string }{ { name: "container-port-ipv4", - ips: []string{"0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6", - ips: []string{"::"}, + ips: []netip.Addr{netip.MustParseAddr("::")}, port: "80", }, { name: "container-port-ipv6-and-ipv4", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "80", }, { name: "container-port-ipv6-and-ipv4-443-udp", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, port: "443/udp", }, { name: "container-port-all-ports", - ips: []string{"::", "0.0.0.0"}, + ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - inspectFunc: func(string) (container.InspectResponse, error) { + inspectFunc: func(string) (client.ContainerInspectResult, error) { ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}} - ci.NetworkSettings.Ports = nat.PortMap{ - "80/tcp": make([]nat.PortBinding, len(tc.ips)), - "443/tcp": make([]nat.PortBinding, len(tc.ips)), - "443/udp": make([]nat.PortBinding, len(tc.ips)), + ci.NetworkSettings.Ports = network.PortMap{ + network.MustParsePort("80/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/tcp"): make([]network.PortBinding, len(tc.ips)), + network.MustParsePort("443/udp"): make([]network.PortBinding, len(tc.ips)), } for i, ip := range tc.ips { - ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "3456", } - ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{ HostIP: ip, HostPort: "4567", } - ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{ + ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{ HostIP: ip, HostPort: "5678", } } - return ci, nil + return client.ContainerInspectResult{Container: ci}, nil }, }) - cmd := NewPortCommand(cli) + cmd := newPortCommand(cli) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"some_container", tc.port}) err := cmd.Execute() diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index d75338718e36..90aa540e2467 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -2,25 +2,34 @@ package container import ( "context" + "errors" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeContainer, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool filter opts.FilterOpt } -// NewPruneCommand returns a new cobra prune command for containers -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// newPruneCommand returns a new cobra prune command for containers. +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -28,18 +37,19 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove all stopped containers", Args: cli.NoArgs, 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 { return err } 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 }, - Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.25"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -52,7 +62,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { const warning = `WARNING! This will remove all stopped containers. Are you sure you want to continue?` -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, _ error) { pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) if !options.force { @@ -65,28 +75,39 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters) + res, err := dockerCli.Client().ContainerPrune(ctx, client.ContainerPruneOptions{ + Filters: pruneFilters, + }) if err != nil { return 0, "", err } - if len(report.ContainersDeleted) > 0 { - output = "Deleted Containers:\n" - for _, id := range report.ContainersDeleted { - output += id + "\n" + var out strings.Builder + if len(res.Report.ContainersDeleted) > 0 { + out.WriteString("Deleted Containers:\n") + for _, id := range res.Report.ContainersDeleted { + out.WriteString(id + "\n") } - spaceReclaimed = report.SpaceReclaimed + spaceReclaimed = res.Report.SpaceReclaimed } - return spaceReclaimed, output, nil + return spaceReclaimed, out.String(), nil } type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Container Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) +// pruneFn calls the Container Prune API 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. + confirmMsg := "all stopped containers" + return 0, confirmMsg, cancelledErr{errors.New("containers prune has been cancelled")} + } + return runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + filter: options.Filter, + }) } diff --git a/cli/command/container/prune_test.go b/cli/command/container/prune_test.go index 6700235c3ddb..68ac21c3e08f 100644 --- a/cli/command/container/prune_test.go +++ b/cli/command/container/prune_test.go @@ -7,8 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" + "github.com/moby/moby/client" ) func TestContainerPrunePromptTermination(t *testing.T) { @@ -16,11 +15,11 @@ func TestContainerPrunePromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { - return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called") + containerPruneFunc: func(ctx context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) { + return client.ContainerPruneResult{}, errors.New("fakeClient containerPruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/rename.go b/cli/command/container/rename.go index a871e38d1ef9..f875bddeacf7 100644 --- a/cli/command/container/rename.go +++ b/cli/command/container/rename.go @@ -1,54 +1,36 @@ package container import ( - "context" "fmt" - "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -type renameOptions struct { - oldName string - newName string -} - -// NewRenameCommand creates a new cobra.Command for `docker rename` -func NewRenameCommand(dockerCli command.Cli) *cobra.Command { - var opts renameOptions - +// newRenameCommand creates a new cobra.Command for "docker container rename". +func newRenameCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "rename CONTAINER NEW_NAME", Short: "Rename a container", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - opts.oldName = args[0] - opts.newName = args[1] - return runRename(cmd.Context(), dockerCli, &opts) + oldName, newName := args[0], args[1] + _, err := dockerCLI.Client().ContainerRename(cmd.Context(), oldName, client.ContainerRenameOptions{ + NewName: newName, + }) + if err != nil { + return fmt.Errorf("failed to rename container: %w", err) + } + return nil }, Annotations: map[string]string{ "aliases": "docker container rename, docker rename", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } return cmd } - -func runRename(ctx context.Context, dockerCli command.Cli, opts *renameOptions) error { - oldName := strings.TrimSpace(opts.oldName) - newName := strings.TrimSpace(opts.newName) - - if oldName == "" || newName == "" { - return errors.New("Error: Neither old nor new names may be empty") - } - - if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { - fmt.Fprintln(dockerCli.Err(), err) - return errors.Errorf("Error: failed to rename container named %s", oldName) - } - return nil -} diff --git a/cli/command/container/rename_test.go b/cli/command/container/rename_test.go deleted file mode 100644 index 9a2cae4b3cac..000000000000 --- a/cli/command/container/rename_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package container - -import ( - "context" - "errors" - "io" - "testing" - - "github.com/docker/cli/internal/test" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestRunRename(t *testing.T) { - testcases := []struct { - doc, oldName, newName, expectedErr string - }{ - { - doc: "success", - oldName: "oldName", - newName: "newName", - expectedErr: "", - }, - { - doc: "empty old name", - oldName: "", - newName: "newName", - expectedErr: "Error: Neither old nor new names may be empty", - }, - { - doc: "empty new name", - oldName: "oldName", - newName: "", - expectedErr: "Error: Neither old nor new names may be empty", - }, - } - - for _, tc := range testcases { - t.Run(tc.doc, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{ - containerRenameFunc: func(ctx context.Context, oldName, newName string) error { - return nil - }, - }) - - cmd := NewRenameCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SetArgs([]string{tc.oldName, tc.newName}) - - err := cmd.Execute() - - if tc.expectedErr != "" { - assert.ErrorContains(t, err, tc.expectedErr) - } else { - assert.NilError(t, err) - } - }) - } -} - -func TestRunRenameClientError(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{ - containerRenameFunc: func(ctx context.Context, oldName, newName string) error { - return errors.New("client error") - }, - }) - - cmd := NewRenameCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SetArgs([]string{"oldName", "newName"}) - - err := cmd.Execute() - - assert.Check(t, is.Error(err, "Error: failed to rename container named oldName")) -} diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 379b6a12eba5..4aad94351f15 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -20,8 +20,8 @@ type restartOptions struct { containers []string } -// NewRestartCommand creates a new cobra.Command for `docker restart` -func NewRestartCommand(dockerCli command.Cli) *cobra.Command { +// newRestartCommand creates a new cobra.Command for "docker container restart". +func newRestartCommand(dockerCLI command.Cli) *cobra.Command { var opts restartOptions cmd := &cobra.Command{ @@ -34,12 +34,13 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command { } opts.containers = args opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time") - return runRestart(cmd.Context(), dockerCli, &opts) + return runRestart(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container restart, docker restart", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -65,7 +66,7 @@ func runRestart(ctx context.Context, dockerCLI command.Cli, opts *restartOptions var errs []error // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" for _, name := range opts.containers { - err := apiClient.ContainerRestart(ctx, name, container.StopOptions{ + _, err := apiClient.ContainerRestart(ctx, name, client.ContainerRestartOptions{ Signal: opts.signal, Timeout: timeout, }) diff --git a/cli/command/container/restart_test.go b/cli/command/container/restart_test.go index f7986a87514f..569571b7175a 100644 --- a/cli/command/container/restart_test.go +++ b/cli/command/container/restart_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -19,7 +19,7 @@ func TestRestart(t *testing.T) { name string args []string restarted []string - expectedOpts container.StopOptions + expectedOpts client.ContainerRestartOptions expectedErr string }{ { @@ -36,19 +36,19 @@ func TestRestart(t *testing.T) { { name: "with -t", args: []string{"-t", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)}, restarted: []string{"container-1"}, }, { name: "with --timeout", args: []string{"--timeout", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)}, restarted: []string{"container-1"}, }, { name: "with --time", args: []string{"--time", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)}, restarted: []string{"container-1"}, }, { @@ -62,21 +62,21 @@ func TestRestart(t *testing.T) { mutex := new(sync.Mutex) cli := test.NewFakeCli(&fakeClient{ - containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + containerRestartFunc: func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) { assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) if containerID == "nosuchcontainer" { - return notFound(errors.New("Error: no such container: " + containerID)) + return client.ContainerRestartResult{}, notFound(errors.New("Error: no such container: " + containerID)) } // TODO(thaJeztah): consider using parallelOperation for restart, similar to "stop" and "remove" mutex.Lock() restarted = append(restarted, containerID) mutex.Unlock() - return nil + return client.ContainerRestartResult{}, nil }, Version: "1.36", }) - cmd := NewRestartCommand(cli) + cmd := newRestartCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go index 3206bb59924c..8251f2a9dd70 100644 --- a/cli/command/container/rm.go +++ b/cli/command/container/rm.go @@ -6,11 +6,12 @@ import ( "fmt" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -22,8 +23,8 @@ type rmOptions struct { containers []string } -// NewRmCommand creates a new cobra.Command for `docker rm` -func NewRmCommand(dockerCli command.Cli) *cobra.Command { +// newRmCommand creates a new cobra.Command for "docker container rm". +func newRmCommand(dockerCLI command.Cli) *cobra.Command { var opts rmOptions cmd := &cobra.Command{ @@ -32,14 +33,15 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runRm(cmd.Context(), dockerCli, &opts) + return runRm(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container rm, docker container remove, docker rm", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool { return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated }), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -53,7 +55,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command { // top-level "docker rm", it also adds a "remove" alias to support // "docker container remove" in addition to "docker container rm". func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRmCommand(dockerCli) + cmd := *newRmCommand(dockerCli) cmd.Aliases = []string{"rm", "remove"} return &cmd } @@ -65,17 +67,18 @@ func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error { if ctrID == "" { return errors.New("container name cannot be empty") } - return apiClient.ContainerRemove(ctx, ctrID, container.RemoveOptions{ + _, err := apiClient.ContainerRemove(ctx, ctrID, client.ContainerRemoveOptions{ RemoveVolumes: opts.rmVolumes, RemoveLinks: opts.rmLink, Force: opts.force, }) + return err }) var errs []error for _, name := range opts.containers { if err := <-errChan; err != nil { - if opts.force && cerrdefs.IsNotFound(err) { + if opts.force && errdefs.IsNotFound(err) { _, _ = fmt.Fprintln(dockerCLI.Err(), err) continue } diff --git a/cli/command/container/rm_test.go b/cli/command/container/rm_test.go index e9850a9b93b8..86f6c4e0a2e9 100644 --- a/cli/command/container/rm_test.go +++ b/cli/command/container/rm_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -27,7 +27,7 @@ func TestRemoveForce(t *testing.T) { mutex := new(sync.Mutex) cli := test.NewFakeCli(&fakeClient{ - containerRemoveFunc: func(ctx context.Context, container string, options container.RemoveOptions) error { + containerRemoveFunc: func(ctx context.Context, container string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) { // containerRemoveFunc is called in parallel for each container // by the remove command so append must be synchronized. mutex.Lock() @@ -35,13 +35,13 @@ func TestRemoveForce(t *testing.T) { mutex.Unlock() if container == "nosuchcontainer" { - return notFound(errors.New("Error: no such container: " + container)) + return client.ContainerRemoveResult{}, notFound(errors.New("Error: no such container: " + container)) } - return nil + return client.ContainerRemoveResult{}, nil }, Version: "1.36", }) - cmd := NewRmCommand(cli) + cmd := newRmCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/run.go b/cli/command/container/run.go index b86ad9d5b275..8eef49b1771d 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -2,8 +2,10 @@ package container import ( "context" + "errors" "fmt" "io" + "os" "strings" "syscall" @@ -11,10 +13,10 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/moby/sys/signal" "github.com/moby/term" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -27,8 +29,8 @@ type runOptions struct { detachKeys string } -// NewRunCommand create a new `docker run` command -func NewRunCommand(dockerCli command.Cli) *cobra.Command { +// newRunCommand create a new "docker run" command. +func newRunCommand(dockerCLI command.Cli) *cobra.Command { var options runOptions var copts *containerOptions @@ -41,13 +43,14 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { copts.Args = args[1:] } - return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts) + return runRun(cmd.Context(), dockerCLI, cmd.Flags(), &options, copts) }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), Annotations: map[string]string{ "category-top": "1", "aliases": "docker container run, docker run", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -66,19 +69,17 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { // with hostname flags.Bool("help", false, "Print usage") - command.AddPlatformFlag(flags, &options.platform) - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + // TODO(thaJeztah): consider adding platform as "image create option" on containerOptions + flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) + + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") copts = addFlags(flags) _ = cmd.RegisterFlagCompletionFunc("detach-keys", completeDetachKeys) - addCompletions(cmd, dockerCli) - - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) + addCompletions(cmd, dockerCLI) return cmd } @@ -100,7 +101,12 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro } } copts.env = *opts.NewListOptsRef(&newEnv, nil) - containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType) + serverInfo, err := dockerCli.Client().Ping(ctx, client.PingOptions{}) + if err != nil { + return err + } + + containerCfg, err := parse(flags, copts, serverInfo.OSType) // just in case the parse does not exit if err != nil { return cli.StatusError{ @@ -174,7 +180,7 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption // ctx should not be cancellable here, as this would kill the stream to the container // and we want to keep the stream open until the process in the container exits or until // the user forcefully terminates the CLI. - closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{ + closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, client.ContainerAttachOptions{ Stream: true, Stdin: config.AttachStdin, Stdout: config.AttachStdout, @@ -194,7 +200,7 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption statusChan := waitExitOrRemoved(statusCtx, apiClient, containerID, copts.autoRemove) // start the container - if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + if _, err := apiClient.ContainerStart(ctx, containerID, client.ContainerStartOptions{}); err != nil { // If we have hijackedIOStreamer, we should notify // hijackedIOStreamer we are going to exit and wait // to avoid the terminal are not restored. @@ -257,7 +263,7 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption return nil } -func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options container.AttachOptions) (func(), error) { +func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options client.ContainerAttachOptions) (func(), error) { resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options) if errAttach != nil { return nil, errAttach @@ -291,7 +297,7 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str inputStream: in, outputStream: out, errorStream: cerr, - resp: resp, + resp: resp.HijackedResponse, tty: config.Tty, detachKeys: options.DetachKeys, } @@ -302,7 +308,7 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str return errAttach }() }() - return resp.Close, nil + return resp.HijackedResponse.Close, nil } // withHelp decorates the error with a suggestion to use "--help". diff --git a/cli/command/container/run_test.go b/cli/command/container/run_test.go index f60ebde360a1..c60ff0629a79 100644 --- a/cli/command/container/run_test.go +++ b/cli/command/container/run_test.go @@ -2,9 +2,7 @@ package container import ( "context" - "encoding/json" "errors" - "fmt" "io" "net" "syscall" @@ -15,13 +13,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/jsonmessage" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/moby/moby/api/types" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/progress" + "github.com/moby/moby/client/pkg/streamformatter" "github.com/spf13/pflag" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -40,7 +36,7 @@ func TestRunValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewRunCommand(test.NewFakeCli(&fakeClient{})) + cmd := newRunCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -57,14 +53,12 @@ func TestRunValidateFlags(t *testing.T) { func TestRunLabel(t *testing.T) { fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) { - return container.CreateResponse{ - ID: "id", - }, nil + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + return client.ContainerCreateResult{ID: "id"}, nil }, - Version: "1.36", + Version: client.MaxAPIVersion, }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"}) assert.NilError(t, cmd.Execute()) } @@ -80,38 +74,41 @@ func TestRunAttach(t *testing.T) { var conn net.Conn attachCh := make(chan struct{}) fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) { - return container.CreateResponse{ - ID: "id", - }, nil + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + return client.ContainerCreateResult{ID: "id"}, nil }, - containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { - server, client := net.Pipe() + containerAttachFunc: func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) { + server, clientConn := net.Pipe() conn = server t.Cleanup(func() { _ = server.Close() }) attachCh <- struct{}{} - return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil + return client.ContainerAttachResult{ + HijackedResponse: client.NewHijackedResponse(clientConn, types.MediaTypeRawStream), + }, nil }, - waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) { + waitFunc: func(_ string) client.ContainerWaitResult { responseChan := make(chan container.WaitResponse, 1) errChan := make(chan error) responseChan <- container.WaitResponse{ StatusCode: 33, } - return responseChan, errChan + return client.ContainerWaitResult{ + Result: responseChan, + Error: errChan, + } }, // use new (non-legacy) wait API - // see: 38591f20d07795aaef45d400df89ca12f29c603b - Version: "1.30", + // see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b + Version: client.MaxAPIVersion, }, func(fc *test.FakeCli) { fc.SetOut(streams.NewOut(tty)) fc.SetIn(streams.NewIn(tty)) }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"-it", "busybox"}) cmd.SilenceUsage = true cmdErrC := make(chan error, 1) @@ -151,44 +148,47 @@ func TestRunAttachTermination(t *testing.T) { killCh := make(chan struct{}) attachCh := make(chan struct{}) fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) { - return container.CreateResponse{ - ID: "id", - }, nil + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + return client.ContainerCreateResult{ID: "id"}, nil }, - containerKillFunc: func(ctx context.Context, containerID, sig string) error { - if sig == "TERM" { + containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { + if options.Signal == "TERM" { close(killCh) } - return nil + return client.ContainerKillResult{}, nil }, - containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { - server, client := net.Pipe() + containerAttachFunc: func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) { + server, clientConn := net.Pipe() conn = server t.Cleanup(func() { _ = server.Close() }) attachCh <- struct{}{} - return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil + return client.ContainerAttachResult{ + HijackedResponse: client.NewHijackedResponse(clientConn, types.MediaTypeRawStream), + }, nil }, - waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) { + waitFunc: func(_ string) client.ContainerWaitResult { responseChan := make(chan container.WaitResponse, 1) errChan := make(chan error) <-killCh responseChan <- container.WaitResponse{ StatusCode: 130, } - return responseChan, errChan + return client.ContainerWaitResult{ + Result: responseChan, + Error: errChan, + } }, // use new (non-legacy) wait API - // see: 38591f20d07795aaef45d400df89ca12f29c603b - Version: "1.30", + // see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b + Version: client.MaxAPIVersion, }, func(fc *test.FakeCli) { fc.SetOut(streams.NewOut(tty)) fc.SetIn(streams.NewIn(tty)) }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"-it", "busybox"}) cmd.SilenceUsage = true cmdErrC := make(chan error, 1) @@ -228,49 +228,43 @@ func TestRunPullTermination(t *testing.T) { attachCh := make(chan struct{}) fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, containerName string, - ) (container.CreateResponse, error) { - return container.CreateResponse{}, errors.New("shouldn't try to create a container") + createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { + return client.ContainerCreateResult{}, errors.New("shouldn't try to create a container") }, - containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { - return types.HijackedResponse{}, errors.New("shouldn't try to attach to a container") + containerAttachFunc: func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) { + return client.ContainerAttachResult{}, errors.New("shouldn't try to attach to a container") }, - imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { - server, client := net.Pipe() + imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) { + server, respReader := net.Pipe() t.Cleanup(func() { _ = server.Close() }) go func() { - enc := json.NewEncoder(server) + id := test.RandomID()[:12] // short-ID + progressOutput := streamformatter.NewJSONProgressOutput(server, true) for i := 0; i < 100; i++ { select { case <-ctx.Done(): assert.NilError(t, server.Close(), "failed to close imageCreateFunc server") return default: - assert.NilError(t, enc.Encode(jsonmessage.JSONMessage{ - Status: "Downloading", - ID: fmt.Sprintf("id-%d", i), - TimeNano: time.Now().UnixNano(), - Time: time.Now().Unix(), - Progress: &jsonmessage.JSONProgress{ - Current: int64(i), - Total: 100, - Start: 0, - }, + assert.NilError(t, progressOutput.WriteProgress(progress.Progress{ + ID: id, + Message: "Downloading", + Current: int64(i), + Total: 100, })) time.Sleep(100 * time.Millisecond) } } }() attachCh <- struct{}{} - return client, nil + return fakeStreamResult{ReadCloser: respReader}, nil }, - Version: "1.30", + Version: client.MaxAPIVersion, }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"--pull", "always", "foobar:latest"}) @@ -300,58 +294,6 @@ func TestRunPullTermination(t *testing.T) { } } -func TestRunCommandWithContentTrustErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - notaryFunc test.NotaryClientFuncType - }{ - { - name: "offline-notary-server", - notaryFunc: notary.GetOfflineNotaryRepository, - expectedError: "client is offline", - args: []string{"image:tag"}, - }, - { - name: "uninitialized-notary-server", - notaryFunc: notary.GetUninitializedNotaryRepository, - expectedError: "remote trust data does not exist", - args: []string{"image:tag"}, - }, - { - name: "empty-notary-server", - notaryFunc: notary.GetEmptyTargetsNotaryRepository, - expectedError: "No valid trust data for tag", - args: []string{"image:tag"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fakeCLI := test.NewFakeCli(&fakeClient{ - createContainerFunc: func(config *container.Config, - hostConfig *container.HostConfig, - networkingConfig *network.NetworkingConfig, - platform *ocispec.Platform, - containerName string, - ) (container.CreateResponse, error) { - return container.CreateResponse{}, errors.New("shouldn't try to pull image") - }, - }, test.EnableContentTrust) - fakeCLI.SetNotaryClient(tc.notaryFunc) - cmd := NewRunCommand(fakeCLI) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - err := cmd.Execute() - statusErr := cli.StatusError{} - assert.Check(t, errors.As(err, &statusErr)) - assert.Check(t, is.Equal(statusErr.StatusCode, 125)) - assert.Check(t, is.ErrorContains(err, tc.expectedError)) - }) - } -} - func TestRunContainerImagePullPolicyInvalid(t *testing.T) { cases := []struct { PullPolicy string diff --git a/cli/command/container/signals.go b/cli/command/container/signals.go index 49e0abac04de..3a98c1962364 100644 --- a/cli/command/container/signals.go +++ b/cli/command/container/signals.go @@ -5,7 +5,7 @@ import ( "os" gosignal "os/signal" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/moby/sys/signal" "github.com/sirupsen/logrus" ) @@ -48,7 +48,10 @@ func ForwardAllSignals(ctx context.Context, apiClient client.ContainerAPIClient, continue } - if err := apiClient.ContainerKill(ctx, cid, sig); err != nil { + _, err := apiClient.ContainerKill(ctx, cid, client.ContainerKillOptions{ + Signal: sig, + }) + if err != nil { logrus.Debugf("Error sending signal: %s", err) } } diff --git a/cli/command/container/signals_test.go b/cli/command/container/signals_test.go index f6bb114122ed..f5e24c47dd0b 100644 --- a/cli/command/container/signals_test.go +++ b/cli/command/container/signals_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/moby/moby/client" "github.com/moby/sys/signal" ) @@ -14,9 +15,9 @@ func TestForwardSignals(t *testing.T) { defer cancel() called := make(chan struct{}) - apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error { + apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { close(called) - return nil + return client.ContainerKillResult{}, nil }} sigc := make(chan os.Signal) diff --git a/cli/command/container/signals_unix_test.go b/cli/command/container/signals_unix_test.go index f0d18689524c..5ca8cb51dc26 100644 --- a/cli/command/container/signals_unix_test.go +++ b/cli/command/container/signals_unix_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/moby/moby/client" "golang.org/x/sys/unix" "gotest.tools/v3/assert" ) @@ -22,9 +23,9 @@ func TestIgnoredSignals(t *testing.T) { defer cancel() var called bool - apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error { + apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) { called = true - return nil + return client.ContainerKillResult{}, nil }} sigc := make(chan os.Signal) diff --git a/cli/command/container/start.go b/cli/command/container/start.go index a71bdc8fde65..0973c51f8011 100644 --- a/cli/command/container/start.go +++ b/cli/command/container/start.go @@ -2,6 +2,7 @@ package container import ( "context" + "errors" "fmt" "io" "strings" @@ -9,10 +10,10 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/moby/sys/signal" "github.com/moby/term" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -27,8 +28,8 @@ type StartOptions struct { Containers []string } -// NewStartCommand creates a new cobra.Command for `docker start` -func NewStartCommand(dockerCli command.Cli) *cobra.Command { +// newStartCommand creates a new cobra.Command for "docker container start". +func newStartCommand(dockerCLI command.Cli) *cobra.Command { var opts StartOptions cmd := &cobra.Command{ @@ -37,14 +38,15 @@ func NewStartCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Containers = args - return RunStart(cmd.Context(), dockerCli, &opts) + return RunStart(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container start, docker start", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool { return ctr.State == container.StateExited || ctr.State == container.StateCreated }), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -78,16 +80,16 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er // 2. Attach to the container. ctr := opts.Containers[0] - c, err := dockerCli.Client().ContainerInspect(ctx, ctr) + c, err := dockerCli.Client().ContainerInspect(ctx, ctr, client.ContainerInspectOptions{}) if err != nil { return err } // We always use c.ID instead of container to maintain consistency during `docker start` - if !c.Config.Tty { + if !c.Container.Config.Tty { sigc := notifyAllSignals() bgCtx := context.WithoutCancel(ctx) - go ForwardAllSignals(bgCtx, dockerCli.Client(), c.ID, sigc) + go ForwardAllSignals(bgCtx, dockerCli.Client(), c.Container.ID, sigc) defer signal.StopCatch(sigc) } @@ -96,9 +98,9 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er detachKeys = opts.DetachKeys } - options := container.AttachOptions{ + options := client.ContainerAttachOptions{ Stream: true, - Stdin: opts.OpenStdin && c.Config.OpenStdin, + Stdin: opts.OpenStdin && c.Container.Config.OpenStdin, Stdout: true, Stderr: true, DetachKeys: detachKeys, @@ -110,11 +112,11 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er in = dockerCli.In() } - resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options) + resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.Container.ID, options) if errAttach != nil { return errAttach } - defer resp.Close() + defer resp.HijackedResponse.Close() cErr := make(chan error, 1) @@ -125,8 +127,8 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er inputStream: in, outputStream: dockerCli.Out(), errorStream: dockerCli.Err(), - resp: resp, - tty: c.Config.Tty, + resp: resp.HijackedResponse, + tty: c.Container.Config.Tty, detachKeys: options.DetachKeys, } @@ -140,17 +142,17 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er // 3. We should open a channel for receiving status code of the container // no matter it's detached, removed on daemon side(--rm) or exit normally. - statusChan := waitExitOrRemoved(ctx, dockerCli.Client(), c.ID, c.HostConfig.AutoRemove) + statusChan := waitExitOrRemoved(ctx, dockerCli.Client(), c.Container.ID, c.Container.HostConfig.AutoRemove) // 4. Start the container. - err = dockerCli.Client().ContainerStart(ctx, c.ID, container.StartOptions{ + _, err = dockerCli.Client().ContainerStart(ctx, c.Container.ID, client.ContainerStartOptions{ CheckpointID: opts.Checkpoint, CheckpointDir: opts.CheckpointDir, }) if err != nil { cancelFun() <-cErr - if c.HostConfig.AutoRemove { + if c.Container.HostConfig.AutoRemove { // wait container to be removed <-statusChan } @@ -158,13 +160,14 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er } // 5. Wait for attachment to break. - if c.Config.Tty && dockerCli.Out().IsTerminal() { - if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil { - fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) + if c.Container.Config.Tty && dockerCli.Out().IsTerminal() { + if err := MonitorTtySize(ctx, dockerCli, c.Container.ID, false); err != nil { + _, _ = fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) } } if attachErr := <-cErr; attachErr != nil { - if _, ok := attachErr.(term.EscapeError); ok { + var escapeError term.EscapeError + if errors.As(attachErr, &escapeError) { // The user entered the detach escape sequence. return nil } @@ -180,10 +183,11 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er return errors.New("you cannot restore multiple containers at once") } ctr := opts.Containers[0] - return dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{ + _, err := dockerCli.Client().ContainerStart(ctx, ctr, client.ContainerStartOptions{ CheckpointID: opts.Checkpoint, CheckpointDir: opts.CheckpointDir, }) + return err default: // We're not going to attach to anything. // Start as many containers as we want. @@ -194,16 +198,16 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er func startContainersWithoutAttachments(ctx context.Context, dockerCli command.Cli, containers []string) error { var failedContainers []string for _, ctr := range containers { - if err := dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{}); err != nil { - fmt.Fprintln(dockerCli.Err(), err) + if _, err := dockerCli.Client().ContainerStart(ctx, ctr, client.ContainerStartOptions{}); err != nil { + _, _ = fmt.Fprintln(dockerCli.Err(), err) failedContainers = append(failedContainers, ctr) continue } - fmt.Fprintln(dockerCli.Out(), ctr) + _, _ = fmt.Fprintln(dockerCli.Out(), ctr) } if len(failedContainers) > 0 { - return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) + return fmt.Errorf("failed to start containers: %s", strings.Join(failedContainers, ", ")) } return nil } diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index 5538e61dce20..ae8cfb465fdd 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -10,15 +10,15 @@ import ( "sync" "time" + "github.com/containerd/errdefs" + "github.com/containerd/log" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/sirupsen/logrus" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -60,11 +60,11 @@ type StatsOptions struct { // filter options may be added in future (within the constraints described // above), but may require daemon-side validation as the list of accepted // filters can differ between daemon- and API versions. - Filters *filters.Args + Filters client.Filters } -// NewStatsCommand creates a new [cobra.Command] for "docker stats". -func NewStatsCommand(dockerCLI command.Cli) *cobra.Command { +// newStatsCommand creates a new [cobra.Command] for "docker container stats". +func newStatsCommand(dockerCLI command.Cli) *cobra.Command { options := StatsOptions{} cmd := &cobra.Command{ @@ -78,7 +78,8 @@ func NewStatsCommand(dockerCLI command.Cli) *cobra.Command { Annotations: map[string]string{ "aliases": "docker container stats, docker stats", }, - ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -107,10 +108,17 @@ var acceptedStatsFilters = map[string]bool{ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error { apiClient := dockerCLI.Client() + // Get the daemonOSType to handle platform-specific stats fields. + // This value is used as a fallback for docker < v29, which did not + // include the OSType field per stats. + daemonOSType = dockerCLI.ServerInfo().OSType + // waitFirst is a WaitGroup to wait first stat data's reach for each container waitFirst := &sync.WaitGroup{} - // closeChan is a non-buffered channel used to collect errors from goroutines. - closeChan := make(chan error) + // closeChan is used to collect errors from goroutines. It uses a small buffer + // to avoid blocking sends when sends occur after closeChan is set to nil or + // after the reader has exited, preventing deadlocks. + closeChan := make(chan error, 4) cStats := stats{} showAll := len(options.Containers) == 0 @@ -122,36 +130,48 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) started := make(chan struct{}) if options.Filters == nil { - f := filters.NewArgs() - options.Filters = &f + options.Filters = make(client.Filters) } - if err := options.Filters.Validate(acceptedStatsFilters); err != nil { - return err + // FIXME(thaJeztah): any way we can (and should?) validate allowed filters? + for filter := range options.Filters { + if _, ok := acceptedStatsFilters[filter]; !ok { + return errdefs.ErrInvalidArgument.WithMessage("invalid filter '" + filter + "'") + } } eh := newEventHandler() if options.All { eh.setHandler(events.ActionCreate, func(e events.Message) { - s := NewStats(e.Actor.ID[:12]) - if cStats.add(s) { + if s := NewStats(e.Actor.ID); cStats.add(s) { waitFirst.Add(1) + log.G(ctx).WithFields(log.Fields{ + "event": e.Action, + "container": e.Actor.ID, + }).Debug("collecting stats for container") go collect(ctx, s, apiClient, !options.NoStream, waitFirst) } }) } eh.setHandler(events.ActionStart, func(e events.Message) { - s := NewStats(e.Actor.ID[:12]) - if cStats.add(s) { + if s := NewStats(e.Actor.ID); cStats.add(s) { waitFirst.Add(1) + log.G(ctx).WithFields(log.Fields{ + "event": e.Action, + "container": e.Actor.ID, + }).Debug("collecting stats for container") go collect(ctx, s, apiClient, !options.NoStream, waitFirst) } }) if !options.All { eh.setHandler(events.ActionDie, func(e events.Message) { - cStats.remove(e.Actor.ID[:12]) + log.G(ctx).WithFields(log.Fields{ + "event": e.Action, + "container": e.Actor.ID, + }).Debug("stop collecting stats for container") + cStats.remove(e.Actor.ID) }) } @@ -162,9 +182,8 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) // the original set of filters. Custom filters are used both // to list containers and to filter events, but the "type" filter // is not valid for filtering containers. - f := options.Filters.Clone() - f.Add("type", string(events.ContainerEventType)) - eventChan, errChan := apiClient.Events(ctx, events.ListOptions{ + f := options.Filters.Clone().Add("type", string(events.ContainerEventType)) + res := apiClient.Events(ctx, client.EventsListOptions{ Filters: f, }) @@ -177,10 +196,17 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) select { case <-stopped: return - case event := <-eventChan: + case <-ctx.Done(): + return + case event := <-res.Messages: c <- event - case err := <-errChan: - closeChan <- err + case err := <-res.Err: + // Prevent blocking if closeChan is full or unread + select { + case closeChan <- err: + default: + // drop if not read; avoids deadlock + } return } } @@ -196,17 +222,19 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) // Fetch the initial list of containers and collect stats for them. // After the initial list was collected, we start listening for events // to refresh the list of containers. - cs, err := apiClient.ContainerList(ctx, container.ListOptions{ + cs, err := apiClient.ContainerList(ctx, client.ContainerListOptions{ All: options.All, - Filters: *options.Filters, + Filters: options.Filters, }) if err != nil { return err } - for _, ctr := range cs { - s := NewStats(ctr.ID[:12]) - if cStats.add(s) { + for _, ctr := range cs.Items { + if s := NewStats(ctr.ID); cStats.add(s) { waitFirst.Add(1) + log.G(ctx).WithFields(log.Fields{ + "container": ctr.ID, + }).Debug("collecting stats for container") go collect(ctx, s, apiClient, !options.NoStream, waitFirst) } } @@ -218,22 +246,25 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) // only a single code-path is needed, and custom filters can be combined // with a list of container names/IDs. - if options.Filters != nil && options.Filters.Len() > 0 { + if len(options.Filters) > 0 { return errors.New("filtering is not supported when specifying a list of containers") } // Create the list of containers, and start collecting stats for all // containers passed. for _, ctr := range options.Containers { - s := NewStats(ctr) - if cStats.add(s) { + if s := NewStats(ctr); cStats.add(s) { waitFirst.Add(1) + log.G(ctx).WithFields(log.Fields{ + "container": ctr, + }).Debug("collecting stats for container") go collect(ctx, s, apiClient, !options.NoStream, waitFirst) } } - // We don't expect any asynchronous errors: closeChan can be closed. + // We don't expect any asynchronous errors: closeChan can be closed and disabled. close(closeChan) + closeChan = nil // make sure each container get at least one valid stat data waitFirst.Wait() @@ -252,19 +283,13 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) } format := options.Format - if len(format) == 0 { + if format == "" { if len(dockerCLI.ConfigFile().StatsFormat) > 0 { format = dockerCLI.ConfigFile().StatsFormat } else { format = formatter.TableFormatKey } } - if daemonOSType == "" { - // Get the daemonOSType if not set already. The daemonOSType variable - // should already be set when collecting stats as part of "collect()", - // so we unlikely hit this code in practice. - daemonOSType = dockerCLI.ServerInfo().OSType - } // Buffer to store formatted stats text. // Once formatted, it will be printed in one write to avoid screen flickering. @@ -275,63 +300,68 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) Format: NewStatsFormat(format, daemonOSType), } - var err error - ticker := time.NewTicker(500 * time.Millisecond) - defer ticker.Stop() - for range ticker.C { - var ccStats []StatsEntry + if options.NoStream { cStats.mu.RLock() + ccStats := make([]StatsEntry, 0, len(cStats.cs)) for _, c := range cStats.cs { ccStats = append(ccStats, c.GetStatistics()) } cStats.mu.RUnlock() - if !options.NoStream { + if len(ccStats) == 0 { + return nil + } + if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { + return err + } + _, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String()) + return nil + } + + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ticker.C: + cStats.mu.RLock() + ccStats := make([]StatsEntry, 0, len(cStats.cs)) + for _, c := range cStats.cs { + ccStats = append(ccStats, c.GetStatistics()) + } + cStats.mu.RUnlock() + // Start by moving the cursor to the top-left _, _ = fmt.Fprint(&statsTextBuffer, "\033[H") - } - if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { - break - } + if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil { + return err + } - if !options.NoStream { for _, line := range strings.Split(statsTextBuffer.String(), "\n") { // In case the new text is shorter than the one we are writing over, // we'll append the "erase line" escape sequence to clear the remaining text. _, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K") } - // We might have fewer containers than before, so let's clear the remaining text _, _ = fmt.Fprint(&statsTextBuffer, "\033[J") - } - _, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String()) - statsTextBuffer.Reset() + _, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String()) + statsTextBuffer.Reset() - if len(cStats.cs) == 0 && !showAll { - break - } - if options.NoStream { - break - } - select { + if len(ccStats) == 0 && !showAll { + return nil + } case err, ok := <-closeChan: - if ok { - if err != nil { - // Suppress "unexpected EOF" errors in the CLI so that - // it shuts down cleanly when the daemon restarts. - if errors.Is(err, io.ErrUnexpectedEOF) { - return nil - } - return err - } + if !ok || err == nil || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + // Suppress "unexpected EOF" errors in the CLI so that + // it shuts down cleanly when the daemon restarts. + return nil } - default: - // just skip + return err + case <-ctx.Done(): + return ctx.Err() } } - return err } // newEventHandler initializes and returns an eventHandler @@ -357,7 +387,12 @@ func (eh *eventHandler) watch(c <-chan events.Message) { if !exists { continue } - logrus.Debugf("event handler: received event: %v", e) + if e.Actor.ID == "" { + log.G(context.TODO()).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action) + continue + } + + log.G(context.TODO()).WithField("event", e).Debugf("event handler: received %s event for: %s", e.Action, e.Actor.ID) go h(e) } } diff --git a/cli/command/container/stats_helpers.go b/cli/command/container/stats_helpers.go index c7084c17b532..4f7c746be2cd 100644 --- a/cli/command/container/stats_helpers.go +++ b/cli/command/container/stats_helpers.go @@ -3,14 +3,13 @@ package container import ( "context" "encoding/json" + "errors" "io" "sync" "time" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" ) type stats struct { @@ -50,41 +49,35 @@ func (s *stats) isKnownContainer(cid string) (int, bool) { return -1, false } -func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) { - logrus.Debugf("collecting stats for %s", s.Container) - var ( - getFirst bool - previousCPU uint64 - previousSystem uint64 - u = make(chan error, 1) - ) +func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) { //nolint:gocyclo + var getFirst bool defer func() { - // if error happens and we get nothing of stats, release wait group whatever + // if error happens, and we get nothing of stats, release wait group whatever if !getFirst { getFirst = true waitFirst.Done() } }() - response, err := cli.ContainerStats(ctx, s.Container, streamStats) + response, err := cli.ContainerStats(ctx, s.Container, client.ContainerStatsOptions{ + Stream: streamStats, + IncludePreviousSample: !streamStats, // collect previous CPU value for the first result when not streaming. + }) if err != nil { s.SetError(err) return } - defer response.Body.Close() - dec := json.NewDecoder(response.Body) + u := make(chan error, 1) go func() { + defer response.Body.Close() + dec := json.NewDecoder(response.Body) for { - var ( - v *container.StatsResponse - memPercent, cpuPercent float64 - blkRead, blkWrite uint64 // Only used on Linux - mem, memLimit float64 - pidsStatsCurrent uint64 - ) - + if ctx.Err() != nil { + return + } + var v container.StatsResponse if err := dec.Decode(&v); err != nil { dec = json.NewDecoder(io.MultiReader(dec.Buffered(), response.Body)) u <- err @@ -95,37 +88,42 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea continue } - daemonOSType = response.OSType + // Daemon versions before v29 did not return per-stats OSType; + // fall back to using the daemon's OSType. + if v.OSType == "" { + v.OSType = daemonOSType + } - if daemonOSType != "windows" { - previousCPU = v.PreCPUStats.CPUUsage.TotalUsage - previousSystem = v.PreCPUStats.SystemUsage - cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) - blkRead, blkWrite = calculateBlockIO(v.BlkioStats) - mem = calculateMemUsageUnixNoCache(v.MemoryStats) - memLimit = float64(v.MemoryStats.Limit) - memPercent = calculateMemPercentUnixNoCache(memLimit, mem) - pidsStatsCurrent = v.PidsStats.Current + if daemonOSType == "windows" { + netRx, netTx := calculateNetwork(v.Networks) + s.SetStatistics(StatsEntry{ + Name: v.Name, + ID: v.ID, + CPUPercentage: calculateCPUPercentWindows(&v), + Memory: float64(v.MemoryStats.PrivateWorkingSet), + NetworkRx: netRx, + NetworkTx: netTx, + BlockRead: float64(v.StorageStats.ReadSizeBytes), + BlockWrite: float64(v.StorageStats.WriteSizeBytes), + }) } else { - cpuPercent = calculateCPUPercentWindows(v) - blkRead = v.StorageStats.ReadSizeBytes - blkWrite = v.StorageStats.WriteSizeBytes - mem = float64(v.MemoryStats.PrivateWorkingSet) + memUsage := calculateMemUsageUnixNoCache(v.MemoryStats) + netRx, netTx := calculateNetwork(v.Networks) + blkRead, blkWrite := calculateBlockIO(v.BlkioStats) + s.SetStatistics(StatsEntry{ + Name: v.Name, + ID: v.ID, + CPUPercentage: calculateCPUPercentUnix(v.PreCPUStats, v.CPUStats), + Memory: memUsage, + MemoryPercentage: calculateMemPercentUnixNoCache(float64(v.MemoryStats.Limit), memUsage), + MemoryLimit: float64(v.MemoryStats.Limit), + NetworkRx: netRx, + NetworkTx: netTx, + BlockRead: float64(blkRead), + BlockWrite: float64(blkWrite), + PidsCurrent: v.PidsStats.Current, + }) } - netRx, netTx := calculateNetwork(v.Networks) - s.SetStatistics(StatsEntry{ - Name: v.Name, - ID: v.ID, - CPUPercentage: cpuPercent, - Memory: mem, - MemoryPercentage: memPercent, - MemoryLimit: memLimit, - NetworkRx: netRx, - NetworkTx: netTx, - BlockRead: float64(blkRead), - BlockWrite: float64(blkWrite), - PidsCurrent: pidsStatsCurrent, - }) u <- nil if !streamStats { return @@ -145,8 +143,8 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea } case err := <-u: s.SetError(err) - if err == io.EOF { - break + if errors.Is(err, io.EOF) { + return } if err != nil { continue @@ -156,6 +154,9 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea getFirst = true waitFirst.Done() } + case <-ctx.Done(): + s.SetError(ctx.Err()) + return } if !streamStats { return @@ -163,18 +164,18 @@ func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, strea } } -func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *container.StatsResponse) float64 { +func calculateCPUPercentUnix(previousCPU container.CPUStats, curCPUStats container.CPUStats) float64 { var ( cpuPercent = 0.0 // calculate the change for the cpu usage of the container in between readings - cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) + cpuDelta = float64(curCPUStats.CPUUsage.TotalUsage) - float64(previousCPU.CPUUsage.TotalUsage) // calculate the change for the entire system between readings - systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) - onlineCPUs = float64(v.CPUStats.OnlineCPUs) + systemDelta = float64(curCPUStats.SystemUsage) - float64(previousCPU.SystemUsage) + onlineCPUs = float64(curCPUStats.OnlineCPUs) ) if onlineCPUs == 0.0 { - onlineCPUs = float64(len(v.CPUStats.CPUUsage.PercpuUsage)) + onlineCPUs = float64(len(curCPUStats.CPUUsage.PercpuUsage)) } if systemDelta > 0.0 && cpuDelta > 0.0 { cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 @@ -186,13 +187,11 @@ func calculateCPUPercentWindows(v *container.StatsResponse) float64 { // Max number of 100ns intervals between the previous time read and now possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals possIntervals /= 100 // Convert to number of 100ns intervals - possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors - - // Intervals used - intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage + possIntervals *= uint64(v.NumProcs) // Multiply by the number of processors // Percentage avoiding divide-by-zero if possIntervals > 0 { + intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage return float64(intervalsUsed) / float64(possIntervals) * 100.0 } return 0.00 diff --git a/cli/command/container/stats_helpers_test.go b/cli/command/container/stats_helpers_test.go index 7db44e2ee23b..081b6be52012 100644 --- a/cli/command/container/stats_helpers_test.go +++ b/cli/command/container/stats_helpers_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" "gotest.tools/v3/assert" ) diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go index c6b331e964fd..0834d52f7eb9 100644 --- a/cli/command/container/stop.go +++ b/cli/command/container/stop.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -20,8 +20,8 @@ type stopOptions struct { containers []string } -// NewStopCommand creates a new cobra.Command for `docker stop` -func NewStopCommand(dockerCli command.Cli) *cobra.Command { +// newStopCommand creates a new cobra.Command for "docker container stop". +func newStopCommand(dockerCLI command.Cli) *cobra.Command { var opts stopOptions cmd := &cobra.Command{ @@ -34,12 +34,13 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command { } opts.containers = args opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time") - return runStop(cmd.Context(), dockerCli, &opts) + return runStop(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container stop, docker stop", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -63,10 +64,11 @@ func runStop(ctx context.Context, dockerCLI command.Cli, opts *stopOptions) erro apiClient := dockerCLI.Client() errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error { - return apiClient.ContainerStop(ctx, id, container.StopOptions{ + _, err := apiClient.ContainerStop(ctx, id, client.ContainerStopOptions{ Signal: opts.signal, Timeout: timeout, }) + return err }) var errs []error for _, ctr := range opts.containers { diff --git a/cli/command/container/stop_test.go b/cli/command/container/stop_test.go index fc2b88c7acc4..ed7fc335e267 100644 --- a/cli/command/container/stop_test.go +++ b/cli/command/container/stop_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -19,7 +19,7 @@ func TestStop(t *testing.T) { name string args []string stopped []string - expectedOpts container.StopOptions + expectedOpts client.ContainerStopOptions expectedErr string }{ { @@ -36,19 +36,19 @@ func TestStop(t *testing.T) { { name: "with -t", args: []string{"-t", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerStopOptions{Timeout: func(to int) *int { return &to }(2)}, stopped: []string{"container-1"}, }, { name: "with --timeout", args: []string{"--timeout", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerStopOptions{Timeout: func(to int) *int { return &to }(2)}, stopped: []string{"container-1"}, }, { name: "with --time", args: []string{"--time", "2", "container-1"}, - expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)}, + expectedOpts: client.ContainerStopOptions{Timeout: func(to int) *int { return &to }(2)}, stopped: []string{"container-1"}, }, { @@ -62,10 +62,10 @@ func TestStop(t *testing.T) { mutex := new(sync.Mutex) cli := test.NewFakeCli(&fakeClient{ - containerStopFunc: func(ctx context.Context, containerID string, options container.StopOptions) error { + containerStopFunc: func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) { assert.Check(t, is.DeepEqual(options, tc.expectedOpts)) if containerID == "nosuchcontainer" { - return notFound(errors.New("Error: no such container: " + containerID)) + return client.ContainerStopResult{}, notFound(errors.New("Error: no such container: " + containerID)) } // containerStopFunc is called in parallel for each container @@ -73,11 +73,11 @@ func TestStop(t *testing.T) { mutex.Lock() stopped = append(stopped, containerID) mutex.Unlock() - return nil + return client.ContainerStopResult{}, nil }, Version: "1.36", }) - cmd := NewStopCommand(cli) + cmd := newStopCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/testdata/container-create-localhost-dns-ipv6.golden b/cli/command/container/testdata/container-create-localhost-dns-ipv6.golden deleted file mode 100644 index bb07a137dcea..000000000000 --- a/cli/command/container/testdata/container-create-localhost-dns-ipv6.golden +++ /dev/null @@ -1 +0,0 @@ -WARNING: Localhost DNS (::1) may fail in containers. diff --git a/cli/command/container/testdata/container-create-localhost-dns.golden b/cli/command/container/testdata/container-create-localhost-dns.golden deleted file mode 100644 index 409082ad6acf..000000000000 --- a/cli/command/container/testdata/container-create-localhost-dns.golden +++ /dev/null @@ -1 +0,0 @@ -WARNING: Localhost DNS (127.0.0.11) may fail in containers. diff --git a/cli/command/container/top.go b/cli/command/container/top.go index 411fcbbf79cd..6ed7350a49d9 100644 --- a/cli/command/container/top.go +++ b/cli/command/container/top.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter/tabwriter" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,8 +19,8 @@ type topOptions struct { args []string } -// NewTopCommand creates a new cobra.Command for `docker top` -func NewTopCommand(dockerCli command.Cli) *cobra.Command { +// newTopCommand creates a new cobra.Command for "docker container top", +func newTopCommand(dockerCLI command.Cli) *cobra.Command { var opts topOptions cmd := &cobra.Command{ @@ -29,12 +30,13 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] opts.args = args[1:] - return runTop(cmd.Context(), dockerCli, &opts) + return runTop(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container top, docker top", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -44,17 +46,19 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command { } func runTop(ctx context.Context, dockerCli command.Cli, opts *topOptions) error { - procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args) + procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, client.ContainerTopOptions{ + Arguments: opts.args, + }) if err != nil { return err } w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) + _, _ = fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) for _, proc := range procList.Processes { - fmt.Fprintln(w, strings.Join(proc, "\t")) + _, _ = fmt.Fprintln(w, strings.Join(proc, "\t")) } - w.Flush() + _ = w.Flush() return nil } diff --git a/cli/command/container/tty.go b/cli/command/container/tty.go index c2a5b4f8a5f8..6d57bf0e3460 100644 --- a/cli/command/container/tty.go +++ b/cli/command/container/tty.go @@ -9,28 +9,34 @@ import ( "time" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/moby/sys/signal" "github.com/sirupsen/logrus" ) -// resizeTtyTo resizes tty to specific height and width -func resizeTtyTo(ctx context.Context, apiClient client.ContainerAPIClient, id string, height, width uint, isExec bool) error { +// TODO(thaJeztah): split resizeTTYTo +type resizeClient interface { + client.ExecAPIClient + client.ContainerAPIClient +} + +// resizeTTYTo resizes TTY to specific height and width. +func resizeTTYTo(ctx context.Context, apiClient resizeClient, id string, height, width uint, isExec bool) error { if height == 0 && width == 0 { return nil } - options := container.ResizeOptions{ - Height: height, - Width: width, - } - var err error if isExec { - err = apiClient.ContainerExecResize(ctx, id, options) + _, err = apiClient.ExecResize(ctx, id, client.ExecResizeOptions{ + Height: height, + Width: width, + }) } else { - err = apiClient.ContainerResize(ctx, id, options) + _, err = apiClient.ContainerResize(ctx, id, client.ContainerResizeOptions{ + Height: height, + Width: width, + }) } if err != nil { @@ -42,26 +48,26 @@ func resizeTtyTo(ctx context.Context, apiClient client.ContainerAPIClient, id st // resizeTty is to resize the tty with cli out's tty size func resizeTty(ctx context.Context, cli command.Cli, id string, isExec bool) error { height, width := cli.Out().GetTtySize() - return resizeTtyTo(ctx, cli.Client(), id, height, width, isExec) + return resizeTTYTo(ctx, cli.Client(), id, height, width, isExec) } -// initTtySize is to init the tty's size to the same as the window, if there is an error, it will retry 10 times. +// initTtySize is to init the TTYs size to the same as the window, if there is an error, it will retry 10 times. func initTtySize(ctx context.Context, cli command.Cli, id string, isExec bool, resizeTtyFunc func(ctx context.Context, cli command.Cli, id string, isExec bool) error) { - rttyFunc := resizeTtyFunc - if rttyFunc == nil { - rttyFunc = resizeTty + rTTYfunc := resizeTtyFunc + if rTTYfunc == nil { + rTTYfunc = resizeTty } - if err := rttyFunc(ctx, cli, id, isExec); err != nil { + if err := rTTYfunc(ctx, cli, id, isExec); err != nil { go func() { var err error for retry := 0; retry < 10; retry++ { time.Sleep(time.Duration(retry+1) * 10 * time.Millisecond) - if err = rttyFunc(ctx, cli, id, isExec); err == nil { + if err = rTTYfunc(ctx, cli, id, isExec); err == nil { break } } if err != nil { - fmt.Fprintln(cli.Err(), "failed to resize tty, using default size") + _, _ = fmt.Fprintln(cli.Err(), "failed to resize tty, using default size") } }() } @@ -78,7 +84,7 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool h, w := cli.Out().GetTtySize() if prevW != w || prevH != h { - resizeTty(ctx, cli, id, isExec) + _ = resizeTty(ctx, cli, id, isExec) } prevH = h prevW = w @@ -89,7 +95,7 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool gosignal.Notify(sigchan, signal.SIGWINCH) go func() { for range sigchan { - resizeTty(ctx, cli, id, isExec) + _ = resizeTty(ctx, cli, id, isExec) } }() } diff --git a/cli/command/container/tty_test.go b/cli/command/container/tty_test.go index 0c4111e40df7..19162721f9c2 100644 --- a/cli/command/container/tty_test.go +++ b/cli/command/container/tty_test.go @@ -8,19 +8,19 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestInitTtySizeErrors(t *testing.T) { expectedError := "failed to resize tty, using default size\n" - fakeContainerExecResizeFunc := func(id string, options container.ResizeOptions) error { - return errors.New("Error response from daemon: no such exec") + fakeContainerExecResizeFunc := func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) { + return client.ExecResizeResult{}, errors.New("error response from daemon: no such exec") } fakeResizeTtyFunc := func(ctx context.Context, cli command.Cli, id string, isExec bool) error { height, width := uint(1024), uint(768) - return resizeTtyTo(ctx, cli.Client(), id, height, width, isExec) + return resizeTTYTo(ctx, cli.Client(), id, height, width, isExec) } ctx := context.Background() cli := test.NewFakeCli(&fakeClient{containerExecResizeFunc: fakeContainerExecResizeFunc}) diff --git a/cli/command/container/unpause.go b/cli/command/container/unpause.go index fd5a516cace4..c4223a87b7b5 100644 --- a/cli/command/container/unpause.go +++ b/cli/command/container/unpause.go @@ -8,7 +8,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -16,8 +17,8 @@ type unpauseOptions struct { containers []string } -// NewUnpauseCommand creates a new cobra.Command for `docker unpause` -func NewUnpauseCommand(dockerCli command.Cli) *cobra.Command { +// newUnpauseCommand creates a new cobra.Command for "docker container unpause". +func newUnpauseCommand(dockerCLI command.Cli) *cobra.Command { var opts unpauseOptions cmd := &cobra.Command{ @@ -26,21 +27,25 @@ func NewUnpauseCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runUnpause(cmd.Context(), dockerCli, &opts) + return runUnpause(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container unpause, docker unpause", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State == container.StatePaused }), + DisableFlagsInUseLine: true, } return cmd } func runUnpause(ctx context.Context, dockerCLI command.Cli, opts *unpauseOptions) error { apiClient := dockerCLI.Client() - errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerUnpause) + errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { + _, err := apiClient.ContainerUnpause(ctx, container, client.ContainerUnpauseOptions{}) + return err + }) var errs []error for _, ctr := range opts.containers { if err := <-errChan; err != nil { diff --git a/cli/command/container/update.go b/cli/command/container/update.go index 275ec5f64634..3bc11412e19a 100644 --- a/cli/command/container/update.go +++ b/cli/command/container/update.go @@ -2,6 +2,7 @@ package container import ( "context" + "errors" "fmt" "strings" @@ -9,8 +10,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - containertypes "github.com/docker/docker/api/types/container" - "github.com/pkg/errors" + containertypes "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -26,7 +27,6 @@ type updateOptions struct { memory opts.MemBytes memoryReservation opts.MemBytes memorySwap opts.MemSwapBytes - kernelMemory opts.MemBytes restartPolicy string pidsLimit int64 cpus opts.NanoCPUs @@ -36,8 +36,8 @@ type updateOptions struct { containers []string } -// NewUpdateCommand creates a new cobra.Command for `docker update` -func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { +// newUpdateCommand creates a new cobra.Command for "docker container update". +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { var options updateOptions cmd := &cobra.Command{ @@ -47,12 +47,13 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { options.containers = args options.nFlag = cmd.Flags().NFlag() - return runUpdate(cmd.Context(), dockerCli, &options) + return runUpdate(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker container update, docker update", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -60,31 +61,30 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") flags.Int64Var(&options.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") - flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) + _ = flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) flags.Int64Var(&options.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds") - flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) + _ = flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) flags.StringVar(&options.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&options.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") flags.VarP(&options.memory, "memory", "m", "Memory limit") flags.Var(&options.memoryReservation, "memory-reservation", "Memory soft limit") flags.Var(&options.memorySwap, "memory-swap", `Swap limit equal to memory plus swap: -1 to enable unlimited swap`) - flags.Var(&options.kernelMemory, "kernel-memory", "Kernel memory limit (deprecated)") - // --kernel-memory is deprecated on API v1.42 and up, but our current annotations - // do not support only showing on < API-version. This option is no longer supported - // by runc, so hiding it unconditionally. - flags.SetAnnotation("kernel-memory", "deprecated", nil) - flags.MarkHidden("kernel-memory") flags.StringVar(&options.restartPolicy, "restart", "", "Restart policy to apply when a container exits") flags.Int64Var(&options.pidsLimit, "pids-limit", 0, `Tune container pids limit (set -1 for unlimited)`) - flags.SetAnnotation("pids-limit", "version", []string{"1.40"}) + _ = flags.SetAnnotation("pids-limit", "version", []string{"1.40"}) flags.Var(&options.cpus, "cpus", "Number of CPUs") - flags.SetAnnotation("cpus", "version", []string{"1.29"}) + _ = flags.SetAnnotation("cpus", "version", []string{"1.29"}) _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + // TODO(thaJeztah): remove in next release (v30.0, or v29.x) + var stub opts.MemBytes + flags.Var(&stub, "kernel-memory", "Kernel memory limit (deprecated)") + _ = flags.MarkDeprecated("kernel-memory", "and no longer supported by the kernel") + return cmd } @@ -103,39 +103,38 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, options *updateOption } } - resources := containertypes.Resources{ - BlkioWeight: options.blkioWeight, - CpusetCpus: options.cpusetCpus, - CpusetMems: options.cpusetMems, - CPUShares: options.cpuShares, - Memory: options.memory.Value(), - MemoryReservation: options.memoryReservation.Value(), - MemorySwap: options.memorySwap.Value(), - KernelMemory: options.kernelMemory.Value(), - CPUPeriod: options.cpuPeriod, - CPUQuota: options.cpuQuota, - CPURealtimePeriod: options.cpuRealtimePeriod, - CPURealtimeRuntime: options.cpuRealtimeRuntime, - NanoCPUs: options.cpus.Value(), - } - + var pidsLimit *int64 if options.pidsLimit != 0 { - resources.PidsLimit = &options.pidsLimit + pidsLimit = &options.pidsLimit } - updateConfig := containertypes.UpdateConfig{ - Resources: resources, - RestartPolicy: restartPolicy, + updateConfig := client.ContainerUpdateOptions{ + Resources: &containertypes.Resources{ + BlkioWeight: options.blkioWeight, + CpusetCpus: options.cpusetCpus, + CpusetMems: options.cpusetMems, + CPUShares: options.cpuShares, + Memory: options.memory.Value(), + MemoryReservation: options.memoryReservation.Value(), + MemorySwap: options.memorySwap.Value(), + CPUPeriod: options.cpuPeriod, + CPUQuota: options.cpuQuota, + CPURealtimePeriod: options.cpuRealtimePeriod, + CPURealtimeRuntime: options.cpuRealtimeRuntime, + NanoCPUs: options.cpus.Value(), + PidsLimit: pidsLimit, + }, + RestartPolicy: &restartPolicy, } var ( warns []string - errs []string + errs []error ) for _, ctr := range options.containers { r, err := dockerCli.Client().ContainerUpdate(ctx, ctr, updateConfig) if err != nil { - errs = append(errs, err.Error()) + errs = append(errs, err) } else { _, _ = fmt.Fprintln(dockerCli.Out(), ctr) } @@ -144,8 +143,5 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, options *updateOption if len(warns) > 0 { _, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(warns, "\n")) } - if len(errs) > 0 { - return errors.New(strings.Join(errs, "\n")) - } - return nil + return errors.Join(errs...) } diff --git a/cli/command/container/utils.go b/cli/command/container/utils.go index 2f823227655d..7943500e5c83 100644 --- a/cli/command/container/utils.go +++ b/cli/command/container/utils.go @@ -3,13 +3,9 @@ package container import ( "context" "errors" - "strconv" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/sirupsen/logrus" ) @@ -19,19 +15,14 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") } - // Older versions used the Events API, and even older versions did not - // support server-side removal. This legacyWaitExitOrRemoved method - // preserves that old behavior and any issues it may have. - if versions.LessThan(apiClient.ClientVersion(), "1.30") { - return legacyWaitExitOrRemoved(ctx, apiClient, containerID, waitRemove) - } - condition := container.WaitConditionNextExit if waitRemove { condition = container.WaitConditionRemoved } - resultC, errC := apiClient.ContainerWait(ctx, containerID, condition) + waitRes := apiClient.ContainerWait(ctx, containerID, client.ContainerWaitOptions{ + Condition: condition, + }) statusC := make(chan int) go func() { @@ -39,14 +30,14 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe select { case <-ctx.Done(): return - case result := <-resultC: + case result := <-waitRes.Result: if result.Error != nil { logrus.Errorf("Error waiting for container: %v", result.Error.Message) statusC <- 125 } else { statusC <- int(result.StatusCode) } - case err := <-errC: + case err := <-waitRes.Error: if errors.Is(err, context.Canceled) { return } @@ -58,81 +49,6 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe return statusC } -func legacyWaitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containerID string, waitRemove bool) <-chan int { - var removeErr error - statusChan := make(chan int) - exitCode := 125 - - // Get events via Events API - f := filters.NewArgs() - f.Add("type", "container") - f.Add("container", containerID) - - eventCtx, cancel := context.WithCancel(ctx) - eventq, errq := apiClient.Events(eventCtx, events.ListOptions{ - Filters: f, - }) - - eventProcessor := func(e events.Message) bool { - stopProcessing := false - switch e.Action { //nolint:exhaustive // TODO(thaJeztah): make exhaustive - case events.ActionDie: - if v, ok := e.Actor.Attributes["exitCode"]; ok { - code, cerr := strconv.Atoi(v) - if cerr != nil { - logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr) - } else { - exitCode = code - } - } - if !waitRemove { - stopProcessing = true - } else if versions.LessThan(apiClient.ClientVersion(), "1.25") { - // If we are talking to an older daemon, `AutoRemove` is not supported. - // We need to fall back to the old behavior, which is client-side removal - go func() { - removeErr = apiClient.ContainerRemove(ctx, containerID, container.RemoveOptions{RemoveVolumes: true}) - if removeErr != nil { - logrus.Errorf("error removing container: %v", removeErr) - cancel() // cancel the event Q - } - }() - } - case events.ActionDetach: - exitCode = 0 - stopProcessing = true - case events.ActionDestroy: - stopProcessing = true - } - return stopProcessing - } - - go func() { - defer func() { - statusChan <- exitCode // must always send an exit code or the caller will block - cancel() - }() - - for { - select { - case <-eventCtx.Done(): - if removeErr != nil { - return - } - case evt := <-eventq: - if eventProcessor(evt) { - return - } - case err := <-errq: - logrus.Errorf("error getting events from daemon: %v", err) - return - } - } - }() - - return statusChan -} - func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, containerID string) error) chan error { if len(containers) == 0 { return nil diff --git a/cli/command/container/utils_test.go b/cli/command/container/utils_test.go index 7d08b37844d2..6101f564dc77 100644 --- a/cli/command/container/utils_test.go +++ b/cli/command/container/utils_test.go @@ -6,13 +6,13 @@ import ( "strings" "testing" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) -func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) { +func waitFn(cid string) client.ContainerWaitResult { resC := make(chan container.WaitResponse) errC := make(chan error, 1) var res container.WaitResponse @@ -33,8 +33,10 @@ func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) { resC <- res } }() - - return resC, errC + return client.ContainerWaitResult{ + Result: resC, + Error: errC, + } } func TestWaitExitOrRemoved(t *testing.T) { @@ -60,10 +62,10 @@ func TestWaitExitOrRemoved(t *testing.T) { }, } - client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion} + apiClient := &fakeClient{waitFunc: waitFn, Version: client.MaxAPIVersion} for _, tc := range tests { t.Run(tc.cid, func(t *testing.T) { - statusC := waitExitOrRemoved(context.Background(), client, tc.cid, true) + statusC := waitExitOrRemoved(context.Background(), apiClient, tc.cid, true) exitCode := <-statusC assert.Check(t, is.Equal(tc.exitCode, exitCode)) }) diff --git a/cli/command/container/wait.go b/cli/command/container/wait.go index 89f2ba17acb6..3aabf25e2db8 100644 --- a/cli/command/container/wait.go +++ b/cli/command/container/wait.go @@ -4,10 +4,12 @@ import ( "context" "errors" "fmt" + "strconv" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -15,8 +17,8 @@ type waitOptions struct { containers []string } -// NewWaitCommand creates a new cobra.Command for `docker wait` -func NewWaitCommand(dockerCli command.Cli) *cobra.Command { +// newWaitCommand creates a new cobra.Command for "docker container wait". +func newWaitCommand(dockerCLI command.Cli) *cobra.Command { var opts waitOptions cmd := &cobra.Command{ @@ -25,12 +27,13 @@ func NewWaitCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runWait(cmd.Context(), dockerCli, &opts) + return runWait(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container wait, docker wait", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), + DisableFlagsInUseLine: true, } return cmd @@ -41,12 +44,12 @@ func runWait(ctx context.Context, dockerCLI command.Cli, opts *waitOptions) erro var errs []error for _, ctr := range opts.containers { - resultC, errC := apiClient.ContainerWait(ctx, ctr, "") + res := apiClient.ContainerWait(ctx, ctr, client.ContainerWaitOptions{}) select { - case result := <-resultC: - _, _ = fmt.Fprintf(dockerCLI.Out(), "%d\n", result.StatusCode) - case err := <-errC: + case result := <-res.Result: + _, _ = fmt.Fprintln(dockerCLI.Out(), strconv.FormatInt(result.StatusCode, 10)) + case err := <-res.Error: errs = append(errs, err) } } diff --git a/cli/command/context.go b/cli/command/context.go index 64e88e449c4b..2ca82a5a88f3 100644 --- a/cli/command/context.go +++ b/cli/command/context.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command diff --git a/cli/command/context/cmd.go b/cli/command/context/cmd.go index f8b9e80d9399..44bbb7e3d693 100644 --- a/cli/command/context/cmd.go +++ b/cli/command/context/cmd.go @@ -3,27 +3,34 @@ package context import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewContextCommand returns the context cli subcommand -func NewContextCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newContextCommand) +} + +// newContextCommand returns the context cli subcommand +func newContextCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "context", Short: "Manage contexts", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, } cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newUseCommand(dockerCli), - newExportCommand(dockerCli), - newImportCommand(dockerCli), - newRemoveCommand(dockerCli), - newUpdateCommand(dockerCli), - newInspectCommand(dockerCli), - newShowCommand(dockerCli), + newCreateCommand(dockerCLI), + newListCommand(dockerCLI), + newUseCommand(dockerCLI), + newExportCommand(dockerCLI), + newImportCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newShowCommand(dockerCLI), ) return cmd } diff --git a/cli/command/context/completion.go b/cli/command/context/completion.go index 701e7e4d4951..65453cc57c8b 100644 --- a/cli/command/context/completion.go +++ b/cli/command/context/completion.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package context diff --git a/cli/command/context/create.go b/cli/command/context/create.go index 313aca142e4a..2d5b7ab6fabe 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package context @@ -8,22 +8,20 @@ import ( "errors" "fmt" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter/tabwriter" "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" "github.com/spf13/cobra" ) -// CreateOptions are the options used for creating a context -type CreateOptions struct { - Name string - Description string - Docker map[string]string - From string +// createOptions are the options used for creating a context +type createOptions struct { + description string + endpoint map[string]string + from string // Additional Metadata to store in the context. This option is not // currently exposed to the user. @@ -44,52 +42,52 @@ func longCreateDescription() string { } func newCreateCommand(dockerCLI command.Cli) *cobra.Command { - opts := &CreateOptions{} + opts := createOptions{} cmd := &cobra.Command{ Use: "create [OPTIONS] CONTEXT", Short: "Create a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Name = args[0] - return RunCreate(dockerCLI, opts) + return runCreate(dockerCLI, args[0], opts) }, - Long: longCreateDescription(), - ValidArgsFunction: completion.NoComplete, + Long: longCreateDescription(), + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringVar(&opts.Description, "description", "", "Description of the context") - flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") - flags.StringVar(&opts.From, "from", "", "create context from a named context") + flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringToStringVar(&opts.endpoint, "docker", nil, "set the docker endpoint") + flags.StringVar(&opts.from, "from", "", "create context from a named context") return cmd } -// RunCreate creates a Docker context -func RunCreate(dockerCLI command.Cli, o *CreateOptions) error { +// runCreate creates a Docker context +func runCreate(dockerCLI command.Cli, name string, opts createOptions) error { s := dockerCLI.ContextStore() - err := checkContextNameForCreation(s, o.Name) + err := checkContextNameForCreation(s, name) if err != nil { return err } switch { - case o.From == "" && o.Docker == nil: - err = createFromExistingContext(s, dockerCLI.CurrentContext(), o) - case o.From != "": - err = createFromExistingContext(s, o.From, o) + case opts.from == "" && opts.endpoint == nil: + err = createFromExistingContext(s, name, dockerCLI.CurrentContext(), opts) + case opts.from != "": + err = createFromExistingContext(s, name, opts.from, opts) default: - err = createNewContext(s, o) + err = createNewContext(s, name, opts) } if err == nil { - _, _ = fmt.Fprintln(dockerCLI.Out(), o.Name) - _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully created context %q\n", o.Name) + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully created context %q\n", name) } return err } -func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { - if o.Docker == nil { +func createNewContext(contextStore store.ReaderWriter, name string, opts createOptions) error { + if opts.endpoint == nil { return errors.New("docker endpoint configuration is required") } - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(contextStore, o.Docker) + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(contextStore, opts.endpoint) if err != nil { return fmt.Errorf("unable to create docker endpoint config: %w", err) } @@ -98,10 +96,10 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { docker.DockerEndpoint: dockerEP, }, Metadata: command.DockerContext{ - Description: o.Description, - AdditionalFields: o.metaData, + Description: opts.description, + AdditionalFields: opts.metaData, }, - Name: o.Name, + Name: name, } contextTLSData := store.ContextTLSData{} if dockerTLS != nil { @@ -115,14 +113,14 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { if err := contextStore.CreateOrUpdate(contextMetadata); err != nil { return err } - return contextStore.ResetTLSMaterial(o.Name, &contextTLSData) + return contextStore.ResetTLSMaterial(name, &contextTLSData) } func checkContextNameForCreation(s store.Reader, name string) error { if err := store.ValidateContextName(name); err != nil { return err } - if _, err := s.GetMetadata(name); !cerrdefs.IsNotFound(err) { + if _, err := s.GetMetadata(name); !errdefs.IsNotFound(err) { if err != nil { return fmt.Errorf("error while getting existing contexts: %w", err) } @@ -131,16 +129,16 @@ func checkContextNameForCreation(s store.Reader, name string) error { return nil } -func createFromExistingContext(s store.ReaderWriter, fromContextName string, o *CreateOptions) error { - if len(o.Docker) != 0 { +func createFromExistingContext(s store.ReaderWriter, name string, fromContextName string, opts createOptions) error { + if len(opts.endpoint) != 0 { return errors.New("cannot use --docker flag when --from is set") } reader := store.Export(fromContextName, &descriptionDecorator{ Reader: s, - description: o.Description, + description: opts.description, }) defer reader.Close() - return store.Import(o.Name, s, reader) + return store.Import(name, s, reader) } type descriptionDecorator struct { diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index 794f95de04d5..0e7ccb03f508 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package context @@ -60,7 +60,8 @@ func TestCreate(t *testing.T) { assert.NilError(t, cli.ContextStore().CreateOrUpdate(store.Metadata{Name: "existing-context"})) tests := []struct { doc string - options CreateOptions + options createOptions + name string expecterErr string }{ { @@ -68,59 +69,53 @@ func TestCreate(t *testing.T) { expecterErr: `context name cannot be empty`, }, { - doc: "reserved name", - options: CreateOptions{ - Name: "default", - }, + doc: "reserved name", + name: "default", expecterErr: `"default" is a reserved context name`, }, { - doc: "whitespace-only name", - options: CreateOptions{ - Name: " ", - }, + doc: "whitespace-only name", + name: " ", expecterErr: `context name " " is invalid`, }, { - doc: "existing context", - options: CreateOptions{ - Name: "existing-context", - }, + doc: "existing context", + name: "existing-context", expecterErr: `context "existing-context" already exists`, }, { - doc: "invalid docker host", - options: CreateOptions{ - Name: "invalid-docker-host", - Docker: map[string]string{ + doc: "invalid docker host", + name: "invalid-docker-host", + options: createOptions{ + endpoint: map[string]string{ "host": "some///invalid/host", }, }, expecterErr: `unable to parse docker host`, }, { - doc: "ssh host with skip-tls-verify=false", - options: CreateOptions{ - Name: "skip-tls-verify-false", - Docker: map[string]string{ + doc: "ssh host with skip-tls-verify=false", + name: "skip-tls-verify-false", + options: createOptions{ + endpoint: map[string]string{ "host": "ssh://example.com,skip-tls-verify=false", }, }, }, { - doc: "ssh host with skip-tls-verify=true", - options: CreateOptions{ - Name: "skip-tls-verify-true", - Docker: map[string]string{ + doc: "ssh host with skip-tls-verify=true", + name: "skip-tls-verify-true", + options: createOptions{ + endpoint: map[string]string{ "host": "ssh://example.com,skip-tls-verify=true", }, }, }, { - doc: "ssh host with skip-tls-verify=INVALID", - options: CreateOptions{ - Name: "skip-tls-verify-invalid", - Docker: map[string]string{ + doc: "ssh host with skip-tls-verify=INVALID", + name: "skip-tls-verify-invalid", + options: createOptions{ + endpoint: map[string]string{ "host": "ssh://example.com", "skip-tls-verify": "INVALID", }, @@ -128,10 +123,10 @@ func TestCreate(t *testing.T) { expecterErr: `unable to create docker endpoint config: skip-tls-verify: parsing "INVALID": invalid syntax`, }, { - doc: "unknown option", - options: CreateOptions{ - Name: "unknown-option", - Docker: map[string]string{ + doc: "unknown option", + name: "unknown-option", + options: createOptions{ + endpoint: map[string]string{ "UNKNOWN": "value", }, }, @@ -140,7 +135,7 @@ func TestCreate(t *testing.T) { } for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { - err := RunCreate(cli, &tc.options) + err := runCreate(cli, tc.name, tc.options) if tc.expecterErr == "" { assert.NilError(t, err) } else { @@ -159,9 +154,8 @@ func assertContextCreateLogging(t *testing.T, cli *test.FakeCli, n string) { func TestCreateOrchestratorEmpty(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{}, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, "test") @@ -187,20 +181,18 @@ func TestCreateFromContext(t *testing.T) { cli := makeFakeCli(t) cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "original", - Description: "original description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, "original", createOptions{ + description: "original description", + endpoint: map[string]string{ keyHost: "tcp://42.42.42.42:2375", }, })) assertContextCreateLogging(t, cli, "original") cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "dummy", - Description: "dummy description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, "dummy", createOptions{ + description: "dummy description", + endpoint: map[string]string{ keyHost: "tcp://24.24.24.24:2375", }, })) @@ -211,11 +203,10 @@ func TestCreateFromContext(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cli.ResetOutputBuffers() - err := RunCreate(cli, &CreateOptions{ - From: "original", - Name: tc.name, - Description: tc.description, - Docker: tc.docker, + err := runCreate(cli, tc.name, createOptions{ + from: "original", + description: tc.description, + endpoint: tc.docker, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, tc.name) @@ -251,10 +242,9 @@ func TestCreateFromCurrent(t *testing.T) { cli := makeFakeCli(t) cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "original", - Description: "original description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, "original", createOptions{ + description: "original description", + endpoint: map[string]string{ keyHost: "tcp://42.42.42.42:2375", }, })) @@ -265,9 +255,8 @@ func TestCreateFromCurrent(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cli.ResetOutputBuffers() - err := RunCreate(cli, &CreateOptions{ - Name: tc.name, - Description: tc.description, + err := runCreate(cli, tc.name, createOptions{ + description: tc.description, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, tc.name) diff --git a/cli/command/context/export-import_test.go b/cli/command/context/export-import_test.go index 9aabd6ab4490..cfbac778ad6f 100644 --- a/cli/command/context/export-import_test.go +++ b/cli/command/context/export-import_test.go @@ -21,14 +21,11 @@ func TestExportImportWithFile(t *testing.T) { "MyCustomMetadata": t.Name(), }) cli.ErrBuffer().Reset() - assert.NilError(t, RunExport(cli, &ExportOptions{ - ContextName: "test", - Dest: contextFile, - })) + assert.NilError(t, runExport(cli, "test", contextFile)) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunImport(cli, "test2", contextFile)) + assert.NilError(t, runImport(cli, "test2", contextFile)) context1, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetMetadata("test2") @@ -55,15 +52,12 @@ func TestExportImportPipe(t *testing.T) { }) cli.ErrBuffer().Reset() cli.OutBuffer().Reset() - assert.NilError(t, RunExport(cli, &ExportOptions{ - ContextName: "test", - Dest: "-", - })) + assert.NilError(t, runExport(cli, "test", "-")) assert.Equal(t, cli.ErrBuffer().String(), "") cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunImport(cli, "test2", "-")) + assert.NilError(t, runImport(cli, "test2", "-")) context1, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetMetadata("test2") @@ -88,6 +82,6 @@ func TestExportExistingFile(t *testing.T) { cli := makeFakeCli(t) cli.ErrBuffer().Reset() assert.NilError(t, os.WriteFile(contextFile, []byte{}, 0o644)) - err := RunExport(cli, &ExportOptions{ContextName: "test", Dest: contextFile}) + err := runExport(cli, "test", contextFile) assert.Assert(t, os.IsExist(err)) } diff --git a/cli/command/context/export.go b/cli/command/context/export.go index 96e35b67505f..53f7e1fb989a 100644 --- a/cli/command/context/export.go +++ b/cli/command/context/export.go @@ -12,29 +12,23 @@ import ( "github.com/spf13/cobra" ) -// ExportOptions are the options used for exporting a context -type ExportOptions struct { - ContextName string - Dest string -} - func newExportCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "export [OPTIONS] CONTEXT [FILE|-]", Short: "Export a context to a tar archive FILE or a tar stream on STDOUT.", Args: cli.RequiresRangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { - opts := &ExportOptions{ - ContextName: args[0], - } + contextName := args[0] + var dest string if len(args) == 2 { - opts.Dest = args[1] + dest = args[1] } else { - opts.Dest = opts.ContextName + ".dockercontext" + dest = contextName + ".dockercontext" } - return RunExport(dockerCLI, opts) + return runExport(dockerCLI, contextName, dest) }, - ValidArgsFunction: completeContextNames(dockerCLI, 1, true), + ValidArgsFunction: completeContextNames(dockerCLI, 1, true), + DisableFlagsInUseLine: true, } } @@ -64,12 +58,12 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error { return nil } -// RunExport exports a Docker context -func RunExport(dockerCli command.Cli, opts *ExportOptions) error { - if err := store.ValidateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName { +// runExport exports a Docker context. +func runExport(dockerCLI command.Cli, contextName string, dest string) error { + if err := store.ValidateContextName(contextName); err != nil && contextName != command.DefaultContextName { return err } - reader := store.Export(opts.ContextName, dockerCli.ContextStore()) + reader := store.Export(contextName, dockerCLI.ContextStore()) defer reader.Close() - return writeTo(dockerCli, reader, opts.Dest) + return writeTo(dockerCLI, reader, dest) } diff --git a/cli/command/context/import.go b/cli/command/context/import.go index 182defcf53dc..47c0bbcac3e4 100644 --- a/cli/command/context/import.go +++ b/cli/command/context/import.go @@ -12,29 +12,30 @@ import ( "github.com/spf13/cobra" ) -func newImportCommand(dockerCli command.Cli) *cobra.Command { +func newImportCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "import CONTEXT FILE|-", Short: "Import a context from a tar or zip file", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return RunImport(dockerCli, args[0], args[1]) + return runImport(dockerCLI, args[0], args[1]) }, // TODO(thaJeztah): this should also include "-" - ValidArgsFunction: completion.FileNames, + ValidArgsFunction: completion.FileNames(), + DisableFlagsInUseLine: true, } return cmd } -// RunImport imports a Docker context -func RunImport(dockerCli command.Cli, name string, source string) error { - if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil { +// runImport imports a Docker context. +func runImport(dockerCLI command.Cli, name string, source string) error { + if err := checkContextNameForCreation(dockerCLI.ContextStore(), name); err != nil { return err } var reader io.Reader if source == "-" { - reader = dockerCli.In() + reader = dockerCLI.In() } else { f, err := os.Open(source) if err != nil { @@ -44,11 +45,11 @@ func RunImport(dockerCli command.Cli, name string, source string) error { reader = f } - if err := store.Import(name, dockerCli.ContextStore(), reader); err != nil { + if err := store.Import(name, dockerCLI.ContextStore(), reader); err != nil { return err } - _, _ = fmt.Fprintln(dockerCli.Out(), name) - _, _ = fmt.Fprintf(dockerCli.Err(), "Successfully imported context %q\n", name) + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully imported context %q\n", name) return nil } diff --git a/cli/command/context/inspect.go b/cli/command/context/inspect.go index 790056587915..9772469e8063 100644 --- a/cli/command/context/inspect.go +++ b/cli/command/context/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package context @@ -35,7 +35,8 @@ func newInspectCommand(dockerCLI command.Cli) *cobra.Command { } return runInspect(dockerCLI, opts) }, - ValidArgsFunction: completeContextNames(dockerCLI, -1, false), + ValidArgsFunction: completeContextNames(dockerCLI, -1, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() diff --git a/cli/command/context/list.go b/cli/command/context/list.go index b4258b91a302..6065a1b85817 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package context @@ -10,12 +10,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/context/docker" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/client" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -24,7 +23,7 @@ type listOptions struct { quiet bool } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { opts := &listOptions{} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -32,9 +31,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List contexts", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, opts) + return runList(dockerCLI, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index ea7dd1e7c9e0..ccd1678ef145 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -19,10 +19,9 @@ func createTestContexts(t *testing.T, cli command.Cli, name ...string) { func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) { t.Helper() - err := RunCreate(cli, &CreateOptions{ - Name: name, - Description: "description of " + name, - Docker: map[string]string{keyHost: "https://someswarmserver.example.com"}, + err := runCreate(cli, name, createOptions{ + description: "description of " + name, + endpoint: map[string]string{keyHost: "https://someswarmserver.example.com"}, metaData: metaData, }) diff --git a/cli/command/context/options.go b/cli/command/context/options.go index 7b0d4aac92dd..429f47cc4139 100644 --- a/cli/command/context/options.go +++ b/cli/command/context/options.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/cli/context" "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) const ( @@ -123,7 +123,7 @@ func getDockerEndpoint(contextStore store.Reader, config map[string]string) (doc return docker.Endpoint{}, fmt.Errorf("invalid docker endpoint options: %w", err) } // FIXME(thaJeztah): this creates a new client (but discards it) only to validate the options; are the validation steps above not enough? - if _, err := client.NewClientWithOpts(opts...); err != nil { + if _, err := client.New(opts...); err != nil { return docker.Endpoint{}, fmt.Errorf("unable to apply docker endpoint options: %w", err) } return ep, nil diff --git a/cli/command/context/remove.go b/cli/command/context/remove.go index 0f73cb1fd749..7627ee73b175 100644 --- a/cli/command/context/remove.go +++ b/cli/command/context/remove.go @@ -10,35 +10,36 @@ import ( "github.com/spf13/cobra" ) -// RemoveOptions are the options used to remove contexts -type RemoveOptions struct { - Force bool +// removeOptions are the options used to remove contexts. +type removeOptions struct { + force bool } func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { - var opts RemoveOptions + var opts removeOptions cmd := &cobra.Command{ Use: "rm CONTEXT [CONTEXT...]", Aliases: []string{"remove"}, Short: "Remove one or more contexts", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return RunRemove(dockerCLI, opts, args) + return runRemove(dockerCLI, opts, args) }, - ValidArgsFunction: completeContextNames(dockerCLI, -1, false), + ValidArgsFunction: completeContextNames(dockerCLI, -1, false), + DisableFlagsInUseLine: true, } - cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force the removal of a context in use") + cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force the removal of a context in use") return cmd } -// RunRemove removes one or more contexts -func RunRemove(dockerCLI command.Cli, opts RemoveOptions, names []string) error { +// runRemove removes one or more contexts. +func runRemove(dockerCLI command.Cli, opts removeOptions, names []string) error { var errs []error currentCtx := dockerCLI.CurrentContext() for _, name := range names { if name == "default" { errs = append(errs, errors.New(`context "default" cannot be removed`)) - } else if err := doRemove(dockerCLI, name, name == currentCtx, opts.Force); err != nil { + } else if err := doRemove(dockerCLI, name, name == currentCtx, opts.force); err != nil { errs = append(errs, err) } else { _, _ = fmt.Fprintln(dockerCLI.Out(), name) diff --git a/cli/command/context/remove_test.go b/cli/command/context/remove_test.go index bbcddec6d16b..d121f39d6df8 100644 --- a/cli/command/context/remove_test.go +++ b/cli/command/context/remove_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "gotest.tools/v3/assert" @@ -14,20 +14,20 @@ import ( func TestRemove(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") - assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"})) + assert.NilError(t, runRemove(cli, removeOptions{}, []string{"other"})) _, err := cli.ContextStore().GetMetadata("current") assert.NilError(t, err) _, err = cli.ContextStore().GetMetadata("other") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } func TestRemoveNotAContext(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") - err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"}) + err := runRemove(cli, removeOptions{}, []string{"not-a-context"}) assert.ErrorContains(t, err, `context "not-a-context" does not exist`) - err = RunRemove(cli, RemoveOptions{Force: true}, []string{"not-a-context"}) + err = runRemove(cli, removeOptions{force: true}, []string{"not-a-context"}) assert.NilError(t, err) } @@ -35,7 +35,7 @@ func TestRemoveCurrent(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") cli.SetCurrentContext("current") - err := RunRemove(cli, RemoveOptions{}, []string{"current"}) + err := runRemove(cli, removeOptions{}, []string{"current"}) assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`) } @@ -49,7 +49,7 @@ func TestRemoveCurrentForce(t *testing.T) { cli := makeFakeCli(t, withCliConfig(testCfg)) createTestContexts(t, cli, "current", "other") cli.SetCurrentContext("current") - assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"})) + assert.NilError(t, runRemove(cli, removeOptions{force: true}, []string{"current"})) reloadedConfig, err := config.Load(configDir) assert.NilError(t, err) assert.Equal(t, "", reloadedConfig.CurrentContext) @@ -59,6 +59,6 @@ func TestRemoveDefault(t *testing.T) { cli := makeFakeCli(t) createTestContext(t, cli, "other", nil) cli.SetCurrentContext("current") - err := RunRemove(cli, RemoveOptions{}, []string{"default"}) + err := runRemove(cli, removeOptions{}, []string{"default"}) assert.ErrorContains(t, err, `context "default" cannot be removed`) } diff --git a/cli/command/context/show.go b/cli/command/context/show.go index dd33f9e02a84..0418444cf228 100644 --- a/cli/command/context/show.go +++ b/cli/command/context/show.go @@ -5,21 +5,21 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/spf13/cobra" ) // newShowCommand creates a new cobra.Command for `docker context sow` -func newShowCommand(dockerCli command.Cli) *cobra.Command { +func newShowCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "Print the name of the current context", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - runShow(dockerCli) + runShow(dockerCLI) return nil }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } return cmd } diff --git a/cli/command/context/update.go b/cli/command/context/update.go index 0995c52ef553..cdd79e578d33 100644 --- a/cli/command/context/update.go +++ b/cli/command/context/update.go @@ -12,11 +12,10 @@ import ( "github.com/spf13/cobra" ) -// UpdateOptions are the options used to update a context -type UpdateOptions struct { - Name string - Description string - Docker map[string]string +// updateOptions are the options used to update a context. +type updateOptions struct { + description string + endpoint map[string]string } func longUpdateDescription() string { @@ -33,31 +32,31 @@ func longUpdateDescription() string { } func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { - opts := &UpdateOptions{} + opts := updateOptions{} cmd := &cobra.Command{ Use: "update [OPTIONS] CONTEXT", Short: "Update a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Name = args[0] - return RunUpdate(dockerCLI, opts) + return runUpdate(dockerCLI, args[0], opts) }, - Long: longUpdateDescription(), - ValidArgsFunction: completeContextNames(dockerCLI, 1, false), + Long: longUpdateDescription(), + ValidArgsFunction: completeContextNames(dockerCLI, 1, false), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringVar(&opts.Description, "description", "", "Description of the context") - flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") + flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringToStringVar(&opts.endpoint, "docker", nil, "set the docker endpoint") return cmd } -// RunUpdate updates a Docker context -func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { - if err := store.ValidateContextName(o.Name); err != nil { +// runUpdate updates a Docker context. +func runUpdate(dockerCLI command.Cli, name string, opts updateOptions) error { + if err := store.ValidateContextName(name); err != nil { return err } s := dockerCLI.ContextStore() - c, err := s.GetMetadata(o.Name) + c, err := s.GetMetadata(name) if err != nil { return err } @@ -65,16 +64,16 @@ func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { if err != nil { return err } - if o.Description != "" { - dockerContext.Description = o.Description + if opts.description != "" { + dockerContext.Description = opts.description } c.Metadata = dockerContext tlsDataToReset := make(map[string]*store.EndpointTLSData) - if o.Docker != nil { - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(s, o.Docker) + if opts.endpoint != nil { + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(s, opts.endpoint) if err != nil { return fmt.Errorf("unable to create docker endpoint config: %w", err) } @@ -88,13 +87,13 @@ func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { return err } for ep, tlsData := range tlsDataToReset { - if err := s.ResetEndpointTLSMaterial(o.Name, ep, tlsData); err != nil { + if err := s.ResetEndpointTLSMaterial(name, ep, tlsData); err != nil { return err } } - _, _ = fmt.Fprintln(dockerCLI.Out(), o.Name) - _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully updated context %q\n", o.Name) + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully updated context %q\n", name) return nil } diff --git a/cli/command/context/update_test.go b/cli/command/context/update_test.go index 0e4a63914559..c2fc968c2ab5 100644 --- a/cli/command/context/update_test.go +++ b/cli/command/context/update_test.go @@ -11,16 +11,14 @@ import ( func TestUpdateDescriptionOnly(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{}, }) assert.NilError(t, err) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunUpdate(cli, &UpdateOptions{ - Name: "test", - Description: "description", + assert.NilError(t, runUpdate(cli, "test", updateOptions{ + description: "description", })) c, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) @@ -35,9 +33,8 @@ func TestUpdateDescriptionOnly(t *testing.T) { func TestUpdateDockerOnly(t *testing.T) { cli := makeFakeCli(t) createTestContext(t, cli, "test", nil) - assert.NilError(t, RunUpdate(cli, &UpdateOptions{ - Name: "test", - Docker: map[string]string{ + assert.NilError(t, runUpdate(cli, "test", updateOptions{ + endpoint: map[string]string{ keyHost: "tcp://some-host", }, })) @@ -52,14 +49,12 @@ func TestUpdateDockerOnly(t *testing.T) { func TestUpdateInvalidDockerHost(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{}, }) assert.NilError(t, err) - err = RunUpdate(cli, &UpdateOptions{ - Name: "test", - Docker: map[string]string{ + err = runUpdate(cli, "test", updateOptions{ + endpoint: map[string]string{ keyHost: "some///invalid/host", }, }) diff --git a/cli/command/context/use.go b/cli/command/context/use.go index 6b0d927a80af..c6e23f139d02 100644 --- a/cli/command/context/use.go +++ b/cli/command/context/use.go @@ -6,7 +6,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/context/store" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,15 +17,16 @@ func newUseCommand(dockerCLI command.Cli) *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - return RunUse(dockerCLI, name) + return runUse(dockerCLI, name) }, - ValidArgsFunction: completeContextNames(dockerCLI, 1, false), + ValidArgsFunction: completeContextNames(dockerCLI, 1, false), + DisableFlagsInUseLine: true, } return cmd } -// RunUse set the current Docker context -func RunUse(dockerCLI command.Cli, name string) error { +// runUse set the current Docker context +func runUse(dockerCLI command.Cli, name string) error { // configValue uses an empty string for "default" var configValue string if name != command.DefaultContextName { diff --git a/cli/command/context/use_test.go b/cli/command/context/use_test.go index 8c7265dac829..997f5c1474a5 100644 --- a/cli/command/context/use_test.go +++ b/cli/command/context/use_test.go @@ -9,7 +9,7 @@ import ( "runtime" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" @@ -23,9 +23,8 @@ func TestUse(t *testing.T) { configFilePath := filepath.Join(configDir, "config.json") testCfg := configfile.New(configFilePath) cli := makeFakeCli(t, withCliConfig(testCfg)) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{}, }) assert.NilError(t, err) assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"})) @@ -47,7 +46,7 @@ func TestUse(t *testing.T) { func TestUseNoExist(t *testing.T) { cli := makeFakeCli(t) err := newUseCommand(cli).RunE(nil, []string{"test"}) - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } // TestUseDefaultWithoutConfigFile verifies that the CLI does not create @@ -89,9 +88,8 @@ func TestUseHostOverride(t *testing.T) { configFilePath := filepath.Join(configDir, "config.json") testCfg := configfile.New(configFilePath) cli := makeFakeCli(t, withCliConfig(testCfg)) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{}, }) assert.NilError(t, err) @@ -136,9 +134,8 @@ func TestUseHostOverrideEmpty(t *testing.T) { assert.NilError(t, cli.Initialize(flags.NewClientOptions())) } loadCli() - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{"host": socketPath}, + err := runCreate(cli, "test", createOptions{ + endpoint: map[string]string{"host": socketPath}, }) assert.NilError(t, err) diff --git a/cli/command/context_test.go b/cli/command/context_test.go index 0f216da97e61..3f3b03c77259 100644 --- a/cli/command/context_test.go +++ b/cli/command/context_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command diff --git a/cli/command/defaultcontextstore.go b/cli/command/defaultcontextstore.go index 9b49b3af2a68..e6315b8e89a4 100644 --- a/cli/command/defaultcontextstore.go +++ b/cli/command/defaultcontextstore.go @@ -1,13 +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.23 +//go:build go1.24 package command import ( + "errors" + "fmt" + "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" cliflags "github.com/docker/cli/cli/flags" - "github.com/pkg/errors" ) const ( @@ -51,8 +53,8 @@ type EndpointDefaultResolver interface { ResolveDefault() (any, *store.EndpointTLSData, error) } -// ResolveDefaultContext creates a Metadata for the current CLI invocation parameters -func ResolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) { +// resolveDefaultContext creates a Metadata for the current CLI invocation parameters +func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) { contextTLSData := store.ContextTLSData{ Endpoints: make(map[string]store.EndpointTLSData), } @@ -185,7 +187,7 @@ func (s *ContextStoreWithDefault) GetTLSData(contextName, endpointName, fileName return nil, err } if defaultContext.TLS.Endpoints[endpointName].Files[fileName] == nil { - return nil, notFound(errors.Errorf("TLS data for %s/%s/%s does not exist", DefaultContextName, endpointName, fileName)) + return nil, notFound(fmt.Errorf("TLS data for %s/%s/%s does not exist", DefaultContextName, endpointName, fileName)) } return defaultContext.TLS.Endpoints[endpointName].Files[fileName], nil } diff --git a/cli/command/defaultcontextstore_test.go b/cli/command/defaultcontextstore_test.go index d0c8d09b758a..5231cc9ff653 100644 --- a/cli/command/defaultcontextstore_test.go +++ b/cli/command/defaultcontextstore_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command @@ -7,7 +7,7 @@ import ( "crypto/rand" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" @@ -59,7 +59,7 @@ func TestDefaultContextInitializer(t *testing.T) { assert.NilError(t, err) t.Setenv("DOCKER_HOST", "ssh://someswarmserver") cli.configFile = &configfile.ConfigFile{} - ctx, err := ResolveDefaultContext(&cliflags.ClientOptions{ + ctx, err := resolveDefaultContext(&cliflags.ClientOptions{ TLS: true, TLSOptions: &tlsconfig.Options{ CAFile: "./testdata/ca.pem", @@ -158,7 +158,7 @@ func TestErrCreateDefault(t *testing.T) { Metadata: testContext{Bar: "baz"}, Name: "default", }) - assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) + assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument)) assert.Error(t, err, "default context cannot be created nor updated") } @@ -166,7 +166,7 @@ func TestErrRemoveDefault(t *testing.T) { meta := testDefaultMetadata() s := testStore(t, meta, store.ContextTLSData{}) err := s.Remove("default") - assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) + assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument)) assert.Error(t, err, "default context cannot be removed") } @@ -174,5 +174,5 @@ func TestErrTLSDataError(t *testing.T) { meta := testDefaultMetadata() s := testStore(t, meta, store.ContextTLSData{}) _, err := s.GetTLSData("default", "noop", "noop") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } diff --git a/cli/command/formatter/buildcache.go b/cli/command/formatter/buildcache.go index ade5de73f4fc..3a3c349988ef 100644 --- a/cli/command/formatter/buildcache.go +++ b/cli/command/formatter/buildcache.go @@ -6,8 +6,8 @@ import ( "strings" "time" - "github.com/docker/docker/api/types/build" "github.com/docker/go-units" + "github.com/moby/moby/api/types/build" ) const ( @@ -51,7 +51,7 @@ shared: {{.Shared}} return Format(source) } -func buildCacheSort(buildCache []*build.CacheRecord) { +func buildCacheSort(buildCache []build.CacheRecord) { sort.Slice(buildCache, func(i, j int) bool { lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt switch { @@ -70,7 +70,7 @@ func buildCacheSort(buildCache []*build.CacheRecord) { } // BuildCacheWrite renders the context for a list of containers -func BuildCacheWrite(ctx Context, buildCaches []*build.CacheRecord) error { +func BuildCacheWrite(ctx Context, buildCaches []build.CacheRecord) error { render := func(format func(subContext SubContext) error) error { buildCacheSort(buildCaches) for _, bc := range buildCaches { @@ -87,7 +87,7 @@ func BuildCacheWrite(ctx Context, buildCaches []*build.CacheRecord) error { type buildCacheContext struct { HeaderContext trunc bool - v *build.CacheRecord + v build.CacheRecord } func newBuildCacheContext() *buildCacheContext { @@ -126,8 +126,6 @@ func (c *buildCacheContext) Parent() string { var parent string if len(c.v.Parents) > 0 { parent = strings.Join(c.v.Parents, ", ") - } else { - parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility } if c.trunc { return TruncateID(parent) diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 0a5c587afe23..979d3eb82d60 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter @@ -13,8 +13,8 @@ import ( "github.com/containerd/platforms" "github.com/distribution/reference" - "github.com/docker/docker/api/types/container" "github.com/docker/go-units" + "github.com/moby/moby/api/types/container" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -170,27 +170,33 @@ func (c *ContainerContext) Image() string { if c.c.Image == "" { return "" } - if c.trunc { - if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) { - return trunc + if !c.trunc { + return c.c.Image + } + if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) { + return trunc + } + ref, err := reference.ParseNormalizedNamed(c.c.Image) + if err != nil { + return c.c.Image + } + + if _, ok := ref.(reference.Digested); ok { + // strip the digest, but preserve the tag (if any) + var tag string + if t, ok := ref.(reference.Tagged); ok { + tag = t.Tag() } - // truncate digest if no-trunc option was not selected - ref, err := reference.ParseNormalizedNamed(c.c.Image) - if err == nil { - if nt, ok := ref.(reference.NamedTagged); ok { - // case for when a tag is provided - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - return reference.FamiliarString(namedTagged) - } - } else { - // case for when a tag is not provided - named := reference.TrimNamed(ref) - return reference.FamiliarString(named) + ref = reference.TrimNamed(ref) + if tag != "" { + if out, err := reference.WithTag(ref, tag); err == nil { + ref = out } } } - return c.c.Image + // Format as "familiar" name with "docker.io[/library]" trimmed. + return reference.FamiliarString(ref) } // Command returns's the container's command. If the trunc option is set, the @@ -241,7 +247,7 @@ func (c *ContainerContext) Ports() string { // State returns the container's current state (e.g. "running" or "paused"). // Refer to [container.ContainerState] for possible states. func (c *ContainerContext) State() string { - return c.c.State + return string(c.c.State) } // Status returns the container's status in a human readable form (for example, @@ -338,7 +344,7 @@ func (c *ContainerContext) Networks() string { // DisplayablePorts returns formatted string representing open ports of container // e.g. "0.0.0.0:80->9090/tcp, 9988/tcp" // it's used by command 'docker ps' -func DisplayablePorts(ports []container.Port) string { +func DisplayablePorts(ports []container.PortSummary) string { type portGroup struct { first uint16 last uint16 @@ -354,13 +360,13 @@ func DisplayablePorts(ports []container.Port) string { for _, port := range ports { current := port.PrivatePort portKey := port.Type - if port.IP != "" { + if port.IP.IsValid() { if port.PublicPort != current { - hAddrPort := net.JoinHostPort(port.IP, strconv.Itoa(int(port.PublicPort))) + hAddrPort := net.JoinHostPort(port.IP.String(), strconv.Itoa(int(port.PublicPort))) hostMappings = append(hostMappings, fmt.Sprintf("%s->%d/%s", hAddrPort, port.PrivatePort, port.Type)) continue } - portKey = port.IP + "/" + port.Type + portKey = port.IP.String() + "/" + port.Type } group := groupMap[portKey] @@ -404,13 +410,13 @@ func formGroup(key string, start, last uint16) string { return group + "/" + groupType } -func comparePorts(i, j container.Port) bool { +func comparePorts(i, j container.PortSummary) bool { if i.PrivatePort != j.PrivatePort { return i.PrivatePort < j.PrivatePort } if i.IP != j.IP { - return i.IP < j.IP + return i.IP.String() < j.IP.String() } if i.PublicPort != j.PublicPort { diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 6974584003cc..a38e75e87d53 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter @@ -7,12 +7,13 @@ import ( "bytes" "encoding/json" "fmt" + "net/netip" "strings" "testing" "time" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -54,14 +55,51 @@ func TestContainerPsContext(t *testing.T) { call: ctx.Image, }, { - container: container.Summary{Image: "verylongimagename"}, + container: container.Summary{Image: "ubuntu:latest"}, trunc: true, - expValue: "verylongimagename", + expValue: "ubuntu:latest", call: ctx.Image, }, { - container: container.Summary{Image: "verylongimagename"}, - expValue: "verylongimagename", + container: container.Summary{Image: "docker.io/library/ubuntu"}, + trunc: true, + expValue: "ubuntu", + call: ctx.Image, + }, + { + container: container.Summary{Image: "docker.io/library/ubuntu:latest"}, + trunc: true, + expValue: "ubuntu:latest", + call: ctx.Image, + }, + { + container: container.Summary{Image: "ubuntu:latest@sha256:a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5"}, + trunc: true, + expValue: "ubuntu:latest", + call: ctx.Image, + }, + { + container: container.Summary{Image: "ubuntu@sha256:a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5"}, + trunc: true, + expValue: "ubuntu", + call: ctx.Image, + }, + { + container: container.Summary{Image: "docker.io/library/ubuntu@sha256:a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5"}, + trunc: true, + expValue: "ubuntu", + call: ctx.Image, + }, + { + container: container.Summary{Image: "docker.io/library/ubuntu:latest@sha256:a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5"}, + trunc: true, + expValue: "ubuntu:latest", + call: ctx.Image, + }, + { + container: container.Summary{Image: "verylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagename"}, + trunc: true, + expValue: "verylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagenameverylongimagename", call: ctx.Image, }, { @@ -100,7 +138,7 @@ func TestContainerPsContext(t *testing.T) { call: ctx.CreatedAt, }, { - container: container.Summary{Ports: []container.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, + container: container.Summary{Ports: []container.PortSummary{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, trunc: true, expValue: "8080/tcp", call: ctx.Ports, @@ -114,7 +152,7 @@ func TestContainerPsContext(t *testing.T) { { container: container.Summary{State: container.StateRunning}, trunc: true, - expValue: container.StateRunning, + expValue: string(container.StateRunning), call: ctx.State, }, { @@ -549,7 +587,7 @@ func TestContainerBackCompat(t *testing.T) { ImageManifestDescriptor: nil, Command: "/bin/sh", Created: createdAtTime.UTC().Unix(), - Ports: []container.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}, + Ports: []container.PortSummary{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}, SizeRw: 123, SizeRootFs: 12345, Labels: map[string]string{"label1": "value1", "label2": "value2"}, @@ -596,14 +634,14 @@ func TestContainerBackCompat(t *testing.T) { } type ports struct { - ports []container.Port + ports []container.PortSummary expected string } func TestDisplayablePorts(t *testing.T) { cases := []ports{ { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9988, Type: "tcp", @@ -612,7 +650,7 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9988, Type: "udp", @@ -621,9 +659,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "0.0.0.0", + IP: netip.MustParseAddr("0.0.0.0"), PrivatePort: 9988, Type: "tcp", }, @@ -631,9 +669,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "0.0.0.0:0->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "::", + IP: netip.MustParseAddr("::"), PrivatePort: 9988, Type: "tcp", }, @@ -641,7 +679,7 @@ func TestDisplayablePorts(t *testing.T) { expected: "[::]:0->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9988, PublicPort: 8899, @@ -651,9 +689,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -662,9 +700,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "4.3.2.1:8899->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", @@ -673,9 +711,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "[::1]:8899->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -684,9 +722,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "4.3.2.1:9988->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 9988, PublicPort: 9988, Type: "tcp", @@ -695,7 +733,7 @@ func TestDisplayablePorts(t *testing.T) { expected: "[::1]:9988->9988/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9988, Type: "udp", @@ -707,14 +745,14 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/udp, 9988/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -723,14 +761,14 @@ func TestDisplayablePorts(t *testing.T) { expected: "1.2.3.4:9998-9999->9998-9999/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9998, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 9999, PrivatePort: 9999, Type: "udp", @@ -739,14 +777,14 @@ func TestDisplayablePorts(t *testing.T) { expected: "[::1]:9998-9999->9998-9999/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -755,14 +793,14 @@ func TestDisplayablePorts(t *testing.T) { expected: "1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8887, PrivatePort: 9998, Type: "udp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PublicPort: 8888, PrivatePort: 9999, Type: "udp", @@ -771,7 +809,7 @@ func TestDisplayablePorts(t *testing.T) { expected: "[::1]:8887->9998/udp, [::1]:8888->9999/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9998, Type: "udp", @@ -783,9 +821,9 @@ func TestDisplayablePorts(t *testing.T) { expected: "9998-9999/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", @@ -798,24 +836,24 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/udp, 1.2.3.4:7766->6677/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 9988, PublicPort: 8899, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", }, { - IP: "::1", + IP: netip.MustParseAddr("::1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -824,18 +862,18 @@ func TestDisplayablePorts(t *testing.T) { expected: "4.3.2.1:3322->2233/tcp, [::1]:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 9988, PublicPort: 8899, Type: "udp", }, { - IP: "1.2.3.4", + IP: netip.MustParseAddr("1.2.3.4"), PrivatePort: 6677, PublicPort: 7766, Type: "tcp", }, { - IP: "4.3.2.1", + IP: netip.MustParseAddr("4.3.2.1"), PrivatePort: 2233, PublicPort: 3322, Type: "tcp", @@ -844,7 +882,7 @@ func TestDisplayablePorts(t *testing.T) { expected: "9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp", }, { - ports: []container.Port{ + ports: []container.PortSummary{ { PrivatePort: 80, Type: "tcp", @@ -858,42 +896,42 @@ func TestDisplayablePorts(t *testing.T) { PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "1.1.1.1", + IP: netip.MustParseAddr("1.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 80, PrivatePort: 1024, Type: "udp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "tcp", }, { - IP: "2.1.1.1", + IP: netip.MustParseAddr("2.1.1.1"), PublicPort: 1024, PrivatePort: 80, Type: "udp", diff --git a/cli/command/formatter/custom.go b/cli/command/formatter/custom.go index c2b9cb2c56a3..27931af76eed 100644 --- a/cli/command/formatter/custom.go +++ b/cli/command/formatter/custom.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go index b663c59b20b7..b14afc1a06c3 100644 --- a/cli/command/formatter/disk_usage.go +++ b/cli/command/formatter/disk_usage.go @@ -7,19 +7,20 @@ import ( "text/template" "github.com/distribution/reference" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/volume" "github.com/docker/go-units" + "github.com/moby/moby/api/types/build" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" ) const ( - defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}" - defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}" - defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}" - defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}" - defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" + defaultDiskUsageImageTableFormat Format = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}" + defaultDiskUsageContainerTableFormat Format = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}" + defaultDiskUsageVolumeTableFormat Format = "table {{.Name}}\t{{.Links}}\t{{.Size}}" + defaultDiskUsageBuildCacheTableFormat Format = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}" + defaultDiskUsageTableFormat Format = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" typeHeader = "TYPE" totalHeader = "TOTAL" @@ -33,19 +34,18 @@ const ( // DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct. type DiskUsageContext struct { Context - Verbose bool - LayersSize int64 - Images []*image.Summary - Containers []*container.Summary - Volumes []*volume.Volume - BuildCache []*build.CacheRecord - BuilderSize int64 + Verbose bool + + ImageDiskUsage client.ImagesDiskUsage + BuildCacheDiskUsage client.BuildCacheDiskUsage + ContainerDiskUsage client.ContainersDiskUsage + VolumeDiskUsage client.VolumesDiskUsage } -func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) { +func (ctx *DiskUsageContext) startSubsection(format Format) (*template.Template, error) { ctx.buffer = &bytes.Buffer{} ctx.header = "" - ctx.Format = Format(format) + ctx.Format = format ctx.preFormat() return ctx.parseFormat() @@ -69,7 +69,7 @@ func NewDiskUsageFormat(source string, verbose bool) Format { {{end -}}` return format case !verbose && source == TableFormatKey: - return Format(defaultDiskUsageTableFormat) + return defaultDiskUsageTableFormat case !verbose && source == RawFormatKey: format := `type: {{.Type}} total: {{.TotalCount}} @@ -96,35 +96,49 @@ func (ctx *DiskUsageContext) Write() (err error) { } err = ctx.contextFormat(tmpl, &diskUsageImagesContext{ - totalSize: ctx.LayersSize, - images: ctx.Images, + totalCount: ctx.ImageDiskUsage.TotalCount, + activeCount: ctx.ImageDiskUsage.ActiveCount, + totalSize: ctx.ImageDiskUsage.TotalSize, + reclaimable: ctx.ImageDiskUsage.Reclaimable, + images: ctx.ImageDiskUsage.Items, }) if err != nil { return err } err = ctx.contextFormat(tmpl, &diskUsageContainersContext{ - containers: ctx.Containers, + totalCount: ctx.ContainerDiskUsage.TotalCount, + activeCount: ctx.ContainerDiskUsage.ActiveCount, + totalSize: ctx.ContainerDiskUsage.TotalSize, + reclaimable: ctx.ContainerDiskUsage.Reclaimable, + containers: ctx.ContainerDiskUsage.Items, }) if err != nil { return err } err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{ - volumes: ctx.Volumes, + totalCount: ctx.VolumeDiskUsage.TotalCount, + activeCount: ctx.VolumeDiskUsage.ActiveCount, + totalSize: ctx.VolumeDiskUsage.TotalSize, + reclaimable: ctx.VolumeDiskUsage.Reclaimable, + volumes: ctx.VolumeDiskUsage.Items, }) if err != nil { return err } err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{ - builderSize: ctx.BuilderSize, - buildCache: ctx.BuildCache, + totalCount: ctx.BuildCacheDiskUsage.TotalCount, + activeCount: ctx.BuildCacheDiskUsage.ActiveCount, + builderSize: ctx.BuildCacheDiskUsage.TotalSize, + reclaimable: ctx.BuildCacheDiskUsage.Reclaimable, + buildCache: ctx.BuildCacheDiskUsage.Items, }) if err != nil { return err } - diskUsageContainersCtx := diskUsageContainersContext{containers: []*container.Summary{}} + diskUsageContainersCtx := diskUsageContainersContext{containers: []container.Summary{}} diskUsageContainersCtx.Header = SubHeaderContext{ "Type": typeHeader, "TotalCount": totalHeader, @@ -146,18 +160,18 @@ type diskUsageContext struct { func (ctx *DiskUsageContext) verboseWrite() error { duc := &diskUsageContext{ - Images: make([]*imageContext, 0, len(ctx.Images)), - Containers: make([]*ContainerContext, 0, len(ctx.Containers)), - Volumes: make([]*volumeContext, 0, len(ctx.Volumes)), - BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)), + Images: make([]*imageContext, 0, len(ctx.ImageDiskUsage.Items)), + Containers: make([]*ContainerContext, 0, len(ctx.ContainerDiskUsage.Items)), + Volumes: make([]*volumeContext, 0, len(ctx.VolumeDiskUsage.Items)), + BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCacheDiskUsage.Items)), } trunc := ctx.Format.IsTable() // First images - for _, i := range ctx.Images { + for _, i := range ctx.ImageDiskUsage.Items { repo := "" tag := "" - if len(i.RepoTags) > 0 && !isDangling(*i) { + if len(i.RepoTags) > 0 && !isDangling(i) { // Only show the first tag ref, err := reference.ParseNormalizedNamed(i.RepoTags[0]) if err != nil { @@ -173,25 +187,25 @@ func (ctx *DiskUsageContext) verboseWrite() error { repo: repo, tag: tag, trunc: trunc, - i: *i, + i: i, }) } // Now containers - for _, c := range ctx.Containers { + for _, c := range ctx.ContainerDiskUsage.Items { // Don't display the virtual size c.SizeRootFs = 0 - duc.Containers = append(duc.Containers, &ContainerContext{trunc: trunc, c: *c}) + duc.Containers = append(duc.Containers, &ContainerContext{trunc: trunc, c: c}) } // And volumes - for _, v := range ctx.Volumes { - duc.Volumes = append(duc.Volumes, &volumeContext{v: *v}) + for _, v := range ctx.VolumeDiskUsage.Items { + duc.Volumes = append(duc.Volumes, &volumeContext{v: v}) } // And build cache - buildCacheSort(ctx.BuildCache) - for _, v := range ctx.BuildCache { + buildCacheSort(ctx.BuildCacheDiskUsage.Items) + for _, v := range ctx.BuildCacheDiskUsage.Items { duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc}) } @@ -212,7 +226,7 @@ func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error { if err != nil { return err } - ctx.Output.Write([]byte("Images space usage:\n\n")) + _, _ = ctx.Output.Write([]byte("Images space usage:\n\n")) for _, img := range duc.Images { if err := ctx.contextFormat(tmpl, img); err != nil { return err @@ -224,7 +238,7 @@ func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error { if err != nil { return err } - ctx.Output.Write([]byte("\nContainers space usage:\n\n")) + _, _ = ctx.Output.Write([]byte("\nContainers space usage:\n\n")) for _, c := range duc.Containers { if err := ctx.contextFormat(tmpl, c); err != nil { return err @@ -248,7 +262,7 @@ func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error { if err != nil { return err } - _, _ = fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize))) + _, _ = fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuildCacheDiskUsage.TotalSize))) for _, v := range duc.BuildCache { if err := ctx.contextFormat(tmpl, v); err != nil { return err @@ -261,8 +275,11 @@ func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error { type diskUsageImagesContext struct { HeaderContext - totalSize int64 - images []*image.Summary + totalSize int64 + reclaimable int64 + totalCount int64 + activeCount int64 + images []image.Summary } func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { @@ -274,18 +291,11 @@ func (*diskUsageImagesContext) Type() string { } func (c *diskUsageImagesContext) TotalCount() string { - return strconv.Itoa(len(c.images)) + return strconv.FormatInt(c.totalCount, 10) } func (c *diskUsageImagesContext) Active() string { - used := 0 - for _, i := range c.images { - if i.Containers > 0 { - used++ - } - } - - return strconv.Itoa(used) + return strconv.FormatInt(c.activeCount, 10) } func (c *diskUsageImagesContext) Size() string { @@ -293,27 +303,19 @@ func (c *diskUsageImagesContext) Size() string { } func (c *diskUsageImagesContext) Reclaimable() string { - var used int64 - - for _, i := range c.images { - if i.Containers != 0 { - if i.Size == -1 || i.SharedSize == -1 { - continue - } - used += i.Size - i.SharedSize - } - } - - reclaimable := c.totalSize - used if c.totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize) + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(c.reclaimable)), (c.reclaimable*100)/c.totalSize) } - return units.HumanSize(float64(reclaimable)) + return units.HumanSize(float64(c.reclaimable)) } type diskUsageContainersContext struct { HeaderContext - containers []*container.Summary + totalCount int64 + activeCount int64 + totalSize int64 + reclaimable int64 + containers []container.Summary } func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { @@ -325,62 +327,32 @@ func (*diskUsageContainersContext) Type() string { } func (c *diskUsageContainersContext) TotalCount() string { - return strconv.Itoa(len(c.containers)) -} - -func (*diskUsageContainersContext) isActive(ctr container.Summary) bool { - switch ctr.State { - case container.StateRunning, container.StatePaused, container.StateRestarting: - return true - case container.StateCreated, container.StateRemoving, container.StateExited, container.StateDead: - return false - default: - // Unknown state (should never happen). - return false - } + return strconv.FormatInt(c.totalCount, 10) } func (c *diskUsageContainersContext) Active() string { - used := 0 - for _, ctr := range c.containers { - if c.isActive(*ctr) { - used++ - } - } - - return strconv.Itoa(used) + return strconv.FormatInt(c.activeCount, 10) } func (c *diskUsageContainersContext) Size() string { - var size int64 - - for _, ctr := range c.containers { - size += ctr.SizeRw - } - - return units.HumanSize(float64(size)) + return units.HumanSize(float64(c.totalSize)) } func (c *diskUsageContainersContext) Reclaimable() string { - var reclaimable, totalSize int64 - - for _, ctr := range c.containers { - if !c.isActive(*ctr) { - reclaimable += ctr.SizeRw - } - totalSize += ctr.SizeRw - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) + if c.totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(c.reclaimable)), (c.reclaimable*100)/c.totalSize) } - return units.HumanSize(float64(reclaimable)) + return units.HumanSize(float64(c.reclaimable)) } type diskUsageVolumesContext struct { HeaderContext - volumes []*volume.Volume + totalCount int64 + activeCount int64 + totalSize int64 + reclaimable int64 + volumes []volume.Volume } func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { @@ -392,56 +364,32 @@ func (*diskUsageVolumesContext) Type() string { } func (c *diskUsageVolumesContext) TotalCount() string { - return strconv.Itoa(len(c.volumes)) + return strconv.FormatInt(c.totalCount, 10) } func (c *diskUsageVolumesContext) Active() string { - used := 0 - for _, v := range c.volumes { - if v.UsageData.RefCount > 0 { - used++ - } - } - - return strconv.Itoa(used) + return strconv.FormatInt(c.activeCount, 10) } func (c *diskUsageVolumesContext) Size() string { - var size int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - size += v.UsageData.Size - } - } - - return units.HumanSize(float64(size)) + return units.HumanSize(float64(c.totalSize)) } func (c *diskUsageVolumesContext) Reclaimable() string { - var reclaimable int64 - var totalSize int64 - - for _, v := range c.volumes { - if v.UsageData.Size != -1 { - if v.UsageData.RefCount == 0 { - reclaimable += v.UsageData.Size - } - totalSize += v.UsageData.Size - } - } - - if totalSize > 0 { - return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) + if c.totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(c.reclaimable)), (c.reclaimable*100)/c.totalSize) } - return units.HumanSize(float64(reclaimable)) + return units.HumanSize(float64(c.reclaimable)) } type diskUsageBuilderContext struct { HeaderContext + totalCount int64 + activeCount int64 builderSize int64 - buildCache []*build.CacheRecord + reclaimable int64 + buildCache []build.CacheRecord } func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) { @@ -453,17 +401,11 @@ func (*diskUsageBuilderContext) Type() string { } func (c *diskUsageBuilderContext) TotalCount() string { - return strconv.Itoa(len(c.buildCache)) + return strconv.FormatInt(c.totalCount, 10) } func (c *diskUsageBuilderContext) Active() string { - numActive := 0 - for _, bc := range c.buildCache { - if bc.InUse { - numActive++ - } - } - return strconv.Itoa(numActive) + return strconv.FormatInt(c.activeCount, 10) } func (c *diskUsageBuilderContext) Size() string { @@ -471,12 +413,5 @@ func (c *diskUsageBuilderContext) Size() string { } func (c *diskUsageBuilderContext) Reclaimable() string { - var inUseBytes int64 - for _, bc := range c.buildCache { - if bc.InUse && !bc.Shared { - inUseBytes += bc.Size - } - } - - return units.HumanSize(float64(c.builderSize - inUseBytes)) + return units.HumanSize(float64(c.reclaimable)) } diff --git a/cli/command/formatter/displayutils.go b/cli/command/formatter/displayutils.go index b062c3391bcb..7b0bd687ff59 100644 --- a/cli/command/formatter/displayutils.go +++ b/cli/command/formatter/displayutils.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter @@ -8,6 +8,7 @@ import ( "strings" "unicode/utf8" + "github.com/moby/moby/client/pkg/stringid" "golang.org/x/text/width" ) @@ -27,23 +28,12 @@ func charWidth(r rune) int { } } -const shortLen = 12 - // TruncateID returns a shorthand version of a string identifier for presentation, // after trimming digest algorithm prefix (if any). // -// This function is a copy of [stringid.TruncateID] for presentation / formatting -// purposes. -// -// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19 +// This function is a wrapper for [stringid.TruncateID] for convenience. func TruncateID(id string) string { - if i := strings.IndexRune(id, ':'); i >= 0 { - id = id[i+1:] - } - if len(id) > shortLen { - id = id[:shortLen] - } - return id + return stringid.TruncateID(id) } // Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…). diff --git a/cli/command/formatter/formatter.go b/cli/command/formatter/formatter.go index 7803cabe45d8..88905cd1be5c 100644 --- a/cli/command/formatter/formatter.go +++ b/cli/command/formatter/formatter.go @@ -1,17 +1,17 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter import ( "bytes" + "fmt" "io" "strings" "text/template" "github.com/docker/cli/cli/command/formatter/tabwriter" "github.com/docker/cli/templates" - "github.com/pkg/errors" ) // Format keys used to specify certain kinds of output formats @@ -76,7 +76,7 @@ func (c *Context) preFormat() { func (c *Context) parseFormat() (*template.Template, error) { tmpl, err := templates.Parse(c.finalFormat) if err != nil { - return nil, errors.Wrap(err, "template parsing error") + return nil, fmt.Errorf("template parsing error: %w", err) } return tmpl, nil } @@ -100,7 +100,7 @@ func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) { func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error { if err := tmpl.Execute(c.buffer, subContext); err != nil { - return errors.Wrap(err, "template parsing error") + return fmt.Errorf("template parsing error: %w", err) } if c.Format.IsTable() && c.header != nil { c.header = subContext.FullHeader() diff --git a/cli/command/formatter/formatter_test.go b/cli/command/formatter/formatter_test.go index e217285a8e20..6d58aaad36d4 100644 --- a/cli/command/formatter/formatter_test.go +++ b/cli/command/formatter/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go index 74c2fe758f84..d24bf50947ce 100644 --- a/cli/command/formatter/image.go +++ b/cli/command/formatter/image.go @@ -5,8 +5,8 @@ import ( "time" "github.com/distribution/reference" - "github.com/docker/docker/api/types/image" "github.com/docker/go-units" + "github.com/moby/moby/api/types/image" ) const ( @@ -202,7 +202,6 @@ func newImageContext() *imageContext { "CreatedAt": CreatedAtHeader, "Size": SizeHeader, "Containers": containersHeader, - "VirtualSize": SizeHeader, // Deprecated: VirtualSize is deprecated, and equivalent to Size. "SharedSize": sharedSizeHeader, "UniqueSize": uniqueSizeHeader, } @@ -257,15 +256,6 @@ func (c *imageContext) Containers() string { return strconv.FormatInt(c.i.Containers, 10) } -// VirtualSize shows the virtual size of the image and all of its parent -// images. Starting with docker 1.10, images are self-contained, and -// the VirtualSize is identical to Size. -// -// Deprecated: VirtualSize is deprecated, and equivalent to [imageContext.Size]. -func (c *imageContext) VirtualSize() string { - return units.HumanSize(float64(c.i.Size)) -} - func (c *imageContext) SharedSize() string { if c.i.SharedSize == -1 { return "N/A" diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go index bb792f043988..c3906687c4fa 100644 --- a/cli/command/formatter/image_test.go +++ b/cli/command/formatter/image_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -68,11 +68,6 @@ func TestImageContext(t *testing.T) { expValue: "10", call: ctx.Containers, }, - { - imageCtx: imageContext{i: image.Summary{Size: 10000}}, - expValue: "10kB", - call: ctx.VirtualSize, //nolint:nolintlint,staticcheck // ignore SA1019: field is deprecated, but still set on API < v1.44. - }, { imageCtx: imageContext{i: image.Summary{SharedSize: 10000}}, expValue: "10kB", diff --git a/cli/command/formatter/reflect.go b/cli/command/formatter/reflect.go index 316583376153..89da07b8a392 100644 --- a/cli/command/formatter/reflect.go +++ b/cli/command/formatter/reflect.go @@ -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: -//go:build go1.23 +//go:build go1.24 package formatter import ( "encoding/json" + "errors" + "fmt" "reflect" "unicode" - - "github.com/pkg/errors" ) // MarshalJSON marshals x into json @@ -25,14 +25,14 @@ func MarshalJSON(x any) ([]byte, error) { func marshalMap(x any) (map[string]any, error) { val := reflect.ValueOf(x) if val.Kind() != reflect.Ptr { - return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind()) + return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind()) } if val.IsNil() { - return nil, errors.Errorf("expected a pointer to a struct, got nil pointer") + return nil, errors.New("expected a pointer to a struct, got nil pointer") } valElem := val.Elem() if valElem.Kind() != reflect.Struct { - return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) + return nil, fmt.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) } typ := val.Type() m := make(map[string]any) @@ -54,7 +54,7 @@ var unmarshallableNames = map[string]struct{}{"FullHeader": {}} // It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()") func marshalForMethod(typ reflect.Method, val reflect.Value) (string, any, error) { if val.Kind() != reflect.Func { - return "", nil, errors.Errorf("expected func, got %v", val.Kind()) + return "", nil, fmt.Errorf("expected func, got %v", val.Kind()) } name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut() _, blackListed := unmarshallableNames[name] diff --git a/cli/command/formatter/reflect_test.go b/cli/command/formatter/reflect_test.go index 5ec0724901fe..81e71f76bd3b 100644 --- a/cli/command/formatter/reflect_test.go +++ b/cli/command/formatter/reflect_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go index bf9ea5d44e5d..e3d4b1922dae 100644 --- a/cli/command/formatter/volume.go +++ b/cli/command/formatter/volume.go @@ -5,8 +5,8 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/volume" "github.com/docker/go-units" + "github.com/moby/moby/api/types/volume" ) const ( @@ -40,10 +40,10 @@ func NewVolumeFormat(source string, quiet bool) Format { } // VolumeWrite writes formatted volumes using the Context -func VolumeWrite(ctx Context, volumes []*volume.Volume) error { +func VolumeWrite(ctx Context, volumes []volume.Volume) error { render := func(format func(subContext SubContext) error) error { for _, vol := range volumes { - if err := format(&volumeContext{v: *vol}); err != nil { + if err := format(&volumeContext{v: vol}); err != nil { return err } } diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go index cae16c7cd103..857f4530e385 100644 --- a/cli/command/formatter/volume_test.go +++ b/cli/command/formatter/volume_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package formatter @@ -11,7 +11,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -124,7 +124,7 @@ foobar_bar }, } - volumes := []*volume.Volume{ + volumes := []volume.Volume{ {Name: "foobar_baz", Driver: "foo"}, {Name: "foobar_bar", Driver: "bar"}, } @@ -144,7 +144,7 @@ foobar_bar } func TestVolumeContextWriteJSON(t *testing.T) { - volumes := []*volume.Volume{ + volumes := []volume.Volume{ {Driver: "foo", Name: "foobar_baz"}, {Driver: "bar", Name: "foobar_bar"}, } @@ -167,7 +167,7 @@ func TestVolumeContextWriteJSON(t *testing.T) { } func TestVolumeContextWriteJSONField(t *testing.T) { - volumes := []*volume.Volume{ + volumes := []volume.Volume{ {Driver: "foo", Name: "foobar_baz"}, {Driver: "bar", Name: "foobar_bar"}, } diff --git a/cli/command/idresolver/client_test.go b/cli/command/idresolver/client_test.go index 569022f642d2..f19b213df09a 100644 --- a/cli/command/idresolver/client_test.go +++ b/cli/command/idresolver/client_test.go @@ -3,26 +3,25 @@ package idresolver import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - nodeInspectFunc func(string) (swarm.Node, []byte, error) - serviceInspectFunc func(string) (swarm.Service, []byte, error) + nodeInspectFunc func(string) (client.NodeInspectResult, error) + serviceInspectFunc func(string) (client.ServiceInspectResult, error) } -func (cli *fakeClient) NodeInspectWithRaw(_ context.Context, nodeID string) (swarm.Node, []byte, error) { +func (cli *fakeClient) NodeInspect(_ context.Context, nodeID string, _ client.NodeInspectOptions) (client.NodeInspectResult, error) { if cli.nodeInspectFunc != nil { return cli.nodeInspectFunc(nodeID) } - return swarm.Node{}, []byte{}, nil + return client.NodeInspectResult{}, nil } -func (cli *fakeClient) ServiceInspectWithRaw(_ context.Context, serviceID string, _ swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { +func (cli *fakeClient) ServiceInspect(_ context.Context, serviceID string, _ client.ServiceInspectOptions) (client.ServiceInspectResult, error) { if cli.serviceInspectFunc != nil { return cli.serviceInspectFunc(serviceID) } - return swarm.Service{}, []byte{}, nil + return client.ServiceInspectResult{}, nil } diff --git a/cli/command/idresolver/idresolver.go b/cli/command/idresolver/idresolver.go index 143139022d17..d1ef3834db10 100644 --- a/cli/command/idresolver/idresolver.go +++ b/cli/command/idresolver/idresolver.go @@ -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: -//go:build go1.23 +//go:build go1.24 package idresolver import ( "context" + "errors" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) // IDResolver provides ID to Name resolution. @@ -30,27 +30,27 @@ func New(apiClient client.APIClient, noResolve bool) *IDResolver { func (r *IDResolver) get(ctx context.Context, t any, id string) (string, error) { switch t.(type) { case swarm.Node: - node, _, err := r.client.NodeInspectWithRaw(ctx, id) + res, err := r.client.NodeInspect(ctx, id, client.NodeInspectOptions{}) if err != nil { // TODO(thaJeztah): should error-handling be more specific, or is it ok to ignore any error? return id, nil //nolint:nilerr // ignore nil-error being returned, as this is a best-effort. } - if node.Spec.Annotations.Name != "" { - return node.Spec.Annotations.Name, nil + if res.Node.Spec.Annotations.Name != "" { + return res.Node.Spec.Annotations.Name, nil } - if node.Description.Hostname != "" { - return node.Description.Hostname, nil + if res.Node.Description.Hostname != "" { + return res.Node.Description.Hostname, nil } return id, nil case swarm.Service: - service, _, err := r.client.ServiceInspectWithRaw(ctx, id, swarm.ServiceInspectOptions{}) + res, err := r.client.ServiceInspect(ctx, id, client.ServiceInspectOptions{}) if err != nil { // TODO(thaJeztah): should error-handling be more specific, or is it ok to ignore any error? return id, nil //nolint:nilerr // ignore nil-error being returned, as this is a best-effort. } - return service.Spec.Annotations.Name, nil + return res.Service.Spec.Annotations.Name, nil default: - return "", errors.Errorf("unsupported type") + return "", errors.New("unsupported type") } } diff --git a/cli/command/idresolver/idresolver_test.go b/cli/command/idresolver/idresolver_test.go index dbf2f3875f84..120d7a8fb2ba 100644 --- a/cli/command/idresolver/idresolver_test.go +++ b/cli/command/idresolver/idresolver_test.go @@ -6,19 +6,20 @@ import ( "testing" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestResolveError(t *testing.T) { - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting node") + apiClient := &fakeClient{ + nodeInspectFunc: func(nodeID string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting node") }, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) _, err := idResolver.Resolve(context.Background(), struct{}{}, "nodeID") assert.Error(t, err, "unsupported type") @@ -26,18 +27,18 @@ func TestResolveError(t *testing.T) { func TestResolveWithNoResolveOption(t *testing.T) { resolved := false - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { + apiClient := &fakeClient{ + nodeInspectFunc: func(nodeID string) (client.NodeInspectResult, error) { resolved = true - return swarm.Node{}, []byte{}, nil + return client.NodeInspectResult{}, nil }, - serviceInspectFunc: func(serviceID string) (swarm.Service, []byte, error) { + serviceInspectFunc: func(serviceID string) (client.ServiceInspectResult, error) { resolved = true - return swarm.Service{}, []byte{}, nil + return client.ServiceInspectResult{}, nil }, } - idResolver := New(cli, true) + idResolver := New(apiClient, true) id, err := idResolver.Resolve(context.Background(), swarm.Node{}, "nodeID") assert.NilError(t, err) @@ -47,14 +48,16 @@ func TestResolveWithNoResolveOption(t *testing.T) { func TestResolveWithCache(t *testing.T) { inspectCounter := 0 - cli := &fakeClient{ - nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { + apiClient := &fakeClient{ + nodeInspectFunc: func(string) (client.NodeInspectResult, error) { inspectCounter++ - return *builders.Node(builders.NodeName("node-foo")), []byte{}, nil + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-foo")), + }, nil }, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) ctx := context.Background() for i := 0; i < 2; i++ { @@ -69,27 +72,31 @@ func TestResolveWithCache(t *testing.T) { func TestResolveNode(t *testing.T) { testCases := []struct { nodeID string - nodeInspectFunc func(string) (swarm.Node, []byte, error) + nodeInspectFunc func(string) (client.NodeInspectResult, error) expectedID string }{ { nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting node") + nodeInspectFunc: func(string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting node") }, expectedID: "nodeID", }, { nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName("node-foo")), []byte{}, nil + nodeInspectFunc: func(string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-foo")), + }, nil }, expectedID: "node-foo", }, { nodeID: "nodeID", - nodeInspectFunc: func(string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName(""), builders.Hostname("node-hostname")), []byte{}, nil + nodeInspectFunc: func(string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName(""), builders.Hostname("node-hostname")), + }, nil }, expectedID: "node-hostname", }, @@ -97,10 +104,10 @@ func TestResolveNode(t *testing.T) { ctx := context.Background() for _, tc := range testCases { - cli := &fakeClient{ + apiClient := &fakeClient{ nodeInspectFunc: tc.nodeInspectFunc, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) id, err := idResolver.Resolve(ctx, swarm.Node{}, tc.nodeID) assert.NilError(t, err) @@ -111,20 +118,22 @@ func TestResolveNode(t *testing.T) { func TestResolveService(t *testing.T) { testCases := []struct { serviceID string - serviceInspectFunc func(string) (swarm.Service, []byte, error) + serviceInspectFunc func(string) (client.ServiceInspectResult, error) expectedID string }{ { serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return swarm.Service{}, []byte{}, errors.New("error inspecting service") + serviceInspectFunc: func(string) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{}, errors.New("error inspecting service") }, expectedID: "serviceID", }, { serviceID: "serviceID", - serviceInspectFunc: func(string) (swarm.Service, []byte, error) { - return *builders.Service(builders.ServiceName("service-foo")), []byte{}, nil + serviceInspectFunc: func(string) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceName("service-foo")), + }, nil }, expectedID: "service-foo", }, @@ -132,10 +141,10 @@ func TestResolveService(t *testing.T) { ctx := context.Background() for _, tc := range testCases { - cli := &fakeClient{ + apiClient := &fakeClient{ serviceInspectFunc: tc.serviceInspectFunc, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) id, err := idResolver.Resolve(ctx, swarm.Service{}, tc.serviceID) assert.NilError(t, err) diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 66beeee2bba2..db33e0e7c64d 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -1,18 +1,17 @@ package image import ( - "archive/tar" - "bufio" "bytes" "context" "encoding/json" + "errors" "fmt" "io" "os" "path/filepath" - "runtime" "strings" + "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/cli-docs-tool/annotation" "github.com/docker/cli/cli" @@ -20,19 +19,16 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/streams" - "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" - "github.com/docker/cli/internal/lazyregexp" "github.com/docker/cli/opts" - "github.com/docker/docker/api" - buildtypes "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/builder/remotecontext/urlutil" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" "github.com/moby/go-archive" - "github.com/pkg/errors" + buildtypes "github.com/moby/moby/api/types/build" + "github.com/moby/moby/api/types/container" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/progress" + "github.com/moby/moby/client/pkg/streamformatter" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) @@ -67,7 +63,6 @@ type buildOptions struct { target string imageIDFile string platform string - untrusted bool } // dockerfileFromStdin returns true when the user specified that the Dockerfile @@ -76,12 +71,6 @@ func (o buildOptions) dockerfileFromStdin() bool { return o.dockerfileName == "-" } -// contextFromStdin returns true when the user specified that the build context -// should be read from stdin -func (o buildOptions) contextFromStdin() bool { - return o.context == "-" -} - func newBuildOptions() buildOptions { ulimits := make(map[string]*container.Ulimit) return buildOptions{ @@ -94,7 +83,14 @@ func newBuildOptions() buildOptions { } // NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewBuildCommand(dockerCLI command.Cli) *cobra.Command { + return newBuildCommand(dockerCLI) +} + +// newBuildCommand creates a new `docker build` command +func newBuildCommand(dockerCLI command.Cli) *cobra.Command { options := newBuildOptions() cmd := &cobra.Command{ @@ -103,7 +99,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.context = args[0] - return runBuild(cmd.Context(), dockerCli, options) + return runBuild(cmd.Context(), dockerCLI, options) }, Annotations: map[string]string{ "category-top": "4", @@ -112,6 +108,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveFilterDirs }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -152,7 +149,9 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"}) flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") flags.SetAnnotation("platform", "version", []string{"1.38"}) @@ -161,7 +160,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation("squash", "experimental", nil) flags.SetAnnotation("squash", "version", []string{"1.25"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } @@ -185,25 +184,34 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { //nolint:gocyclo func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) error { var ( - err error buildCtx io.ReadCloser dockerfileCtx io.ReadCloser contextDir string - tempDir string relDockerfile string progBuff io.Writer buildBuff io.Writer remote string ) + if options.platform != "" { + _, err := platforms.Parse(options.platform) + if err != nil { + return err + } + } + + contextType, err := build.DetectContextType(options.context) + if err != nil { + return err + } + if options.dockerfileFromStdin() { - if options.contextFromStdin() { + if contextType == build.ContextTypeStdin { return errors.New("invalid argument: can't use stdin for both build context and dockerfile") } dockerfileCtx = dockerCli.In() } - specifiedContext := options.context progBuff = dockerCli.Out() buildBuff = dockerCli.Out() if options.quiet { @@ -213,42 +221,47 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) if options.imageIDFile != "" { // Avoid leaving a stale file if we eventually fail if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) { - return errors.Wrap(err, "Removing image ID file") + return fmt.Errorf("removing image ID file: %w", err) } } - switch { - case options.contextFromStdin(): + switch contextType { + case build.ContextTypeStdin: // buildCtx is tar archive. if stdin was dockerfile then it is wrapped buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) - case isLocalDir(specifiedContext): - contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) - if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx + if err != nil { + return fmt.Errorf("unable to prepare context from STDIN: %w", err) + } + case build.ContextTypeLocal: + contextDir, relDockerfile, err = build.GetContextFromLocalDir(options.context, options.dockerfileName) + if err != nil { + return fmt.Errorf("unable to prepare context: %s", err) + } + if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + // Dockerfile is outside build-context; read the Dockerfile and pass it as dockerfileCtx dockerfileCtx, err = os.Open(options.dockerfileName) if err != nil { - return errors.Errorf("unable to open Dockerfile: %v", err) + return fmt.Errorf("unable to open Dockerfile: %w", err) } defer dockerfileCtx.Close() } - case urlutil.IsGitURL(specifiedContext): - tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) - case urlutil.IsURL(specifiedContext): - buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) - default: - return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) - } - - if err != nil { - if options.quiet && urlutil.IsURL(specifiedContext) { - _, _ = fmt.Fprintln(dockerCli.Err(), progBuff) + case build.ContextTypeGit: + var tempDir string + tempDir, relDockerfile, err = build.GetContextFromGitURL(options.context, options.dockerfileName) + if err != nil { + return fmt.Errorf("unable to prepare context: %w", err) } - return errors.Errorf("unable to prepare context: %s", err) - } - - if tempDir != "" { - defer os.RemoveAll(tempDir) + defer func() { + _ = os.RemoveAll(tempDir) + }() contextDir = tempDir + case build.ContextTypeRemote: + buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName) + if err != nil && options.quiet { + _, _ = fmt.Fprintln(dockerCli.Err(), progBuff) + } + default: + return fmt.Errorf("unable to prepare context: path %q not found", options.context) } // read from a directory into tar archive @@ -259,7 +272,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { - return errors.Wrap(err, "error checking context") + return fmt.Errorf("checking context: %w", err) } // And canonicalize dockerfile name to a platform-independent one @@ -283,29 +296,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } } - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - var resolvedTags []*resolvedTag - if !options.untrusted { - translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref) - } - // if there is a tar wrapper, the dockerfile needs to be replaced inside it - if buildCtx != nil { - // Wrap the tar archive to replace the Dockerfile entry with the rewritten - // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileForContentTrust(ctx, buildCtx, relDockerfile, translator, &resolvedTags) - } else if dockerfileCtx != nil { - // if there was not archive context still do the possible replacements in Dockerfile - newDockerfile, _, err := rewriteDockerfileFromForContentTrust(ctx, dockerfileCtx, translator) - if err != nil { - return err - } - dockerfileCtx = io.NopCloser(bytes.NewBuffer(newDockerfile)) - } - } - if options.compress { buildCtx, err = build.Compress(buildCtx) if err != nil { @@ -333,21 +323,31 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) configFile := dockerCli.ConfigFile() creds, _ := configFile.GetAllCredentials() authConfigs := make(map[string]registrytypes.AuthConfig, len(creds)) - for k, auth := range creds { - authConfigs[k] = registrytypes.AuthConfig(auth) + for k, authConfig := range creds { + authConfigs[k] = registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + } } buildOpts := imageBuildOptions(dockerCli, options) - buildOpts.Version = buildtypes.BuilderV1 buildOpts.Dockerfile = relDockerfile buildOpts.AuthConfigs = authConfigs buildOpts.RemoteContext = remote + ctx, cancel := context.WithCancel(ctx) + defer cancel() + response, err := dockerCli.Client().ImageBuild(ctx, body, buildOpts) if err != nil { if options.quiet { _, _ = fmt.Fprintf(dockerCli.Err(), "%s", progBuff) } - cancel() return err } defer response.Body.Close() @@ -364,7 +364,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) err = jsonstream.Display(ctx, response.Body, streams.NewOut(buildBuff), jsonstream.WithAuxCallback(aux)) if err != nil { - if jerr, ok := err.(*jsonstream.JSONError); ok { + var jerr *jsonstream.JSONError + if errors.As(err, &jerr) { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 @@ -377,16 +378,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) return err } - // Windows: show error message about modified file permissions if the - // daemon isn't running Windows. - if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - _, _ = fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ - "image from Windows against a non-Windows Docker host. All files and "+ - "directories added to build context will have '-rwxr-xr-x' permissions. "+ - "It is recommended to double check and reset permissions for sensitive "+ - "files and directories.") - } - // Everything worked so if -q was provided the output from the daemon // should be just the image ID and we'll print that to stdout. if options.quiet { @@ -396,32 +387,16 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) if options.imageIDFile != "" { if imageID == "" { - return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile) + return fmt.Errorf("server did not provide an image ID. Cannot write %s", options.imageIDFile) } if err := os.WriteFile(options.imageIDFile, []byte(imageID), 0o666); err != nil { return err } } - if !options.untrusted { - // Since the build was successful, now we must tag any of the resolved - // images from the above Dockerfile rewrite. - for _, resolved := range resolvedTags { - if err := trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), resolved.digestRef, resolved.tagRef); err != nil { - return err - } - } - } return nil } -func isLocalDir(c string) bool { - _, err := os.Stat(c) - return err == nil -} - -type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) - // validateTag checks if the given image name can be resolved. func validateTag(rawRepo string) (string, error) { _, err := reference.ParseNormalizedNamed(rawRepo) @@ -432,117 +407,16 @@ func validateTag(rawRepo string) (string, error) { return rawRepo, nil } -var dockerfileFromLinePattern = lazyregexp.New(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) - -// resolvedTag records the repository, tag, and resolved digest reference -// from a Dockerfile rewrite. -type resolvedTag struct { - digestRef reference.Canonical - tagRef reference.NamedTagged -} - -// rewriteDockerfileFromForContentTrust rewrites the given Dockerfile by resolving images in -// "FROM " instructions to a digest reference. `translator` is a -// function that takes a repository name and tag reference and returns a -// trusted digest reference. -// This should be called *only* when content trust is enabled -func rewriteDockerfileFromForContentTrust(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { - scanner := bufio.NewScanner(dockerfile) - buf := bytes.NewBuffer(nil) - - // Scan the lines of the Dockerfile, looking for a "FROM" line. - for scanner.Scan() { - line := scanner.Text() - - matches := dockerfileFromLinePattern.FindStringSubmatch(line) - if matches != nil && matches[1] != api.NoBaseImageSpecifier { - // Replace the line with a resolved "FROM repo@digest" - var ref reference.Named - ref, err = reference.ParseNormalizedNamed(matches[1]) - if err != nil { - return nil, nil, err - } - ref = reference.TagNameOnly(ref) - if ref, ok := ref.(reference.NamedTagged); ok { - trustedRef, err := translator(ctx, ref) - if err != nil { - return nil, nil, err - } - - line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, "FROM "+reference.FamiliarString(trustedRef)) - resolvedTags = append(resolvedTags, &resolvedTag{ - digestRef: trustedRef, - tagRef: ref, - }) - } - } +func imageBuildOptions(dockerCli command.Cli, options buildOptions) client.ImageBuildOptions { + configFile := dockerCli.ConfigFile() - _, err := fmt.Fprintln(buf, line) - if err != nil { - return nil, nil, err - } + var buildPlatforms []ocispec.Platform + if options.platform != "" { + // Already validated. + buildPlatforms = append(buildPlatforms, platforms.MustParse(options.platform)) } - - return buf.Bytes(), resolvedTags, scanner.Err() -} - -// replaceDockerfileForContentTrust wraps the given input tar archive stream and -// uses the translator to replace the Dockerfile which uses a trusted reference. -// Returns a new tar archive stream with the replaced Dockerfile. -func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { - pipeReader, pipeWriter := io.Pipe() - go func() { - tarReader := tar.NewReader(inputTarStream) - tarWriter := tar.NewWriter(pipeWriter) - - defer inputTarStream.Close() - - for { - hdr, err := tarReader.Next() - if err == io.EOF { - // Signals end of archive. - _ = tarWriter.Close() - _ = pipeWriter.Close() - return - } - if err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - - content := io.Reader(tarReader) - if hdr.Name == dockerfileName { - // This entry is the Dockerfile. Since the tar archive was - // generated from a directory on the local filesystem, the - // Dockerfile will only appear once in the archive. - var newDockerfile []byte - newDockerfile, *resolvedTags, err = rewriteDockerfileFromForContentTrust(ctx, content, translator) - if err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - hdr.Size = int64(len(newDockerfile)) - content = bytes.NewBuffer(newDockerfile) - } - - if err := tarWriter.WriteHeader(hdr); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - - if _, err := io.Copy(tarWriter, content); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - } - }() - - return pipeReader -} - -func imageBuildOptions(dockerCli command.Cli, options buildOptions) buildtypes.ImageBuildOptions { - configFile := dockerCli.ConfigFile() - return buildtypes.ImageBuildOptions{ + return client.ImageBuildOptions{ + Version: buildtypes.BuilderV1, Memory: options.memory.Value(), MemorySwap: options.memorySwap.Value(), Tags: options.tags.GetSlice(), @@ -568,6 +442,6 @@ func imageBuildOptions(dockerCli command.Cli, options buildOptions) buildtypes.I Squash: options.squash, ExtraHosts: options.extraHosts.GetSlice(), Target: options.target, - Platform: options.platform, + Platforms: buildPlatforms, } } diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index ca70d5484c77..2de860507c55 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -6,6 +6,7 @@ import ( "bytes" "crypto/rand" "encoding/hex" + "errors" "fmt" "io" "net/http" @@ -16,18 +17,22 @@ import ( "strings" "time" - "github.com/docker/docker/builder/remotecontext/git" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/cli/cli/command/image/build/internal/git" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" + "github.com/moby/moby/client/pkg/progress" + "github.com/moby/moby/client/pkg/streamformatter" "github.com/moby/patternmatcher" - "github.com/pkg/errors" ) +// DefaultDockerfileName is the Default filename with Docker commands, read by docker build +// +// Deprecated: this const is no longer used and will be removed in the next release. +const DefaultDockerfileName string = "Dockerfile" + const ( - // DefaultDockerfileName is the Default filename with Docker commands, read by docker build - DefaultDockerfileName string = "Dockerfile" + // defaultDockerfileName is the Default filename with Docker commands, read by docker build + defaultDockerfileName string = "Dockerfile" // archiveHeaderSize is the number of bytes in an archive header archiveHeaderSize = 512 ) @@ -49,10 +54,10 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { if err != nil { if os.IsPermission(err) { - return errors.Errorf("can't stat '%s'", filePath) + return fmt.Errorf("can't stat '%s'", filePath) } if os.IsNotExist(err) { - return errors.Errorf("file ('%s') not found or excluded by .dockerignore", filePath) + return fmt.Errorf("file ('%s') not found or excluded by .dockerignore", filePath) } return err } @@ -78,9 +83,9 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { if !f.IsDir() { currentFile, err := os.Open(filePath) if err != nil && os.IsPermission(err) { - return errors.Errorf("no permission to read from '%s'", filePath) + return fmt.Errorf("no permission to read from '%s'", filePath) } - currentFile.Close() + _ = currentFile.Close() } return nil }) @@ -97,28 +102,48 @@ func filepathMatches(matcher *patternmatcher.PatternMatcher, file string) (bool, // DetectArchiveReader detects whether the input stream is an archive or a // Dockerfile and returns a buffered version of input, safe to consume in lieu -// of input. If an archive is detected, isArchive is set to true, and to false +// of input. If an archive is detected, ok is set to true, and to false +// otherwise, in which case it is safe to assume input represents the contents +// of a Dockerfile. +// +// Deprecated: this utility was only used internally, and will be removed in the next release. +func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err error) { + return detectArchiveReader(input) +} + +// detectArchiveReader detects whether the input stream is an archive or a +// Dockerfile and returns a buffered version of input, safe to consume in lieu +// of input. If an archive is detected, ok is set to true, and to false // otherwise, in which case it is safe to assume input represents the contents // of a Dockerfile. -func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, err error) { +func detectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err error) { buf := bufio.NewReader(input) magic, err := buf.Peek(archiveHeaderSize * 2) if err != nil && err != io.EOF { - return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err) + return nil, false, fmt.Errorf("failed to peek context header from STDIN: %w", err) } - return newReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil + return newReadCloserWrapper(buf, func() error { return input.Close() }), isArchive(magic), nil } // WriteTempDockerfile writes a Dockerfile stream to a temporary file with a -// name specified by DefaultDockerfileName and returns the path to the +// name specified by defaultDockerfileName and returns the path to the // temporary directory containing the Dockerfile. +// +// Deprecated: this utility was only used internally, and will be removed in the next release. func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { + return writeTempDockerfile(rc) +} + +// writeTempDockerfile writes a Dockerfile stream to a temporary file with a +// name specified by defaultDockerfileName and returns the path to the +// temporary directory containing the Dockerfile. +func writeTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { // err is a named return value, due to the defer call below. dockerfileDir, err = os.MkdirTemp("", "docker-build-tempdockerfile-") if err != nil { - return "", errors.Errorf("unable to create temporary context directory: %v", err) + return "", fmt.Errorf("unable to create temporary context directory: %w", err) } defer func() { if err != nil { @@ -126,7 +151,7 @@ func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { } }() - f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName)) + f, err := os.Create(filepath.Join(dockerfileDir, defaultDockerfileName)) if err != nil { return "", err } @@ -141,12 +166,12 @@ func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { // Dockerfile or tar archive. Returns a tar archive used as a context and a // path to the Dockerfile inside the tar. func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { - rc, isArchive, err := DetectArchiveReader(rc) + rc, ok, err := detectArchiveReader(rc) if err != nil { return nil, "", err } - if isArchive { + if ok { return rc, dockerfileName, nil } @@ -159,7 +184,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC return nil, "", errors.New("ambiguous Dockerfile source: both stdin and flag correspond to Dockerfiles") } - dockerfileDir, err := WriteTempDockerfile(rc) + dockerfileDir, err := writeTempDockerfile(rc) if err != nil { return nil, "", err } @@ -171,14 +196,22 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC return newReadCloserWrapper(tarArchive, func() error { err := tarArchive.Close() - os.RemoveAll(dockerfileDir) + _ = os.RemoveAll(dockerfileDir) return err - }), DefaultDockerfileName, nil + }), defaultDockerfileName, nil } // IsArchive checks for the magic bytes of a tar or any supported compression // algorithm. +// +// Deprecated: this utility was used internally and will be removed in the next release. func IsArchive(header []byte) bool { + return isArchive(header) +} + +// isArchive checks for the magic bytes of a tar or any supported compression +// algorithm. +func isArchive(header []byte) bool { if compression.Detect(header) != compression.None { return true } @@ -194,20 +227,20 @@ func IsArchive(header []byte) bool { // success. func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) { if _, err := exec.LookPath("git"); err != nil { - return "", "", errors.Wrapf(err, "unable to find 'git'") + return "", "", fmt.Errorf("unable to find 'git': %w", err) } absContextDir, err := git.Clone(gitURL) if err != nil { - return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory") + return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %w", err) } - absContextDir, err = ResolveAndValidateContextPath(absContextDir) + absContextDir, err = resolveAndValidateContextPath(absContextDir) if err != nil { return "", "", err } relDockerfile, err := getDockerfileRelPath(absContextDir, dockerfileName) if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - return "", "", errors.Errorf("the Dockerfile (%s) must be within the build context", dockerfileName) + return "", "", fmt.Errorf("the Dockerfile (%s) must be within the build context", dockerfileName) } return absContextDir, relDockerfile, err @@ -220,7 +253,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { response, err := getWithStatusError(remoteURL) if err != nil { - return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) + return nil, "", fmt.Errorf("unable to download remote context %s: %w", remoteURL, err) } progressOutput := streamformatter.NewProgressOutput(out) @@ -242,11 +275,11 @@ func getWithStatusError(url string) (resp *http.Response, err error) { } msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status) body, err := io.ReadAll(resp.Body) - resp.Body.Close() + _ = resp.Body.Close() if err != nil { - return nil, errors.Wrapf(err, "%s: error reading body", msg) + return nil, fmt.Errorf("%s: error reading body: %w", msg, err) } - return nil, errors.Errorf("%s: %s", msg, bytes.TrimSpace(body)) + return nil, fmt.Errorf("%s: %s", msg, bytes.TrimSpace(body)) } // GetContextFromLocalDir uses the given local directory as context for a @@ -254,7 +287,7 @@ func getWithStatusError(url string) (resp *http.Response, err error) { // the relative path of the dockerfile in that context directory, and a non-nil // error on success. func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, error) { - localDir, err := ResolveAndValidateContextPath(localDir) + localDir, err := resolveAndValidateContextPath(localDir) if err != nil { return "", "", err } @@ -264,7 +297,7 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, er // current directory and not the context directory. if dockerfileName != "" && dockerfileName != "-" { if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { - return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) + return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %w", err) } } @@ -274,10 +307,21 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, er // ResolveAndValidateContextPath uses the given context directory for a `docker build` // and returns the absolute path to the context directory. +// +// Deprecated: this utility was used internally and will be removed in the next +// release. Use [DetectContextType] to detect the context-type, and use +// [GetContextFromLocalDir], [GetContextFromLocalDir], [GetContextFromGitURL], +// or [GetContextFromURL] instead. func ResolveAndValidateContextPath(givenContextDir string) (string, error) { + return resolveAndValidateContextPath(givenContextDir) +} + +// resolveAndValidateContextPath uses the given context directory for a `docker build` +// and returns the absolute path to the context directory. +func resolveAndValidateContextPath(givenContextDir string) (string, error) { absContextDir, err := filepath.Abs(givenContextDir) if err != nil { - return "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) + return "", fmt.Errorf("unable to get absolute context directory of given context directory %q: %w", givenContextDir, err) } // The context dir might be a symbolic link, so follow it to the actual @@ -290,17 +334,17 @@ func ResolveAndValidateContextPath(givenContextDir string) (string, error) { if !isUNC(absContextDir) { absContextDir, err = filepath.EvalSymlinks(absContextDir) if err != nil { - return "", errors.Errorf("unable to evaluate symlinks in context path: %v", err) + return "", fmt.Errorf("unable to evaluate symlinks in context path: %w", err) } } stat, err := os.Lstat(absContextDir) if err != nil { - return "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err) + return "", fmt.Errorf("unable to stat context directory %q: %w", absContextDir, err) } if !stat.IsDir() { - return "", errors.Errorf("context must be a directory: %s", absContextDir) + return "", fmt.Errorf("context must be a directory: %s", absContextDir) } return absContextDir, err } @@ -318,12 +362,12 @@ func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error) if absDockerfile == "" { // No -f/--file was specified so use the default relative to the // context directory. - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) + absDockerfile = filepath.Join(absContextDir, defaultDockerfileName) // Just to be nice ;-) look for 'dockerfile' too but only // use it if we found it, otherwise ignore this check if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { - altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) + altPath := filepath.Join(absContextDir, strings.ToLower(defaultDockerfileName)) if _, err = os.Lstat(altPath); err == nil { absDockerfile = altPath } @@ -345,20 +389,20 @@ func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error) if !isUNC(absDockerfile) { absDockerfile, err = filepath.EvalSymlinks(absDockerfile) if err != nil { - return "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) + return "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %w", err) } } if _, err := os.Lstat(absDockerfile); err != nil { if os.IsNotExist(err) { - return "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + return "", fmt.Errorf("cannot locate Dockerfile: %q", absDockerfile) } - return "", errors.Errorf("unable to stat Dockerfile: %v", err) + return "", fmt.Errorf("unable to stat Dockerfile: %w", err) } relDockerfile, err := filepath.Rel(absContextDir, absDockerfile) if err != nil { - return "", errors.Errorf("unable to get relative Dockerfile path: %v", err) + return "", fmt.Errorf("unable to get relative Dockerfile path: %w", err) } return relDockerfile, nil @@ -374,7 +418,7 @@ func isUNC(path string) bool { // the relative path to the dockerfile in the context. func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { file, err := io.ReadAll(dockerfileCtx) - dockerfileCtx.Close() + _ = dockerfileCtx.Close() if err != nil { return nil, "", err } @@ -438,17 +482,19 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { go func() { compressWriter, err := compression.CompressStream(pipeWriter, archive.Gzip) if err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) } - defer buildCtx.Close() + defer func() { + _ = buildCtx.Close() + }() if _, err := io.Copy(compressWriter, buildCtx); err != nil { - pipeWriter.CloseWithError(errors.Wrap(err, "failed to compress context")) - compressWriter.Close() + _ = pipeWriter.CloseWithError(fmt.Errorf("failed to compress context: %w", err)) + _ = compressWriter.Close() return } - compressWriter.Close() - pipeWriter.Close() + _ = compressWriter.Close() + _ = pipeWriter.Close() }() return pipeReader, nil diff --git a/cli/command/image/build/context_detect.go b/cli/command/image/build/context_detect.go new file mode 100644 index 000000000000..c5edc4ecf161 --- /dev/null +++ b/cli/command/image/build/context_detect.go @@ -0,0 +1,39 @@ +package build + +import ( + "fmt" + "os" + + "github.com/docker/cli/cli/command/image/build/internal/urlutil" +) + +// ContextType describes the type (source) of build-context specified. +type ContextType string + +const ( + ContextTypeStdin ContextType = "stdin" // ContextTypeStdin indicates that the build-context is a TAR archive passed through STDIN. + ContextTypeLocal ContextType = "local" // ContextTypeLocal indicates that the build-context is a local directory. + ContextTypeRemote ContextType = "remote" // ContextTypeRemote indicates that the build-context is a remote URL. + ContextTypeGit ContextType = "git" // ContextTypeGit indicates that the build-context is a GIT URL. +) + +// DetectContextType detects the type (source) of the build-context. +func DetectContextType(specifiedContext string) (ContextType, error) { + switch { + case specifiedContext == "-": + return ContextTypeStdin, nil + case isLocalDir(specifiedContext): + return ContextTypeLocal, nil + case urlutil.IsGitURL(specifiedContext): + return ContextTypeGit, nil + case urlutil.IsURL(specifiedContext): + return ContextTypeRemote, nil + default: + return "", fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) + } +} + +func isLocalDir(c string) bool { + _, err := os.Stat(c) + return err == nil +} diff --git a/cli/command/image/build/context_test.go b/cli/command/image/build/context_test.go index 45533d6ebb4c..b0c83a46b1d2 100644 --- a/cli/command/image/build/context_test.go +++ b/cli/command/image/build/context_test.go @@ -31,7 +31,7 @@ func prepareNoFiles(t *testing.T) string { func prepareOneFile(t *testing.T) string { t.Helper() contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) return contextDir } @@ -66,7 +66,7 @@ func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) chdir(t, contextDir) @@ -74,23 +74,23 @@ func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirLocalFile(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) testFilename := createTestTempFile(t, contextDir, "tmpTest", "test") absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") @@ -112,13 +112,13 @@ func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { contextDir := createTestTempDir(t) chdir(t, contextDir) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, defaultDockerfileName) assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromReaderString(t *testing.T) { @@ -135,7 +135,7 @@ func TestGetContextFromReaderString(t *testing.T) { } buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) + _, _ = buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() @@ -150,8 +150,8 @@ func TestGetContextFromReaderString(t *testing.T) { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + if relDockerfile != defaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", defaultDockerfileName, relDockerfile) } } @@ -164,12 +164,12 @@ func TestGetContextFromReaderStringConflict(t *testing.T) { func TestGetContextFromReaderTar(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) tarStream, err := archive.Tar(contextDir, compression.None) assert.NilError(t, err) - tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) + tarArchive, relDockerfile, err := GetContextFromReader(tarStream, defaultDockerfileName) assert.NilError(t, err) tarReader := tar.NewReader(tarArchive) @@ -177,12 +177,12 @@ func TestGetContextFromReaderTar(t *testing.T) { header, err := tarReader.Next() assert.NilError(t, err) - if header.Name != DefaultDockerfileName { - t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) + if header.Name != defaultDockerfileName { + t.Fatalf("Dockerfile name should be: %s, got: %s", defaultDockerfileName, header.Name) } buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) + _, _ = buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() @@ -197,8 +197,8 @@ func TestGetContextFromReaderTar(t *testing.T) { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + if relDockerfile != defaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", defaultDockerfileName, relDockerfile) } } @@ -223,7 +223,7 @@ func TestValidateContextDirectoryWithOneFile(t *testing.T) { } func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) + testValidateContextDirectory(t, prepareOneFile, []string{defaultDockerfileName}) } // createTestTempDir creates a temporary directory for testing. It returns the @@ -263,7 +263,7 @@ func chdir(t *testing.T, dir string) { } func TestIsArchive(t *testing.T) { - testcases := []struct { + tests := []struct { doc string header []byte expected bool @@ -289,13 +289,15 @@ func TestIsArchive(t *testing.T) { expected: false, }, } - for _, testcase := range testcases { - assert.Check(t, is.Equal(testcase.expected, IsArchive(testcase.header)), testcase.doc) + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + assert.Check(t, is.Equal(tc.expected, isArchive(tc.header)), tc.doc) + }) } } func TestDetectArchiveReader(t *testing.T) { - testcases := []struct { + tests := []struct { file string desc string expected bool @@ -316,14 +318,18 @@ func TestDetectArchiveReader(t *testing.T) { expected: false, }, } - for _, testcase := range testcases { - content, err := os.Open(testcase.file) - assert.NilError(t, err) - defer content.Close() - - _, isArchive, err := DetectArchiveReader(content) - assert.NilError(t, err) - assert.Check(t, is.Equal(testcase.expected, isArchive), testcase.file) + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + content, err := os.Open(tc.file) + assert.NilError(t, err) + defer func() { + _ = content.Close() + }() + + _, isArchive, err := detectArchiveReader(content) + assert.NilError(t, err) + assert.Check(t, is.Equal(tc.expected, isArchive), tc.file) + }) } } diff --git a/cli/command/image/build/dockerignore.go b/cli/command/image/build/dockerignore.go index 357210437c03..8d11aa5e10d6 100644 --- a/cli/command/image/build/dockerignore.go +++ b/cli/command/image/build/dockerignore.go @@ -21,7 +21,9 @@ func ReadDockerignore(contextDir string) ([]string, error) { case err != nil: return nil, err } - defer f.Close() + defer func() { + _ = f.Close() + }() patterns, err := ignorefile.ReadAll(f) if err != nil { diff --git a/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go b/cli/command/image/build/internal/git/gitutils.go similarity index 89% rename from vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go rename to cli/command/image/build/internal/git/gitutils.go index b02993511fbb..e9245cb098a6 100644 --- a/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go +++ b/cli/command/image/build/internal/git/gitutils.go @@ -1,6 +1,7 @@ package git import ( + "fmt" "net/http" "net/url" "os" @@ -9,7 +10,6 @@ import ( "strings" "github.com/moby/sys/symlink" - "github.com/pkg/errors" ) type gitRepo struct { @@ -61,17 +61,17 @@ func (repo gitRepo) clone() (checkoutDir string, retErr error) { }() if out, err := repo.gitWithinDir(root, "init"); err != nil { - return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out) + return "", fmt.Errorf("failed to init repo at %s: %s: %w", root, out, err) } // Add origin remote for compatibility with previous implementation that // used "git clone" and also to make sure local refs are created for branches if out, err := repo.gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil { - return "", errors.Wrapf(err, "failed add origin repo at %s: %s", repo.remote, out) + return "", fmt.Errorf("failed add origin repo at %s: %s: %w", repo.remote, out, err) } if output, err := repo.gitWithinDir(root, fetch...); err != nil { - return "", errors.Wrapf(err, "error fetching: %s", output) + return "", fmt.Errorf("error fetching: %s: %w", output, err) } checkoutDir, err = repo.checkout(root) @@ -83,7 +83,7 @@ func (repo gitRepo) clone() (checkoutDir string, retErr error) { cmd.Dir = root output, err := cmd.CombinedOutput() if err != nil { - return "", errors.Wrapf(err, "error initializing submodules: %s", output) + return "", fmt.Errorf("error initializing submodules: %s: %w", output, err) } return checkoutDir, nil @@ -113,7 +113,7 @@ func parseRemoteURL(remoteURL string) (gitRepo, error) { } if strings.HasPrefix(repo.ref, "-") { - return gitRepo{}, errors.Errorf("invalid refspec: %s", repo.ref) + return gitRepo{}, fmt.Errorf("invalid refspec: %s", repo.ref) } return repo, nil @@ -148,6 +148,9 @@ func supportsShallowClone(remoteURL string) bool { // Try a HEAD request and fallback to a Get request on error res, err := http.Head(serviceURL) // #nosec G107 + if err == nil { + _ = res.Body.Close() + } if err != nil || res.StatusCode != http.StatusOK { res, err = http.Get(serviceURL) // #nosec G107 if err == nil { @@ -175,14 +178,14 @@ func (repo gitRepo) checkout(root string) (string, error) { if output, err := repo.gitWithinDir(root, "checkout", repo.ref); err != nil { // If checking out by branch name fails check out the last fetched ref if _, err2 := repo.gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil { - return "", errors.Wrapf(err, "error checking out %s: %s", repo.ref, output) + return "", fmt.Errorf("error checking out %s: %s: %w", repo.ref, output, err) } } if repo.subdir != "" { newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, repo.subdir), root) if err != nil { - return "", errors.Wrapf(err, "error setting git context, %q not within git root", repo.subdir) + return "", fmt.Errorf("error setting git context, %q not within git root: %w", repo.subdir, err) } fi, err := os.Stat(newCtx) @@ -190,7 +193,7 @@ func (repo gitRepo) checkout(root string) (string, error) { return "", err } if !fi.IsDir() { - return "", errors.Errorf("error setting git context, not a directory: %s", newCtx) + return "", fmt.Errorf("error setting git context, not a directory: %s", newCtx) } root = newCtx } diff --git a/cli/command/image/build/internal/git/gitutils_test.go b/cli/command/image/build/internal/git/gitutils_test.go new file mode 100644 index 000000000000..61dfb56d62f8 --- /dev/null +++ b/cli/command/image/build/internal/git/gitutils_test.go @@ -0,0 +1,382 @@ +package git + +import ( + "bytes" + "fmt" + "net/http" + "net/http/cgi" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestParseRemoteURL(t *testing.T) { + tests := []struct { + doc string + url string + expected gitRepo + }{ + { + doc: "git scheme uppercase, no url-fragment", + url: "GIT://github.com/user/repo.git", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "git scheme, no url-fragment", + url: "git://github.com/user/repo.git", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "git scheme, with url-fragment", + url: "git://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "https scheme, no url-fragment", + url: "https://github.com/user/repo.git", + expected: gitRepo{ + remote: "https://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "https scheme, with url-fragment", + url: "https://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "https://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "git@, no url-fragment", + url: "git@github.com:user/repo.git", + expected: gitRepo{ + remote: "git@github.com:user/repo.git", + ref: "master", + }, + }, + { + doc: "git@, with url-fragment", + url: "git@github.com:user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "git@github.com:user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "ssh, no url-fragment", + url: "ssh://github.com/user/repo.git", + expected: gitRepo{ + remote: "ssh://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "ssh, with url-fragment", + url: "ssh://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "ssh://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "ssh, with url-fragment and user", + url: "ssh://foo%40barcorp.com@github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "ssh://foo%40barcorp.com@github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + repo, err := parseRemoteURL(tc.url) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(tc.expected, repo, cmp.AllowUnexported(gitRepo{}))) + }) + } +} + +func TestCloneArgsSmartHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, err := url.Parse(server.URL) + assert.NilError(t, err) + + serverURL.Path = "/repo.git" + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("service") + w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q)) + }) + + args := fetchArgs(serverURL.String(), "master") + exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func TestCloneArgsDumbHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, _ := url.Parse(server.URL) + + serverURL.Path = "/repo.git" + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + }) + + args := fetchArgs(serverURL.String(), "master") + exp := []string{"fetch", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func TestCloneArgsGit(t *testing.T) { + args := fetchArgs("git://github.com/docker/docker", "master") + exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func gitGetConfig(name string) string { + b, err := gitRepo{}.gitWithinDir("", "config", "--get", name) + if err != nil { + // since we are interested in empty or non empty string, + // we can safely ignore the err here. + return "" + } + return strings.TrimSpace(string(b)) +} + +func TestCheckoutGit(t *testing.T) { + root := t.TempDir() + + gitpath, err := exec.LookPath("git") + assert.NilError(t, err) + gitversion, _ := exec.Command(gitpath, "version").CombinedOutput() + t.Logf("%s", gitversion) // E.g. "git version 2.30.2" + + // Serve all repositories under root using the Smart HTTP protocol so + // they can be cloned. The Dumb HTTP protocol is incompatible with + // shallow cloning but we unconditionally shallow-clone submodules, and + // we explicitly disable the file protocol. + // (Another option would be to use `git daemon` and the Git protocol, + // but that listens on a fixed port number which is a recipe for + // disaster in CI. Funnily enough, `git daemon --port=0` works but there + // is no easy way to discover which port got picked!) + + // Associate git-http-backend logs with the current (sub)test. + // Incompatible with parallel subtests. + currentSubtest := t + githttp := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var logs bytes.Buffer + (&cgi.Handler{ + Path: gitpath, + Args: []string{"http-backend"}, + Dir: root, + Env: []string{ + "GIT_PROJECT_ROOT=" + root, + "GIT_HTTP_EXPORT_ALL=1", + }, + Stderr: &logs, + }).ServeHTTP(w, r) + if logs.Len() == 0 { + return + } + for { + line, err := logs.ReadString('\n') + currentSubtest.Log("git-http-backend: " + line) + if err != nil { + break + } + } + }) + server := httptest.NewServer(&githttp) + defer server.Close() + + eol := "\n" + autocrlf := gitGetConfig("core.autocrlf") + switch autocrlf { + case "true": + eol = "\r\n" + case "false", "input", "": + // accepted values + default: + t.Logf(`unknown core.autocrlf value: "%s"`, autocrlf) + } + + must := func(out []byte, err error) { + t.Helper() + if len(out) > 0 { + t.Logf("%s", out) + } + assert.NilError(t, err) + } + + gitDir := filepath.Join(root, "repo") + must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", gitDir)) + must(gitRepo{}.gitWithinDir(gitDir, "config", "user.email", "test@docker.com")) + must(gitRepo{}.gitWithinDir(gitDir, "config", "user.name", "Docker test")) + assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0o644)) + + subDir := filepath.Join(gitDir, "subdir") + assert.NilError(t, os.Mkdir(subDir, 0o755)) + assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0o644)) + + if runtime.GOOS != "windows" { + assert.NilError(t, os.Symlink("../subdir", filepath.Join(gitDir, "parentlink"))) + assert.NilError(t, os.Symlink("/subdir", filepath.Join(gitDir, "absolutelink"))) + } + + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "First commit")) + must(gitRepo{}.gitWithinDir(gitDir, "checkout", "-b", "test")) + + assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0o644)) + assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0o644)) + + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "Branch commit")) + must(gitRepo{}.gitWithinDir(gitDir, "checkout", "master")) + + // set up submodule + subrepoDir := filepath.Join(root, "subrepo") + must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", subrepoDir)) + must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.email", "test@docker.com")) + must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.name", "Docker test")) + + assert.NilError(t, os.WriteFile(filepath.Join(subrepoDir, "subfile"), []byte("subcontents"), 0o644)) + + must(gitRepo{}.gitWithinDir(subrepoDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(subrepoDir, "commit", "-am", "Subrepo initial")) + + must(gitRepo{}.gitWithinDir(gitDir, "submodule", "add", server.URL+"/subrepo", "sub")) + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "With submodule")) + + type singleCase struct { + frag string + exp string + fail bool + submodule bool + } + + cases := []singleCase{ + {"", "FROM scratch", false, true}, + {"master", "FROM scratch", false, true}, + {":subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, + {":nosubdir", "", true, false}, // missing directory error + {":Dockerfile", "", true, false}, // not a directory error + {"master:nosubdir", "", true, false}, + {"master:subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, + {"master:../subdir", "", true, false}, + {"test", "FROM scratch" + eol + "EXPOSE 3000", false, false}, + {"test:", "FROM scratch" + eol + "EXPOSE 3000", false, false}, + {"test:subdir", "FROM busybox" + eol + "EXPOSE 5000", false, false}, + } + + if runtime.GOOS != "windows" { + // Windows GIT (2.7.1 x64) does not support parentlink/absolutelink. Sample output below + // git --work-tree .\repo --git-dir .\repo\.git add -A + // error: readlink("absolutelink"): Function not implemented + // error: unable to index file absolutelink + // fatal: adding files failed + cases = append(cases, singleCase{frag: "master:absolutelink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + cases = append(cases, singleCase{frag: "master:parentlink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + } + + for _, c := range cases { + t.Run(c.frag, func(t *testing.T) { + currentSubtest = t + ref, subdir := getRefAndSubdir(c.frag) + r, err := gitRepo{remote: server.URL + "/repo", ref: ref, subdir: subdir}.clone() + + if c.fail { + assert.Check(t, is.ErrorContains(err, "")) + return + } + assert.NilError(t, err) + defer os.RemoveAll(r) + if c.submodule { + b, err := os.ReadFile(filepath.Join(r, "sub/subfile")) + assert.NilError(t, err) + assert.Check(t, is.Equal("subcontents", string(b))) + } else { + _, err := os.Stat(filepath.Join(r, "sub/subfile")) + assert.ErrorContains(t, err, "") + assert.Assert(t, os.IsNotExist(err)) + } + + b, err := os.ReadFile(filepath.Join(r, "Dockerfile")) + assert.NilError(t, err) + assert.Check(t, is.Equal(c.exp, string(b))) + }) + } +} + +func TestValidGitTransport(t *testing.T) { + gitUrls := []string{ + "git://github.com/docker/docker", + "git@github.com:docker/docker.git", + "git@bitbucket.org:atlassianlabs/atlassian-docker.git", + "https://github.com/docker/docker.git", + "http://github.com/docker/docker.git", + "http://github.com/docker/docker.git#branch", + "http://github.com/docker/docker.git#:dir", + } + incompleteGitUrls := []string{ + "github.com/docker/docker", + } + + for _, u := range gitUrls { + if !isGitTransport(u) { + t.Fatalf("%q should be detected as valid Git prefix", u) + } + } + + for _, u := range incompleteGitUrls { + if isGitTransport(u) { + t.Fatalf("%q should not be detected as valid Git prefix", u) + } + } +} + +func TestGitInvalidRef(t *testing.T) { + gitUrls := []string{ + "git://github.com/moby/moby#--foo bar", + "git@github.com/moby/moby#--upload-pack=sleep;:", + "git@g.com:a/b.git#-B", + "git@g.com:a/b.git#with space", + } + + for _, u := range gitUrls { + _, err := Clone(u) + assert.Assert(t, err != nil) + // On Windows, git has different case for the "invalid refspec" error, + // so we can't use ErrorContains. + assert.Check(t, is.Contains(strings.ToLower(err.Error()), "invalid refspec")) + } +} diff --git a/vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go b/cli/command/image/build/internal/urlutil/urlutil.go similarity index 98% rename from vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go rename to cli/command/image/build/internal/urlutil/urlutil.go index a2225b5960e1..b1cf47ce316b 100644 --- a/vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go +++ b/cli/command/image/build/internal/urlutil/urlutil.go @@ -8,7 +8,7 @@ package urlutil import ( "strings" - "github.com/docker/docker/internal/lazyregexp" + "github.com/docker/cli/internal/lazyregexp" ) // urlPathWithFragmentSuffix matches fragments to use as Git reference and build diff --git a/cli/command/image/build/internal/urlutil/urlutil_test.go b/cli/command/image/build/internal/urlutil/urlutil_test.go new file mode 100644 index 000000000000..f6d0a35de3ec --- /dev/null +++ b/cli/command/image/build/internal/urlutil/urlutil_test.go @@ -0,0 +1,42 @@ +package urlutil + +import "testing" + +var ( + gitUrls = []string{ + "git://github.com/docker/docker", + "git@github.com:docker/docker.git", + "git@bitbucket.org:atlassianlabs/atlassian-docker.git", + "https://github.com/docker/docker.git", + "http://github.com/docker/docker.git", + "http://github.com/docker/docker.git#branch", + "http://github.com/docker/docker.git#:dir", + } + incompleteGitUrls = []string{ + "github.com/docker/docker", + } + invalidGitUrls = []string{ + "http://github.com/docker/docker.git:#branch", + "https://github.com/docker/dgit", + } +) + +func TestIsGIT(t *testing.T) { + for _, url := range gitUrls { + if !IsGitURL(url) { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range incompleteGitUrls { + if !IsGitURL(url) { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range invalidGitUrls { + if IsGitURL(url) { + t.Fatalf("%q should not be detected as valid Git prefix", url) + } + } +} diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 22105e47c024..27884e3bd1ea 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -13,9 +13,9 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/build" "github.com/google/go-cmp/cmp" "github.com/moby/go-archive/compression" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/skip" @@ -25,7 +25,7 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { t.Setenv("DOCKER_BUILDKIT", "0") buffer := new(bytes.Buffer) fakeBuild := newFakeBuild() - fakeImageBuild := func(ctx context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) { + fakeImageBuild := func(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) { tee := io.TeeReader(buildContext, buffer) gzipReader, err := gzip.NewReader(tee) assert.NilError(t, err) @@ -47,7 +47,6 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { options.compress = true options.dockerfileName = "-" options.context = dir.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"} @@ -74,7 +73,6 @@ func TestRunBuildResetsUidAndGidInContext(t *testing.T) { options := newBuildOptions() options.context = dir.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) headers := fakeBuild.headers(t) @@ -109,7 +107,6 @@ COPY data /data options := newBuildOptions() options.context = dir.Path() options.dockerfileName = df.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"} @@ -123,7 +120,7 @@ COPY data /data // to support testing (ex: docker/cli#294) func TestRunBuildFromGitHubSpecialCase(t *testing.T) { t.Setenv("DOCKER_BUILDKIT", "0") - cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{})) + cmd := newBuildCommand(test.NewFakeCli(&fakeClient{})) // Clone a small repo that exists so git doesn't prompt for credentials cmd.SetArgs([]string{"github.com/docker/for-win"}) cmd.SetOut(io.Discard) @@ -145,8 +142,8 @@ func TestRunBuildFromLocalGitHubDir(t *testing.T) { err = os.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0o644) assert.NilError(t, err) - client := test.NewFakeCli(&fakeClient{}) - cmd := NewBuildCommand(client) + fakeCLI := test.NewFakeCli(&fakeClient{}) + cmd := newBuildCommand(fakeCLI) cmd.SetArgs([]string{buildDir}) cmd.SetOut(io.Discard) err = cmd.Execute() @@ -170,7 +167,6 @@ RUN echo hello world cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build}) options := newBuildOptions() options.context = tmpDir.Join("context-link") - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"}) @@ -178,18 +174,18 @@ RUN echo hello world type fakeBuild struct { context *tar.Reader - options build.ImageBuildOptions + options client.ImageBuildOptions } func newFakeBuild() *fakeBuild { return &fakeBuild{} } -func (f *fakeBuild) build(_ context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) { +func (f *fakeBuild) build(_ context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) { f.context = tar.NewReader(buildContext) f.options = options body := new(bytes.Buffer) - return build.ImageBuildResponse{Body: io.NopCloser(body)}, nil + return client.ImageBuildResult{Body: io.NopCloser(body)}, nil } func (f *fakeBuild) headers(t *testing.T) []*tar.Header { diff --git a/cli/command/image/client_test.go b/cli/command/image/client_test.go index dd81f6372554..fd2ab4a1cb2a 100644 --- a/cli/command/image/client_test.go +++ b/cli/command/image/client_test.go @@ -3,124 +3,130 @@ package image import ( "context" "io" + "net/http" "strings" "time" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - imageTagFunc func(string, string) error - imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) - imageRemoveFunc func(image string, options image.RemoveOptions) ([]image.DeleteResponse, error) - imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error) - infoFunc func() (system.Info, error) - imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error) - imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error) - imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) - imageListFunc func(options image.ListOptions) ([]image.Summary, error) - imageInspectFunc func(img string) (image.InspectResponse, error) - imageImportFunc func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) - imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) - imageBuildFunc func(context.Context, io.Reader, build.ImageBuildOptions) (build.ImageBuildResponse, error) + imageTagFunc func(options client.ImageTagOptions) (client.ImageTagResult, error) + imageSaveFunc func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) + imageRemoveFunc func(image string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) + imagePushFunc func(ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) + infoFunc func() (client.SystemInfoResult, error) + imagePullFunc func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) + imagePruneFunc func(options client.ImagePruneOptions) (client.ImagePruneResult, error) + imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) + imageListFunc func(options client.ImageListOptions) (client.ImageListResult, error) + imageInspectFunc func(img string) (client.ImageInspectResult, error) + imageImportFunc func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) + imageHistoryFunc func(img string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) + imageBuildFunc func(context.Context, io.Reader, client.ImageBuildOptions) (client.ImageBuildResult, error) } -func (cli *fakeClient) ImageTag(_ context.Context, img, ref string) error { +type fakeStreamResult struct { + io.ReadCloser + client.ImagePushResponse // same interface as [client.ImagePullResponse] +} + +func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) } +func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() } + +func (cli *fakeClient) ImageTag(_ context.Context, options client.ImageTagOptions) (client.ImageTagResult, error) { if cli.imageTagFunc != nil { - return cli.imageTagFunc(img, ref) + return cli.imageTagFunc(options) } - return nil + return client.ImageTagResult{}, nil } -func (cli *fakeClient) ImageSave(_ context.Context, images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) { +func (cli *fakeClient) ImageSave(_ context.Context, images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { if cli.imageSaveFunc != nil { return cli.imageSaveFunc(images, options...) } - return io.NopCloser(strings.NewReader("")), nil + return http.NoBody, nil } -func (cli *fakeClient) ImageRemove(_ context.Context, img string, - options image.RemoveOptions, -) ([]image.DeleteResponse, error) { +func (cli *fakeClient) ImageRemove(_ context.Context, img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { if cli.imageRemoveFunc != nil { return cli.imageRemoveFunc(img, options) } - return []image.DeleteResponse{}, nil + return client.ImageRemoveResult{}, nil } -func (cli *fakeClient) ImagePush(_ context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) { +func (cli *fakeClient) ImagePush(_ context.Context, ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) { if cli.imagePushFunc != nil { return cli.imagePushFunc(ref, options) } - return io.NopCloser(strings.NewReader("")), nil + // FIXME(thaJeztah): how to mock this? + return fakeStreamResult{ReadCloser: http.NoBody}, nil } -func (cli *fakeClient) Info(_ context.Context) (system.Info, error) { +func (cli *fakeClient) Info(_ context.Context, _ client.InfoOptions) (client.SystemInfoResult, error) { if cli.infoFunc != nil { return cli.infoFunc() } - return system.Info{}, nil + return client.SystemInfoResult{}, nil } -func (cli *fakeClient) ImagePull(_ context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) { +func (cli *fakeClient) ImagePull(_ context.Context, ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) { if cli.imagePullFunc != nil { return cli.imagePullFunc(ref, options) } - return io.NopCloser(strings.NewReader("")), nil + // FIXME(thaJeztah): how to mock this? + return fakeStreamResult{ReadCloser: http.NoBody}, nil } -func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (image.PruneReport, error) { - if cli.imagesPruneFunc != nil { - return cli.imagesPruneFunc(pruneFilter) +func (cli *fakeClient) ImagePrune(_ context.Context, opts client.ImagePruneOptions) (client.ImagePruneResult, error) { + if cli.imagePruneFunc != nil { + return cli.imagePruneFunc(opts) } - return image.PruneReport{}, nil + return client.ImagePruneResult{}, nil } -func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) { +func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { if cli.imageLoadFunc != nil { return cli.imageLoadFunc(input, options...) } - return image.LoadResponse{}, nil + return http.NoBody, nil } -func (cli *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) { +func (cli *fakeClient) ImageList(_ context.Context, options client.ImageListOptions) (client.ImageListResult, error) { if cli.imageListFunc != nil { return cli.imageListFunc(options) } - return []image.Summary{}, nil + return client.ImageListResult{}, nil } -func (cli *fakeClient) ImageInspect(_ context.Context, img string, _ ...client.ImageInspectOption) (image.InspectResponse, error) { +func (cli *fakeClient) ImageInspect(_ context.Context, img string, _ ...client.ImageInspectOption) (client.ImageInspectResult, error) { if cli.imageInspectFunc != nil { return cli.imageInspectFunc(img) } - return image.InspectResponse{}, nil + return client.ImageInspectResult{}, nil } -func (cli *fakeClient) ImageImport(_ context.Context, source image.ImportSource, ref string, - options image.ImportOptions, -) (io.ReadCloser, error) { +func (cli *fakeClient) ImageImport(_ context.Context, source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { if cli.imageImportFunc != nil { return cli.imageImportFunc(source, ref, options) } - return io.NopCloser(strings.NewReader("")), nil + return http.NoBody, nil } -func (cli *fakeClient) ImageHistory(_ context.Context, img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { +func (cli *fakeClient) ImageHistory(_ context.Context, img string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { if cli.imageHistoryFunc != nil { return cli.imageHistoryFunc(img, options...) } - return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil + return client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, + }, nil } -func (cli *fakeClient) ImageBuild(ctx context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) { +func (cli *fakeClient) ImageBuild(ctx context.Context, buildContext io.Reader, options client.ImageBuildOptions) (client.ImageBuildResult, error) { if cli.imageBuildFunc != nil { return cli.imageBuildFunc(ctx, buildContext, options) } - return build.ImageBuildResponse{Body: io.NopCloser(strings.NewReader(""))}, nil + return client.ImageBuildResult{Body: io.NopCloser(strings.NewReader(""))}, nil } diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go index c035da17f05c..aa3a5962f389 100644 --- a/cli/command/image/cmd.go +++ b/cli/command/image/cmd.go @@ -3,30 +3,47 @@ package image import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewImageCommand returns a cobra command for `image` subcommands -func NewImageCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newBuildCommand) + commands.Register(newPullCommand) + commands.Register(newPushCommand) + commands.Register(newImagesCommand) + commands.Register(newImageCommand) + commands.RegisterLegacy(newHistoryCommand) + commands.RegisterLegacy(newImportCommand) + commands.RegisterLegacy(newLoadCommand) + commands.RegisterLegacy(newRemoveCommand) + commands.RegisterLegacy(newSaveCommand) + commands.RegisterLegacy(newTagCommand) +} + +// newImageCommand returns a cobra command for `image` subcommands +func newImageCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Manage images", Args: cli.NoArgs, RunE: command.ShowHelp(dockerCli.Err()), + + DisableFlagsInUseLine: true, } cmd.AddCommand( - NewBuildCommand(dockerCli), - NewHistoryCommand(dockerCli), - NewImportCommand(dockerCli), - NewLoadCommand(dockerCli), - NewPullCommand(dockerCli), - NewPushCommand(dockerCli), - NewSaveCommand(dockerCli), - NewTagCommand(dockerCli), + newBuildCommand(dockerCli), + newHistoryCommand(dockerCli), + newImportCommand(dockerCli), + newLoadCommand(dockerCli), + newPullCommand(dockerCli), + newPushCommand(dockerCli), + newSaveCommand(dockerCli), + newTagCommand(dockerCli), newListCommand(dockerCli), - newRemoveCommand(dockerCli), + newImageRemoveCommand(dockerCli), newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), + newPruneCommand(dockerCli), ) return cmd } diff --git a/cli/command/image/formatter_history.go b/cli/command/image/formatter_history.go index e2fcd155ce23..3a2cc51df57a 100644 --- a/cli/command/image/formatter_history.go +++ b/cli/command/image/formatter_history.go @@ -6,8 +6,9 @@ import ( "time" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/image" "github.com/docker/go-units" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" ) const ( @@ -19,8 +20,8 @@ const ( commentHeader = "COMMENT" ) -// NewHistoryFormat returns a format for rendering an HistoryContext -func NewHistoryFormat(source string, quiet bool, human bool) formatter.Format { +// newHistoryFormat returns a format for rendering a historyContext. +func newHistoryFormat(source string, quiet bool, human bool) formatter.Format { if source == formatter.TableFormatKey { switch { case quiet: @@ -35,27 +36,32 @@ func NewHistoryFormat(source string, quiet bool, human bool) formatter.Format { return formatter.Format(source) } -// HistoryWrite writes the context -func HistoryWrite(ctx formatter.Context, human bool, histories []image.HistoryResponseItem) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, history := range histories { - historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} - if err := format(historyCtx); err != nil { +// historyWrite writes the context +func historyWrite(fmtCtx formatter.Context, human bool, history client.ImageHistoryResult) error { + historyCtx := &historyContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": historyIDHeader, + "CreatedSince": formatter.CreatedSinceHeader, + "CreatedAt": formatter.CreatedAtHeader, + "CreatedBy": createdByHeader, + "Size": formatter.SizeHeader, + "Comment": commentHeader, + }, + }, + } + return fmtCtx.Write(historyCtx, func(format func(subContext formatter.SubContext) error) error { + for _, h := range history.Items { + if err := format(&historyContext{ + trunc: fmtCtx.Trunc, + h: h, + human: human, + }); err != nil { return err } } return nil - } - historyCtx := &historyContext{} - historyCtx.Header = formatter.SubHeaderContext{ - "ID": historyIDHeader, - "CreatedSince": formatter.CreatedSinceHeader, - "CreatedAt": formatter.CreatedAtHeader, - "CreatedBy": createdByHeader, - "Size": formatter.SizeHeader, - "Comment": commentHeader, - } - return ctx.Write(historyCtx, render) + }) } type historyContext struct { diff --git a/cli/command/image/formatter_history_test.go b/cli/command/image/formatter_history_test.go index 611f7d50674f..77c38dd09e7a 100644 --- a/cli/command/image/formatter_history_test.go +++ b/cli/command/image/formatter_history_test.go @@ -9,7 +9,8 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -106,8 +107,8 @@ func TestHistoryContext_CreatedSince(t *testing.T) { } func TestHistoryContext_CreatedBy(t *testing.T) { - const withTabs = `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` //nolint:revive // ignore line-length-limit - const expected = `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` //nolint:revive // ignore line-length-limit + const withTabs = `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb https://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` //nolint:revive // ignore line-length-limit + const expected = `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb https://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` //nolint:revive // ignore line-length-limit var ctx historyContext cases := []historyCase{ @@ -196,20 +197,22 @@ func TestHistoryContext_Table(t *testing.T) { out := bytes.NewBufferString("") unixTime := time.Now().AddDate(0, 0, -1).Unix() oldDate := time.Now().AddDate(-17, 0, 0).Unix() - histories := []image.HistoryResponseItem{ - { - ID: "imageID1", - Created: unixTime, - CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", - Size: int64(182964289), - Comment: "Hi", - Tags: []string{"image:tag2"}, + histories := client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{ + { + ID: "imageID1", + Created: unixTime, + CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", + Size: int64(182964289), + Comment: "Hi", + Tags: []string{"image:tag2"}, + }, + {ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID5", Created: 0, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID6", Created: oldDate, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, }, - {ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID5", Created: 0, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, - {ID: "imageID6", Created: oldDate, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, } //nolint:dupword // ignore "Duplicate words (CREATED) found" @@ -237,7 +240,7 @@ imageID6 17 years ago /bin/bash echo 183MB }{ { formatter.Context{ - Format: NewHistoryFormat("table", false, true), + Format: newHistoryFormat("table", false, true), Trunc: true, Output: out, }, @@ -245,7 +248,7 @@ imageID6 17 years ago /bin/bash echo 183MB }, { formatter.Context{ - Format: NewHistoryFormat("table", false, true), + Format: newHistoryFormat("table", false, true), Trunc: false, Output: out, }, @@ -255,7 +258,7 @@ imageID6 17 years ago /bin/bash echo 183MB for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { - err := HistoryWrite(tc.context, true, histories) + err := historyWrite(tc.context, true, histories) assert.NilError(t, err) assert.Equal(t, out.String(), tc.expected) // Clean buffer diff --git a/cli/command/image/history.go b/cli/command/image/history.go index 075b3d84db31..ef9be33a5d05 100644 --- a/cli/command/image/history.go +++ b/cli/command/image/history.go @@ -2,6 +2,7 @@ package image import ( "context" + "fmt" "github.com/containerd/platforms" "github.com/docker/cli/cli" @@ -9,8 +10,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -24,8 +24,8 @@ type historyOptions struct { format string } -// NewHistoryCommand creates a new `docker history` command -func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { +// newHistoryCommand creates a new "docker image history" command. +func newHistoryCommand(dockerCLI command.Cli) *cobra.Command { var opts historyOptions cmd := &cobra.Command{ @@ -34,12 +34,13 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.image = args[0] - return runHistory(cmd.Context(), dockerCli, opts) + return runHistory(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), Annotations: map[string]string{ "aliases": "docker image history, docker history", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -51,7 +52,7 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&opts.platform, "platform", "", `Show history for the given platform. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`) _ = flags.SetAnnotation("platform", "version", []string{"1.48"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } @@ -60,7 +61,7 @@ func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) if opts.platform != "" { p, err := platforms.Parse(opts.platform) if err != nil { - return errors.Wrap(err, "invalid platform") + return fmt.Errorf("invalid platform: %w", err) } options = append(options, client.ImageHistoryWithPlatform(p)) } @@ -77,8 +78,8 @@ func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) historyCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewHistoryFormat(format, opts.quiet, opts.human), + Format: newHistoryFormat(format, opts.quiet, opts.human), Trunc: !opts.noTrunc, } - return HistoryWrite(historyCtx, opts.human, history) + return historyWrite(historyCtx, opts.human, history) } diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go index 7ecc054d0a1b..f130821000ff 100644 --- a/cli/command/image/history_test.go +++ b/cli/command/image/history_test.go @@ -8,8 +8,8 @@ import ( "time" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -19,7 +19,7 @@ func TestNewHistoryCommandErrors(t *testing.T) { name string args []string expectedError string - imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) + imageHistoryFunc func(img string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) }{ { name: "wrong-args", @@ -30,8 +30,8 @@ func TestNewHistoryCommandErrors(t *testing.T) { name: "client-error", args: []string{"image:tag"}, expectedError: "something went wrong", - imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{}}, errors.New("something went wrong") + imageHistoryFunc: func(string, ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { + return client.ImageHistoryResult{}, errors.New("something went wrong") }, }, { @@ -42,7 +42,7 @@ func TestNewHistoryCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) + cmd := newHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -55,17 +55,19 @@ func TestNewHistoryCommandSuccess(t *testing.T) { testCases := []struct { name string args []string - imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) + imageHistoryFunc func(img string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) }{ { name: "simple", args: []string{"image:tag"}, - imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - Comment: "none", - }}, nil + imageHistoryFunc: func(string, ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { + return client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{{ + ID: "1234567890123456789", + Created: time.Now().Unix(), + Comment: "none", + }}, + }, nil }, }, { @@ -75,36 +77,42 @@ func TestNewHistoryCommandSuccess(t *testing.T) { { name: "non-human", args: []string{"--human=false", "image:tag"}, - imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "abcdef", - Created: time.Date(2017, 1, 1, 12, 0, 3, 0, time.UTC).Unix(), - CreatedBy: "rose", - Comment: "new history item!", - }}, nil + imageHistoryFunc: func(string, ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { + return client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{{ + ID: "abcdef", + Created: time.Date(2017, 1, 1, 12, 0, 3, 0, time.UTC).Unix(), + CreatedBy: "rose", + Comment: "new history item!", + }}, + }, nil }, }, { name: "quiet-no-trunc", args: []string{"--quiet", "--no-trunc", "image:tag"}, - imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil + imageHistoryFunc: func(string, ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { + return client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{{ + ID: "1234567890123456789", + Created: time.Now().Unix(), + }}, + }, nil }, }, { name: "platform", args: []string{"--platform", "linux/amd64", "image:tag"}, - imageHistoryFunc: func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) { + imageHistoryFunc: func(img string, options ...client.ImageHistoryOption) (client.ImageHistoryResult, error) { // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/ // assert.Check(t, is.Contains(options, client.ImageHistoryWithPlatform(ocispec.Platform{OS: "linux", Architecture: "amd64"}))) - return []image.HistoryResponseItem{{ - ID: "1234567890123456789", - Created: time.Now().Unix(), - }}, nil + return client.ImageHistoryResult{ + Items: []image.HistoryResponseItem{{ + ID: "1234567890123456789", + Created: time.Now().Unix(), + }}, + }, nil }, }, } @@ -114,7 +122,7 @@ func TestNewHistoryCommandSuccess(t *testing.T) { // printed in the current timezone t.Setenv("TZ", "UTC") cli := test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}) - cmd := NewHistoryCommand(cli) + cmd := newHistoryCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/cli/command/image/import.go b/cli/command/image/import.go index a55ea4ab5bca..b4be964050d0 100644 --- a/cli/command/image/import.go +++ b/cli/command/image/import.go @@ -5,12 +5,14 @@ import ( "os" "strings" + "github.com/containerd/platforms" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/jsonstream" dockeropts "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/client" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) @@ -22,8 +24,8 @@ type importOptions struct { platform string } -// NewImportCommand creates a new `docker import` command -func NewImportCommand(dockerCli command.Cli) *cobra.Command { +// newImportCommand creates a new "docker image import" command. +func newImportCommand(dockerCLI command.Cli) *cobra.Command { var options importOptions cmd := &cobra.Command{ @@ -35,11 +37,12 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { options.reference = args[1] } - return runImport(cmd.Context(), dockerCli, options) + return runImport(cmd.Context(), dockerCLI, options) }, Annotations: map[string]string{ "aliases": "docker image import, docker import", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -47,24 +50,25 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command { options.changes = dockeropts.NewListOpts(nil) flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image") flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image") - command.AddPlatformFlag(flags, &options.platform) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } func runImport(ctx context.Context, dockerCli command.Cli, options importOptions) error { - var source image.ImportSource + var source client.ImageImportSource switch { case options.source == "-": // import from STDIN - source = image.ImportSource{ + source = client.ImageImportSource{ Source: dockerCli.In(), SourceName: options.source, } case strings.HasPrefix(options.source, "https://"), strings.HasPrefix(options.source, "http://"): // import from a remote source (handled by the daemon) - source = image.ImportSource{ + source = client.ImageImportSource{ SourceName: options.source, } default: @@ -74,16 +78,26 @@ func runImport(ctx context.Context, dockerCli command.Cli, options importOptions return err } defer file.Close() - source = image.ImportSource{ + source = client.ImageImportSource{ Source: file, SourceName: "-", } } - responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, image.ImportOptions{ + // TODO(thaJeztah): add a platform option-type / flag-type. + var ociPlatform ocispec.Platform + if options.platform != "" { + var err error + ociPlatform, err = platforms.Parse(options.platform) + if err != nil { + return err + } + } + + responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, client.ImageImportOptions{ Message: options.message, Changes: options.changes.GetSlice(), - Platform: options.platform, + Platform: ociPlatform, }) if err != nil { return err diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go index d6d69948df1f..2b364d3efd29 100644 --- a/cli/command/image/import_test.go +++ b/cli/command/image/import_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -17,7 +17,7 @@ func TestNewImportCommandErrors(t *testing.T) { name string args []string expectedError string - imageImportFunc func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) + imageImportFunc func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) }{ { name: "wrong-args", @@ -28,13 +28,13 @@ func TestNewImportCommandErrors(t *testing.T) { name: "import-failed", args: []string{"testdata/import-command-success.input.txt"}, expectedError: "something went wrong", - imageImportFunc: func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { + imageImportFunc: func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { return nil, errors.New("something went wrong") }, }, } for _, tc := range testCases { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -43,7 +43,7 @@ func TestNewImportCommandErrors(t *testing.T) { } func TestNewImportCommandInvalidFile(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) @@ -54,7 +54,7 @@ func TestNewImportCommandSuccess(t *testing.T) { testCases := []struct { name string args []string - imageImportFunc func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) + imageImportFunc func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) }{ { name: "simple", @@ -67,7 +67,7 @@ func TestNewImportCommandSuccess(t *testing.T) { { name: "double", args: []string{"-", "image:local"}, - imageImportFunc: func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { + imageImportFunc: func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { assert.Check(t, is.Equal("image:local", ref)) return io.NopCloser(strings.NewReader("")), nil }, @@ -75,7 +75,7 @@ func TestNewImportCommandSuccess(t *testing.T) { { name: "message", args: []string{"--message", "test message", "-"}, - imageImportFunc: func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { + imageImportFunc: func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { assert.Check(t, is.Equal("test message", options.Message)) return io.NopCloser(strings.NewReader("")), nil }, @@ -83,7 +83,7 @@ func TestNewImportCommandSuccess(t *testing.T) { { name: "change", args: []string{"--change", "ENV DEBUG=true", "-"}, - imageImportFunc: func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { + imageImportFunc: func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { assert.Check(t, is.Equal("ENV DEBUG=true", options.Changes[0])) return io.NopCloser(strings.NewReader("")), nil }, @@ -91,7 +91,7 @@ func TestNewImportCommandSuccess(t *testing.T) { { name: "change legacy syntax", args: []string{"--change", "ENV DEBUG true", "-"}, - imageImportFunc: func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { + imageImportFunc: func(source client.ImageImportSource, ref string, options client.ImageImportOptions) (client.ImageImportResult, error) { assert.Check(t, is.Equal("ENV DEBUG true", options.Changes[0])) return io.NopCloser(strings.NewReader("")), nil }, @@ -99,7 +99,7 @@ func TestNewImportCommandSuccess(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go index 5c7a41806a30..e0b0a04fc722 100644 --- a/cli/command/image/inspect.go +++ b/cli/command/image/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package image @@ -13,8 +13,8 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) @@ -26,7 +26,7 @@ type inspectOptions struct { } // newInspectCommand creates a new cobra.Command for `docker image inspect` -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -35,9 +35,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.refs = args - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.ImageNames(dockerCli, -1), + ValidArgsFunction: completion.ImageNames(dockerCLI, -1), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -51,7 +52,7 @@ If the image or the server is not multi-platform capable, the command will error 'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`) flags.SetAnnotation("platform", "version", []string{"1.49"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } diff --git a/cli/command/image/inspect_test.go b/cli/command/image/inspect_test.go index 08348ace2b63..ea49eb5bc60c 100644 --- a/cli/command/image/inspect_test.go +++ b/cli/command/image/inspect_test.go @@ -6,7 +6,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -41,39 +42,41 @@ func TestNewInspectCommandSuccess(t *testing.T) { name string args []string imageCount int - imageInspectFunc func(img string) (image.InspectResponse, error) + imageInspectFunc func(img string) (client.ImageInspectResult, error) }{ { name: "simple", args: []string{"image"}, imageCount: 1, - imageInspectFunc: func(img string) (image.InspectResponse, error) { + imageInspectFunc: func(img string) (client.ImageInspectResult, error) { imageInspectInvocationCount++ assert.Check(t, is.Equal("image", img)) - return image.InspectResponse{}, nil + return client.ImageInspectResult{}, nil }, }, { name: "format", imageCount: 1, args: []string{"--format='{{.ID}}'", "image"}, - imageInspectFunc: func(img string) (image.InspectResponse, error) { + imageInspectFunc: func(img string) (client.ImageInspectResult, error) { imageInspectInvocationCount++ - return image.InspectResponse{ID: img}, nil + return client.ImageInspectResult{ + InspectResponse: image.InspectResponse{ID: img}, + }, nil }, }, { name: "simple-many", args: []string{"image1", "image2"}, imageCount: 2, - imageInspectFunc: func(img string) (image.InspectResponse, error) { + imageInspectFunc: func(img string) (client.ImageInspectResult, error) { imageInspectInvocationCount++ if imageInspectInvocationCount == 1 { assert.Check(t, is.Equal("image1", img)) } else { assert.Check(t, is.Equal("image2", img)) } - return image.InspectResponse{}, nil + return client.ImageInspectResult{}, nil }, }, } diff --git a/cli/command/image/list.go b/cli/command/image/list.go index ce2238dc8298..b06e5c1779cd 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -1,3 +1,6 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + package image import ( @@ -5,13 +8,16 @@ import ( "errors" "fmt" "io" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -24,12 +30,11 @@ type imagesOptions struct { showDigests bool format string filter opts.FilterOpt - calledAs string tree bool } -// NewImagesCommand creates a new `docker images` command -func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { +// newImagesCommand creates a new `docker images` command +func newImagesCommand(dockerCLI command.Cli) *cobra.Command { options := imagesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -40,16 +45,21 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { if len(args) > 0 { options.matchName = args[0] } - // Pass through how the command was invoked. We use this to print - // warnings when an ambiguous argument was passed when using the - // legacy (top-level) "docker images" subcommand. - options.calledAs = cmd.CalledAs() - return runImages(cmd.Context(), dockerCLI, options) + numImages, err := runImages(cmd.Context(), dockerCLI, options) + if err != nil { + return err + } + if numImages == 0 && options.matchName != "" && cmd.CalledAs() == "images" { + printAmbiguousHint(dockerCLI.Err(), options.matchName) + } + return nil }, Annotations: map[string]string{ "category-top": "7", "aliases": "docker image ls, docker image list, docker images", }, + DisableFlagsInUseLine: true, + ValidArgsFunction: completion.ImageNamesWithBase(dockerCLI, 1), } flags := cmd.Flags() @@ -69,55 +79,59 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { } func newListCommand(dockerCLI command.Cli) *cobra.Command { - cmd := *NewImagesCommand(dockerCLI) + cmd := *newImagesCommand(dockerCLI) cmd.Aliases = []string{"list"} cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" return &cmd } -func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) error { +func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) (int, error) { filters := options.filter.Value() if options.matchName != "" { filters.Add("reference", options.matchName) } - if options.tree { - if options.quiet { - return errors.New("--quiet is not yet supported with --tree") - } - if options.noTrunc { - return errors.New("--no-trunc is not yet supported with --tree") - } - if options.showDigests { - return errors.New("--show-digest is not yet supported with --tree") - } - if options.format != "" { - return errors.New("--format is not yet supported with --tree") - } + useTree, err := shouldUseTree(options) + if err != nil { + return 0, err + } - return runTree(ctx, dockerCLI, treeOptions{ - all: options.all, - filters: filters, - }) + listOpts := client.ImageListOptions{ + All: options.all, + Filters: filters, + Manifests: useTree, } - images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{ - All: options.all, - Filters: filters, - }) + res, err := dockerCLI.Client().ImageList(ctx, listOpts) if err != nil { - return err + return 0, err + } + + images := res.Items + if !options.all { + if dangling, ok := filters["dangling"]; !ok || dangling["false"] { + images = slices.DeleteFunc(images, isDangling) + } } format := options.format if len(format) == 0 { - if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet { + if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet && !options.tree { format = dockerCLI.ConfigFile().ImagesFormat + useTree = false } else { format = formatter.TableFormatKey } } + if useTree { + return runTree(ctx, dockerCLI, treeOptions{ + images: images, + filters: filters, + expanded: options.tree, + }) + } + imageCtx := formatter.ImageContext{ Context: formatter.Context{ Output: dockerCLI.Out(), @@ -127,17 +141,42 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions Digest: options.showDigests, } if err := formatter.ImageWrite(imageCtx, images); err != nil { - return err + return 0, err } - if options.matchName != "" && len(images) == 0 && options.calledAs == "images" { - printAmbiguousHint(dockerCLI.Err(), options.matchName) + return len(images), nil +} + +func shouldUseTree(options imagesOptions) (bool, error) { + if options.quiet { + if options.tree { + return false, errors.New("--quiet is not yet supported with --tree") + } + return false, nil + } + if options.noTrunc { + if options.tree { + return false, errors.New("--no-trunc is not yet supported with --tree") + } + return false, nil + } + if options.showDigests { + if options.tree { + return false, errors.New("--show-digest is not yet supported with --tree") + } + return false, nil + } + if options.format != "" { + if options.tree { + return false, errors.New("--format is not yet supported with --tree") + } + return false, nil } - return nil + return true, nil } // isDangling is a copy of [formatter.isDangling]. func isDangling(img image.Summary) bool { - if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 { + if len(img.RepoTags) == 0 { return true } return len(img.RepoTags) == 1 && img.RepoTags[0] == ":" && len(img.RepoDigests) == 1 && img.RepoDigests[0] == "@" diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go index 8b3ff715becf..8ceafa12c29f 100644 --- a/cli/command/image/list_test.go +++ b/cli/command/image/list_test.go @@ -4,13 +4,14 @@ import ( "errors" "fmt" "io" + "slices" "testing" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" ) @@ -19,7 +20,7 @@ func TestNewImagesCommandErrors(t *testing.T) { name string args []string expectedError string - imageListFunc func(options image.ListOptions) ([]image.Summary, error) + imageListFunc func(options client.ImageListOptions) (client.ImageListResult, error) }{ { name: "wrong-args", @@ -29,17 +30,17 @@ func TestNewImagesCommandErrors(t *testing.T) { { name: "failed-list", expectedError: "something went wrong", - imageListFunc: func(options image.ListOptions) ([]image.Summary, error) { - return []image.Summary{}, errors.New("something went wrong") + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + return client.ImageListResult{}, errors.New("something went wrong") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) + cmd := newImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) + cmd.SetArgs(nilToEmptySlice(tc.args)) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) } @@ -50,7 +51,7 @@ func TestNewImagesCommandSuccess(t *testing.T) { name string args []string imageFormat string - imageListFunc func(options image.ListOptions) ([]image.Summary, error) + imageListFunc func(options client.ImageListOptions) (client.ImageListResult, error) }{ { name: "simple", @@ -67,17 +68,17 @@ func TestNewImagesCommandSuccess(t *testing.T) { { name: "match-name", args: []string{"image"}, - imageListFunc: func(options image.ListOptions) ([]image.Summary, error) { - assert.Check(t, is.Equal("image", options.Filters.Get("reference")[0])) - return []image.Summary{}, nil + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + assert.Check(t, options.Filters["reference"]["image"]) + return client.ImageListResult{}, nil }, }, { name: "filters", args: []string{"--filter", "name=value"}, - imageListFunc: func(options image.ListOptions) ([]image.Summary, error) { - assert.Check(t, is.Equal("value", options.Filters.Get("name")[0])) - return []image.Summary{}, nil + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + assert.Check(t, options.Filters["name"]["value"]) + return client.ImageListResult{}, nil }, }, } @@ -85,10 +86,10 @@ func TestNewImagesCommandSuccess(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) - cmd := NewImagesCommand(cli) + cmd := newImagesCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) + cmd.SetArgs(nilToEmptySlice(tc.args)) err := cmd.Execute() assert.NilError(t, err) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name)) @@ -98,13 +99,14 @@ func TestNewImagesCommandSuccess(t *testing.T) { func TestNewListCommandAlias(t *testing.T) { cmd := newListCommand(test.NewFakeCli(&fakeClient{})) + cmd.SetArgs([]string{""}) assert.Check(t, cmd.HasAlias("list")) assert.Check(t, !cmd.HasAlias("other")) } func TestNewListCommandAmbiguous(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) - cmd := NewImagesCommand(cli) + cmd := newImagesCommand(cli) cmd.SetOut(io.Discard) // Set the Use field to mimic that the command was called as "docker images", @@ -115,3 +117,90 @@ func TestNewListCommandAmbiguous(t *testing.T) { assert.NilError(t, err) golden.Assert(t, cli.ErrBuffer().String(), "list-command-ambiguous.golden") } + +func TestImagesFilterDangling(t *testing.T) { + // Create test images with different states + items := []image.Summary{ + { + ID: "sha256:87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7", + RepoTags: []string{"myimage:latest"}, + RepoDigests: []string{"myimage@sha256:abc123"}, + }, + { + ID: "sha256:0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f", + RepoTags: []string{}, + RepoDigests: []string{}, + }, + { + ID: "sha256:a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478", + RepoTags: []string{}, + RepoDigests: []string{"image@sha256:a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478"}, + }, + } + + testCases := []struct { + name string + args []string + imageListFunc func(options client.ImageListOptions) (client.ImageListResult, error) + }{ + { + name: "dangling-true", + args: []string{"-f", "dangling=true"}, + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + // Verify the filter is passed to the API + assert.Check(t, options.Filters["dangling"]["true"]) + // dangling=true is handled on the server side and returns only dangling images + return client.ImageListResult{Items: []image.Summary{items[1], items[2]}}, nil + }, + }, + { + name: "dangling-false", + args: []string{"-f", "dangling=false"}, + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + // Verify the filter is passed to the API + assert.Check(t, options.Filters["dangling"]["false"]) + // Return all images including dangling + return client.ImageListResult{Items: slices.Clone(items)}, nil + }, + }, + { + name: "no-dangling-filter", + args: []string{}, + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + // Verify no dangling filter is passed to the API + _, exists := options.Filters["dangling"] + assert.Check(t, !exists) + // Return all images including dangling + return client.ImageListResult{Items: slices.Clone(items)}, nil + }, + }, + { + name: "all-flag", + args: []string{"--all"}, + imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) { + // Verify the All flag is set + assert.Check(t, options.All) + // Return all images including dangling + return client.ImageListResult{Items: slices.Clone(items)}, nil + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) + cmd := newImagesCommand(cli) + cmd.SetArgs(tc.args) + err := cmd.Execute() + assert.NilError(t, err) + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-filter-dangling.%s.golden", tc.name)) + }) + } +} + +func nilToEmptySlice[T any](s []T) []T { + if s == nil { + return []T{} + } + return s +} diff --git a/cli/command/image/load.go b/cli/command/image/load.go index 8d3d3906d0e0..4fc939466189 100644 --- a/cli/command/image/load.go +++ b/cli/command/image/load.go @@ -2,6 +2,8 @@ package image import ( "context" + "errors" + "fmt" "io" "github.com/containerd/platforms" @@ -9,20 +11,20 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/jsonstream" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/moby/sys/sequential" - "github.com/pkg/errors" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) type loadOptions struct { input string quiet bool - platform string + platform []string } -// NewLoadCommand creates a new `docker load` command -func NewLoadCommand(dockerCli command.Cli) *cobra.Command { +// newLoadCommand creates a new "docker image load" command. +func newLoadCommand(dockerCLI command.Cli) *cobra.Command { var opts loadOptions cmd := &cobra.Command{ @@ -30,22 +32,23 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command { Short: "Load an image from a tar archive or STDIN", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runLoad(cmd.Context(), dockerCli, opts) + return runLoad(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker image load, docker load", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output") - flags.StringVar(&opts.platform, "platform", "", `Load only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`) + flags.StringSliceVar(&opts.platform, "platform", []string{}, `Load only the given platform(s). Formatted as a comma-separated list of "os[/arch[/variant]]" (e.g., "linux/amd64,linux/arm64/v8").`) _ = flags.SetAnnotation("platform", "version", []string{"1.48"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } @@ -58,7 +61,7 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error // To avoid getting stuck, verify that a tar file is given either in // the input flag or through stdin and if not display an error message and exit. if dockerCli.In().IsTerminal() { - return errors.Errorf("requested load from stdin, but stdin is empty") + return errors.New("requested load from stdin, but stdin is empty") } default: // We use sequential.Open to use sequential file access on Windows, avoiding @@ -67,7 +70,7 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error if err != nil { return err } - defer file.Close() + defer func() { _ = file.Close() }() input = file } @@ -76,25 +79,23 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error options = append(options, client.ImageLoadWithQuiet(true)) } - if opts.platform != "" { - p, err := platforms.Parse(opts.platform) + platformList := []ocispec.Platform{} + for _, p := range opts.platform { + pp, err := platforms.Parse(p) if err != nil { - return errors.Wrap(err, "invalid platform") + return fmt.Errorf("invalid platform: %w", err) } - // TODO(thaJeztah): change flag-type to support multiple platforms. - options = append(options, client.ImageLoadWithPlatforms(p)) + platformList = append(platformList, pp) + } + if len(platformList) > 0 { + options = append(options, client.ImageLoadWithPlatforms(platformList...)) } - response, err := dockerCli.Client().ImageLoad(ctx, input, options...) + res, err := dockerCli.Client().ImageLoad(ctx, input, options...) if err != nil { return err } - defer response.Body.Close() - - if response.Body != nil && response.JSON { - return jsonstream.Display(ctx, response.Body, dockerCli.Out()) - } + defer func() { _ = res.Close() }() - _, err = io.Copy(dockerCli.Out(), response.Body) - return err + return jsonstream.Display(ctx, res, dockerCli.Out()) } diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go index d3a7dfd7dd98..9ca0f321d5cf 100644 --- a/cli/command/image/load_test.go +++ b/cli/command/image/load_test.go @@ -8,8 +8,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -20,7 +19,7 @@ func TestNewLoadCommandErrors(t *testing.T) { args []string isTerminalIn bool expectedError string - imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) + imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) }{ { name: "wrong-args", @@ -37,16 +36,16 @@ func TestNewLoadCommandErrors(t *testing.T) { name: "pull-error", args: []string{}, expectedError: "something went wrong", - imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) { - return image.LoadResponse{}, errors.New("something went wrong") + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + return nil, errors.New("something went wrong") }, }, { name: "invalid platform", args: []string{"--platform", ""}, expectedError: `invalid platform`, - imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) { - return image.LoadResponse{}, nil + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + return io.NopCloser(strings.NewReader("")), nil }, }, } @@ -54,7 +53,7 @@ func TestNewLoadCommandErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) cli.In().SetIsTerminal(tc.isTerminalIn) - cmd := NewLoadCommand(cli) + cmd := newLoadCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -65,7 +64,7 @@ func TestNewLoadCommandErrors(t *testing.T) { func TestNewLoadCommandInvalidInput(t *testing.T) { expectedError := "open *" - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{})) + cmd := newLoadCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"--input", "*"}) @@ -73,51 +72,73 @@ func TestNewLoadCommandInvalidInput(t *testing.T) { assert.ErrorContains(t, err, expectedError) } +func mockImageLoadResult(content string) client.ImageLoadResult { + return io.NopCloser(strings.NewReader(content)) +} + func TestNewLoadCommandSuccess(t *testing.T) { testCases := []struct { name string args []string - imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) + imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) }{ { name: "simple", args: []string{}, - imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) { - return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil - }, - }, - { - name: "json", - args: []string{}, - imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) { - return image.LoadResponse{ - Body: io.NopCloser(strings.NewReader(`{"ID": "1"}`)), - JSON: true, - }, nil + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + // FIXME(thaJeztah): how to mock this? + // return client.ImageLoadResult{ + // Body: io.NopCloser(strings.NewReader(`{"ID":"simple","Status":"success"}`)), + // }, nil + return mockImageLoadResult(`{"ID":"simple","Status":"success"}`), nil }, }, { name: "input-file", args: []string{"--input", "testdata/load-command-success.input.txt"}, - imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) { - return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + // FIXME(thaJeztah): how to mock this? + // return client.ImageLoadResult{Body: io.NopCloser(strings.NewReader(`{"ID":"input-file","Status":"success"}`))}, nil + return mockImageLoadResult(`{"ID":"input-file","Status":"success"}`), nil }, }, { - name: "with platform", + name: "with-single-platform", args: []string{"--platform", "linux/amd64"}, - imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) { + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/ // assert.Check(t, is.Contains(options, client.ImageHistoryWithPlatform(ocispec.Platform{OS: "linux", Architecture: "amd64"}))) - return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil + // FIXME(thaJeztah): how to mock this? + // return client.ImageLoadResult{Body: io.NopCloser(strings.NewReader(`{"ID":"single-platform","Status":"success"}`))}, nil + return mockImageLoadResult(`{"ID":"single-platform","Status":"success"}`), nil + }, + }, + { + name: "with-comma-separated-platforms", + args: []string{"--platform", "linux/amd64,linux/arm64/v8,linux/riscv64"}, + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/ + // FIXME(thaJeztah): how to mock this? + // return client.ImageLoadResult{Body: io.NopCloser(strings.NewReader(`{"ID":"with-comma-separated-platforms","Status":"success"}`))}, nil + return mockImageLoadResult(`{"ID":"with-comma-separated-platforms","Status":"success"}`), nil + }, + }, + { + name: "with-multiple-platform-options", + args: []string{"--platform", "linux/amd64", "--platform", "linux/arm64/v8", "--platform", "linux/riscv64"}, + imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (client.ImageLoadResult, error) { + assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/ + // FIXME(thaJeztah): how to mock this? + // return client.ImageLoadResult{Body: io.NopCloser(strings.NewReader(`{"ID":"with-multiple-platform-options","Status":"success"}`))}, nil + return mockImageLoadResult(`{"ID":"with-multiple-platform-options","Status":"success"}`), nil }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) - cmd := NewLoadCommand(cli) + cmd := newLoadCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index eec9b1c0ba92..ca12bcf5998d 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -2,28 +2,36 @@ package image import ( "context" + "errors" "fmt" "strconv" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeImage, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool all bool filter opts.FilterOpt } -// NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// newPruneCommand returns a new cobra prune command for images +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -31,18 +39,19 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused images", Args: cli.NoArgs, 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 { return err } 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 }, - Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.25"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -61,9 +70,8 @@ Are you sure you want to continue?` ) func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := options.filter.Value().Clone() + pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) pruneFilters.Add("dangling", strconv.FormatBool(!options.all)) - pruneFilters = command.PruneFilters(dockerCli, pruneFilters) warning := danglingWarning if options.all { @@ -79,15 +87,17 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters) + res, err := dockerCli.Client().ImagePrune(ctx, client.ImagePruneOptions{ + Filters: pruneFilters, + }) if err != nil { return 0, "", err } - if len(report.ImagesDeleted) > 0 { - var sb strings.Builder + var sb strings.Builder + if len(res.Report.ImagesDeleted) > 0 { sb.WriteString("Deleted Images:\n") - for _, st := range report.ImagesDeleted { + for _, st := range res.Report.ImagesDeleted { if st.Untagged != "" { sb.WriteString("untagged: ") sb.WriteString(st.Untagged) @@ -98,19 +108,31 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) sb.WriteByte('\n') } } - output = sb.String() - spaceReclaimed = report.SpaceReclaimed } - return spaceReclaimed, output, nil + return res.Report.SpaceReclaimed, sb.String(), nil } type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Image Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter}) +// pruneFn calls the Image Prune API 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 images without at least one container associated to them" + } else { + confirmMsg = "all dangling images" + } + return 0, confirmMsg, cancelledErr{errors.New("image prune has been cancelled")} + } + return runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + all: options.All, + filter: options.Filter, + }) } diff --git a/cli/command/image/prune_test.go b/cli/command/image/prune_test.go index 87c8d0da12ba..a3c0ceac36ff 100644 --- a/cli/command/image/prune_test.go +++ b/cli/command/image/prune_test.go @@ -10,19 +10,18 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" ) func TestNewPruneCommandErrors(t *testing.T) { testCases := []struct { - name string - args []string - expectedError string - imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error) + name string + args []string + expectedError string + imagePruneFunc func(client.ImagePruneOptions) (client.ImagePruneResult, error) }{ { name: "wrong-args", @@ -33,15 +32,15 @@ func TestNewPruneCommandErrors(t *testing.T) { name: "prune-error", args: []string{"--force"}, expectedError: "something went wrong", - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - return image.PruneReport{}, errors.New("something went wrong") + imagePruneFunc: func(client.ImagePruneOptions) (client.ImagePruneResult, error) { + return client.ImagePruneResult{}, errors.New("something went wrong") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{ - imagesPruneFunc: tc.imagesPruneFunc, + cmd := newPruneCommand(test.NewFakeCli(&fakeClient{ + imagePruneFunc: tc.imagePruneFunc, })) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -53,56 +52,60 @@ func TestNewPruneCommandErrors(t *testing.T) { func TestNewPruneCommandSuccess(t *testing.T) { testCases := []struct { - name string - args []string - imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error) + name string + args []string + imagePruneFunc func(client.ImagePruneOptions) (client.ImagePruneResult, error) }{ { name: "all", args: []string{"--all"}, - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - assert.Check(t, is.Equal("false", pruneFilter.Get("dangling")[0])) - return image.PruneReport{}, nil + imagePruneFunc: func(opts client.ImagePruneOptions) (client.ImagePruneResult, error) { + assert.Check(t, opts.Filters["dangling"]["false"]) + return client.ImagePruneResult{}, nil }, }, { name: "force-deleted", args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - assert.Check(t, is.Equal("true", pruneFilter.Get("dangling")[0])) - return image.PruneReport{ - ImagesDeleted: []image.DeleteResponse{{Deleted: "image1"}}, - SpaceReclaimed: 1, + imagePruneFunc: func(opts client.ImagePruneOptions) (client.ImagePruneResult, error) { + assert.Check(t, opts.Filters["dangling"]["true"]) + return client.ImagePruneResult{ + Report: image.PruneReport{ + ImagesDeleted: []image.DeleteResponse{{Deleted: "image1"}}, + SpaceReclaimed: 1, + }, }, nil }, }, { name: "label-filter", args: []string{"--force", "--filter", "label=foobar"}, - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - assert.Check(t, is.Equal("foobar", pruneFilter.Get("label")[0])) - return image.PruneReport{}, nil + imagePruneFunc: func(opts client.ImagePruneOptions) (client.ImagePruneResult, error) { + assert.Check(t, opts.Filters["label"]["foobar"]) + return client.ImagePruneResult{}, nil }, }, { name: "force-untagged", args: []string{"--force"}, - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - assert.Check(t, is.Equal("true", pruneFilter.Get("dangling")[0])) - return image.PruneReport{ - ImagesDeleted: []image.DeleteResponse{{Untagged: "image1"}}, - SpaceReclaimed: 2, + imagePruneFunc: func(opts client.ImagePruneOptions) (client.ImagePruneResult, error) { + assert.Check(t, opts.Filters["dangling"]["true"]) + return client.ImagePruneResult{ + Report: image.PruneReport{ + ImagesDeleted: []image.DeleteResponse{{Untagged: "image1"}}, + SpaceReclaimed: 2, + }, }, nil }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc}) + cli := test.NewFakeCli(&fakeClient{imagePruneFunc: tc.imagePruneFunc}) // when prompted, answer "Y" to confirm the prune. // will not be prompted if --force is used. cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader("Y\n")))) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() @@ -117,11 +120,11 @@ func TestPrunePromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - imagesPruneFunc: func(pruneFilter filters.Args) (image.PruneReport, error) { - return image.PruneReport{}, errors.New("fakeClient imagesPruneFunc should not be called") + imagePruneFunc: func(client.ImagePruneOptions) (client.ImagePruneResult, error) { + return client.ImagePruneResult{}, errors.New("fakeClient imagePruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index 235e3a7a1754..7485d21a6880 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -2,32 +2,33 @@ package image import ( "context" + "errors" "fmt" - "strings" + "io" + "os" + "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/trust" - "github.com/pkg/errors" + "github.com/docker/cli/cli/streams" + "github.com/docker/cli/internal/jsonstream" + "github.com/moby/moby/client" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) -// PullOptions defines what and how to pull -type PullOptions = pullOptions - // pullOptions defines what and how to pull. type pullOptions struct { - remote string - all bool - platform string - quiet bool - untrusted bool + remote string + all bool + platform string + quiet bool } -// NewPullCommand creates a new `docker pull` command -func NewPullCommand(dockerCli command.Cli) *cobra.Command { +// newPullCommand creates a new `docker pull` command +func newPullCommand(dockerCLI command.Cli) *cobra.Command { var opts pullOptions cmd := &cobra.Command{ @@ -36,13 +37,16 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runPull(cmd.Context(), dockerCli, opts) + return runPull(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "category-top": "5", "aliases": "docker image pull, docker pull", }, - ValidArgsFunction: completion.NoComplete, + // Complete with local images to help pulling the latest version + // of images that are in the image cache. + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -50,19 +54,17 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") - command.AddPlatformFlag(flags, &opts.platform) - command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } -// RunPull performs a pull against the engine based on the specified options -func RunPull(ctx context.Context, dockerCLI command.Cli, opts PullOptions) error { - return runPull(ctx, dockerCLI, opts) -} - // runPull performs a pull against the engine based on the specified options func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error { distributionRef, err := reference.ParseNormalizedNamed(opts.remote) @@ -78,24 +80,39 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error } } - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(dockerCLI), distributionRef.String()) + var ociPlatforms []ocispec.Platform + if opts.platform != "" { + // TODO(thaJeztah): add a platform option-type / flag-type. + p, err := platforms.Parse(opts.platform) + if err != nil { + return err + } + ociPlatforms = append(ociPlatforms, p) + } + + encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), distributionRef.String()) if err != nil { return err } - // Check if reference has a digest - _, isCanonical := distributionRef.(reference.Canonical) - if !opts.untrusted && !isCanonical { - err = trustedPull(ctx, dockerCLI, imgRefAndAuth, opts) - } else { - err = imagePullPrivileged(ctx, dockerCLI, imgRefAndAuth, opts) - } + responseBody, err := dockerCLI.Client().ImagePull(ctx, reference.FamiliarString(distributionRef), client.ImagePullOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: nil, + All: opts.all, + Platforms: ociPlatforms, + }) if err != nil { - if strings.Contains(err.Error(), "when fetching 'plugin'") { - return errors.New(err.Error() + " - Use `docker plugin install`") - } return err } - _, _ = fmt.Fprintln(dockerCLI.Out(), imgRefAndAuth.Reference().String()) + defer responseBody.Close() + + out := dockerCLI.Out() + if opts.quiet { + out = streams.NewOut(io.Discard) + } + if err := jsonstream.Display(ctx, responseBody, out); err != nil { + return err + } + _, _ = fmt.Fprintln(dockerCLI.Out(), distributionRef.String()) return nil } diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go index a853949f32ca..eb8edfbca699 100644 --- a/cli/command/image/pull_test.go +++ b/cli/command/image/pull_test.go @@ -1,15 +1,13 @@ package image import ( - "errors" "fmt" "io" - "strings" + "net/http" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -40,7 +38,7 @@ func TestNewPullCommandErrors(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) - cmd := NewPullCommand(cli) + cmd := newPullCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -74,12 +72,13 @@ func TestNewPullCommandSuccess(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { + imagePullFunc: func(ref string, options client.ImagePullOptions) (client.ImagePullResponse, error) { assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name) - return io.NopCloser(strings.NewReader("")), nil + // FIXME(thaJeztah): how to mock this? + return fakeStreamResult{ReadCloser: http.NoBody}, nil }, }) - cmd := NewPullCommand(cli) + cmd := newPullCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -89,47 +88,3 @@ func TestNewPullCommandSuccess(t *testing.T) { }) } } - -func TestNewPullCommandWithContentTrustErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - notaryFunc test.NotaryClientFuncType - }{ - { - name: "offline-notary-server", - notaryFunc: notary.GetOfflineNotaryRepository, - expectedError: "client is offline", - args: []string{"image:tag"}, - }, - { - name: "uninitialized-notary-server", - notaryFunc: notary.GetUninitializedNotaryRepository, - expectedError: "remote trust data does not exist", - args: []string{"image:tag"}, - }, - { - name: "empty-notary-server", - notaryFunc: notary.GetEmptyTargetsNotaryRepository, - expectedError: "No valid trust data for tag", - args: []string{"image:tag"}, - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{ - imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image") - }, - }, test.EnableContentTrust) - cli.SetNotaryClient(tc.notaryFunc) - cmd := NewPullCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) - err := cmd.Execute() - assert.ErrorContains(t, err, tc.expectedError) - }) - } -} diff --git a/cli/command/image/push.go b/cli/command/image/push.go index a875ac0d9f8d..b2aa73d52441 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -1,11 +1,12 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package image import ( "context" "encoding/json" + "errors" "fmt" "io" @@ -17,26 +18,22 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/tui" - "github.com/docker/docker/api/types/auxprogress" - "github.com/docker/docker/api/types/image" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" + "github.com/moby/moby/api/types/auxprogress" + "github.com/moby/moby/client" "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" "github.com/spf13/cobra" ) type pushOptions struct { - all bool - remote string - untrusted bool - quiet bool - platform string + all bool + remote string + quiet bool + platform string } -// NewPushCommand creates a new `docker push` command -func NewPushCommand(dockerCli command.Cli) *cobra.Command { +// newPushCommand creates a new `docker push` command +func newPushCommand(dockerCLI command.Cli) *cobra.Command { var opts pushOptions cmd := &cobra.Command{ @@ -45,19 +42,23 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runPush(cmd.Context(), dockerCli, opts) + return runPush(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "category-top": "6", "aliases": "docker image push, docker push", }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") - command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") // Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to // pushing the image as-is. This also avoids forcing the platform selection @@ -68,14 +69,12 @@ Image index won't be pushed, meaning that other manifests, including attestation 'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`) flags.SetAnnotation("platform", "version", []string{"1.46"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } // runPush performs a push against the engine based on the specified options. -// -//nolint:gocyclo // ignore cyclomatic complexity 17 of func `runPush` is high (> 16) for now. func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error { var platform *ocispec.Platform out := tui.NewOutput(dockerCli.Out()) @@ -94,9 +93,11 @@ To push the complete multi-platform image, remove the --platform flag. } ref, err := reference.ParseNormalizedNamed(opts.remote) - switch { - case err != nil: + if err != nil { return err + } + + switch { case opts.all && !reference.IsNameOnly(ref): return errors.New("tag can't be used with --all-tags/-a") case !opts.all && reference.IsNameOnly(ref): @@ -106,47 +107,33 @@ To push the complete multi-platform image, remove the --platform flag. } } - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), ref.String()) if err != nil { return err } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") - } - options := image.PushOptions{ + + responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), client.ImagePushOptions{ All: opts.all, RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, Platform: platform, - } - - responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) + }) if err != nil { return err } defer func() { + _ = responseBody.Close() for _, note := range notes { out.PrintNote(note) } }() - defer responseBody.Close() - if !opts.untrusted { - // TODO pushTrustedReference currently doesn't respect `--quiet` - return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody) - } - if opts.quiet { err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux())) if err == nil { - fmt.Fprintln(dockerCli.Out(), ref.String()) + _, _ = fmt.Fprintln(dockerCli.Out(), ref.String()) } return err } diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go index 88415ea8e0df..dd38cdc84b15 100644 --- a/cli/command/image/push_test.go +++ b/cli/command/image/push_test.go @@ -3,11 +3,11 @@ package image import ( "errors" "io" - "strings" + "net/http" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -16,7 +16,7 @@ func TestNewPushCommandErrors(t *testing.T) { name string args []string expectedError string - imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error) + imagePushFunc func(ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) }{ { name: "wrong-args", @@ -31,16 +31,16 @@ func TestNewPushCommandErrors(t *testing.T) { { name: "push-failed", args: []string{"image:repo"}, - expectedError: "Failed to push", - imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), errors.New("Failed to push") + expectedError: "failed to push", + imagePushFunc: func(ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) { + return nil, errors.New("failed to push") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}) - cmd := NewPushCommand(cli) + cmd := newPushCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -66,14 +66,16 @@ func TestNewPushCommandSuccess(t *testing.T) { `, }, } + for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), nil + imagePushFunc: func(ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) { + // FIXME(thaJeztah): how to mock this? + return fakeStreamResult{ReadCloser: http.NoBody}, nil }, }) - cmd := NewPushCommand(cli) + cmd := newPushCommand(cli) cmd.SetOut(cli.OutBuffer()) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go index 7c8b49ee0a99..ea9e513edc91 100644 --- a/cli/command/image/remove.go +++ b/cli/command/image/remove.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -20,8 +20,8 @@ type removeOptions struct { platforms []string } -// NewRemoveCommand creates a new `docker remove` command -func NewRemoveCommand(dockerCLI command.Cli) *cobra.Command { +// newRemoveCommand creates a new "docker image remove" command +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var options removeOptions cmd := &cobra.Command{ @@ -35,6 +35,7 @@ func NewRemoveCommand(dockerCLI command.Cli) *cobra.Command { Annotations: map[string]string{ "aliases": "docker image rm, docker image remove, docker rmi", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -46,12 +47,13 @@ func NewRemoveCommand(dockerCLI command.Cli) *cobra.Command { flags.StringSliceVar(&options.platforms, "platform", nil, `Remove only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`) _ = flags.SetAnnotation("platform", "version", []string{"1.50"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRemoveCommand(dockerCli) +// newImageRemoveCommand is a sub-command under `image` (`docker image rm`) +func newImageRemoveCommand(dockerCli command.Cli) *cobra.Command { + cmd := *newRemoveCommand(dockerCli) cmd.Aliases = []string{"rmi", "remove"} cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" return &cmd @@ -60,7 +62,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { func runRemove(ctx context.Context, dockerCLI command.Cli, opts removeOptions, images []string) error { apiClient := dockerCLI.Client() - options := image.RemoveOptions{ + options := client.ImageRemoveOptions{ Force: opts.force, PruneChildren: !opts.noPrune, } @@ -77,14 +79,14 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, opts removeOptions, i fatalErr := false var errs []error for _, img := range images { - dels, err := apiClient.ImageRemove(ctx, img, options) + res, err := apiClient.ImageRemove(ctx, img, options) if err != nil { - if !cerrdefs.IsNotFound(err) { + if !errdefs.IsNotFound(err) { fatalErr = true } errs = append(errs, err) } else { - for _, del := range dels { + for _, del := range res.Items { if del.Deleted != "" { _, _ = fmt.Fprintln(dockerCLI.Out(), "Deleted:", del.Deleted) } else { diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go index 16c0a3c4a1c6..e9386bc9d939 100644 --- a/cli/command/image/remove_test.go +++ b/cli/command/image/remove_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/image" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -24,7 +25,8 @@ func (n notFound) Error() string { func (notFound) NotFound() {} func TestNewRemoveCommandAlias(t *testing.T) { - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{})) + cmd := newImageRemoveCommand(test.NewFakeCli(&fakeClient{})) + cmd.SetArgs([]string{""}) assert.Check(t, cmd.HasAlias("rmi")) assert.Check(t, cmd.HasAlias("remove")) assert.Check(t, !cmd.HasAlias("other")) @@ -35,7 +37,7 @@ func TestNewRemoveCommandErrors(t *testing.T) { name string args []string expectedError string - imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) + imageRemoveFunc func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) }{ { name: "wrong args", @@ -45,30 +47,30 @@ func TestNewRemoveCommandErrors(t *testing.T) { name: "ImageRemove fail with force option", args: []string{"-f", "image1"}, expectedError: "error removing image", - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { assert.Check(t, is.Equal("image1", img)) - return []image.DeleteResponse{}, errors.New("error removing image") + return client.ImageRemoveResult{}, errors.New("error removing image") }, }, { name: "ImageRemove fail", args: []string{"arg1"}, expectedError: "error removing image", - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { assert.Check(t, !options.Force) assert.Check(t, options.PruneChildren) - return []image.DeleteResponse{}, errors.New("error removing image") + return client.ImageRemoveResult{}, errors.New("error removing image") }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{ imageRemoveFunc: tc.imageRemoveFunc, })) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) + cmd.SetArgs(nilToEmptySlice(tc.args)) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) }) } @@ -78,24 +80,26 @@ func TestNewRemoveCommandSuccess(t *testing.T) { testCases := []struct { name string args []string - imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) + imageRemoveFunc func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) expectedStderr string }{ { name: "Image Deleted", args: []string{"image1"}, - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { assert.Check(t, is.Equal("image1", img)) - return []image.DeleteResponse{{Deleted: img}}, nil + return client.ImageRemoveResult{ + Items: []image.DeleteResponse{{Deleted: img}}, + }, nil }, }, { name: "Image not found with force option", args: []string{"-f", "image1"}, - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { assert.Check(t, is.Equal("image1", img)) assert.Check(t, is.Equal(true, options.Force)) - return []image.DeleteResponse{}, notFound{"image1"} + return client.ImageRemoveResult{}, notFound{"image1"} }, expectedStderr: "Error: No such image: image1\n", }, @@ -103,29 +107,35 @@ func TestNewRemoveCommandSuccess(t *testing.T) { { name: "Image Untagged", args: []string{"image1"}, - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { assert.Check(t, is.Equal("image1", img)) - return []image.DeleteResponse{{Untagged: img}}, nil + return client.ImageRemoveResult{ + Items: []image.DeleteResponse{{Untagged: img}}, + }, nil }, }, { name: "Image Deleted and Untagged", args: []string{"image1", "image2"}, - imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) { + imageRemoveFunc: func(img string, options client.ImageRemoveOptions) (client.ImageRemoveResult, error) { if img == "image1" { - return []image.DeleteResponse{{Untagged: img}}, nil + return client.ImageRemoveResult{ + Items: []image.DeleteResponse{{Untagged: img}}, + }, nil } - return []image.DeleteResponse{{Deleted: img}}, nil + return client.ImageRemoveResult{ + Items: []image.DeleteResponse{{Deleted: img}}, + }, nil }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc}) - cmd := NewRemoveCommand(cli) + cmd := newRemoveCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.SetArgs(tc.args) + cmd.SetArgs(nilToEmptySlice(tc.args)) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal(tc.expectedStderr, cli.ErrBuffer().String())) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name)) diff --git a/cli/command/image/save.go b/cli/command/image/save.go index 64bd72a74d8d..f900cb1da88b 100644 --- a/cli/command/image/save.go +++ b/cli/command/image/save.go @@ -2,26 +2,28 @@ package image import ( "context" + "errors" + "fmt" "io" "github.com/containerd/platforms" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/moby/sys/atomicwriter" - "github.com/pkg/errors" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" ) type saveOptions struct { images []string output string - platform string + platform []string } -// NewSaveCommand creates a new `docker save` command -func NewSaveCommand(dockerCli command.Cli) *cobra.Command { +// newSaveCommand creates a new "docker image save" command. +func newSaveCommand(dockerCLI command.Cli) *cobra.Command { var opts saveOptions cmd := &cobra.Command{ @@ -30,34 +32,39 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.images = args - return runSave(cmd.Context(), dockerCli, opts) + return runSave(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker image save, docker save", }, - ValidArgsFunction: completion.ImageNames(dockerCli, -1), + ValidArgsFunction: completion.ImageNames(dockerCLI, -1), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") - flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`) + flags.StringSliceVar(&opts.platform, "platform", []string{}, `Save only the given platform(s). Formatted as a comma-separated list of "os[/arch[/variant]]" (e.g., "linux/amd64,linux/arm64/v8")`) _ = flags.SetAnnotation("platform", "version", []string{"1.48"}) - _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms()) return cmd } // runSave performs a save against the engine based on the specified options func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error { var options []client.ImageSaveOption - if opts.platform != "" { - p, err := platforms.Parse(opts.platform) + + platformList := []ocispec.Platform{} + for _, p := range opts.platform { + pp, err := platforms.Parse(p) if err != nil { - return errors.Wrap(err, "invalid platform") + return fmt.Errorf("invalid platform: %w", err) } - // TODO(thaJeztah): change flag-type to support multiple platforms. - options = append(options, client.ImageSaveWithPlatforms(p)) + platformList = append(platformList, pp) + } + if len(platformList) > 0 { + options = append(options, client.ImageSaveWithPlatforms(platformList...)) } var output io.Writer @@ -69,7 +76,7 @@ func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error } else { writer, err := atomicwriter.New(opts.output, 0o600) if err != nil { - return errors.Wrap(err, "failed to save image") + return fmt.Errorf("failed to save image: %w", err) } defer writer.Close() output = writer diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go index 7a3e93eb3547..dbdad6c32b82 100644 --- a/cli/command/image/save_test.go +++ b/cli/command/image/save_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -19,7 +19,7 @@ func TestNewSaveCommandErrors(t *testing.T) { args []string isTerminal bool expectedError string - imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) + imageSaveFunc func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) }{ { name: "wrong args", @@ -37,14 +37,14 @@ func TestNewSaveCommandErrors(t *testing.T) { args: []string{"arg1"}, isTerminal: false, expectedError: "error saving image", - imageSaveFunc: func([]string, ...client.ImageSaveOption) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), errors.New("error saving image") + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { + return nil, errors.New("error saving image") }, }, { name: "output directory does not exist", - args: []string{"-o", "fakedir/out.tar", "arg1"}, - expectedError: `failed to save image: invalid output path: stat fakedir: no such file or directory`, + args: []string{"-o", "fake-dir/out.tar", "arg1"}, + expectedError: `failed to save image: invalid output path: stat fake-dir: no such file or directory`, }, { name: "output file is irregular", @@ -61,7 +61,7 @@ func TestNewSaveCommandErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}) cli.Out().SetIsTerminal(tc.isTerminal) - cmd := NewSaveCommand(cli) + cmd := newSaveCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -74,13 +74,13 @@ func TestNewSaveCommandSuccess(t *testing.T) { testCases := []struct { args []string isTerminal bool - imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) + imageSaveFunc func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) deferredFunc func() }{ { args: []string{"-o", "save_tmp_file", "arg1"}, isTerminal: true, - imageSaveFunc: func(images []string, _ ...client.ImageSaveOption) (io.ReadCloser, error) { + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { assert.Assert(t, is.Len(images, 1)) assert.Check(t, is.Equal("arg1", images[0])) return io.NopCloser(strings.NewReader("")), nil @@ -92,7 +92,7 @@ func TestNewSaveCommandSuccess(t *testing.T) { { args: []string{"arg1", "arg2"}, isTerminal: false, - imageSaveFunc: func(images []string, _ ...client.ImageSaveOption) (io.ReadCloser, error) { + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { assert.Assert(t, is.Len(images, 2)) assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg2", images[1])) @@ -102,7 +102,7 @@ func TestNewSaveCommandSuccess(t *testing.T) { { args: []string{"--platform", "linux/amd64", "arg1"}, isTerminal: false, - imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) { + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { assert.Assert(t, is.Len(images, 1)) assert.Check(t, is.Equal("arg1", images[0])) // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied @@ -111,10 +111,30 @@ func TestNewSaveCommandSuccess(t *testing.T) { return io.NopCloser(strings.NewReader("")), nil }, }, + { + args: []string{"--platform", "linux/amd64,linux/arm64/v8,linux/riscv64", "arg1"}, + isTerminal: false, + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { + assert.Assert(t, is.Len(images, 1)) + assert.Check(t, is.Equal("arg1", images[0])) + assert.Check(t, len(options) > 0) // can be 1 or 2 depending on whether a terminal is attached :/ + return io.NopCloser(strings.NewReader("")), nil + }, + }, + { + args: []string{"--platform", "linux/amd64", "--platform", "linux/arm64/v8", "--platform", "linux/riscv64", "arg1"}, + isTerminal: false, + imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (client.ImageSaveResult, error) { + assert.Assert(t, is.Len(images, 1)) + assert.Check(t, is.Equal("arg1", images[0])) + assert.Check(t, len(options) > 0) // can be 1 or 2 depending on whether a terminal is attached :/ + return io.NopCloser(strings.NewReader("")), nil + }, + }, } for _, tc := range testCases { t.Run(strings.Join(tc.args, " "), func(t *testing.T) { - cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ + cmd := newSaveCommand(test.NewFakeCli(&fakeClient{ imageSaveFunc: tc.imageSaveFunc, })) cmd.SetOut(io.Discard) diff --git a/cli/command/image/tag.go b/cli/command/image/tag.go index 7a495afca044..3a2ef56dd7b8 100644 --- a/cli/command/image/tag.go +++ b/cli/command/image/tag.go @@ -1,36 +1,31 @@ package image import ( - "context" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -type tagOptions struct { - image string - name string -} - -// NewTagCommand creates a new `docker tag` command -func NewTagCommand(dockerCli command.Cli) *cobra.Command { - var opts tagOptions - +// newTagCommand creates a new "docker image tag" command. +func newTagCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - opts.image = args[0] - opts.name = args[1] - return runTag(cmd.Context(), dockerCli, opts) + _, err := dockerCLI.Client().ImageTag(cmd.Context(), client.ImageTagOptions{ + Source: args[0], + Target: args[1], + }) + return err }, Annotations: map[string]string{ "aliases": "docker image tag, docker tag", }, - ValidArgsFunction: completion.ImageNames(dockerCli, 2), + ValidArgsFunction: completion.ImageNames(dockerCLI, 2), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -38,7 +33,3 @@ func NewTagCommand(dockerCli command.Cli) *cobra.Command { return cmd } - -func runTag(ctx context.Context, dockerCli command.Cli, opts tagOptions) error { - return dockerCli.Client().ImageTag(ctx, opts.image, opts.name) -} diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go index 65ceb60323ed..e3c7768833e2 100644 --- a/cli/command/image/tag_test.go +++ b/cli/command/image/tag_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -17,7 +18,7 @@ func TestCliNewTagCommandErrors(t *testing.T) { } expectedError := "'tag' requires 2 arguments" for _, args := range testCases { - cmd := NewTagCommand(test.NewFakeCli(&fakeClient{})) + cmd := newTagCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs(args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -26,12 +27,12 @@ func TestCliNewTagCommandErrors(t *testing.T) { } func TestCliNewTagCommand(t *testing.T) { - cmd := NewTagCommand( + cmd := newTagCommand( test.NewFakeCli(&fakeClient{ - imageTagFunc: func(image string, ref string) error { - assert.Check(t, is.Equal("image1", image)) - assert.Check(t, is.Equal("image2", ref)) - return nil + imageTagFunc: func(options client.ImageTagOptions) (client.ImageTagResult, error) { + assert.Check(t, is.Equal("image1", options.Source)) + assert.Check(t, is.Equal("image2", options.Target)) + return client.ImageTagResult{}, nil }, })) cmd.SetArgs([]string{"image1", "image2"}) diff --git a/cli/command/image/testdata/inspect-command-success.simple-many.golden b/cli/command/image/testdata/inspect-command-success.simple-many.golden index e56385272589..d0f265403be9 100644 --- a/cli/command/image/testdata/inspect-command-success.simple-many.golden +++ b/cli/command/image/testdata/inspect-command-success.simple-many.golden @@ -3,18 +3,10 @@ "Id": "", "RepoTags": null, "RepoDigests": null, - "Parent": "", - "Comment": "", - "DockerVersion": "", - "Author": "", "Config": null, "Architecture": "", "Os": "", "Size": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, "RootFS": {}, "Metadata": { "LastTagTime": "0001-01-01T00:00:00Z" @@ -24,18 +16,10 @@ "Id": "", "RepoTags": null, "RepoDigests": null, - "Parent": "", - "Comment": "", - "DockerVersion": "", - "Author": "", "Config": null, "Architecture": "", "Os": "", "Size": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, "RootFS": {}, "Metadata": { "LastTagTime": "0001-01-01T00:00:00Z" diff --git a/cli/command/image/testdata/inspect-command-success.simple.golden b/cli/command/image/testdata/inspect-command-success.simple.golden index c8676749a7bd..d2fee407957f 100644 --- a/cli/command/image/testdata/inspect-command-success.simple.golden +++ b/cli/command/image/testdata/inspect-command-success.simple.golden @@ -3,18 +3,10 @@ "Id": "", "RepoTags": null, "RepoDigests": null, - "Parent": "", - "Comment": "", - "DockerVersion": "", - "Author": "", "Config": null, "Architecture": "", "Os": "", "Size": 0, - "GraphDriver": { - "Data": null, - "Name": "" - }, "RootFS": {}, "Metadata": { "LastTagTime": "0001-01-01T00:00:00Z" diff --git a/cli/command/image/testdata/list-command-ambiguous.golden b/cli/command/image/testdata/list-command-ambiguous.golden index 2d8ffa9efa90..f49f1091ebfb 100644 --- a/cli/command/image/testdata/list-command-ambiguous.golden +++ b/cli/command/image/testdata/list-command-ambiguous.golden @@ -1,2 +1,3 @@ +WARNING: This output is designed for human readability. For machine-readable output, please use --format. No images found matching "ls": did you mean "docker image ls"? diff --git a/cli/command/image/testdata/list-command-filter-dangling.all-flag.golden b/cli/command/image/testdata/list-command-filter-dangling.all-flag.golden new file mode 100644 index 000000000000..79a821391845 --- /dev/null +++ b/cli/command/image/testdata/list-command-filter-dangling.all-flag.golden @@ -0,0 +1,4 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +myimage:latest 87428fc52280 0B 0B + 0263829989b6 0B 0B + a3a5e715f0cc 0B 0B diff --git a/cli/command/image/testdata/list-command-filter-dangling.dangling-false.golden b/cli/command/image/testdata/list-command-filter-dangling.dangling-false.golden new file mode 100644 index 000000000000..c51f6c1b1878 --- /dev/null +++ b/cli/command/image/testdata/list-command-filter-dangling.dangling-false.golden @@ -0,0 +1,2 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +myimage:latest 87428fc52280 0B 0B diff --git a/cli/command/image/testdata/list-command-filter-dangling.dangling-true.golden b/cli/command/image/testdata/list-command-filter-dangling.dangling-true.golden new file mode 100644 index 000000000000..a9ab1b8cc34a --- /dev/null +++ b/cli/command/image/testdata/list-command-filter-dangling.dangling-true.golden @@ -0,0 +1,3 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA + 0263829989b6 0B 0B + a3a5e715f0cc 0B 0B diff --git a/cli/command/image/testdata/list-command-filter-dangling.no-dangling-filter.golden b/cli/command/image/testdata/list-command-filter-dangling.no-dangling-filter.golden new file mode 100644 index 000000000000..c51f6c1b1878 --- /dev/null +++ b/cli/command/image/testdata/list-command-filter-dangling.no-dangling-filter.golden @@ -0,0 +1,2 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +myimage:latest 87428fc52280 0B 0B diff --git a/cli/command/image/testdata/list-command-success.filters.golden b/cli/command/image/testdata/list-command-success.filters.golden index 28b0b71e7865..cca2da199537 100644 --- a/cli/command/image/testdata/list-command-success.filters.golden +++ b/cli/command/image/testdata/list-command-success.filters.golden @@ -1 +1 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE +IMAGE ID DISK USAGE CONTENT SIZE EXTRA diff --git a/cli/command/image/testdata/list-command-success.match-name.golden b/cli/command/image/testdata/list-command-success.match-name.golden index 28b0b71e7865..cca2da199537 100644 --- a/cli/command/image/testdata/list-command-success.match-name.golden +++ b/cli/command/image/testdata/list-command-success.match-name.golden @@ -1 +1 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE +IMAGE ID DISK USAGE CONTENT SIZE EXTRA diff --git a/cli/command/image/testdata/list-command-success.simple.golden b/cli/command/image/testdata/list-command-success.simple.golden index 28b0b71e7865..cca2da199537 100644 --- a/cli/command/image/testdata/list-command-success.simple.golden +++ b/cli/command/image/testdata/list-command-success.simple.golden @@ -1 +1 @@ -REPOSITORY TAG IMAGE ID CREATED SIZE +IMAGE ID DISK USAGE CONTENT SIZE EXTRA diff --git a/cli/command/image/testdata/load-command-success.input-file.golden b/cli/command/image/testdata/load-command-success.input-file.golden index 51da4200abb9..0f454425f4c2 100644 --- a/cli/command/image/testdata/load-command-success.input-file.golden +++ b/cli/command/image/testdata/load-command-success.input-file.golden @@ -1 +1 @@ -Success \ No newline at end of file +input-file: success diff --git a/cli/command/image/testdata/load-command-success.json.golden b/cli/command/image/testdata/load-command-success.json.golden deleted file mode 100644 index c17f16ecd705..000000000000 --- a/cli/command/image/testdata/load-command-success.json.golden +++ /dev/null @@ -1 +0,0 @@ -1: diff --git a/cli/command/image/testdata/load-command-success.simple.golden b/cli/command/image/testdata/load-command-success.simple.golden index 51da4200abb9..f503a70c61dc 100644 --- a/cli/command/image/testdata/load-command-success.simple.golden +++ b/cli/command/image/testdata/load-command-success.simple.golden @@ -1 +1 @@ -Success \ No newline at end of file +simple: success diff --git a/cli/command/image/testdata/load-command-success.with platform.golden b/cli/command/image/testdata/load-command-success.with platform.golden deleted file mode 100644 index 51da4200abb9..000000000000 --- a/cli/command/image/testdata/load-command-success.with platform.golden +++ /dev/null @@ -1 +0,0 @@ -Success \ No newline at end of file diff --git a/cli/command/image/testdata/load-command-success.with-comma-separated-platforms.golden b/cli/command/image/testdata/load-command-success.with-comma-separated-platforms.golden new file mode 100644 index 000000000000..9cd1f6fc340f --- /dev/null +++ b/cli/command/image/testdata/load-command-success.with-comma-separated-platforms.golden @@ -0,0 +1 @@ +with-comma-separated-platforms: success diff --git a/cli/command/image/testdata/load-command-success.with-multiple-platform-options.golden b/cli/command/image/testdata/load-command-success.with-multiple-platform-options.golden new file mode 100644 index 000000000000..172a51a1aac6 --- /dev/null +++ b/cli/command/image/testdata/load-command-success.with-multiple-platform-options.golden @@ -0,0 +1 @@ +with-multiple-platform-options: success diff --git a/cli/command/image/testdata/load-command-success.with-single-platform.golden b/cli/command/image/testdata/load-command-success.with-single-platform.golden new file mode 100644 index 000000000000..59ff47762f3c --- /dev/null +++ b/cli/command/image/testdata/load-command-success.with-single-platform.golden @@ -0,0 +1 @@ +single-platform: success diff --git a/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden b/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden new file mode 100644 index 000000000000..2fd950562bd9 --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.expanded-view-with-platforms.golden @@ -0,0 +1,5 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +multiplatform:latest aaaaaaaaaaaa 25.5 MB 20.2 MB U +├─ linux/amd64 bbbbbbbbbbbb 12.1 MB 10.0 MB +└─ linux/arm64 cccccccccccc 13.4 MB 10.2 MB U + diff --git a/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden b/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden new file mode 100644 index 000000000000..ce0b92828cda --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.mixed-tagged-untagged-with-children.golden @@ -0,0 +1,10 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +app:v1 +app:latest 101010101010 30.5 MB 25.2 MB U +└─ linux/amd64 202020202020 15.2 MB 12.6 MB U + + 303030303030 12.3 MB 10.1 MB +└─ linux/arm/v7 404040404040 6.1 MB 5.0 MB + +base:alpine 505050505050 5.5 MB 5.5 MB + diff --git a/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden b/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden new file mode 100644 index 000000000000..465b67857312 --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.untagged-with-platforms.golden @@ -0,0 +1,5 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA + dddddddddddd 18.5 MB 15.2 MB +├─ linux/amd64 eeeeeeeeeeee 9.2 MB 7.6 MB +└─ linux/arm64 ffffffffffff 9.3 MB 7.6 MB + diff --git a/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden b/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden new file mode 100644 index 000000000000..6c58f0aa4b7c --- /dev/null +++ b/cli/command/image/testdata/tree-command-success.width-calculation-untagged.golden @@ -0,0 +1,4 @@ +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +a:1 111111111111 5.5 MB 2.5 MB + 222222222222 3.2 MB 1.6 MB +short:v1 333333333333 7.1 MB 3.5 MB U diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index c12c7096b5c5..70b74828679b 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -1,29 +1,33 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package image import ( "context" "fmt" + "os" "slices" - "sort" "strings" "github.com/containerd/platforms" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/tui" - "github.com/docker/docker/api/types/filters" - imagetypes "github.com/docker/docker/api/types/image" "github.com/docker/go-units" + imagetypes "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "github.com/morikuni/aec" "github.com/opencontainers/go-digest" ) +const untaggedName = "" + type treeOptions struct { - all bool - filters filters.Args + images []imagetypes.Summary + filters client.Filters + expanded bool } type treeView struct { @@ -33,18 +37,8 @@ type treeView struct { imageSpacing bool } -func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error { - images, err := dockerCLI.Client().ImageList(ctx, imagetypes.ListOptions{ - All: opts.all, - Filters: opts.filters, - Manifests: true, - }) - if err != nil { - return err - } - if !opts.all { - images = slices.DeleteFunc(images, isDangling) - } +func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) (int, error) { + images := opts.images view := treeView{ images: make([]topImage, 0, len(images)), @@ -52,7 +46,10 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error attested := make(map[digest.Digest]bool) for _, img := range images { - details := imageDetails{ + if ctx.Err() != nil { + return 0, ctx.Err() + } + topDetails := imageDetails{ ID: img.ID, DiskUsage: units.HumanSizeWithPrecision(float64(img.Size), 3), InUse: img.Containers > 0, @@ -71,43 +68,88 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error continue } + inUse := len(im.ImageData.Containers) > 0 + if inUse { + // Mark top-level parent image as used if any of its subimages are used. + topDetails.InUse = true + } + + if !opts.expanded { + continue + } + sub := subImage{ Platform: platforms.Format(im.ImageData.Platform), Available: im.Available, Details: imageDetails{ ID: im.ID, DiskUsage: units.HumanSizeWithPrecision(float64(im.Size.Total), 3), - InUse: len(im.ImageData.Containers) > 0, + InUse: inUse, ContentSize: units.HumanSizeWithPrecision(float64(im.Size.Content), 3), }, } - if sub.Details.InUse { - // Mark top-level parent image as used if any of its subimages are used. - details.InUse = true - } - children = append(children, sub) // Add extra spacing between images if there's at least one entry with children. view.imageSpacing = true } - details.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3) + topDetails.ContentSize = units.HumanSizeWithPrecision(float64(totalContent), 3) + + // Sort tags for this image + sortedTags := make([]string, len(img.RepoTags)) + copy(sortedTags, img.RepoTags) + slices.Sort(sortedTags) - view.images = append(view.images, topImage{ - Names: img.RepoTags, - Details: details, - Children: children, - created: img.Created, - }) + if opts.expanded { + view.images = append(view.images, topImage{ + Names: sortedTags, + Details: topDetails, + Children: children, + created: img.Created, + }) + continue + } + + if len(sortedTags) == 0 { + view.images = append(view.images, topImage{ + Details: topDetails, + Children: children, + created: img.Created, + }) + } + for _, tag := range sortedTags { + view.images = append(view.images, topImage{ + Names: []string{tag}, + Details: topDetails, + Children: children, + created: img.Created, + }) + } } - sort.Slice(view.images, func(i, j int) bool { - return view.images[i].created > view.images[j].created + slices.SortFunc(view.images, func(a, b topImage) int { + nameA := "" + if len(a.Names) > 0 { + nameA = a.Names[0] + } + nameB := "" + if len(b.Names) > 0 { + nameB = b.Names[0] + } + // Empty names sort last + if (nameA == "") != (nameB == "") { + if nameB == "" { + return -1 + } + return 1 + } + return strings.Compare(nameA, nameB) }) - return printImageTree(dockerCLI, view) + printImageTree(dockerCLI, view) + return len(view.images), nil } type imageDetails struct { @@ -190,32 +232,37 @@ func getPossibleChips(view treeView) (chips []imageChip) { return possible } -func printImageTree(dockerCLI command.Cli, view treeView) error { - out := tui.NewOutput(dockerCLI.Out()) - _, width := out.GetTtySize() - if width == 0 { - width = 80 +func printImageTree(outs command.Streams, view treeView) { + if streamRedirected(outs.Out()) { + _, _ = fmt.Fprintln(outs.Err(), "WARNING: This output is designed for human readability. For machine-readable output, please use --format.") } - if width < 20 { + + out := tui.NewOutput(outs.Out()) + isTerm := out.IsTerminal() + + _, width := out.GetTtySize() + limitWidth := width == 0 + if isTerm && width < 20 { width = 20 } topNameColor := out.Color(aec.NewBuilder(aec.BlueF, aec.Bold).ANSI) normalColor := out.Color(tui.ColorSecondary) untaggedColor := out.Color(tui.ColorTertiary) - isTerm := out.IsTerminal() + titleColor := out.Color(tui.ColorTitle) - out.PrintlnWithColor(tui.ColorWarning, "WARNING: This is an experimental feature. The output may change and shouldn't be depended on.") - - out.Println(generateLegend(out, width)) - out.Println() + // Legend is right-aligned, so don't print it if the width is unlimited + if !limitWidth { + out.Println(generateLegend(out, width)) + } possibleChips := getPossibleChips(view) columns := []imgColumn{ { - Title: "Image", - Align: alignLeft, - Width: 0, + Title: "Image", + Align: alignLeft, + Width: 0, + NoEllipsis: true, }, { Title: "ID", @@ -247,7 +294,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { Width: func() int { maxChipsWidth := 0 for _, chip := range possibleChips { - s := chip.String(isTerm) + s := out.Sprint(chip) l := tui.Width(s) maxChipsWidth += l } @@ -260,15 +307,15 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { }(), Color: &tui.ColorNone, DetailsValue: func(d *imageDetails) string { - var out string + var b strings.Builder for _, chip := range possibleChips { if chip.check(d) { - out += chip.String(isTerm) + b.WriteString(out.Sprint(chip)) } else { - out += chipPlaceholder.String(isTerm) + b.WriteString(out.Sprint(chipPlaceholder)) } } - return out + return b.String() }, }, } @@ -281,7 +328,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { _, _ = fmt.Fprint(out, strings.Repeat(" ", columnSpacing)) } - _, _ = fmt.Fprint(out, h.Print(tui.ColorTitle, strings.ToUpper(h.Title))) + _, _ = fmt.Fprint(out, h.Print(titleColor, strings.ToUpper(h.Title))) } _, _ = fmt.Fprintln(out) @@ -296,8 +343,6 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { printChildren(out, columns, img, normalColor) _, _ = fmt.Fprintln(out) } - - return nil } // adjustColumns adjusts the width of the first column to maximize the space @@ -305,25 +350,27 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { // to display their content. func adjustColumns(width uint, columns []imgColumn, images []topImage) []imgColumn { nameWidth := int(width) - for idx, h := range columns { - if h.Width == 0 { - continue - } - d := h.Width - if idx > 0 { - d += columnSpacing - } - // If the first column gets too short, remove remaining columns - if nameWidth-d < 12 { - columns = columns[:idx] - break + if nameWidth > 0 { + for idx, h := range columns { + if h.Width == 0 { + continue + } + d := h.Width + if idx > 0 { + d += columnSpacing + } + // If the first column gets too short, remove remaining columns + if nameWidth-d < 12 { + columns = columns[:idx] + break + } + nameWidth -= d } - nameWidth -= d } // Try to make the first column as narrow as possible widest := widestFirstColumnValue(columns, images) - if nameWidth > widest { + if width == 0 || nameWidth > widest { nameWidth = widest } columns[0].Width = nameWidth @@ -333,13 +380,14 @@ func adjustColumns(width uint, columns []imgColumn, images []topImage) []imgColu func generateLegend(out tui.Output, width uint) string { var legend string legend += out.Sprint(tui.InfoHeader) + var legendSb371 strings.Builder for idx, chip := range allChips { - legend += " " + out.Sprint(chip) + " " + chip.desc + legendSb371.WriteString(" " + out.Sprint(chip) + " " + chip.desc) if idx < len(allChips)-1 { - legend += " |" + legendSb371.WriteString(" |") } } - legend += " " + legend += legendSb371.String() r := int(width) - tui.Width(legend) if r < 0 { @@ -386,25 +434,31 @@ func printChildren(out tui.Output, headers []imgColumn, img topImage, normalColo func printNames(out tui.Output, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) { if len(img.Names) == 0 { - _, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "")) + _, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, untaggedName)) } - // TODO: Replace with namesLongestToShortest := slices.SortedFunc(slices.Values(img.Names)) - // once we move to Go 1.23. - namesLongestToShortest := make([]string, len(img.Names)) - copy(namesLongestToShortest, img.Names) - sort.Slice(namesLongestToShortest, func(i, j int) bool { - return len(namesLongestToShortest[i]) > len(namesLongestToShortest[j]) - }) - - for nameIdx, name := range namesLongestToShortest { - // Don't limit first names to the column width because only the last - // name will be printed alongside other columns. - if nameIdx < len(img.Names)-1 { - _, fullWidth := out.GetTtySize() - _, _ = fmt.Fprintln(out, color.Apply(tui.Ellipsis(name, int(fullWidth)))) - } else { - _, _ = fmt.Fprint(out, headers[0].Print(color, name)) + for nameIdx, name := range img.Names { + nameWidth := tui.Width(name) + lastName := nameIdx == len(img.Names)-1 + multiLine := nameWidth > headers[0].Width + + _, _ = fmt.Fprint(out, headers[0].Print(color, name)) + + // Print each name on its own line, including the last, + // unless the last name fits into the column. + // + // IMAGE ID ... + // anImage 171e65262c80 ... + // firstName + // lastNameIsALongOne + // eade5be814e8 ... + // anotherLongName + // bb747ca923a5 ... + if !lastName || multiLine { + _, _ = fmt.Fprintln(out) + } + if multiLine && lastName { + _, _ = fmt.Fprint(out, strings.Repeat(" ", headers[0].Width)) } } } @@ -424,6 +478,7 @@ type imgColumn struct { DetailsValue func(*imageDetails) string Color *aec.ANSI + NoEllipsis bool } func (h imgColumn) Print(clr aec.ANSI, s string) string { @@ -440,12 +495,16 @@ func (h imgColumn) Print(clr aec.ANSI, s string) string { func (h imgColumn) PrintC(clr aec.ANSI, s string) string { ln := tui.Width(s) - if ln > h.Width { - return clr.Apply(tui.Ellipsis(s, h.Width)) - } - fill := h.Width - ln + if fill < 0 { + if h.NoEllipsis { + fill = 0 + } else { + return clr.Apply(tui.Ellipsis(s, h.Width)) + } + } + l := fill / 2 r := fill - l @@ -454,27 +513,44 @@ func (h imgColumn) PrintC(clr aec.ANSI, s string) string { func (h imgColumn) PrintL(clr aec.ANSI, s string) string { ln := tui.Width(s) - if ln > h.Width { - return clr.Apply(tui.Ellipsis(s, h.Width)) + + fill := h.Width - ln + + if fill < 0 { + if h.NoEllipsis { + fill = 0 + } else { + return clr.Apply(tui.Ellipsis(s, h.Width)) + } } - return clr.Apply(s) + strings.Repeat(" ", h.Width-ln) + return clr.Apply(s) + strings.Repeat(" ", fill) } func (h imgColumn) PrintR(clr aec.ANSI, s string) string { ln := tui.Width(s) - if ln > h.Width { - return clr.Apply(tui.Ellipsis(s, h.Width)) + fill := h.Width - ln + + if fill < 0 { + if h.NoEllipsis { + fill = 0 + } else { + return clr.Apply(tui.Ellipsis(s, h.Width)) + } } - return strings.Repeat(" ", h.Width-ln) + clr.Apply(s) + return strings.Repeat(" ", fill) + clr.Apply(s) } // widestFirstColumnValue calculates the width needed to fully display the image names and platforms. func widestFirstColumnValue(headers []imgColumn, images []topImage) int { width := len(headers[0].Title) for _, img := range images { - for _, name := range img.Names { + names := img.Names + if len(names) == 0 { + names = []string{untaggedName} + } + for _, name := range names { if len(name) > width { width = len(name) } @@ -488,3 +564,17 @@ func widestFirstColumnValue(headers []imgColumn, images []topImage) int { } return width } + +func streamRedirected(s *streams.Out) bool { + fd := s.FD() + if os.Stdout.Fd() != fd { + return true + } + + fi, err := os.Stdout.Stat() + if err != nil { + return true + } + + return fi.Mode()&os.ModeCharDevice == 0 +} diff --git a/cli/command/image/tree_test.go b/cli/command/image/tree_test.go new file mode 100644 index 000000000000..4516c08a1bae --- /dev/null +++ b/cli/command/image/tree_test.go @@ -0,0 +1,355 @@ +package image + +import ( + "fmt" + "strings" + "testing" + + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" + "gotest.tools/v3/golden" +) + +func TestPrintImageTreeAnsiTty(t *testing.T) { + testCases := []struct { + name string + stdinTty bool + stdoutTty bool + stderrTty bool + expectedAnsi bool + noColorEnv bool + }{ + { + name: "non-terminal", + stdinTty: false, + stdoutTty: false, + stderrTty: false, + + expectedAnsi: false, + }, + { + name: "terminal", + stdinTty: true, + stdoutTty: true, + stderrTty: true, + + expectedAnsi: true, + }, + { + name: "stdout-tty-only", + stdinTty: false, + stdoutTty: true, + stderrTty: false, + + expectedAnsi: true, + }, + { + name: "stdin-stderr-tty-only", + stdinTty: true, + stdoutTty: false, + stderrTty: true, + + expectedAnsi: false, + }, + { + name: "stdout-stdin-tty", + stdinTty: true, + stdoutTty: true, + stderrTty: false, + + expectedAnsi: true, + }, + { + name: "stdout-stderr-tty", + stdinTty: false, + stdoutTty: true, + stderrTty: true, + + expectedAnsi: true, + }, + { + name: "stdin-tty-only", + stdinTty: true, + stdoutTty: false, + stderrTty: false, + + expectedAnsi: false, + }, + { + name: "stderr-tty-only", + stdinTty: false, + stdoutTty: false, + stderrTty: true, + + expectedAnsi: false, + }, + { + name: "no-color-env", + stdinTty: false, + stdoutTty: false, + stderrTty: false, + + noColorEnv: true, + expectedAnsi: false, + }, + { + name: "no-color-env-terminal", + stdinTty: true, + stdoutTty: true, + stderrTty: true, + + noColorEnv: true, + expectedAnsi: false, + }, + } + + mockView := treeView{ + images: []topImage{ + { + Names: []string{"test-image:latest"}, + Details: imageDetails{ + ID: "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + DiskUsage: "10.5 MB", + InUse: true, + ContentSize: "5.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + DiskUsage: "5.1 MB", + InUse: false, + ContentSize: "2.5 MB", + }, + }, + }, + }, + }, + imageSpacing: false, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(nil) + cli.In().SetIsTerminal(tc.stdinTty) + cli.Out().SetIsTerminal(tc.stdoutTty) + cli.Err().SetIsTerminal(tc.stderrTty) + if tc.noColorEnv { + t.Setenv("NO_COLOR", "1") + } else { + t.Setenv("NO_COLOR", "") + } + + printImageTree(cli, mockView) + + out := cli.OutBuffer().String() + assert.Check(t, len(out) > 0, "Output should not be empty") + + hasAnsi := strings.Contains(out, "\x1b[") + if tc.expectedAnsi { + assert.Check(t, hasAnsi, "Output should contain ANSI escape codes, output: %s", out) + } else { + assert.Check(t, !hasAnsi, "Output should not contain ANSI escape codes, output: %s", out) + } + }) + } +} + +func TestPrintImageTreeGolden(t *testing.T) { + testCases := []struct { + name string + view treeView + expanded bool + }{ + { + name: "width-calculation-untagged", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"a:1"}, + Details: imageDetails{ + ID: "sha256:1111111111111111111111111111111111111111111111111111111111111111", + DiskUsage: "5.5 MB", + InUse: false, + ContentSize: "2.5 MB", + }, + }, + { + // Untagged image name is longer than "a:1" + Names: []string{}, + Details: imageDetails{ + ID: "sha256:2222222222222222222222222222222222222222222222222222222222222222", + DiskUsage: "3.2 MB", + InUse: false, + ContentSize: "1.6 MB", + }, + }, + { + Names: []string{"short:v1"}, + Details: imageDetails{ + ID: "sha256:3333333333333333333333333333333333333333333333333333333333333333", + DiskUsage: "7.1 MB", + InUse: true, + ContentSize: "3.5 MB", + }, + }, + }, + imageSpacing: false, + }, + }, + { + name: "expanded-view-with-platforms", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"multiplatform:latest"}, + Details: imageDetails{ + ID: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + DiskUsage: "25.5 MB", + InUse: true, + ContentSize: "20.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + DiskUsage: "12.1 MB", + InUse: false, + ContentSize: "10.0 MB", + }, + }, + { + Platform: "linux/arm64", + Available: true, + Details: imageDetails{ + ID: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + DiskUsage: "13.4 MB", + InUse: true, + ContentSize: "10.2 MB", + }, + }, + }, + }, + }, + imageSpacing: true, + }, + }, + { + name: "untagged-with-platforms", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{}, + Details: imageDetails{ + ID: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + DiskUsage: "18.5 MB", + InUse: false, + ContentSize: "15.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + DiskUsage: "9.2 MB", + InUse: false, + ContentSize: "7.6 MB", + }, + }, + { + Platform: "linux/arm64", + Available: false, + Details: imageDetails{ + ID: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + DiskUsage: "9.3 MB", + InUse: false, + ContentSize: "7.6 MB", + }, + }, + }, + }, + }, + imageSpacing: true, + }, + }, + { + name: "mixed-tagged-untagged-with-children", + expanded: false, + view: treeView{ + images: []topImage{ + { + Names: []string{"app:v1", "app:latest"}, + Details: imageDetails{ + ID: "sha256:1010101010101010101010101010101010101010101010101010101010101010", + DiskUsage: "30.5 MB", + InUse: true, + ContentSize: "25.2 MB", + }, + Children: []subImage{ + { + Platform: "linux/amd64", + Available: true, + Details: imageDetails{ + ID: "sha256:2020202020202020202020202020202020202020202020202020202020202020", + DiskUsage: "15.2 MB", + InUse: true, + ContentSize: "12.6 MB", + }, + }, + }, + }, + { + Names: []string{}, + Details: imageDetails{ + ID: "sha256:3030303030303030303030303030303030303030303030303030303030303030", + DiskUsage: "12.3 MB", + InUse: false, + ContentSize: "10.1 MB", + }, + Children: []subImage{ + { + Platform: "linux/arm/v7", + Available: true, + Details: imageDetails{ + ID: "sha256:4040404040404040404040404040404040404040404040404040404040404040", + DiskUsage: "6.1 MB", + InUse: false, + ContentSize: "5.0 MB", + }, + }, + }, + }, + { + Names: []string{"base:alpine"}, + Details: imageDetails{ + ID: "sha256:5050505050505050505050505050505050505050505050505050505050505050", + DiskUsage: "5.5 MB", + InUse: false, + ContentSize: "5.5 MB", + }, + }, + }, + imageSpacing: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cli := test.NewFakeCli(nil) + cli.Out().SetIsTerminal(false) + + printImageTree(cli, tc.view) + + golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("tree-command-success.%s.golden", tc.name)) + }) + } +} diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go deleted file mode 100644 index e2980e847713..000000000000 --- a/cli/command/image/trust.go +++ /dev/null @@ -1,219 +0,0 @@ -package image - -import ( - "context" - "encoding/hex" - "fmt" - "io" - - "github.com/distribution/reference" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/streams" - "github.com/docker/cli/cli/trust" - "github.com/docker/cli/internal/jsonstream" - "github.com/docker/docker/api/types/image" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/tuf/data" -) - -type target struct { - name string - digest digest.Digest - size int64 -} - -// notaryClientProvider is used in tests to provide a dummy notary client. -type notaryClientProvider interface { - NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) -} - -// newNotaryClient provides a Notary Repository to interact with signed metadata for an image. -func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) (client.Repository, error) { - if ncp, ok := cli.(notaryClientProvider); ok { - // notaryClientProvider is used in tests to provide a dummy notary client. - return ncp.NotaryClient(imgRefAndAuth, []string{"pull"}) - } - return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), "pull") -} - -// pushTrustedReference pushes a canonical reference to the trust server. -func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { - return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent()) -} - -// trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts pullOptions) error { - refs, err := getTrustedPullTargets(cli, imgRefAndAuth) - if err != nil { - return err - } - - ref := imgRefAndAuth.Reference() - for i, r := range refs { - displayTag := r.name - if displayTag != "" { - displayTag = ":" + displayTag - } - _, _ = fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest) - - trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest) - if err != nil { - return err - } - updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), trustedRef.String()) - if err != nil { - return err - } - if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, pullOptions{ - all: false, - platform: opts.platform, - quiet: opts.quiet, - remote: opts.remote, - }); err != nil { - return err - } - - tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name) - if err != nil { - return err - } - - // Use familiar references when interacting with client and output - familiarRef := reference.FamiliarString(tagged) - trustedFamiliarRef := reference.FamiliarString(trustedRef) - _, _ = fmt.Fprintf(cli.Err(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) - if err := cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef); err != nil { - return err - } - } - return nil -} - -func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth) ([]target, error) { - notaryRepo, err := newNotaryClient(cli, imgRefAndAuth) - if err != nil { - return nil, errors.Wrap(err, "error establishing connection to trust repository") - } - - ref := imgRefAndAuth.Reference() - tagged, isTagged := ref.(reference.NamedTagged) - if !isTagged { - // List all targets - targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(ref.Name(), err) - } - var refs []target - for _, tgt := range targets { - t, err := convertTarget(tgt.Target) - if err != nil { - _, _ = fmt.Fprintf(cli.Err(), "Skipping target for %q\n", reference.FamiliarName(ref)) - continue - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole { - continue - } - refs = append(refs, t) - } - if len(refs) == 0 { - return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name())) - } - return refs, nil - } - - t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(ref.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag())) - } - - logrus.Debugf("retrieving target for %s role", t.Role) - r, err := convertTarget(t.Target) - return []target{r}, err -} - -// imagePullPrivileged pulls the image and displays it to the output -func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts pullOptions) error { - encodedAuth, err := registrytypes.EncodeAuthConfig(*imgRefAndAuth.AuthConfig()) - if err != nil { - return err - } - var requestPrivilege registrytypes.RequestAuthConfig - if cli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull") - } - responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), image.PullOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - All: opts.all, - Platform: opts.platform, - }) - if err != nil { - return err - } - defer responseBody.Close() - - out := cli.Out() - if opts.quiet { - out = streams.NewOut(io.Discard) - } - return jsonstream.Display(ctx, responseBody, out) -} - -// TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), ref.String()) - if err != nil { - return nil, err - } - - notaryRepo, err := newNotaryClient(cli, imgRefAndAuth) - if err != nil { - return nil, errors.Wrap(err, "error establishing connection to trust repository") - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err) - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), client.ErrNoSuchTarget(ref.Tag())) - } - r, err := convertTarget(t.Target) - if err != nil { - return nil, err - } - return reference.WithDigest(reference.TrimNamed(ref), r.digest) -} - -func convertTarget(t client.Target) (target, error) { - h, ok := t.Hashes["sha256"] - if !ok { - return target{}, errors.New("no valid hash, expecting sha256") - } - return target{ - name: t.Name, - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, - }, nil -} - -// AuthResolver returns an auth resolver function from a command.Cli -func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { - return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { - return command.ResolveAuthConfig(cli.ConfigFile(), index) - } -} diff --git a/cli/command/inspect/inspector.go b/cli/command/inspect/inspector.go index dab48e6f3cd6..526cfda9f8c0 100644 --- a/cli/command/inspect/inspector.go +++ b/cli/command/inspect/inspector.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package inspect diff --git a/cli/command/manifest/annotate.go b/cli/command/manifest/annotate.go index 0f2a10aade1e..76385f079c8c 100644 --- a/cli/command/manifest/annotate.go +++ b/cli/command/manifest/annotate.go @@ -5,14 +5,14 @@ import ( "fmt" "path/filepath" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/manifest/store" - registryclient "github.com/docker/cli/cli/registry/client" - "github.com/docker/docker/api/types/registry" + "github.com/docker/cli/internal/registryclient" + "github.com/moby/moby/api/types/registry" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -44,6 +44,27 @@ func newManifestStore(dockerCLI command.Cli) store.Store { return store.NewStore(filepath.Join(config.Dir(), "manifests")) } +// authConfigKey is the key used to store credentials for Docker Hub. It is +// a copy of [registry.IndexServer]. +// +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer +const authConfigKey = "https://index.docker.io/v1/" + +// getAuthConfigKey special-cases using the full index address of the official +// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. +// +// It is similar to [registry.GetAuthConfigKey], but does not require on +// [registrytypes.IndexInfo] as intermediate. +// +// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey +// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo +func getAuthConfigKey(domainName string) string { + if domainName == "docker.io" || domainName == "index.docker.io" { + return authConfigKey + } + return domainName +} + // newRegistryClient returns a client for communicating with a Docker distribution // registry func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient { @@ -51,14 +72,27 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient // manifestStoreProvider is used in tests to provide a dummy store. return msp.RegistryClient(allowInsecure) } - resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig { - return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index) + cfg := dockerCLI.ConfigFile() + resolver := func(ctx context.Context, domainName string) registry.AuthConfig { + configKey := getAuthConfigKey(domainName) + a, _ := cfg.GetAuthConfig(configKey) + return registry.AuthConfig{ + Username: a.Username, + Password: a.Password, + ServerAddress: a.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: a.Auth, + IdentityToken: a.IdentityToken, + RegistryToken: a.RegistryToken, + } } + // FIXME(thaJeztah): this should use the userAgent as configured on the dockerCLI. return registryclient.NewRegistryClient(resolver, command.UserAgent(), allowInsecure) } // NewAnnotateCommand creates a new `docker manifest annotate` command -func newAnnotateCommand(dockerCli command.Cli) *cobra.Command { +func newAnnotateCommand(dockerCLI command.Cli) *cobra.Command { var opts annotateOptions cmd := &cobra.Command{ @@ -68,8 +102,9 @@ func newAnnotateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.target = args[0] opts.image = args[1] - return runManifestAnnotate(dockerCli, opts) + return runManifestAnnotate(dockerCLI, opts) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -86,17 +121,17 @@ func newAnnotateCommand(dockerCli command.Cli) *cobra.Command { func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error { targetRef, err := normalizeReference(opts.target) if err != nil { - return errors.Wrapf(err, "annotate: error parsing name for manifest list %s", opts.target) + return fmt.Errorf("annotate: error parsing name for manifest list %s: %w", opts.target, err) } imgRef, err := normalizeReference(opts.image) if err != nil { - return errors.Wrapf(err, "annotate: error parsing name for manifest %s", opts.image) + return fmt.Errorf("annotate: error parsing name for manifest %s: %w", opts.image, err) } manifestStore := newManifestStore(dockerCLI) imageManifest, err := manifestStore.Get(targetRef, imgRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target) case err != nil: return err @@ -123,7 +158,7 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error { } if !isValidOSArch(imageManifest.Descriptor.Platform.OS, imageManifest.Descriptor.Platform.Architecture) { - return errors.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch) + return fmt.Errorf("manifest entry for image has unsupported os/arch combination: %s/%s", opts.os, opts.arch) } return manifestStore.Save(targetRef, imgRef, imageManifest) } diff --git a/cli/command/manifest/client_test.go b/cli/command/manifest/client_test.go index 497251c70e16..16b7912ab6f5 100644 --- a/cli/command/manifest/client_test.go +++ b/cli/command/manifest/client_test.go @@ -5,7 +5,7 @@ import ( "github.com/distribution/reference" manifesttypes "github.com/docker/cli/cli/manifest/types" - "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/internal/registryclient" "github.com/docker/distribution" "github.com/opencontainers/go-digest" ) @@ -45,4 +45,4 @@ func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Name return digest.Digest(""), nil } -var _ client.RegistryClient = &fakeRegistryClient{} +var _ registryclient.RegistryClient = &fakeRegistryClient{} diff --git a/cli/command/manifest/cmd.go b/cli/command/manifest/cmd.go index 939f02b7bc5c..ee254c918984 100644 --- a/cli/command/manifest/cmd.go +++ b/cli/command/manifest/cmd.go @@ -5,12 +5,17 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewManifestCommand returns a cobra command for `manifest` subcommands -func NewManifestCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newManifestCommand) +} + +// newManifestCommand returns a cobra command for `manifest` subcommands +func newManifestCommand(dockerCLI command.Cli) *cobra.Command { // use dockerCli as command.Cli cmd := &cobra.Command{ Use: "manifest COMMAND", @@ -18,16 +23,17 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command { Long: manifestDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - _, _ = fmt.Fprint(dockerCli.Err(), "\n"+cmd.UsageString()) + _, _ = fmt.Fprint(dockerCLI.Err(), "\n"+cmd.UsageString()) }, - Annotations: map[string]string{"experimentalCLI": ""}, + Annotations: map[string]string{"experimentalCLI": ""}, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newCreateListCommand(dockerCli), - newInspectCommand(dockerCli), - newAnnotateCommand(dockerCli), - newPushListCommand(dockerCli), - newRmManifestListCommand(dockerCli), + newCreateListCommand(dockerCLI), + newInspectCommand(dockerCLI), + newAnnotateCommand(dockerCLI), + newPushListCommand(dockerCLI), + newRmManifestListCommand(dockerCLI), ) return cmd } diff --git a/cli/command/manifest/create_list.go b/cli/command/manifest/create_list.go index 58c18124dfce..3d8a6fed5f3f 100644 --- a/cli/command/manifest/create_list.go +++ b/cli/command/manifest/create_list.go @@ -2,12 +2,12 @@ package manifest import ( "context" + "errors" "fmt" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/manifest/store" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -16,7 +16,7 @@ type createOpts struct { insecure bool } -func newCreateListCommand(dockerCli command.Cli) *cobra.Command { +func newCreateListCommand(dockerCLI command.Cli) *cobra.Command { opts := createOpts{} cmd := &cobra.Command{ @@ -24,8 +24,9 @@ func newCreateListCommand(dockerCli command.Cli) *cobra.Command { Short: "Create a local manifest list for annotating and pushing to a registry", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return createManifestList(cmd.Context(), dockerCli, args, opts) + return createManifestList(cmd.Context(), dockerCLI, args, opts) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -38,18 +39,18 @@ func createManifestList(ctx context.Context, dockerCLI command.Cli, args []strin newRef := args[0] targetRef, err := normalizeReference(newRef) if err != nil { - return errors.Wrapf(err, "error parsing name for manifest list %s", newRef) + return fmt.Errorf("error parsing name for manifest list %s: %w", newRef, err) } manifestStore := newManifestStore(dockerCLI) _, err = manifestStore.GetList(targetRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): // New manifest list case err != nil: return err case !opts.amend: - return errors.Errorf("refusing to amend an existing manifest list with no --amend flag") + return errors.New("refusing to amend an existing manifest list with no --amend flag") } // Now create the local manifest list transaction by looking up the manifest schemas diff --git a/cli/command/manifest/inspect.go b/cli/command/manifest/inspect.go index 217383a07f61..b4beff387772 100644 --- a/cli/command/manifest/inspect.go +++ b/cli/command/manifest/inspect.go @@ -11,7 +11,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/manifest/types" "github.com/docker/distribution/manifest/manifestlist" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -23,7 +22,7 @@ type inspectOptions struct { } // NewInspectCommand creates a new `docker manifest inspect` command -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -38,8 +37,9 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { opts.list = args[0] opts.ref = args[1] } - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -99,14 +99,14 @@ func printManifest(dockerCli command.Cli, manifest types.ImageManifest, opts ins if err := json.Indent(buffer, raw, "", "\t"); err != nil { return err } - fmt.Fprintln(dockerCli.Out(), buffer.String()) + _, _ = fmt.Fprintln(dockerCli.Out(), buffer.String()) return nil } jsonBytes, err := json.MarshalIndent(manifest, "", "\t") if err != nil { return err } - dockerCli.Out().Write(append(jsonBytes, '\n')) + _, _ = dockerCli.Out().Write(append(jsonBytes, '\n')) return nil } @@ -114,12 +114,12 @@ func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []t if !opts.verbose { targetRepo := reference.TrimNamed(namedRef) - manifests := []manifestlist.ManifestDescriptor{} + manifests := make([]manifestlist.ManifestDescriptor, 0, len(list)) // More than one response. This is a manifest list. for _, img := range list { mfd, err := buildManifestDescriptor(targetRepo, img) if err != nil { - return errors.Wrap(err, "failed to assemble ManifestDescriptor") + return fmt.Errorf("failed to assemble ManifestDescriptor: %w", err) } manifests = append(manifests, mfd) } @@ -131,13 +131,13 @@ func printManifestList(dockerCli command.Cli, namedRef reference.Named, list []t if err != nil { return err } - fmt.Fprintln(dockerCli.Out(), string(jsonBytes)) + _, _ = fmt.Fprintln(dockerCli.Out(), string(jsonBytes)) return nil } jsonBytes, err := json.MarshalIndent(list, "", "\t") if err != nil { return err } - dockerCli.Out().Write(append(jsonBytes, '\n')) + _, _ = dockerCli.Out().Write(append(jsonBytes, '\n')) return nil } diff --git a/cli/command/manifest/push.go b/cli/command/manifest/push.go index ccfa84cd6c5b..024bf6e025a2 100644 --- a/cli/command/manifest/push.go +++ b/cli/command/manifest/push.go @@ -10,12 +10,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/manifest/types" - registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/internal/registryclient" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/ocischema" "github.com/docker/distribution/manifest/schema2" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -43,7 +42,7 @@ type pushRequest struct { insecure bool } -func newPushListCommand(dockerCli command.Cli) *cobra.Command { +func newPushListCommand(dockerCLI command.Cli) *cobra.Command { opts := pushOpts{} cmd := &cobra.Command{ @@ -52,8 +51,9 @@ func newPushListCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.target = args[0] - return runPush(cmd.Context(), dockerCli, opts) + return runPush(cmd.Context(), dockerCLI, opts) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -73,7 +73,7 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOpts) error { return err } if len(manifests) == 0 { - return errors.Errorf("%s not found", targetRef) + return fmt.Errorf("%s not found", targetRef) } req, err := buildPushRequest(manifests, targetRef, opts.insecure) @@ -123,13 +123,12 @@ func buildPushRequest(manifests []types.ImageManifest, targetRef reference.Named func buildManifestList(manifests []types.ImageManifest, targetRef reference.Named) (*manifestlist.DeserializedManifestList, error) { targetRepo := reference.TrimNamed(targetRef) - descriptors := []manifestlist.ManifestDescriptor{} + descriptors := make([]manifestlist.ManifestDescriptor, 0, len(manifests)) for _, imageManifest := range manifests { if imageManifest.Descriptor.Platform == nil || imageManifest.Descriptor.Platform.Architecture == "" || imageManifest.Descriptor.Platform.OS == "" { - return nil, errors.Errorf( - "manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref) + return nil, fmt.Errorf("manifest %s must have an OS and Architecture to be pushed to a registry", imageManifest.Ref) } descriptor, err := buildManifestDescriptor(targetRepo, imageManifest) if err != nil { @@ -145,7 +144,7 @@ func buildManifestDescriptor(targetRepo reference.Named, imageManifest types.Ima manifestRepoHostname := reference.Domain(reference.TrimNamed(imageManifest.Ref)) targetRepoHostname := reference.Domain(reference.TrimNamed(targetRepo)) if manifestRepoHostname != targetRepoHostname { - return manifestlist.ManifestDescriptor{}, errors.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname) + return manifestlist.ManifestDescriptor{}, fmt.Errorf("cannot use source images from a different registry than the target image: %s != %s", manifestRepoHostname, targetRepoHostname) } manifest := manifestlist.ManifestDescriptor{ @@ -162,8 +161,7 @@ func buildManifestDescriptor(targetRepo reference.Named, imageManifest types.Ima } if err := manifest.Descriptor.Digest.Validate(); err != nil { - return manifestlist.ManifestDescriptor{}, errors.Wrapf(err, - "digest parse of image %q failed", imageManifest.Ref) + return manifestlist.ManifestDescriptor{}, fmt.Errorf("digest parse of image %q failed: %w", imageManifest.Ref, err) } return manifest, nil @@ -215,7 +213,7 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere dig := imageManifest.Descriptor.Digest if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 { - return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) + return mountRequest{}, fmt.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) } var manifest schema2.DeserializedManifest @@ -234,7 +232,7 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere dig := imageManifest.Descriptor.Digest if dig2 := dig.Algorithm().FromBytes(dt); dig != dig2 { - return mountRequest{}, errors.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) + return mountRequest{}, fmt.Errorf("internal digest mismatch for %s: expected %s, got %s", imageManifest.Ref, dig, dig2) } var manifest ocischema.DeserializedManifest @@ -248,15 +246,15 @@ func buildPutManifestRequest(imageManifest types.ImageManifest, targetRef refere } func pushList(ctx context.Context, dockerCLI command.Cli, req pushRequest) error { - rclient := newRegistryClient(dockerCLI, req.insecure) + registryClient := newRegistryClient(dockerCLI, req.insecure) - if err := mountBlobs(ctx, rclient, req.targetRef, req.manifestBlobs); err != nil { + if err := mountBlobs(ctx, registryClient, req.targetRef, req.manifestBlobs); err != nil { return err } - if err := pushReferences(ctx, dockerCLI.Out(), rclient, req.mountRequests); err != nil { + if err := pushReferences(ctx, dockerCLI.Out(), registryClient, req.mountRequests); err != nil { return err } - dgst, err := rclient.PutManifest(ctx, req.targetRef, req.list) + dgst, err := registryClient.PutManifest(ctx, req.targetRef, req.list) if err != nil { return err } diff --git a/cli/command/manifest/rm.go b/cli/command/manifest/rm.go index dda4e3e489dd..38bb059e5ad4 100644 --- a/cli/command/manifest/rm.go +++ b/cli/command/manifest/rm.go @@ -18,6 +18,7 @@ func newRmManifestListCommand(dockerCLI command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runRemove(cmd.Context(), newManifestStore(dockerCLI), args) }, + DisableFlagsInUseLine: true, } return cmd diff --git a/cli/command/manifest/util.go b/cli/command/manifest/util.go index be9fe9ea1ba4..c10a98b0d8f4 100644 --- a/cli/command/manifest/util.go +++ b/cli/command/manifest/util.go @@ -3,9 +3,9 @@ package manifest import ( "context" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/manifest/store" "github.com/docker/cli/cli/manifest/types" ) @@ -72,7 +72,7 @@ func normalizeReference(ref string) (reference.Named, error) { func getManifest(ctx context.Context, dockerCLI command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) { data, err := newManifestStore(dockerCLI).Get(listRef, namedRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): return newRegistryClient(dockerCLI, insecure).GetManifest(ctx, namedRef) case err != nil: return types.ImageManifest{}, err diff --git a/cli/command/network/client_test.go b/cli/command/network/client_test.go index aa13a3d09eb4..20496e17e6b1 100644 --- a/cli/command/network/client_test.go +++ b/cli/command/network/client_test.go @@ -3,67 +3,65 @@ package network import ( "context" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - networkCreateFunc func(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) - networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error - networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error + networkCreateFunc func(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) + networkConnectFunc func(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) + networkDisconnectFunc func(ctx context.Context, networkID string, options client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) networkRemoveFunc func(ctx context.Context, networkID string) error - networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) - networkPruneFunc func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) - networkInspectFunc func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, []byte, error) + networkListFunc func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) + networkPruneFunc func(ctx context.Context, options client.NetworkPruneOptions) (client.NetworkPruneResult, error) + networkInspectFunc func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) } -func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) { +func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) { if c.networkCreateFunc != nil { return c.networkCreateFunc(ctx, name, options) } - return network.CreateResponse{}, nil + return client.NetworkCreateResult{}, nil } -func (c *fakeClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error { +func (c *fakeClient) NetworkConnect(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) { if c.networkConnectFunc != nil { - return c.networkConnectFunc(ctx, networkID, container, config) + return c.networkConnectFunc(ctx, networkID, options) } - return nil + return client.NetworkConnectResult{}, nil } -func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error { +func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID string, options client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) { if c.networkDisconnectFunc != nil { - return c.networkDisconnectFunc(ctx, networkID, container, force) + return c.networkDisconnectFunc(ctx, networkID, options) } - return nil + return client.NetworkDisconnectResult{}, nil } -func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { +func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { if c.networkListFunc != nil { return c.networkListFunc(ctx, options) } - return []network.Inspect{}, nil + return client.NetworkListResult{}, nil } -func (c *fakeClient) NetworkRemove(ctx context.Context, networkID string) error { +func (c *fakeClient) NetworkRemove(ctx context.Context, networkID string, _ client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) { if c.networkRemoveFunc != nil { - return c.networkRemoveFunc(ctx, networkID) + return client.NetworkRemoveResult{}, c.networkRemoveFunc(ctx, networkID) } - return nil + return client.NetworkRemoveResult{}, nil } -func (c *fakeClient) NetworkInspectWithRaw(ctx context.Context, networkID string, opts network.InspectOptions) (network.Inspect, []byte, error) { +func (c *fakeClient) NetworkInspect(ctx context.Context, networkID string, opts client.NetworkInspectOptions) (client.NetworkInspectResult, error) { if c.networkInspectFunc != nil { return c.networkInspectFunc(ctx, networkID, opts) } - return network.Inspect{}, nil, nil + return client.NetworkInspectResult{}, nil } -func (c *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) { +func (c *fakeClient) NetworksPrune(ctx context.Context, opts client.NetworkPruneOptions) (client.NetworkPruneResult, error) { if c.networkPruneFunc != nil { - return c.networkPruneFunc(ctx, pruneFilter) + return c.networkPruneFunc(ctx, opts) } - return network.PruneReport{}, nil + return client.NetworkPruneResult{}, nil } diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go index 3db3088ebe95..7f4ec64e1f80 100644 --- a/cli/command/network/cmd.go +++ b/cli/command/network/cmd.go @@ -3,26 +3,33 @@ package network import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewNetworkCommand returns a cobra command for `network` subcommands -func NewNetworkCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newNetworkCommand) +} + +// newNetworkCommand returns a cobra command for `network` subcommands +func newNetworkCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "network", Short: "Manage networks", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.21"}, + + DisableFlagsInUseLine: true, } cmd.AddCommand( - newConnectCommand(dockerCli), - newCreateCommand(dockerCli), - newDisconnectCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), + newConnectCommand(dockerCLI), + newCreateCommand(dockerCLI), + newDisconnectCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPruneCommand(dockerCLI), ) return cmd } diff --git a/cli/command/network/connect.go b/cli/command/network/connect.go index e24976ec7e0f..a629005f786b 100644 --- a/cli/command/network/connect.go +++ b/cli/command/network/connect.go @@ -3,25 +3,27 @@ package network import ( "context" "errors" + "net" + "net/netip" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) type connectOptions struct { network string container string - ipaddress string - ipv6address string + ipaddress net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly + ipv6address net.IP // TODO(thaJeztah): we need a flag-type to handle netip.Addr directly links opts.ListOpts aliases []string - linklocalips []string + linklocalips []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly driverOpts []string gwPriority int } @@ -47,14 +49,15 @@ func newConnectCommand(dockerCLI command.Cli) *cobra.Command { nw := args[0] return completion.ContainerNames(dockerCLI, true, not(isConnected(nw)))(cmd, args, toComplete) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringVar(&options.ipaddress, "ip", "", `IPv4 address (e.g., "172.30.100.104")`) - flags.StringVar(&options.ipv6address, "ip6", "", `IPv6 address (e.g., "2001:db8::33")`) + flags.IPVar(&options.ipaddress, "ip", nil, `IPv4 address (e.g., "172.30.100.104")`) + flags.IPVar(&options.ipv6address, "ip6", nil, `IPv6 address (e.g., "2001:db8::33")`) flags.Var(&options.links, "link", "Add link to another container") flags.StringSliceVar(&options.aliases, "alias", []string{}, "Add network-scoped alias for the container") - flags.StringSliceVar(&options.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") + flags.IPSliceVar(&options.linklocalips, "link-local-ip", nil, "Add a link-local address for the container") flags.StringSliceVar(&options.driverOpts, "driver-opt", []string{}, "driver options for the network") flags.IntVar(&options.gwPriority, "gw-priority", 0, "Highest gw-priority provides the default gateway. Accepts positive and negative values.") return cmd @@ -65,18 +68,21 @@ func runConnect(ctx context.Context, apiClient client.NetworkAPIClient, options if err != nil { return err } - - return apiClient.NetworkConnect(ctx, options.network, options.container, &network.EndpointSettings{ - IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: options.ipaddress, - IPv6Address: options.ipv6address, - LinkLocalIPs: options.linklocalips, + _, err = apiClient.NetworkConnect(ctx, options.network, client.NetworkConnectOptions{ + Container: options.container, + EndpointConfig: &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: toNetipAddr(options.ipaddress), + IPv6Address: toNetipAddr(options.ipv6address), + LinkLocalIPs: toNetipAddrSlice(options.linklocalips), + }, + Links: options.links.GetSlice(), + Aliases: options.aliases, + DriverOpts: driverOpts, + GwPriority: options.gwPriority, }, - Links: options.links.GetSlice(), - Aliases: options.aliases, - DriverOpts: driverOpts, - GwPriority: options.gwPriority, }) + return err } func convertDriverOpt(options []string) (map[string]string, error) { @@ -92,3 +98,41 @@ func convertDriverOpt(options []string) (map[string]string, error) { } return driverOpt, nil } + +func toNetipAddrSlice(ips []net.IP) []netip.Addr { + if len(ips) == 0 { + return nil + } + netIPs := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + netIPs = append(netIPs, toNetipAddr(ip)) + } + return netIPs +} + +func toNetipAddr(ip net.IP) netip.Addr { + a, _ := netip.AddrFromSlice(ip) + return a.Unmap() +} + +// toPrefix converts n into a netip.Prefix. If n is not a valid IPv4 or IPV6 +// address, ToPrefix returns netip.Prefix{}, false. +// +// TODO(thaJeztah): create internal package similar to https://github.com/moby/moby/blob/0769fe708773892d6ac399ee137e71a777b35de7/daemon/internal/netiputil/netiputil.go#L21-L42 +func toPrefix(n net.IPNet) (netip.Prefix, bool) { + if ll := len(n.Mask); ll != net.IPv4len && ll != net.IPv6len { + return netip.Prefix{}, false + } + + addr, ok := netip.AddrFromSlice(n.IP) + if !ok { + return netip.Prefix{}, false + } + + ones, bits := n.Mask.Size() + if ones == 0 && bits == 0 { + return netip.Prefix{}, false + } + + return netip.PrefixFrom(addr.Unmap(), ones), true +} diff --git a/cli/command/network/connect_test.go b/cli/command/network/connect_test.go index a64ede90b8ed..52ede09412f7 100644 --- a/cli/command/network/connect_test.go +++ b/cli/command/network/connect_test.go @@ -4,10 +4,13 @@ import ( "context" "errors" "io" + "net/netip" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/network" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,7 +18,7 @@ import ( func TestNetworkConnectErrors(t *testing.T) { testCases := []struct { args []string - networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error + networkConnectFunc func(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) expectedError string }{ { @@ -23,8 +26,8 @@ func TestNetworkConnectErrors(t *testing.T) { }, { args: []string{"toto", "titi"}, - networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error { - return errors.New("error connecting network") + networkConnectFunc: func(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) { + return client.NetworkConnectResult{}, errors.New("error connecting network") }, expectedError: "error connecting network", }, @@ -46,9 +49,9 @@ func TestNetworkConnectErrors(t *testing.T) { func TestNetworkConnectWithFlags(t *testing.T) { expectedConfig := &network.EndpointSettings{ IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: "192.168.4.1", - IPv6Address: "fdef:f401:8da0:1234::5678", - LinkLocalIPs: []string{"169.254.42.42"}, + IPv4Address: netip.MustParseAddr("192.168.4.1"), + IPv6Address: netip.MustParseAddr("fdef:f401:8da0:1234::5678"), + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.42.42")}, }, Links: []string{"otherctr"}, Aliases: []string{"poor-yorick"}, @@ -59,9 +62,9 @@ func TestNetworkConnectWithFlags(t *testing.T) { GwPriority: 100, } cli := test.NewFakeCli(&fakeClient{ - networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error { - assert.Check(t, is.DeepEqual(expectedConfig, config)) - return nil + networkConnectFunc: func(ctx context.Context, networkID string, options client.NetworkConnectOptions) (client.NetworkConnectResult, error) { + assert.Check(t, is.DeepEqual(expectedConfig, options.EndpointConfig, cmpopts.EquateComparable(netip.Addr{}))) + return client.NetworkConnectResult{}, nil }, }) args := []string{"mynet", "myctr"} diff --git a/cli/command/network/create.go b/cli/command/network/create.go index c8fca4129429..cf676a4cea9b 100644 --- a/cli/command/network/create.go +++ b/cli/command/network/create.go @@ -6,14 +6,14 @@ import ( "fmt" "io" "net" + "net/netip" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -35,9 +35,9 @@ type createOptions struct { type ipamOptions struct { driver string - subnets []string - ipRanges []string - gateways []string + subnets []string // TODO(thaJeztah): change to []net.IPNet? This won't accept a bare address (without "/xxx"); we need a flag-type to handle []netip.Prefix directly + ipRanges []net.IPNet // TODO(thaJeztah): we need a flag-type to handle []netip.Prefix directly + gateways []net.IP // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly auxAddresses opts.MapOpts driverOpts opts.MapOpts } @@ -69,7 +69,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { return runCreate(cmd.Context(), dockerCLI.Client(), dockerCLI.Out(), options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -92,8 +93,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.ipam.driver, "ipam-driver", "default", "IP Address Management Driver") flags.StringSliceVar(&options.ipam.subnets, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") - flags.StringSliceVar(&options.ipam.ipRanges, "ip-range", []string{}, "Allocate container ip from a sub-range") - flags.StringSliceVar(&options.ipam.gateways, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") + flags.IPNetSliceVar(&options.ipam.ipRanges, "ip-range", nil, "Allocate container ip from a sub-range") + flags.IPSliceVar(&options.ipam.gateways, "gateway", nil, "IPv4 or IPv6 Gateway for the master subnet") flags.Var(&options.ipam.auxAddresses, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") flags.Var(&options.ipam.driverOpts, "ipam-opt", "Set IPAM driver specific options") @@ -107,13 +108,7 @@ func runCreate(ctx context.Context, apiClient client.NetworkAPIClient, output io return err } - var configFrom *network.ConfigReference - if options.configFrom != "" { - configFrom = &network.ConfigReference{ - Network: options.configFrom, - } - } - resp, err := apiClient.NetworkCreate(ctx, options.name, network.CreateOptions{ + resp, err := apiClient.NetworkCreate(ctx, options.name, client.NetworkCreateOptions{ Driver: options.driver, Options: options.driverOpts.GetAll(), IPAM: ipamCfg, @@ -124,7 +119,7 @@ func runCreate(ctx context.Context, apiClient client.NetworkAPIClient, output io Ingress: options.ingress, Scope: options.scope, ConfigOnly: options.configOnly, - ConfigFrom: configFrom, + ConfigFrom: options.configFrom, Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), }) if err != nil { @@ -162,32 +157,36 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { return nil, errors.New("multiple overlapping subnet configuration is not supported") } } - iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} + sn, err := netip.ParsePrefix(s) + if err != nil { + return nil, err + } + iData[s] = &network.IPAMConfig{Subnet: sn, AuxAddress: map[string]netip.Addr{}} } // Validate and add valid ip ranges for _, r := range options.ipRanges { match := false for _, s := range options.subnets { - if _, _, err := net.ParseCIDR(r); err != nil { - return nil, err - } - ok, err := subnetMatches(s, r) + ok, err := subnetMatches(s, r.String()) if err != nil { return nil, err } if !ok { continue } - if iData[s].IPRange != "" { - return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) + + // Using "IsValid" to check if a valid IPRange was already set. + if iData[s].IPRange.IsValid() { + return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r.String(), iData[s].IPRange.String(), s) + } + if ipRange, ok := toPrefix(r); ok { + iData[s].IPRange = ipRange + match = true } - d := iData[s] - d.IPRange = r - match = true } if !match { - return nil, fmt.Errorf("no matching subnet for range %s", r) + return nil, fmt.Errorf("no matching subnet for range %s", r.String()) } } @@ -195,18 +194,18 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { for _, g := range options.gateways { match := false for _, s := range options.subnets { - ok, err := subnetMatches(s, g) + ok, err := subnetMatches(s, g.String()) if err != nil { return nil, err } if !ok { continue } - if iData[s].Gateway != "" { + if iData[s].Gateway.IsValid() { return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) } d := iData[s] - d.Gateway = g + d.Gateway = toNetipAddr(g) match = true } if !match { @@ -215,17 +214,24 @@ func createIPAMConfig(options ipamOptions) (*network.IPAM, error) { } // Validate and add aux-addresses - for key, aa := range options.auxAddresses.GetAll() { + for name, aa := range options.auxAddresses.GetAll() { + if aa == "" { + continue + } + auxAddr, err := netip.ParseAddr(aa) + if err != nil { + return nil, err + } match := false for _, s := range options.subnets { - ok, err := subnetMatches(s, aa) + ok, err := subnetMatches(s, auxAddr.String()) if err != nil { return nil, err } if !ok { continue } - iData[s].AuxAddress[key] = aa + iData[s].AuxAddress[name] = auxAddr match = true } if !match { diff --git a/cli/command/network/create_test.go b/cli/command/network/create_test.go index 97348c86d845..7de25edb63fa 100644 --- a/cli/command/network/create_test.go +++ b/cli/command/network/create_test.go @@ -4,11 +4,14 @@ import ( "context" "errors" "io" + "net/netip" "strings" "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/network" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -17,7 +20,7 @@ func TestNetworkCreateErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - networkCreateFunc func(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) + networkCreateFunc func(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) expectedError string }{ { @@ -25,8 +28,8 @@ func TestNetworkCreateErrors(t *testing.T) { }, { args: []string{"toto"}, - networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) { - return network.CreateResponse{}, errors.New("error creating network") + networkCreateFunc: func(ctx context.Context, name string, createBody client.NetworkCreateOptions) (client.NetworkCreateResult, error) { + return client.NetworkCreateResult{}, errors.New("error creating network") }, expectedError: "error creating network", }, @@ -34,24 +37,15 @@ func TestNetworkCreateErrors(t *testing.T) { args: []string{"toto"}, flags: map[string]string{ "ip-range": "255.255.0.0/24", - "gateway": "255.0.255.0/24", + "gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.255.0/24") "subnet": "10.1.2.0.30.50", }, - expectedError: "invalid CIDR address: 10.1.2.0.30.50", + expectedError: `netip.ParsePrefix("10.1.2.0.30.50"): no '/'`, }, { args: []string{"toto"}, flags: map[string]string{ - "ip-range": "255.255.0.0.30/24", - "gateway": "255.0.255.0/24", - "subnet": "255.0.0.0/24", - }, - expectedError: "invalid CIDR address: 255.255.0.0.30/24", - }, - { - args: []string{"toto"}, - flags: map[string]string{ - "gateway": "255.0.0.0/24", + "gateway": "255.0.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24") }, expectedError: "every ip-range or gateway must have a corresponding subnet", }, @@ -66,7 +60,7 @@ func TestNetworkCreateErrors(t *testing.T) { args: []string{"toto"}, flags: map[string]string{ "ip-range": "255.0.0.0/24", - "gateway": "255.0.0.0/24", + "gateway": "255.0.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24") }, expectedError: "every ip-range or gateway must have a corresponding subnet", }, @@ -74,7 +68,7 @@ func TestNetworkCreateErrors(t *testing.T) { args: []string{"toto"}, flags: map[string]string{ "ip-range": "255.255.0.0/24", - "gateway": "255.0.255.0/24", + "gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24") "subnet": "10.1.2.0/23,10.1.3.248/30", }, expectedError: "multiple overlapping subnet configuration is not supported", @@ -82,17 +76,17 @@ func TestNetworkCreateErrors(t *testing.T) { { args: []string{"toto"}, flags: map[string]string{ - "ip-range": "192.168.1.0/24,192.168.1.200/24", + "ip-range": "192.168.1.0/25,192.168.1.128/25", "gateway": "192.168.1.1,192.168.1.4", "subnet": "192.168.2.0/24,192.168.1.250/24", }, - expectedError: "cannot configure multiple ranges (192.168.1.200/24, 192.168.1.0/24) on the same subnet (192.168.1.250/24)", + expectedError: "cannot configure multiple ranges (192.168.1.128/25, 192.168.1.0/25) on the same subnet (192.168.1.250/24)", }, { args: []string{"toto"}, flags: map[string]string{ "ip-range": "255.255.200.0/24,255.255.120.0/24", - "gateway": "255.0.255.0/24", + "gateway": "255.0.255.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.0.0.0/24") "subnet": "255.255.255.0/24,255.255.0.255/24", }, expectedError: "no matching subnet for range 255.255.200.0/24", @@ -118,36 +112,46 @@ func TestNetworkCreateErrors(t *testing.T) { { args: []string{"toto"}, flags: map[string]string{ - "gateway": "255.255.0.0/24", + "gateway": "255.255.0.0", // FIXME(thaJeztah): this used to accept a CIDR ("255.255.0.0/24") "subnet": "255.255.0.0/24", - "aux-address": "255.255.0.30/24", + "aux-address": "router=255.255.1.30", // outside 255.255.0.0/24 // FIXME(thaJeztah): this used to accept a CIDR ("255.255.0.30/24") }, expectedError: "no matching subnet for aux-address", }, - { - args: []string{"toto"}, - flags: map[string]string{ - "ip-range": "192.168.83.1-192.168.83.254", - "gateway": "192.168.80.1", - "subnet": "192.168.80.0/20", - }, - expectedError: "invalid CIDR address: 192.168.83.1-192.168.83.254", - }, } for _, tc := range testCases { - cmd := newCreateCommand( - test.NewFakeCli(&fakeClient{ - networkCreateFunc: tc.networkCreateFunc, - }), - ) - cmd.SetArgs(tc.args) - for key, value := range tc.flags { - assert.NilError(t, cmd.Flags().Set(key, value)) + var args []string + for flag, val := range tc.flags { + args = append(args, flag+"="+val) + } + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + var name string + if len(args) == 0 { + name = "no args" + } else { + name = strings.Join(args, ",") } - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + t.Run(name, func(t *testing.T) { + cmd := newCreateCommand( + test.NewFakeCli(&fakeClient{ + networkCreateFunc: tc.networkCreateFunc, + }), + ) + if len(tc.args) == 0 { + cmd.SetArgs([]string{}) + } else { + cmd.SetArgs(tc.args) + } + for key, value := range tc.flags { + assert.NilError(t, cmd.Flags().Set(key, value)) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + }) } } @@ -155,17 +159,17 @@ func TestNetworkCreateWithFlags(t *testing.T) { expectedDriver := "foo" expectedOpts := []network.IPAMConfig{ { - Subnet: "192.168.4.0/24", - IPRange: "192.168.4.0/24", - Gateway: "192.168.4.1/24", - AuxAddress: map[string]string{}, + Subnet: netip.MustParsePrefix("192.168.4.0/24"), + IPRange: netip.MustParsePrefix("192.168.4.0/24"), + Gateway: netip.MustParseAddr("192.168.4.1"), // FIXME(thaJeztah): this used to accept a CIDR ("192.168.4.1/24") + AuxAddress: map[string]netip.Addr{}, }, } cli := test.NewFakeCli(&fakeClient{ - networkCreateFunc: func(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) { + networkCreateFunc: func(ctx context.Context, name string, options client.NetworkCreateOptions) (client.NetworkCreateResult, error) { assert.Check(t, is.Equal(expectedDriver, options.Driver), "not expected driver error") - assert.Check(t, is.DeepEqual(expectedOpts, options.IPAM.Config), "not expected driver error") - return network.CreateResponse{ + assert.Check(t, is.DeepEqual(expectedOpts, options.IPAM.Config, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})), "not expected driver error") + return client.NetworkCreateResult{ ID: name, }, nil }, @@ -174,10 +178,10 @@ func TestNetworkCreateWithFlags(t *testing.T) { cmd := newCreateCommand(cli) cmd.SetArgs(args) - cmd.Flags().Set("driver", "foo") - cmd.Flags().Set("ip-range", "192.168.4.0/24") - cmd.Flags().Set("gateway", "192.168.4.1/24") - cmd.Flags().Set("subnet", "192.168.4.0/24") + assert.Check(t, cmd.Flags().Set("driver", "foo")) + assert.Check(t, cmd.Flags().Set("ip-range", "192.168.4.0/24")) + assert.Check(t, cmd.Flags().Set("gateway", "192.168.4.1")) // FIXME(thaJeztah): this used to accept a CIDR ("192.168.4.1/24") + assert.Check(t, cmd.Flags().Set("subnet", "192.168.4.0/24")) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal("banana", strings.TrimSpace(cli.OutBuffer().String()))) } @@ -220,9 +224,9 @@ func TestNetworkCreateIPv4(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) { + networkCreateFunc: func(ctx context.Context, name string, createBody client.NetworkCreateOptions) (client.NetworkCreateResult, error) { assert.Check(t, is.DeepEqual(createBody.EnableIPv4, tc.expected)) - return network.CreateResponse{ID: name}, nil + return client.NetworkCreateResult{ID: name}, nil }, }) cmd := newCreateCommand(cli) @@ -274,9 +278,9 @@ func TestNetworkCreateIPv6(t *testing.T) { for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) { + networkCreateFunc: func(ctx context.Context, name string, createBody client.NetworkCreateOptions) (client.NetworkCreateResult, error) { assert.Check(t, is.DeepEqual(tc.expected, createBody.EnableIPv6)) - return network.CreateResponse{ID: name}, nil + return client.NetworkCreateResult{ID: name}, nil }, }) cmd := newCreateCommand(cli) diff --git a/cli/command/network/disconnect.go b/cli/command/network/disconnect.go index 521aee33f509..f868df06f253 100644 --- a/cli/command/network/disconnect.go +++ b/cli/command/network/disconnect.go @@ -1,23 +1,19 @@ package network import ( - "context" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) type disconnectOptions struct { - network string - container string - force bool + force bool } -func newDisconnectCommand(dockerCli command.Cli) *cobra.Command { +func newDisconnectCommand(dockerCLI command.Cli) *cobra.Command { opts := disconnectOptions{} cmd := &cobra.Command{ @@ -25,17 +21,21 @@ func newDisconnectCommand(dockerCli command.Cli) *cobra.Command { Short: "Disconnect a container from a network", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - opts.network = args[0] - opts.container = args[1] - return runDisconnect(cmd.Context(), dockerCli.Client(), opts) + network := args[0] + _, err := dockerCLI.Client().NetworkDisconnect(cmd.Context(), network, client.NetworkDisconnectOptions{ + Container: args[1], + Force: opts.force, + }) + return err }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { - return completion.NetworkNames(dockerCli)(cmd, args, toComplete) + return completion.NetworkNames(dockerCLI)(cmd, args, toComplete) } - network := args[0] - return completion.ContainerNames(dockerCli, true, isConnected(network))(cmd, args, toComplete) + nw := args[0] + return completion.ContainerNames(dockerCLI, true, isConnected(nw))(cmd, args, toComplete) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -44,10 +44,6 @@ func newDisconnectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runDisconnect(ctx context.Context, apiClient client.NetworkAPIClient, opts disconnectOptions) error { - return apiClient.NetworkDisconnect(ctx, opts.network, opts.container, opts.force) -} - func isConnected(network string) func(container.Summary) bool { return func(ctr container.Summary) bool { if ctr.NetworkSettings == nil { diff --git a/cli/command/network/disconnect_test.go b/cli/command/network/disconnect_test.go index c9aeea4ec703..d35d293f1c98 100644 --- a/cli/command/network/disconnect_test.go +++ b/cli/command/network/disconnect_test.go @@ -7,13 +7,14 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) func TestNetworkDisconnectErrors(t *testing.T) { testCases := []struct { args []string - networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error + networkDisconnectFunc func(ctx context.Context, networkID string, options client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) expectedError string }{ { @@ -21,8 +22,8 @@ func TestNetworkDisconnectErrors(t *testing.T) { }, { args: []string{"toto", "titi"}, - networkDisconnectFunc: func(ctx context.Context, networkID, container string, force bool) error { - return errors.New("error disconnecting network") + networkDisconnectFunc: func(ctx context.Context, networkID string, options client.NetworkDisconnectOptions) (client.NetworkDisconnectResult, error) { + return client.NetworkDisconnectResult{}, errors.New("error disconnecting network") }, expectedError: "error disconnecting network", }, diff --git a/cli/command/network/formatter.go b/cli/command/network/formatter.go index 6a15a6be1070..2a62bdb925af 100644 --- a/cli/command/network/formatter.go +++ b/cli/command/network/formatter.go @@ -5,7 +5,8 @@ import ( "strings" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" ) const ( @@ -17,8 +18,8 @@ const ( internalHeader = "INTERNAL" ) -// NewFormat returns a Format for rendering using a network Context -func NewFormat(source string, quiet bool) formatter.Format { +// newFormat returns a [formatter.Format] for rendering a networkContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -34,30 +35,34 @@ func NewFormat(source string, quiet bool) formatter.Format { return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, networks []network.Summary) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, nw := range networks { - networkCtx := &networkContext{trunc: ctx.Trunc, n: nw} - if err := format(networkCtx); err != nil { +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, networks client.NetworkListResult) error { + networkCtx := networkContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": networkIDHeader, + "Name": formatter.NameHeader, + "Driver": formatter.DriverHeader, + "Scope": formatter.ScopeHeader, + "IPv4": ipv4Header, + "IPv6": ipv6Header, + "Internal": internalHeader, + "Labels": formatter.LabelsHeader, + "CreatedAt": formatter.CreatedAtHeader, + }, + }, + } + return fmtCtx.Write(&networkCtx, func(format func(subContext formatter.SubContext) error) error { + for _, nw := range networks.Items { + if err := format(&networkContext{ + trunc: fmtCtx.Trunc, + n: nw, + }); err != nil { return err } } return nil - } - networkCtx := networkContext{} - networkCtx.Header = formatter.SubHeaderContext{ - "ID": networkIDHeader, - "Name": formatter.NameHeader, - "Driver": formatter.DriverHeader, - "Scope": formatter.ScopeHeader, - "IPv4": ipv4Header, - "IPv6": ipv6Header, - "Internal": internalHeader, - "Labels": formatter.LabelsHeader, - "CreatedAt": formatter.CreatedAtHeader, - } - return ctx.Write(&networkCtx, render) + }) } type networkContext struct { diff --git a/cli/command/network/formatter_test.go b/cli/command/network/formatter_test.go index ddeeb895fdb6..3f864065462e 100644 --- a/cli/command/network/formatter_test.go +++ b/cli/command/network/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package network @@ -13,7 +13,8 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -28,39 +29,39 @@ func TestNetworkContext(t *testing.T) { call func() string }{ {networkContext{ - n: network.Summary{ID: networkID}, + n: network.Summary{Network: network.Network{ID: networkID}}, trunc: false, }, networkID, ctx.ID}, {networkContext{ - n: network.Summary{ID: networkID}, + n: network.Summary{Network: network.Network{ID: networkID}}, trunc: true, }, formatter.TruncateID(networkID), ctx.ID}, {networkContext{ - n: network.Summary{Name: "network_name"}, + n: network.Summary{Network: network.Network{Name: "network_name"}}, }, "network_name", ctx.Name}, {networkContext{ - n: network.Summary{Driver: "driver_name"}, + n: network.Summary{Network: network.Network{Driver: "driver_name"}}, }, "driver_name", ctx.Driver}, {networkContext{ - n: network.Summary{EnableIPv4: true}, + n: network.Summary{Network: network.Network{EnableIPv4: true}}, }, "true", ctx.IPv4}, {networkContext{ - n: network.Summary{EnableIPv6: true}, + n: network.Summary{Network: network.Network{EnableIPv6: true}}, }, "true", ctx.IPv6}, {networkContext{ - n: network.Summary{EnableIPv6: false}, + n: network.Summary{Network: network.Network{EnableIPv6: false}}, }, "false", ctx.IPv6}, {networkContext{ - n: network.Summary{Internal: true}, + n: network.Summary{Network: network.Network{Internal: true}}, }, "true", ctx.Internal}, {networkContext{ - n: network.Summary{Internal: false}, + n: network.Summary{Network: network.Network{Internal: false}}, }, "false", ctx.Internal}, {networkContext{ n: network.Summary{}, }, "", ctx.Labels}, {networkContext{ - n: network.Summary{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + n: network.Summary{Network: network.Network{Labels: map[string]string{"label1": "value1", "label2": "value2"}}}, }, "label1=value1,label2=value2", ctx.Labels}, } @@ -91,27 +92,27 @@ func TestNetworkContextWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `NETWORK ID NAME DRIVER SCOPE networkID1 foobar_baz foo local networkID2 foobar_bar bar local `, }, { - formatter.Context{Format: NewFormat("table", true)}, + formatter.Context{Format: newFormat("table", true)}, `networkID1 networkID2 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", false)}, + formatter.Context{Format: newFormat("table {{.Name}}", false)}, `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar @@ -119,7 +120,7 @@ foobar_bar }, // Raw Format { - formatter.Context{Format: NewFormat("raw", false)}, + formatter.Context{Format: newFormat("raw", false)}, `network_id: networkID1 name: foobar_baz driver: foo @@ -133,21 +134,21 @@ scope: local `, }, { - formatter.Context{Format: NewFormat("raw", true)}, + formatter.Context{Format: newFormat("raw", true)}, `network_id: networkID1 network_id: networkID2 `, }, // Custom Format { - formatter.Context{Format: NewFormat("{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.Name}}", false)}, `foobar_baz foobar_bar `, }, // Custom Format with CreatedAt { - formatter.Context{Format: NewFormat("{{.Name}} {{.CreatedAt}}", false)}, + formatter.Context{Format: newFormat("{{.Name}} {{.CreatedAt}}", false)}, `foobar_baz 2016-01-01 00:00:00 +0000 UTC foobar_bar 2017-01-01 00:00:00 +0000 UTC `, @@ -158,15 +159,33 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC timestamp2, _ := time.Parse("2006-01-02", "2017-01-01") networks := []network.Summary{ - {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, - {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, + { + Network: network.Network{ + ID: "networkID1", + Name: "foobar_baz", + Driver: "foo", + Scope: "local", + Created: timestamp1, + }, + }, + { + Network: network.Network{ + ID: "networkID2", + Name: "foobar_bar", + Driver: "bar", + Scope: "local", + Created: timestamp2, + }, + }, } for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, networks) + err := formatWrite(tc.context, client.NetworkListResult{ + Items: networks, + }) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -178,8 +197,18 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC func TestNetworkContextWriteJSON(t *testing.T) { networks := []network.Summary{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, + { + Network: network.Network{ + ID: "networkID1", + Name: "foobar_baz", + }, + }, + { + Network: network.Network{ + ID: "networkID2", + Name: "foobar_bar", + }, + }, } expectedJSONs := []map[string]any{ {"Driver": "", "ID": "networkID1", "IPv4": "false", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, @@ -187,7 +216,9 @@ func TestNetworkContextWriteJSON(t *testing.T) { } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, networks) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, client.NetworkListResult{ + Items: networks, + }) if err != nil { t.Fatal(err) } @@ -202,11 +233,23 @@ func TestNetworkContextWriteJSON(t *testing.T) { func TestNetworkContextWriteJSONField(t *testing.T) { networks := []network.Summary{ - {ID: "networkID1", Name: "foobar_baz"}, - {ID: "networkID2", Name: "foobar_bar"}, + { + Network: network.Network{ + ID: "networkID1", + Name: "foobar_baz", + }, + }, + { + Network: network.Network{ + ID: "networkID2", + Name: "foobar_bar", + }, + }, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, networks) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, client.NetworkListResult{ + Items: networks, + }) if err != nil { t.Fatal(err) } diff --git a/cli/command/network/inspect.go b/cli/command/network/inspect.go index 749d52c717f5..accf7c484217 100644 --- a/cli/command/network/inspect.go +++ b/cli/command/network/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package network @@ -12,8 +12,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -34,7 +33,8 @@ func newInspectCommand(dockerCLI command.Cli) *cobra.Command { opts.names = args return runInspect(cmd.Context(), dockerCLI.Client(), dockerCLI.Out(), opts) }, - ValidArgsFunction: completion.NetworkNames(dockerCLI), + ValidArgsFunction: completion.NetworkNames(dockerCLI), + DisableFlagsInUseLine: true, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -45,6 +45,10 @@ func newInspectCommand(dockerCLI command.Cli) *cobra.Command { func runInspect(ctx context.Context, apiClient client.NetworkAPIClient, output io.Writer, opts inspectOptions) error { return inspect.Inspect(output, opts.names, opts.format, func(name string) (any, []byte, error) { - return apiClient.NetworkInspectWithRaw(ctx, name, network.InspectOptions{Verbose: opts.verbose}) + res, err := apiClient.NetworkInspect(ctx, name, client.NetworkInspectOptions{Verbose: opts.verbose}) + if err != nil { + return nil, nil, err + } + return res.Network, res.Raw, nil }) } diff --git a/cli/command/network/list.go b/cli/command/network/list.go index 0fb3934103a5..70d94ec78655 100644 --- a/cli/command/network/list.go +++ b/cli/command/network/list.go @@ -6,12 +6,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/network" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -22,7 +21,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { options := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -31,9 +30,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List networks", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), dockerCli, options) + return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -45,30 +45,30 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() - networkResources, err := client.NetworkList(ctx, network.ListOptions{Filters: options.filter.Value()}) +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() + res, err := apiClient.NetworkList(ctx, client.NetworkListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 { - if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().NetworksFormat + if len(dockerCLI.ConfigFile().NetworksFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().NetworksFormat } else { format = formatter.TableFormatKey } } - sort.Slice(networkResources, func(i, j int) bool { - return sortorder.NaturalLess(networkResources[i].Name, networkResources[j].Name) + sort.Slice(res.Items, func(i, j int) bool { + return sortorder.NaturalLess(res.Items[i].Name, res.Items[j].Name) }) networksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), Trunc: !options.noTrunc, } - return FormatWrite(networksCtx, networkResources) + return formatWrite(networksCtx, res) } diff --git a/cli/command/network/list_test.go b/cli/command/network/list_test.go index b3d253bf4d6d..f6b52c0604e6 100644 --- a/cli/command/network/list_test.go +++ b/cli/command/network/list_test.go @@ -8,9 +8,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/google/go-cmp/cmp" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -18,12 +17,12 @@ import ( func TestNetworkListErrors(t *testing.T) { testCases := []struct { - networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) + networkListFunc func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) expectedError string }{ { - networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { - return []network.Summary{}, errors.New("error creating network") + networkListFunc: func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { + return client.NetworkListResult{}, errors.New("error creating network") }, expectedError: "error creating network", }, @@ -44,7 +43,7 @@ func TestNetworkListErrors(t *testing.T) { func TestNetworkList(t *testing.T) { testCases := []struct { doc string - networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) + networkListFunc func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) flags map[string]string golden string }{ @@ -54,16 +53,22 @@ func TestNetworkList(t *testing.T) { "filter": "image.name=ubuntu", }, golden: "network-list.golden", - networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { - expectedOpts := network.ListOptions{ - Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")), + networkListFunc: func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { + expectedOpts := client.NetworkListOptions{ + Filters: make(client.Filters).Add("image.name", "ubuntu"), } - assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{}))) + assert.Check(t, is.DeepEqual(expectedOpts, options)) - return []network.Summary{*builders.NetworkResource(builders.NetworkResourceID("123454321"), - builders.NetworkResourceName("network_1"), - builders.NetworkResourceDriver("09.7.01"), - builders.NetworkResourceScope("global"))}, nil + return client.NetworkListResult{ + Items: []network.Summary{ + *builders.NetworkResource( + builders.NetworkResourceID("123454321"), + builders.NetworkResourceName("network_1"), + builders.NetworkResourceDriver("09.7.01"), + builders.NetworkResourceScope("global"), + ), + }, + }, nil }, }, { @@ -72,11 +77,13 @@ func TestNetworkList(t *testing.T) { "format": "{{ .Name }}", }, golden: "network-list-sort.golden", - networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { - return []network.Summary{ - *builders.NetworkResource(builders.NetworkResourceName("network-2-foo")), - *builders.NetworkResource(builders.NetworkResourceName("network-1-foo")), - *builders.NetworkResource(builders.NetworkResourceName("network-10-foo")), + networkListFunc: func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { + return client.NetworkListResult{ + Items: []network.Summary{ + *builders.NetworkResource(builders.NetworkResourceName("network-2-foo")), + *builders.NetworkResource(builders.NetworkResourceName("network-1-foo")), + *builders.NetworkResource(builders.NetworkResourceName("network-10-foo")), + }, }, nil }, }, diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index fce7adf3ed9a..2d7dc8571ce7 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -4,21 +4,31 @@ import ( "context" "errors" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeNetwork, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { force bool filter opts.FilterOpt } -// NewPruneCommand returns a new cobra prune command for networks -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// newPruneCommand returns a new cobra prune command for networks +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -26,16 +36,17 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove all unused networks", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - output, err := runPrune(cmd.Context(), dockerCli, options) + output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - _, _ = fmt.Fprintln(dockerCli.Out(), output) + _, _ = fmt.Fprintln(dockerCLI.Out(), output) } return nil }, - Annotations: map[string]string{"version": "1.25"}, + Annotations: map[string]string{"version": "1.25"}, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -48,7 +59,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { const warning = `WARNING! This will remove all custom networks not used by at least one container. Are you sure you want to continue?` -func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (output string, err error) { +func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (output string, _ error) { pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) if !options.force { @@ -61,28 +72,39 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters) + res, err := dockerCli.Client().NetworkPrune(ctx, client.NetworkPruneOptions{ + Filters: pruneFilters, + }) if err != nil { return "", err } - if len(report.NetworksDeleted) > 0 { - output = "Deleted Networks:\n" - for _, id := range report.NetworksDeleted { - output += id + "\n" + var out strings.Builder + if len(res.Report.NetworksDeleted) > 0 { + out.WriteString("Deleted Networks:\n") + for _, id := range res.Report.NetworksDeleted { + out.WriteString(id + "\n") } } - return output, nil + return out.String(), nil } type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Network Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - output, err := runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) +// pruneFn calls the Network Prune API 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. + confirmMsg := "all networks not used by at least one container" + return 0, confirmMsg, cancelledErr{errors.New("network prune has been cancelled")} + } + output, err := runPrune(ctx, dockerCLI, pruneOptions{ + force: true, + filter: options.Filter, + }) return 0, output, err } diff --git a/cli/command/network/prune_test.go b/cli/command/network/prune_test.go index 67a57e8c8af1..15b12b2966ce 100644 --- a/cli/command/network/prune_test.go +++ b/cli/command/network/prune_test.go @@ -7,8 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/client" ) func TestNetworkPrunePromptTermination(t *testing.T) { @@ -16,11 +15,11 @@ func TestNetworkPrunePromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) { - return network.PruneReport{}, errors.New("fakeClient networkPruneFunc should not be called") + networkPruneFunc: func(ctx context.Context, opts client.NetworkPruneOptions) (client.NetworkPruneResult, error) { + return client.NetworkPruneResult{}, errors.New("fakeClient networkPruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/network/remove.go b/cli/command/network/remove.go index 69578a48c0ab..314039a6b352 100644 --- a/cli/command/network/remove.go +++ b/cli/command/network/remove.go @@ -5,12 +5,12 @@ import ( "fmt" "strconv" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,7 +18,7 @@ type removeOptions struct { force bool } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -27,9 +27,10 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove one or more networks", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), dockerCli, args, &opts) + return runRemove(cmd.Context(), dockerCLI, args, &opts) }, - ValidArgsFunction: completion.NetworkNames(dockerCli), + ValidArgsFunction: completion.NetworkNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -48,8 +49,8 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, networks []string, op status := 0 for _, name := range networks { - nw, _, err := apiClient.NetworkInspectWithRaw(ctx, name, network.InspectOptions{}) - if err == nil && nw.Ingress { + res, err := apiClient.NetworkInspect(ctx, name, client.NetworkInspectOptions{}) + if err == nil && res.Network.Ingress { r, err := prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), ingressWarning) if err != nil { return err @@ -58,8 +59,9 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, networks []string, op continue } } - if err := apiClient.NetworkRemove(ctx, name); err != nil { - if opts.force && cerrdefs.IsNotFound(err) { + _, err = apiClient.NetworkRemove(ctx, name, client.NetworkRemoveOptions{}) + if err != nil { + if opts.force && errdefs.IsNotFound(err) { continue } _, _ = fmt.Fprintln(dockerCLI.Err(), err) diff --git a/cli/command/network/remove_test.go b/cli/command/network/remove_test.go index fe929d403e0b..c2d8cc7b587a 100644 --- a/cli/command/network/remove_test.go +++ b/cli/command/network/remove_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -110,12 +111,16 @@ func TestNetworkRemovePromptTermination(t *testing.T) { networkRemoveFunc: func(ctx context.Context, networkID string) error { return errors.New("fakeClient networkRemoveFunc should not be called") }, - networkInspectFunc: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, []byte, error) { - return network.Inspect{ - ID: "existing-network", - Name: "existing-network", - Ingress: true, - }, nil, nil + networkInspectFunc: func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { + return client.NetworkInspectResult{ + Network: network.Inspect{ + Network: network.Network{ + ID: "existing-network", + Name: "existing-network", + Ingress: true, + }, + }, + }, nil }, }) cmd := newRemoveCommand(cli) diff --git a/cli/command/node/client_test.go b/cli/command/node/client_test.go index 88c2390cae76..2e55394b6b79 100644 --- a/cli/command/node/client_test.go +++ b/cli/command/node/client_test.go @@ -3,75 +3,73 @@ package node import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - infoFunc func() (system.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeListFunc func() ([]swarm.Node, error) - nodeRemoveFunc func() error - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) - serviceInspectFunc func(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) + infoFunc func() (client.SystemInfoResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) + nodeListFunc func() (client.NodeListResult, error) + nodeRemoveFunc func() (client.NodeRemoveResult, error) + nodeUpdateFunc func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) + taskInspectFunc func(taskID string) (client.TaskInspectResult, error) + taskListFunc func(options client.TaskListOptions) (client.TaskListResult, error) + serviceInspectFunc func(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error) } -func (cli *fakeClient) NodeInspectWithRaw(context.Context, string) (swarm.Node, []byte, error) { +func (cli *fakeClient) NodeInspect(context.Context, string, client.NodeInspectOptions) (client.NodeInspectResult, error) { if cli.nodeInspectFunc != nil { return cli.nodeInspectFunc() } - return swarm.Node{}, []byte{}, nil + return client.NodeInspectResult{}, nil } -func (cli *fakeClient) NodeList(context.Context, swarm.NodeListOptions) ([]swarm.Node, error) { +func (cli *fakeClient) NodeList(context.Context, client.NodeListOptions) (client.NodeListResult, error) { if cli.nodeListFunc != nil { return cli.nodeListFunc() } - return []swarm.Node{}, nil + return client.NodeListResult{}, nil } -func (cli *fakeClient) NodeRemove(context.Context, string, swarm.NodeRemoveOptions) error { +func (cli *fakeClient) NodeRemove(context.Context, string, client.NodeRemoveOptions) (client.NodeRemoveResult, error) { if cli.nodeRemoveFunc != nil { return cli.nodeRemoveFunc() } - return nil + return client.NodeRemoveResult{}, nil } -func (cli *fakeClient) NodeUpdate(_ context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { +func (cli *fakeClient) NodeUpdate(_ context.Context, nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { if cli.nodeUpdateFunc != nil { - return cli.nodeUpdateFunc(nodeID, version, node) + return cli.nodeUpdateFunc(nodeID, options) } - return nil + return client.NodeUpdateResult{}, nil } -func (cli *fakeClient) Info(context.Context) (system.Info, error) { +func (cli *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { if cli.infoFunc != nil { return cli.infoFunc() } - return system.Info{}, nil + return client.SystemInfoResult{}, nil } -func (cli *fakeClient) TaskInspectWithRaw(_ context.Context, taskID string) (swarm.Task, []byte, error) { +func (cli *fakeClient) TaskInspect(_ context.Context, taskID string, _ client.TaskInspectOptions) (client.TaskInspectResult, error) { if cli.taskInspectFunc != nil { return cli.taskInspectFunc(taskID) } - return swarm.Task{}, []byte{}, nil + return client.TaskInspectResult{}, nil } -func (cli *fakeClient) TaskList(_ context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { +func (cli *fakeClient) TaskList(_ context.Context, options client.TaskListOptions) (client.TaskListResult, error) { if cli.taskListFunc != nil { return cli.taskListFunc(options) } - return []swarm.Task{}, nil + return client.TaskListResult{}, nil } -func (cli *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { +func (cli *fakeClient) ServiceInspect(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error) { if cli.serviceInspectFunc != nil { return cli.serviceInspectFunc(ctx, serviceID, opts) } - return swarm.Service{}, []byte{}, nil + return client.ServiceInspectResult{}, nil } diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go index 655f0170b756..c86cce7632d2 100644 --- a/cli/command/node/cmd.go +++ b/cli/command/node/cmd.go @@ -6,31 +6,36 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/docker/cli/internal/commands" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// NewNodeCommand returns a cobra command for `node` subcommands -func NewNodeCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newNodeCommand) +} + +// newNodeCommand returns a cobra command for `node` subcommands +func newNodeCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "node", Short: "Manage Swarm nodes", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newDemoteCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newPromoteCommand(dockerCli), - newRemoveCommand(dockerCli), - newPsCommand(dockerCli), - newUpdateCommand(dockerCli), + newDemoteCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newPromoteCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPsCommand(dockerCLI), + newUpdateCommand(dockerCLI), ) return cmd } @@ -40,21 +45,23 @@ func NewNodeCommand(dockerCli command.Cli) *cobra.Command { // the `/info` endpoint. func Reference(ctx context.Context, apiClient client.APIClient, ref string) (string, error) { if ref == "self" { - info, err := apiClient.Info(ctx) + res, err := apiClient.Info(ctx, client.InfoOptions{}) if err != nil { return "", err } - if info.Swarm.NodeID == "" { + if res.Info.Swarm.NodeID == "" { // If there's no node ID in /info, the node probably // isn't a manager. Call a swarm-specific endpoint to // get a more specific error message. - _, err = apiClient.NodeList(ctx, swarm.NodeListOptions{}) + // + // FIXME(thaJeztah): this should not require calling a Swarm endpoint, and we could just suffice with info / ping (which has swarm status). + _, err = apiClient.NodeList(ctx, client.NodeListOptions{}) if err != nil { return "", err } return "", errors.New("node ID not found in /info") } - return info.Swarm.NodeID, nil + return res.Info.Swarm.NodeID, nil } return ref, nil } diff --git a/cli/command/node/completion.go b/cli/command/node/completion.go index e9265d21c26c..f3ccf757279e 100644 --- a/cli/command/node/completion.go +++ b/cli/command/node/completion.go @@ -4,7 +4,7 @@ import ( "os" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,13 +17,13 @@ func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionF // https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43 showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_NODE_IDS") == "yes" return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().NodeList(cmd.Context(), swarm.NodeListOptions{}) + res, err := dockerCLI.Client().NodeList(cmd.Context(), client.NodeListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } - names := make([]string, 0, len(list)+1) - for _, node := range list { + names := make([]string, 0, len(res.Items)+1) + for _, node := range res.Items { if showIDs { names = append(names, node.Description.Hostname, node.ID) } else { diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go index 5ede173cd1cc..1941c6f24151 100644 --- a/cli/command/node/demote.go +++ b/cli/command/node/demote.go @@ -6,33 +6,33 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" "github.com/spf13/cobra" ) -func newDemoteCommand(dockerCli command.Cli) *cobra.Command { +func newDemoteCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "demote NODE [NODE...]", Short: "Demote one or more nodes from manager in the swarm", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDemote(cmd.Context(), dockerCli, args) + return runDemote(cmd.Context(), dockerCLI, args) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } } -func runDemote(ctx context.Context, dockerCli command.Cli, nodes []string) error { +func runDemote(ctx context.Context, dockerCLI command.Cli, nodes []string) error { demote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleWorker { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s is already a worker.\n", node.ID) return errNoRoleChange } node.Spec.Role = swarm.NodeRoleWorker return nil } - success := func(nodeID string) { - _, _ = fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID) - } - return updateNodes(ctx, dockerCli, nodes, demote, success) + return updateNodes(ctx, dockerCLI.Client(), nodes, demote, func(nodeID string) { + _, _ = fmt.Fprintf(dockerCLI.Out(), "Manager %s demoted in the swarm.\n", nodeID) + }) } diff --git a/cli/command/node/demote_test.go b/cli/command/node/demote_test.go index 00f3988c4f32..494de911e4a1 100644 --- a/cli/command/node/demote_test.go +++ b/cli/command/node/demote_test.go @@ -7,15 +7,16 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) func TestNodeDemoteErrors(t *testing.T) { testCases := []struct { args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + nodeInspectFunc func() (client.NodeInspectResult, error) + nodeUpdateFunc func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) expectedError string }{ { @@ -23,15 +24,15 @@ func TestNodeDemoteErrors(t *testing.T) { }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.New("error updating the node") + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + return client.NodeUpdateResult{}, errors.New("error updating the node") }, expectedError: "error updating the node", }, @@ -52,14 +53,16 @@ func TestNodeDemoteErrors(t *testing.T) { func TestNodeDemoteNoChange(t *testing.T) { cmd := newDemoteCommand( test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.New("expected role worker, got " + string(node.Role)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Role != swarm.NodeRoleWorker { + return client.NodeUpdateResult{}, errors.New("expected role worker, got " + string(options.Spec.Role)) } - return nil + return client.NodeUpdateResult{}, nil }, })) cmd.SetArgs([]string{"nodeID"}) @@ -69,14 +72,16 @@ func TestNodeDemoteNoChange(t *testing.T) { func TestNodeDemoteMultipleNode(t *testing.T) { cmd := newDemoteCommand( test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleWorker { - return errors.New("expected role worker, got " + string(node.Role)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Role != swarm.NodeRoleWorker { + return client.NodeUpdateResult{}, errors.New("expected role worker, got " + string(options.Spec.Role)) } - return nil + return client.NodeUpdateResult{}, nil }, })) cmd.SetArgs([]string{"nodeID1", "nodeID2"}) diff --git a/cli/command/node/formatter.go b/cli/command/node/formatter.go index c49ad30abe5b..68f5d2a9e928 100644 --- a/cli/command/node/formatter.go +++ b/cli/command/node/formatter.go @@ -8,9 +8,9 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" "github.com/docker/go-units" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -79,8 +79,8 @@ TLS Info: tlsStatusHeader = "TLS STATUS" ) -// NewFormat returns a Format for rendering using a node Context -func NewFormat(source string, quiet bool) formatter.Format { +// newFormat returns a Format for rendering using a nodeContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return nodeInspectPrettyTemplate @@ -98,35 +98,39 @@ func NewFormat(source string, quiet bool) formatter.Format { return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, nodes []swarm.Node, info system.Info) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, node := range nodes { - nodeCtx := &nodeContext{n: node, info: info} - if err := format(nodeCtx); err != nil { +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, nodes client.NodeListResult, info client.SystemInfoResult) error { + nodeCtx := &nodeContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": nodeIDHeader, + "Self": selfHeader, + "Hostname": hostnameHeader, + "Status": formatter.StatusHeader, + "Availability": availabilityHeader, + "ManagerStatus": managerStatusHeader, + "EngineVersion": engineVersionHeader, + "TLSStatus": tlsStatusHeader, + }, + }, + } + return fmtCtx.Write(nodeCtx, func(format func(subContext formatter.SubContext) error) error { + for _, node := range nodes.Items { + if err := format(&nodeContext{ + n: node, + info: info.Info.Swarm, + }); err != nil { return err } } return nil - } - nodeCtx := nodeContext{} - nodeCtx.Header = formatter.SubHeaderContext{ - "ID": nodeIDHeader, - "Self": selfHeader, - "Hostname": hostnameHeader, - "Status": formatter.StatusHeader, - "Availability": availabilityHeader, - "ManagerStatus": managerStatusHeader, - "EngineVersion": engineVersionHeader, - "TLSStatus": tlsStatusHeader, - } - return ctx.Write(&nodeCtx, render) + }) } type nodeContext struct { formatter.HeaderContext n swarm.Node - info system.Info + info swarm.Info } func (c *nodeContext) MarshalJSON() ([]byte, error) { @@ -138,7 +142,7 @@ func (c *nodeContext) ID() string { } func (c *nodeContext) Self() bool { - return c.n.ID == c.info.Swarm.NodeID + return c.n.ID == c.info.NodeID } func (c *nodeContext) Hostname() string { @@ -166,10 +170,10 @@ func (c *nodeContext) ManagerStatus() string { } func (c *nodeContext) TLSStatus() string { - if c.info.Swarm.Cluster == nil || reflect.DeepEqual(c.info.Swarm.Cluster.TLSInfo, swarm.TLSInfo{}) || reflect.DeepEqual(c.n.Description.TLSInfo, swarm.TLSInfo{}) { + if c.info.Cluster == nil || reflect.DeepEqual(c.info.Cluster.TLSInfo, swarm.TLSInfo{}) || reflect.DeepEqual(c.n.Description.TLSInfo, swarm.TLSInfo{}) { return "Unknown" } - if reflect.DeepEqual(c.n.Description.TLSInfo, c.info.Swarm.Cluster.TLSInfo) { + if reflect.DeepEqual(c.n.Description.TLSInfo, c.info.Cluster.TLSInfo) { return "Ready" } return "Needs Rotation" @@ -179,12 +183,12 @@ func (c *nodeContext) EngineVersion() string { return c.n.Description.Engine.EngineVersion } -// InspectFormatWrite renders the context for a list of nodes -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != nodeInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// inspectFormatWrite renders the context for a list of nodes. +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != nodeInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } - render := func(format func(subContext formatter.SubContext) error) error { + return fmtCtx.Write(&nodeInspectContext{}, func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { nodeI, _, err := getRef(ref) if err != nil { @@ -199,8 +203,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } } return nil - } - return ctx.Write(&nodeInspectContext{}, render) + }) } type nodeInspectContext struct { diff --git a/cli/command/node/formatter_test.go b/cli/command/node/formatter_test.go index 8983f337a6cb..fa44d44111dc 100644 --- a/cli/command/node/formatter_test.go +++ b/cli/command/node/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package node @@ -12,8 +12,9 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -74,7 +75,7 @@ func TestNodeContextWrite(t *testing.T) { }, // Table format { - context: formatter.Context{Format: NewFormat("table", false)}, + context: formatter.Context{Format: newFormat("table", false)}, expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce nodeID2 foobar_bar Bar Active Reachable 1.2.3 @@ -82,7 +83,7 @@ nodeID3 foobar_boo Boo Active ` + "\n", // clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table", true)}, + context: formatter.Context{Format: newFormat("table", true)}, expected: `nodeID1 nodeID2 nodeID3 @@ -90,7 +91,7 @@ nodeID3 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)}, + context: formatter.Context{Format: newFormat("table {{.Hostname}}", false)}, expected: `HOSTNAME foobar_baz foobar_bar @@ -99,7 +100,7 @@ foobar_boo clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)}, + context: formatter.Context{Format: newFormat("table {{.Hostname}}", true)}, expected: `HOSTNAME foobar_baz foobar_bar @@ -108,7 +109,7 @@ foobar_boo clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, expected: `ID HOSTNAME TLS STATUS nodeID1 foobar_baz Needs Rotation nodeID2 foobar_bar Ready @@ -117,7 +118,7 @@ nodeID3 foobar_boo Unknown clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { // no cluster TLS status info, TLS status for all nodes is unknown - context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, expected: `ID HOSTNAME TLS STATUS nodeID1 foobar_baz Unknown nodeID2 foobar_bar Unknown @@ -127,7 +128,7 @@ nodeID3 foobar_boo Unknown }, // Raw Format { - context: formatter.Context{Format: NewFormat("raw", false)}, + context: formatter.Context{Format: newFormat("raw", false)}, expected: `node_id: nodeID1 hostname: foobar_baz status: Foo @@ -148,7 +149,7 @@ manager_status: ` + "\n\n", // to preserve whitespace clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("raw", true)}, + context: formatter.Context{Format: newFormat("raw", true)}, expected: `node_id: nodeID1 node_id: nodeID2 node_id: nodeID3 @@ -157,7 +158,7 @@ node_id: nodeID3 }, // Custom Format { - context: formatter.Context{Format: NewFormat("{{.Hostname}} {{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("{{.Hostname}} {{.TLSStatus}}", false)}, expected: `foobar_baz Needs Rotation foobar_bar Ready foobar_boo Unknown @@ -166,38 +167,40 @@ foobar_boo Unknown }, } - nodes := []swarm.Node{ - { - ID: "nodeID1", - Description: swarm.NodeDescription{ - Hostname: "foobar_baz", - TLSInfo: swarm.TLSInfo{TrustRoot: "no"}, - Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}, + nodes := client.NodeListResult{ + Items: []swarm.Node{ + { + ID: "nodeID1", + Description: swarm.NodeDescription{ + Hostname: "foobar_baz", + TLSInfo: swarm.TLSInfo{TrustRoot: "no"}, + Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}, + }, + Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, + Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, + ManagerStatus: &swarm.ManagerStatus{Leader: true}, }, - Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, - Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, - ManagerStatus: &swarm.ManagerStatus{Leader: true}, - }, - { - ID: "nodeID2", - Description: swarm.NodeDescription{ - Hostname: "foobar_bar", - TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, - Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}, + { + ID: "nodeID2", + Description: swarm.NodeDescription{ + Hostname: "foobar_bar", + TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, + Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}, + }, + Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, + Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, + ManagerStatus: &swarm.ManagerStatus{ + Leader: false, + Reachability: swarm.Reachability("Reachable"), + }, }, - Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, - Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, - ManagerStatus: &swarm.ManagerStatus{ - Leader: false, - Reachability: swarm.Reachability("Reachable"), + { + ID: "nodeID3", + Description: swarm.NodeDescription{Hostname: "foobar_boo"}, + Status: swarm.NodeStatus{State: swarm.NodeState("boo")}, + Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, }, }, - { - ID: "nodeID3", - Description: swarm.NodeDescription{Hostname: "foobar_boo"}, - Status: swarm.NodeStatus{State: swarm.NodeState("boo")}, - Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, - }, } for _, tc := range cases { @@ -205,7 +208,11 @@ foobar_boo Unknown var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, nodes, system.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}}) + err := formatWrite(tc.context, nodes, client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{Cluster: &tc.clusterInfo}, + }, + }) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -218,7 +225,7 @@ foobar_boo Unknown func TestNodeContextWriteJSON(t *testing.T) { cases := []struct { expected []map[string]any - info system.Info + info client.SystemInfoResult }{ { expected: []map[string]any{ @@ -226,7 +233,6 @@ func TestNodeContextWriteJSON(t *testing.T) { {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": ""}, {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"}, }, - info: system.Info{}, }, { expected: []map[string]any{ @@ -234,11 +240,13 @@ func TestNodeContextWriteJSON(t *testing.T) { {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Needs Rotation", "EngineVersion": ""}, {"Availability": "", "Hostname": "foobar_boo", "ID": "nodeID3", "ManagerStatus": "", "Status": "", "Self": false, "TLSStatus": "Unknown", "EngineVersion": "18.03.0-ce"}, }, - info: system.Info{ - Swarm: swarm.Info{ - Cluster: &swarm.ClusterInfo{ - TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, - RootRotationInProgress: true, + info: client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{ + Cluster: &swarm.ClusterInfo{ + TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, + RootRotationInProgress: true, + }, }, }, }, @@ -246,13 +254,15 @@ func TestNodeContextWriteJSON(t *testing.T) { } for _, testcase := range cases { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}}, - {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, + nodes := client.NodeListResult{ + Items: []swarm.Node{ + {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz", TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}, Engine: swarm.EngineDescription{EngineVersion: "1.2.3"}}}, + {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar", TLSInfo: swarm.TLSInfo{TrustRoot: "no"}}}, + {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, + }, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) if err != nil { t.Fatal(err) } @@ -267,12 +277,14 @@ func TestNodeContextWriteJSON(t *testing.T) { } func TestNodeContextWriteJSONField(t *testing.T) { - nodes := []swarm.Node{ - {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, - {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, + nodes := client.NodeListResult{ + Items: []swarm.Node{ + {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, + {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, + }, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, system.Info{}) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, client.SystemInfoResult{}) if err != nil { t.Fatal(err) } @@ -281,7 +293,7 @@ func TestNodeContextWriteJSONField(t *testing.T) { var s string err := json.Unmarshal([]byte(line), &s) assert.NilError(t, err, msg) - assert.Check(t, is.Equal(nodes[i].ID, s), msg) + assert.Check(t, is.Equal(nodes.Items[i].ID, s), msg) } } @@ -317,10 +329,10 @@ func TestNodeInspectWriteContext(t *testing.T) { } out := bytes.NewBufferString("") context := formatter.Context{ - Format: NewFormat("pretty", false), + Format: newFormat("pretty", false), Output: out, } - err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) { + err := inspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) { return node, nil, nil }) if err != nil { diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go index f293861d478b..cd65f7a3e4f9 100644 --- a/cli/command/node/inspect.go +++ b/cli/command/node/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package node @@ -12,6 +12,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -21,7 +22,7 @@ type inspectOptions struct { pretty bool } -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -30,9 +31,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.nodeIds = args - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -41,35 +43,34 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" } getRef := func(ref string) (any, []byte, error) { - nodeRef, err := Reference(ctx, client, ref) + nodeRef, err := Reference(ctx, apiClient, ref) if err != nil { return nil, nil, err } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - return node, nil, err + res, err := apiClient.NodeInspect(ctx, nodeRef, client.NodeInspectOptions{}) + return res.Node, res.Raw, err } - f := opts.format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } nodeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f, false), + Output: dockerCLI.Out(), + Format: newFormat(opts.format, false), } - if err := InspectFormatWrite(nodeCtx, opts.nodeIds, getRef); err != nil { + if err := inspectFormatWrite(nodeCtx, opts.nodeIds, getRef); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/node/inspect_test.go b/cli/command/node/inspect_test.go index a8256c6fc49e..1f07b6946f0a 100644 --- a/cli/command/node/inspect_test.go +++ b/cli/command/node/inspect_test.go @@ -8,8 +8,9 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -18,8 +19,8 @@ func TestNodeInspectErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - infoFunc func() (system.Info, error) + nodeInspectFunc func() (client.NodeInspectResult, error) + infoFunc func() (client.SystemInfoResult, error) expectedError string }{ { @@ -27,28 +28,32 @@ func TestNodeInspectErrors(t *testing.T) { }, { args: []string{"self"}, - infoFunc: func() (system.Info, error) { - return system.Info{}, errors.New("error asking for node info") + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("error asking for node info") }, expectedError: "error asking for node info", }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, - infoFunc: func() (system.Info, error) { - return system.Info{}, errors.New("error asking for node info") + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("error asking for node info") }, expectedError: "error inspecting the node", }, { args: []string{"self"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, - infoFunc: func() (system.Info, error) { - return system.Info{Swarm: swarm.Info{NodeID: "abc"}}, nil + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{NodeID: "abc"}, + }, + }, nil }, expectedError: "error inspecting the node", }, @@ -57,8 +62,8 @@ func TestNodeInspectErrors(t *testing.T) { flags: map[string]string{ "pretty": "true", }, - infoFunc: func() (system.Info, error) { - return system.Info{}, errors.New("error asking for node info") + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("error asking for node info") }, expectedError: "error asking for node info", }, @@ -82,26 +87,30 @@ func TestNodeInspectErrors(t *testing.T) { func TestNodeInspectPretty(t *testing.T) { testCases := []struct { name string - nodeInspectFunc func() (swarm.Node, []byte, error) + nodeInspectFunc func() (client.NodeInspectResult, error) }{ { name: "simple", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeLabels(map[string]string{ - "lbl1": "value1", - })), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeLabels(map[string]string{"lbl1": "value1"})), + }, nil }, }, { name: "manager", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, }, { name: "manager-leader", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager(builders.Leader())), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager(builders.Leader())), + }, nil }, }, } diff --git a/cli/command/node/list.go b/cli/command/node/list.go index 3d319deeb946..526ec655878f 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -6,15 +6,12 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type listOptions struct { @@ -23,7 +20,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { options := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -32,38 +29,33 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List nodes in the swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), dockerCli, options) + return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs") flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() - nodes, err := client.NodeList( - ctx, - swarm.NodeListOptions{Filters: options.filter.Value()}) + res, err := apiClient.NodeList(ctx, client.NodeListOptions{ + Filters: options.filter.Value(), + }) if err != nil { return err } - info := system.Info{} - if len(nodes) > 0 && !options.quiet { + var info client.SystemInfoResult + if len(res.Items) > 0 && !options.quiet { // only non-empty nodes and not quiet, should we call /info api - info, err = client.Info(ctx) + info, err = apiClient.Info(ctx, client.InfoOptions{}) if err != nil { return err } @@ -72,17 +64,17 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er format := options.format if len(format) == 0 { format = formatter.TableFormatKey - if len(dockerCli.ConfigFile().NodesFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().NodesFormat + if len(dockerCLI.ConfigFile().NodesFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().NodesFormat } } nodesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), } - sort.Slice(nodes, func(i, j int) bool { - return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname) + sort.Slice(res.Items, func(i, j int) bool { + return sortorder.NaturalLess(res.Items[i].Description.Hostname, res.Items[j].Description.Hostname) }) - return FormatWrite(nodesCtx, nodes, info) + return formatWrite(nodesCtx, res, info) } diff --git a/cli/command/node/list_test.go b/cli/command/node/list_test.go index adbddc6c5416..b2cc4611d118 100644 --- a/cli/command/node/list_test.go +++ b/cli/command/node/list_test.go @@ -8,8 +8,9 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -17,26 +18,26 @@ import ( func TestNodeListErrorOnAPIFailure(t *testing.T) { testCases := []struct { - nodeListFunc func() ([]swarm.Node, error) - infoFunc func() (system.Info, error) + nodeListFunc func() (client.NodeListResult, error) + infoFunc func() (client.SystemInfoResult, error) expectedError string }{ { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{}, errors.New("error listing nodes") + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{}, errors.New("error listing nodes") }, expectedError: "error listing nodes", }, { - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - { - ID: "nodeID", + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + {ID: "nodeID"}, }, }, nil }, - infoFunc: func() (system.Info, error) { - return system.Info{}, errors.New("error asking for node info") + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("error asking for node info") }, expectedError: "error asking for node info", }, @@ -55,17 +56,19 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { func TestNodeList(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("node-2-foo"), builders.Manager(builders.Leader()), builders.EngineVersion(".")), - *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("node-10-foo"), builders.Manager(), builders.EngineVersion("18.03.0-ce")), - *builders.Node(builders.NodeID("nodeID3"), builders.Hostname("node-1-foo")), + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("node-2-foo"), builders.Manager(builders.Leader()), builders.EngineVersion(".")), + *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("node-10-foo"), builders.Manager(), builders.EngineVersion("18.03.0-ce")), + *builders.Node(builders.NodeID("nodeID3"), builders.Hostname("node-1-foo")), + }, }, nil }, - infoFunc: func() (system.Info, error) { - return system.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{NodeID: "nodeID1"}, }, }, nil }, @@ -78,9 +81,11 @@ func TestNodeList(t *testing.T) { func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *builders.Node(builders.NodeID("nodeID1")), + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + *builders.Node(builders.NodeID("nodeID1")), + }, }, nil }, }) @@ -92,17 +97,19 @@ func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { func TestNodeListDefaultFormatFromConfig(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("nodeHostname1"), builders.Manager(builders.Leader())), - *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("nodeHostname2"), builders.Manager()), - *builders.Node(builders.NodeID("nodeID3"), builders.Hostname("nodeHostname3")), + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("nodeHostname1"), builders.Manager(builders.Leader())), + *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("nodeHostname2"), builders.Manager()), + *builders.Node(builders.NodeID("nodeID3"), builders.Hostname("nodeHostname3")), + }, }, nil }, - infoFunc: func() (system.Info, error) { - return system.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{NodeID: "nodeID1"}, }, }, nil }, @@ -117,16 +124,18 @@ func TestNodeListDefaultFormatFromConfig(t *testing.T) { func TestNodeListFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("nodeHostname1"), builders.Manager(builders.Leader())), - *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("nodeHostname2"), builders.Manager()), + nodeListFunc: func() (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + *builders.Node(builders.NodeID("nodeID1"), builders.Hostname("nodeHostname1"), builders.Manager(builders.Leader())), + *builders.Node(builders.NodeID("nodeID2"), builders.Hostname("nodeHostname2"), builders.Manager()), + }, }, nil }, - infoFunc: func() (system.Info, error) { - return system.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{NodeID: "nodeID1"}, }, }, nil }, diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go index 983229526447..fb4da49fef70 100644 --- a/cli/command/node/promote.go +++ b/cli/command/node/promote.go @@ -6,33 +6,33 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" "github.com/spf13/cobra" ) -func newPromoteCommand(dockerCli command.Cli) *cobra.Command { +func newPromoteCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "promote NODE [NODE...]", Short: "Promote one or more nodes to manager in the swarm", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runPromote(cmd.Context(), dockerCli, args) + return runPromote(cmd.Context(), dockerCLI, args) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } } -func runPromote(ctx context.Context, dockerCli command.Cli, nodes []string) error { +func runPromote(ctx context.Context, dockerCLI command.Cli, nodes []string) error { promote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleManager { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s is already a manager.\n", node.ID) return errNoRoleChange } node.Spec.Role = swarm.NodeRoleManager return nil } - success := func(nodeID string) { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) - } - return updateNodes(ctx, dockerCli, nodes, promote, success) + return updateNodes(ctx, dockerCLI.Client(), nodes, promote, func(nodeID string) { + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) + }) } diff --git a/cli/command/node/promote_test.go b/cli/command/node/promote_test.go index 0fdcb9e1f97c..d91169acc447 100644 --- a/cli/command/node/promote_test.go +++ b/cli/command/node/promote_test.go @@ -7,15 +7,16 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) func TestNodePromoteErrors(t *testing.T) { testCases := []struct { args []string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + nodeInspectFunc func() (client.NodeInspectResult, error) + nodeUpdateFunc func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) expectedError string }{ { @@ -23,15 +24,15 @@ func TestNodePromoteErrors(t *testing.T) { }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.New("error updating the node") + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + return client.NodeUpdateResult{}, errors.New("error updating the node") }, expectedError: "error updating the node", }, @@ -52,14 +53,16 @@ func TestNodePromoteErrors(t *testing.T) { func TestNodePromoteNoChange(t *testing.T) { cmd := newPromoteCommand( test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.New("expected role manager, got" + string(node.Role)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Role != swarm.NodeRoleManager { + return client.NodeUpdateResult{}, errors.New("expected role manager, got" + string(options.Spec.Role)) } - return nil + return client.NodeUpdateResult{}, nil }, })) cmd.SetArgs([]string{"nodeID"}) @@ -69,14 +72,16 @@ func TestNodePromoteNoChange(t *testing.T) { func TestNodePromoteMultipleNode(t *testing.T) { cmd := newPromoteCommand( test.NewFakeCli(&fakeClient{ - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.New("expected role manager, got" + string(node.Role)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Role != swarm.NodeRoleManager { + return client.NodeUpdateResult{}, errors.New("expected role manager, got" + string(options.Spec.Role)) } - return nil + return client.NodeUpdateResult{}, nil }, })) cmd.SetArgs([]string{"nodeID1", "nodeID2"}) diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go index c34ea0e676e3..4ad64530b187 100644 --- a/cli/command/node/ps.go +++ b/cli/command/node/ps.go @@ -2,18 +2,15 @@ package node import ( "context" - "strings" + "errors" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/cli/command/task" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type psOptions struct { @@ -25,7 +22,7 @@ type psOptions struct { filter opts.FilterOpt } -func newPsCommand(dockerCli command.Cli) *cobra.Command { +func newPsCommand(dockerCLI command.Cli) *cobra.Command { options := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -39,9 +36,10 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { options.nodeIDs = args } - return runPs(cmd.Context(), dockerCli, options) + return runPs(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output") @@ -50,62 +48,52 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } -func runPs(ctx context.Context, dockerCli command.Cli, options psOptions) error { - client := dockerCli.Client() +func runPs(ctx context.Context, dockerCLI command.Cli, options psOptions) error { + apiClient := dockerCLI.Client() var ( - errs []string - tasks []swarm.Task + errs []error + tasks = client.TaskListResult{} ) for _, nodeID := range options.nodeIDs { - nodeRef, err := Reference(ctx, client, nodeID) + nodeRef, err := Reference(ctx, apiClient, nodeID) if err != nil { - errs = append(errs, err.Error()) + errs = append(errs, err) continue } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + res, err := apiClient.NodeInspect(ctx, nodeRef, client.NodeInspectOptions{}) if err != nil { - errs = append(errs, err.Error()) + errs = append(errs, err) continue } filter := options.filter.Value() - filter.Add("node", node.ID) + filter.Add("node", res.Node.ID) - nodeTasks, err := client.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) + nodeTasks, err := apiClient.TaskList(ctx, client.TaskListOptions{Filters: filter}) if err != nil { - errs = append(errs, err.Error()) + errs = append(errs, err) continue } - tasks = append(tasks, nodeTasks...) + tasks.Items = append(tasks.Items, nodeTasks.Items...) } format := options.format if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet) + format = task.DefaultFormat(dockerCLI.ConfigFile(), options.quiet) } - if len(errs) == 0 || len(tasks) != 0 { - if err := task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format); err != nil { - errs = append(errs, err.Error()) + if len(errs) == 0 || len(tasks.Items) != 0 { + if err := task.Print(ctx, dockerCLI, tasks, idresolver.New(apiClient, options.noResolve), !options.noTrunc, options.quiet, format); err != nil { + errs = append(errs, err) } } - if len(errs) > 0 { - return errors.Errorf("%s", strings.Join(errs, "\n")) - } - - return nil + return errors.Join(errs...) } diff --git a/cli/command/node/ps_test.go b/cli/command/node/ps_test.go index 66e4ce855b69..16b93a21c816 100644 --- a/cli/command/node/ps_test.go +++ b/cli/command/node/ps_test.go @@ -10,8 +10,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -20,29 +20,28 @@ func TestNodePsErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - infoFunc func() (system.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + infoFunc func() (client.SystemInfoResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) + taskListFunc func(options client.TaskListOptions) (client.TaskListResult, error) expectedError string }{ { - infoFunc: func() (system.Info, error) { - return system.Info{}, errors.New("error asking for node info") + infoFunc: func() (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("error asking for node info") }, expectedError: "error asking for node info", }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{}, errors.New("error returning the task list") + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{}, errors.New("error returning the task list") }, expectedError: "error returning the task list", }, @@ -51,7 +50,6 @@ func TestNodePsErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ infoFunc: tc.infoFunc, nodeInspectFunc: tc.nodeInspectFunc, - taskInspectFunc: tc.taskInspectFunc, taskListFunc: tc.taskListFunc, }) cmd := newPsCommand(cli) @@ -70,72 +68,82 @@ func TestNodePs(t *testing.T) { name string args []string flags map[string]string - infoFunc func() (system.Info, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) - taskInspectFunc func(taskID string) (swarm.Task, []byte, error) - serviceInspectFunc func(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) + nodeInspectFunc func() (client.NodeInspectResult, error) + taskListFunc func(options client.TaskListOptions) (client.TaskListResult, error) + taskInspectFunc func(taskID string) (client.TaskInspectResult, error) + serviceInspectFunc func(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error) }{ { name: "simple", args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *builders.Task(builders.WithStatus(builders.Timestamp(time.Now().Add(-2*time.Hour)), builders.PortStatus([]swarm.PortConfig{ - { - TargetPort: 80, - PublishedPort: 80, - Protocol: "tcp", - }, - }))), + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.WithStatus(builders.Timestamp(time.Now().Add(-2*time.Hour)), builders.PortStatus([]swarm.PortConfig{ + { + TargetPort: 80, + PublishedPort: 80, + Protocol: "tcp", + }, + }))), + }, }, nil }, - serviceInspectFunc: func(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return swarm.Service{ - ID: serviceID, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: serviceID, + serviceInspectFunc: func(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: swarm.Service{ + ID: serviceID, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: serviceID, + }, }, }, - }, []byte{}, nil + }, nil }, }, { name: "with-errors", args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{ - *builders.Task(builders.TaskID("taskID1"), builders.TaskServiceID("failure"), - builders.WithStatus(builders.Timestamp(time.Now().Add(-2*time.Hour)), builders.StatusErr("a task error"))), - *builders.Task(builders.TaskID("taskID2"), builders.TaskServiceID("failure"), - builders.WithStatus(builders.Timestamp(time.Now().Add(-3*time.Hour)), builders.StatusErr("a task error"))), - *builders.Task(builders.TaskID("taskID3"), builders.TaskServiceID("failure"), - builders.WithStatus(builders.Timestamp(time.Now().Add(-4*time.Hour)), builders.StatusErr("a task error"))), + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskID("taskID1"), builders.TaskServiceID("failure"), + builders.WithStatus(builders.Timestamp(time.Now().Add(-2*time.Hour)), builders.StatusErr("a task error"))), + *builders.Task(builders.TaskID("taskID2"), builders.TaskServiceID("failure"), + builders.WithStatus(builders.Timestamp(time.Now().Add(-3*time.Hour)), builders.StatusErr("a task error"))), + *builders.Task(builders.TaskID("taskID3"), builders.TaskServiceID("failure"), + builders.WithStatus(builders.Timestamp(time.Now().Add(-4*time.Hour)), builders.StatusErr("a task error"))), + }, }, nil }, - serviceInspectFunc: func(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return swarm.Service{ - ID: serviceID, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: serviceID, + serviceInspectFunc: func(ctx context.Context, serviceID string, opts client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: swarm.Service{ + ID: serviceID, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: serviceID, + }, }, }, - }, []byte{}, nil + }, nil }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - infoFunc: tc.infoFunc, nodeInspectFunc: tc.nodeInspectFunc, taskInspectFunc: tc.taskInspectFunc, taskListFunc: tc.taskListFunc, diff --git a/cli/command/node/remove.go b/cli/command/node/remove.go index c7768f51ea96..3d3d41354b00 100644 --- a/cli/command/node/remove.go +++ b/cli/command/node/remove.go @@ -7,7 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -15,7 +15,7 @@ type removeOptions struct { force bool } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { opts := removeOptions{} cmd := &cobra.Command{ @@ -24,9 +24,10 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove one or more nodes from the swarm", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), dockerCli, args, opts) + return runRemove(cmd.Context(), dockerCLI, args, opts) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm") @@ -38,7 +39,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, nodeIDs []string, opt var errs []error for _, id := range nodeIDs { - if err := apiClient.NodeRemove(ctx, id, swarm.NodeRemoveOptions{Force: opts.force}); err != nil { + if _, err := apiClient.NodeRemove(ctx, id, client.NodeRemoveOptions{Force: opts.force}); err != nil { errs = append(errs, err) continue } diff --git a/cli/command/node/remove_test.go b/cli/command/node/remove_test.go index 9e672153c4aa..3a356953a05d 100644 --- a/cli/command/node/remove_test.go +++ b/cli/command/node/remove_test.go @@ -6,13 +6,14 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) func TestNodeRemoveErrors(t *testing.T) { testCases := []struct { args []string - nodeRemoveFunc func() error + nodeRemoveFunc func() (client.NodeRemoveResult, error) expectedError string }{ { @@ -20,8 +21,8 @@ func TestNodeRemoveErrors(t *testing.T) { }, { args: []string{"nodeID"}, - nodeRemoveFunc: func() error { - return errors.New("error removing the node") + nodeRemoveFunc: func() (client.NodeRemoveResult, error) { + return client.NodeRemoveResult{}, errors.New("error removing the node") }, expectedError: "error removing the node", }, diff --git a/cli/command/node/update.go b/cli/command/node/update.go index 0c0d3bdc287a..1bfd4949e282 100644 --- a/cli/command/node/update.go +++ b/cli/command/node/update.go @@ -2,21 +2,22 @@ package node import ( "context" + "errors" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) var errNoRoleChange = errors.New("role was already set to the requested value") -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { options := newNodeOptions() cmd := &cobra.Command{ @@ -24,9 +25,10 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { Short: "Update a node", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), args[0]) + return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), args[0]) }, - ValidArgsFunction: completeNodeNames(dockerCli), + ValidArgsFunction: completeNodeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -38,39 +40,34 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { _ = cmd.RegisterFlagCompletionFunc(flagRole, completion.FromList("worker", "manager")) _ = cmd.RegisterFlagCompletionFunc(flagAvailability, completion.FromList("active", "pause", "drain")) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) + return cmd } -func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { - success := func(_ string) { - fmt.Fprintln(dockerCli.Out(), nodeID) - } - return updateNodes(ctx, dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) +func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, nodeID string) error { + return updateNodes(ctx, dockerCLI.Client(), []string{nodeID}, mergeNodeUpdate(flags), func(_ string) { + _, _ = fmt.Fprintln(dockerCLI.Out(), nodeID) + }) } -func updateNodes(ctx context.Context, dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { - client := dockerCli.Client() - +func updateNodes(ctx context.Context, apiClient client.NodeAPIClient, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { for _, nodeID := range nodes { - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) + res, err := apiClient.NodeInspect(ctx, nodeID, client.NodeInspectOptions{}) if err != nil { return err } - err = mergeNode(&node) + err = mergeNode(&res.Node) if err != nil { - if err == errNoRoleChange { + if errors.Is(err, errNoRoleChange) { continue } return err } - err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec) + _, err = apiClient.NodeUpdate(ctx, res.Node.ID, client.NodeUpdateOptions{ + Version: res.Node.Version, + Spec: res.Node.Spec, + }) if err != nil { return err } @@ -111,7 +108,7 @@ func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error { for _, k := range keys { // if a key doesn't exist, fail the command explicitly if _, exists := spec.Annotations.Labels[k]; !exists { - return errors.Errorf("key %s doesn't exist in node's labels", k) + return fmt.Errorf("key %s doesn't exist in node's labels", k) } delete(spec.Annotations.Labels, k) } diff --git a/cli/command/node/update_test.go b/cli/command/node/update_test.go index 233d54b574e1..2ce4b65934b3 100644 --- a/cli/command/node/update_test.go +++ b/cli/command/node/update_test.go @@ -8,7 +8,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -16,8 +17,8 @@ func TestNodeUpdateErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + nodeInspectFunc func() (client.NodeInspectResult, error) + nodeUpdateFunc func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) expectedError string }{ { @@ -29,29 +30,31 @@ func TestNodeUpdateErrors(t *testing.T) { }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return errors.New("error updating the node") + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + return client.NodeUpdateResult{}, errors.New("error updating the node") }, expectedError: "error updating the node", }, { args: []string{"nodeID"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeLabels(map[string]string{ + "key": "value", + })), + }, nil }, flags: map[string]string{ - "label-rm": "notpresent", + "label-rm": "not-present", }, - expectedError: "key notpresent doesn't exist in node's labels", + expectedError: "key not-present doesn't exist in node's labels", }, } for _, tc := range testCases { @@ -74,22 +77,24 @@ func TestNodeUpdate(t *testing.T) { testCases := []struct { args []string flags map[string]string - nodeInspectFunc func() (swarm.Node, []byte, error) - nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + nodeInspectFunc func() (client.NodeInspectResult, error) + nodeUpdateFunc func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) }{ { args: []string{"nodeID"}, flags: map[string]string{ "role": "manager", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Role != swarm.NodeRoleManager { - return errors.New("expected role manager, got " + string(node.Role)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Role != swarm.NodeRoleManager { + return client.NodeUpdateResult{}, errors.New("expected role manager, got " + string(options.Spec.Role)) } - return nil + return client.NodeUpdateResult{}, nil }, }, { @@ -97,14 +102,16 @@ func TestNodeUpdate(t *testing.T) { flags: map[string]string{ "availability": "drain", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if node.Availability != swarm.NodeAvailabilityDrain { - return errors.New("expected drain availability, got " + string(node.Availability)) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if options.Spec.Availability != swarm.NodeAvailabilityDrain { + return client.NodeUpdateResult{}, errors.New("expected drain availability, got " + string(options.Spec.Availability)) } - return nil + return client.NodeUpdateResult{}, nil }, }, { @@ -112,14 +119,16 @@ func TestNodeUpdate(t *testing.T) { flags: map[string]string{ "label-add": "lbl", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if _, present := node.Annotations.Labels["lbl"]; !present { - return fmt.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if _, present := options.Spec.Annotations.Labels["lbl"]; !present { + return client.NodeUpdateResult{}, fmt.Errorf("expected 'lbl' label, got %v", options.Spec.Annotations.Labels) } - return nil + return client.NodeUpdateResult{}, nil }, }, { @@ -127,14 +136,16 @@ func TestNodeUpdate(t *testing.T) { flags: map[string]string{ "label-add": "key=value", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if value, present := node.Annotations.Labels["key"]; !present || value != "value" { - return fmt.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if value, present := options.Spec.Annotations.Labels["key"]; !present || value != "value" { + return client.NodeUpdateResult{}, fmt.Errorf("expected 'key' label to be 'value', got %v", options.Spec.Annotations.Labels) } - return nil + return client.NodeUpdateResult{}, nil }, }, { @@ -142,16 +153,18 @@ func TestNodeUpdate(t *testing.T) { flags: map[string]string{ "label-rm": "key", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeLabels(map[string]string{ - "key": "value", - })), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeLabels(map[string]string{ + "key": "value", + })), + }, nil }, - nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - if len(node.Annotations.Labels) > 0 { - return fmt.Errorf("expected no labels, got %v", node.Annotations.Labels) + nodeUpdateFunc: func(nodeID string, options client.NodeUpdateOptions) (client.NodeUpdateResult, error) { + if len(options.Spec.Annotations.Labels) > 0 { + return client.NodeUpdateResult{}, fmt.Errorf("expected no labels, got %v", options.Spec.Annotations.Labels) } - return nil + return client.NodeUpdateResult{}, nil }, }, } diff --git a/cli/command/plugin/client_test.go b/cli/command/plugin/client_test.go index 4b6901b1455c..d501ab7d7683 100644 --- a/cli/command/plugin/client_test.go +++ b/cli/command/plugin/client_test.go @@ -3,83 +3,80 @@ package plugin import ( "context" "io" + "net/http" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - pluginCreateFunc func(createContext io.Reader, createOptions types.PluginCreateOptions) error - pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error - pluginEnableFunc func(name string, options types.PluginEnableOptions) error - pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error - pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) - pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error) - pluginInspectFunc func(name string) (*types.Plugin, []byte, error) - pluginUpgradeFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) + pluginCreateFunc func(createContext io.Reader, options client.PluginCreateOptions) (client.PluginCreateResult, error) + pluginDisableFunc func(name string, options client.PluginDisableOptions) (client.PluginDisableResult, error) + pluginEnableFunc func(name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) + pluginRemoveFunc func(name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) + pluginInstallFunc func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) + pluginListFunc func(options client.PluginListOptions) (client.PluginListResult, error) + pluginInspectFunc func(name string) (client.PluginInspectResult, error) + pluginUpgradeFunc func(name string, options client.PluginUpgradeOptions) (client.PluginUpgradeResult, error) } -func (c *fakeClient) PluginCreate(_ context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error { +func (c *fakeClient) PluginCreate(_ context.Context, createContext io.Reader, options client.PluginCreateOptions) (client.PluginCreateResult, error) { if c.pluginCreateFunc != nil { - return c.pluginCreateFunc(createContext, createOptions) + return c.pluginCreateFunc(createContext, options) } - return nil + return client.PluginCreateResult{}, nil } -func (c *fakeClient) PluginEnable(_ context.Context, name string, enableOptions types.PluginEnableOptions) error { +func (c *fakeClient) PluginEnable(_ context.Context, name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) { if c.pluginEnableFunc != nil { - return c.pluginEnableFunc(name, enableOptions) + return c.pluginEnableFunc(name, options) } - return nil + return client.PluginEnableResult{}, nil } -func (c *fakeClient) PluginDisable(_ context.Context, name string, disableOptions types.PluginDisableOptions) error { +func (c *fakeClient) PluginDisable(_ context.Context, name string, options client.PluginDisableOptions) (client.PluginDisableResult, error) { if c.pluginDisableFunc != nil { - return c.pluginDisableFunc(name, disableOptions) + return c.pluginDisableFunc(name, options) } - return nil + return client.PluginDisableResult{}, nil } -func (c *fakeClient) PluginRemove(_ context.Context, name string, removeOptions types.PluginRemoveOptions) error { +func (c *fakeClient) PluginRemove(_ context.Context, name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) { if c.pluginRemoveFunc != nil { - return c.pluginRemoveFunc(name, removeOptions) + return c.pluginRemoveFunc(name, options) } - return nil + return client.PluginRemoveResult{}, nil } -func (c *fakeClient) PluginInstall(_ context.Context, name string, installOptions types.PluginInstallOptions) (io.ReadCloser, error) { +func (c *fakeClient) PluginInstall(_ context.Context, name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) { if c.pluginInstallFunc != nil { - return c.pluginInstallFunc(name, installOptions) + return c.pluginInstallFunc(name, options) } - return nil, nil + return client.PluginInstallResult{}, nil } -func (c *fakeClient) PluginList(_ context.Context, filter filters.Args) (types.PluginsListResponse, error) { +func (c *fakeClient) PluginList(_ context.Context, options client.PluginListOptions) (client.PluginListResult, error) { if c.pluginListFunc != nil { - return c.pluginListFunc(filter) + return c.pluginListFunc(options) } - - return types.PluginsListResponse{}, nil + return client.PluginListResult{}, nil } -func (c *fakeClient) PluginInspectWithRaw(_ context.Context, name string) (*types.Plugin, []byte, error) { +func (c *fakeClient) PluginInspect(_ context.Context, name string, _ client.PluginInspectOptions) (client.PluginInspectResult, error) { if c.pluginInspectFunc != nil { return c.pluginInspectFunc(name) } - - return nil, nil, nil + return client.PluginInspectResult{}, nil } -func (*fakeClient) Info(context.Context) (system.Info, error) { - return system.Info{}, nil +func (*fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, nil } -func (c *fakeClient) PluginUpgrade(_ context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) { +func (c *fakeClient) PluginUpgrade(_ context.Context, name string, options client.PluginUpgradeOptions) (client.PluginUpgradeResult, error) { if c.pluginUpgradeFunc != nil { return c.pluginUpgradeFunc(name, options) } - return nil, nil + // FIXME(thaJeztah): how to mock this? + return http.NoBody, nil } diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index 2e79ab1d49b5..653e163d88c7 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -3,30 +3,37 @@ package plugin import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newPluginCommand) +} + +// newPluginCommand returns a cobra command for `plugin` subcommands +func newPluginCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "plugin", Short: "Manage plugins", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.25"}, + + DisableFlagsInUseLine: true, } cmd.AddCommand( - newDisableCommand(dockerCli), - newEnableCommand(dockerCli), - newInspectCommand(dockerCli), - newInstallCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newSetCommand(dockerCli), - newPushCommand(dockerCli), - newCreateCommand(dockerCli), - newUpgradeCommand(dockerCli), + newDisableCommand(dockerCLI), + newEnableCommand(dockerCLI), + newInspectCommand(dockerCLI), + newInstallCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newSetCommand(dockerCLI), + newPushCommand(dockerCLI), + newCreateCommand(dockerCLI), + newUpgradeCommand(dockerCLI), ) return cmd } diff --git a/cli/command/plugin/completion.go b/cli/command/plugin/completion.go new file mode 100644 index 000000000000..0149d5368684 --- /dev/null +++ b/cli/command/plugin/completion.go @@ -0,0 +1,47 @@ +package plugin + +import ( + "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" + "github.com/spf13/cobra" +) + +type pluginState string + +const ( + stateAny pluginState = "" + stateEnabled pluginState = "enabled" + stateDisabled pluginState = "disabled" +) + +// completeNames offers completion for plugin names in the given state. +// The state argument can be one of: +// +// - "all": all plugins +// - "enabled": all enabled plugins +// - "disabled": all disabled plugins +func completeNames(dockerCLI completion.APIClientProvider, state pluginState) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + f := make(client.Filters) + switch state { + case stateEnabled: + f.Add("enabled", "true") + case stateDisabled: + f.Add("enabled", "false") + case stateAny: + // no filter + } + + res, err := dockerCLI.Client().PluginList(cmd.Context(), client.PluginListOptions{ + Filters: f, + }) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, v := range res.Items { + names = append(names, v.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/cli/command/plugin/create.go b/cli/command/plugin/create.go index 8f408d76f3fa..6d087612fb55 100644 --- a/cli/command/plugin/create.go +++ b/cli/command/plugin/create.go @@ -3,6 +3,7 @@ package plugin import ( "context" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -10,11 +11,10 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -33,14 +33,14 @@ func validateConfig(path string) error { return err } - m := types.PluginConfig{} + m := plugin.Config{} err = json.NewDecoder(dt).Decode(&m) _ = dt.Close() return err } -// validateContextDir validates the given dir and returns abs path on success. +// validateContextDir validates the given dir and returns its absolute path on success. func validateContextDir(contextDir string) (string, error) { absContextDir, err := filepath.Abs(contextDir) if err != nil { @@ -52,7 +52,7 @@ func validateContextDir(contextDir string) (string, error) { } if !stat.IsDir() { - return "", errors.Errorf("context must be a directory") + return "", errors.New("context must be a directory") } return absContextDir, nil @@ -64,7 +64,7 @@ type pluginCreateOptions struct { compress bool } -func newCreateCommand(dockerCli command.Cli) *cobra.Command { +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { options := pluginCreateOptions{} cmd := &cobra.Command{ @@ -74,9 +74,10 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { options.repoName = args[0] options.context = args[1] - return runCreate(cmd.Context(), dockerCli, options) + return runCreate(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, // TODO(thaJeztah): should provide "directory" completion for the second arg + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -113,7 +114,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateO return err } - err = dockerCli.Client().PluginCreate(ctx, createCtx, types.PluginCreateOptions{RepoName: options.repoName}) + _, err = dockerCli.Client().PluginCreate(ctx, createCtx, client.PluginCreateOptions{RepoName: options.repoName}) if err != nil { return err } diff --git a/cli/command/plugin/create_test.go b/cli/command/plugin/create_test.go index 0472f6eae153..871ec4328a2f 100644 --- a/cli/command/plugin/create_test.go +++ b/cli/command/plugin/create_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -96,8 +96,8 @@ func TestCreateErrorFromDaemon(t *testing.T) { defer tmpDir.Remove() cmd := newCreateCommand(test.NewFakeCli(&fakeClient{ - pluginCreateFunc: func(createContext io.Reader, createOptions types.PluginCreateOptions) error { - return errors.New("error creating plugin") + pluginCreateFunc: func(createContext io.Reader, createOptions client.PluginCreateOptions) (client.PluginCreateResult, error) { + return client.PluginCreateResult{}, errors.New("error creating plugin") }, })) cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()}) @@ -113,8 +113,8 @@ func TestCreatePlugin(t *testing.T) { defer tmpDir.Remove() cli := test.NewFakeCli(&fakeClient{ - pluginCreateFunc: func(createContext io.Reader, createOptions types.PluginCreateOptions) error { - return nil + pluginCreateFunc: func(createContext io.Reader, createOptions client.PluginCreateOptions) (client.PluginCreateResult, error) { + return client.PluginCreateResult{}, nil }, }) diff --git a/cli/command/plugin/disable.go b/cli/command/plugin/disable.go index 084b50365d1e..54ecc176c236 100644 --- a/cli/command/plugin/disable.go +++ b/cli/command/plugin/disable.go @@ -1,36 +1,34 @@ package plugin import ( - "context" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newDisableCommand(dockerCli command.Cli) *cobra.Command { - var force bool +func newDisableCommand(dockerCLI command.Cli) *cobra.Command { + var opts client.PluginDisableOptions cmd := &cobra.Command{ Use: "disable [OPTIONS] PLUGIN", Short: "Disable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDisable(cmd.Context(), dockerCli, args[0], force) + name := args[0] + if _, err := dockerCLI.Client().PluginDisable(cmd.Context(), name, opts); err != nil { + return err + } + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + return nil }, + ValidArgsFunction: completeNames(dockerCLI, stateEnabled), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVarP(&force, "force", "f", false, "Force the disable of an active plugin") + flags.BoolVarP(&opts.Force, "force", "f", false, "Force the disable of an active plugin") return cmd } - -func runDisable(ctx context.Context, dockerCli command.Cli, name string, force bool) error { - if err := dockerCli.Client().PluginDisable(ctx, name, types.PluginDisableOptions{Force: force}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/disable_test.go b/cli/command/plugin/disable_test.go index 3920efa10133..480a100d8997 100644 --- a/cli/command/plugin/disable_test.go +++ b/cli/command/plugin/disable_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,7 +15,7 @@ func TestPluginDisableErrors(t *testing.T) { testCases := []struct { args []string expectedError string - pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error + pluginDisableFunc func(name string, disableOptions client.PluginDisableOptions) (client.PluginDisableResult, error) }{ { args: []string{}, @@ -28,8 +28,8 @@ func TestPluginDisableErrors(t *testing.T) { { args: []string{"plugin-foo"}, expectedError: "error disabling plugin", - pluginDisableFunc: func(name string, disableOptions types.PluginDisableOptions) error { - return errors.New("error disabling plugin") + pluginDisableFunc: func(name string, disableOptions client.PluginDisableOptions) (client.PluginDisableResult, error) { + return client.PluginDisableResult{}, errors.New("error disabling plugin") }, }, } @@ -48,8 +48,8 @@ func TestPluginDisableErrors(t *testing.T) { func TestPluginDisable(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - pluginDisableFunc: func(name string, disableOptions types.PluginDisableOptions) error { - return nil + pluginDisableFunc: func(name string, disableOptions client.PluginDisableOptions) (client.PluginDisableResult, error) { + return client.PluginDisableResult{}, nil }, }) cmd := newDisableCommand(cli) diff --git a/cli/command/plugin/enable.go b/cli/command/plugin/enable.go index e1724fd50cf8..65c67906631b 100644 --- a/cli/command/plugin/enable.go +++ b/cli/command/plugin/enable.go @@ -6,43 +6,38 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -type enableOpts struct { - timeout int - name string -} - -func newEnableCommand(dockerCli command.Cli) *cobra.Command { - var opts enableOpts +func newEnableCommand(dockerCLI command.Cli) *cobra.Command { + var opts client.PluginEnableOptions cmd := &cobra.Command{ Use: "enable [OPTIONS] PLUGIN", Short: "Enable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runEnable(cmd.Context(), dockerCli, &opts) + name := args[0] + if err := runEnable(cmd.Context(), dockerCLI, name, opts); err != nil { + return err + } + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + return nil }, + ValidArgsFunction: completeNames(dockerCLI, stateDisabled), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.IntVar(&opts.timeout, "timeout", 30, "HTTP client timeout (in seconds)") + flags.IntVar(&opts.Timeout, "timeout", 30, "HTTP client timeout (in seconds)") return cmd } -func runEnable(ctx context.Context, dockerCli command.Cli, opts *enableOpts) error { - name := opts.name - if opts.timeout < 0 { - return errors.Errorf("negative timeout %d is invalid", opts.timeout) - } - - if err := dockerCli.Client().PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { - return err +func runEnable(ctx context.Context, dockerCli command.Cli, name string, opts client.PluginEnableOptions) error { + if opts.Timeout < 0 { + return fmt.Errorf("negative timeout %d is invalid", opts.Timeout) } - fmt.Fprintln(dockerCli.Out(), name) - return nil + _, err := dockerCli.Client().PluginEnable(ctx, name, opts) + return err } diff --git a/cli/command/plugin/enable_test.go b/cli/command/plugin/enable_test.go index bb0861e6ce20..f74ca5832977 100644 --- a/cli/command/plugin/enable_test.go +++ b/cli/command/plugin/enable_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,7 +15,7 @@ func TestPluginEnableErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - pluginEnableFunc func(name string, options types.PluginEnableOptions) error + pluginEnableFunc func(name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) expectedError string }{ { @@ -28,8 +28,8 @@ func TestPluginEnableErrors(t *testing.T) { }, { args: []string{"plugin-foo"}, - pluginEnableFunc: func(name string, options types.PluginEnableOptions) error { - return errors.New("failed to enable plugin") + pluginEnableFunc: func(name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) { + return client.PluginEnableResult{}, errors.New("failed to enable plugin") }, expectedError: "failed to enable plugin", }, @@ -48,7 +48,7 @@ func TestPluginEnableErrors(t *testing.T) { })) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -58,8 +58,8 @@ func TestPluginEnableErrors(t *testing.T) { func TestPluginEnable(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - pluginEnableFunc: func(name string, options types.PluginEnableOptions) error { - return nil + pluginEnableFunc: func(name string, options client.PluginEnableOptions) (client.PluginEnableResult, error) { + return client.PluginEnableResult{}, nil }, }) diff --git a/cli/command/plugin/formatter.go b/cli/command/plugin/formatter.go index 27c24b52cdeb..283f3df283ff 100644 --- a/cli/command/plugin/formatter.go +++ b/cli/command/plugin/formatter.go @@ -4,7 +4,8 @@ import ( "strings" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" ) const ( @@ -12,10 +13,16 @@ const ( enabledHeader = "ENABLED" pluginIDHeader = "ID" + + rawFormat = `plugin_id: {{.ID}} +name: {{.Name}} +description: {{.Description}} +enabled: {{.Enabled}} +` ) -// NewFormat returns a Format for rendering using a plugin Context -func NewFormat(source string, quiet bool) formatter.Format { +// newFormat returns a Format for rendering using a pluginContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -26,37 +33,41 @@ func NewFormat(source string, quiet bool) formatter.Format { if quiet { return `plugin_id: {{.ID}}` } - return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` + return rawFormat } return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, plugins []*types.Plugin) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, plugin := range plugins { - pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} - if err := format(pluginCtx); err != nil { +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, plugins client.PluginListResult) error { + pluginCtx := &pluginContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": pluginIDHeader, + "Name": formatter.NameHeader, + "Description": formatter.DescriptionHeader, + "Enabled": enabledHeader, + "PluginReference": formatter.ImageHeader, + }, + }, + } + return fmtCtx.Write(pluginCtx, func(format func(subContext formatter.SubContext) error) error { + for _, p := range plugins.Items { + if err := format(&pluginContext{ + trunc: fmtCtx.Trunc, + p: p, + }); err != nil { return err } } return nil - } - pluginCtx := pluginContext{} - pluginCtx.Header = formatter.SubHeaderContext{ - "ID": pluginIDHeader, - "Name": formatter.NameHeader, - "Description": formatter.DescriptionHeader, - "Enabled": enabledHeader, - "PluginReference": formatter.ImageHeader, - } - return ctx.Write(&pluginCtx, render) + }) } type pluginContext struct { formatter.HeaderContext trunc bool - p types.Plugin + p plugin.Plugin } func (c *pluginContext) MarshalJSON() ([]byte, error) { diff --git a/cli/command/plugin/formatter_test.go b/cli/command/plugin/formatter_test.go index 63f1ce29e983..08f6d98fc6d8 100644 --- a/cli/command/plugin/formatter_test.go +++ b/cli/command/plugin/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package plugin @@ -11,7 +11,8 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -19,85 +20,106 @@ import ( func TestPluginContext(t *testing.T) { pluginID := test.RandomID() - var ctx pluginContext - cases := []struct { + var pCtx pluginContext + tests := []struct { pluginCtx pluginContext expValue string call func() string }{ - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: false, - }, pluginID, ctx.ID}, - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: true, - }, formatter.TruncateID(pluginID), ctx.ID}, - {pluginContext{ - p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", ctx.Name}, - {pluginContext{ - p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", ctx.Description}, + { + pluginCtx: pluginContext{ + p: plugin.Plugin{ID: pluginID}, + trunc: false, + }, + expValue: pluginID, + call: pCtx.ID, + }, + { + pluginCtx: pluginContext{ + p: plugin.Plugin{ID: pluginID}, + trunc: true, + }, + expValue: formatter.TruncateID(pluginID), + call: pCtx.ID, + }, + { + pluginCtx: pluginContext{ + p: plugin.Plugin{Name: "plugin_name"}, + }, + expValue: "plugin_name", + call: pCtx.Name, + }, + { + pluginCtx: pluginContext{ + p: plugin.Plugin{Config: plugin.Config{Description: "plugin_description"}}, + }, + expValue: "plugin_description", + call: pCtx.Description, + }, } - for _, c := range cases { - ctx = c.pluginCtx - v := c.call() + for _, tc := range tests { + pCtx = tc.pluginCtx + v := tc.call() if strings.Contains(v, ",") { - test.CompareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) + test.CompareMultipleValues(t, v, tc.expValue) + } else if v != tc.expValue { + t.Fatalf("Expected %s, was %s\n", tc.expValue, v) } } } func TestPluginContextWrite(t *testing.T) { - cases := []struct { + tests := []struct { + doc string context formatter.Context expected string }{ - // Errors { - formatter.Context{Format: "{{InvalidFunction}}"}, - `template parsing error: template: :1: function "InvalidFunction" not defined`, + doc: "invalid function", + context: formatter.Context{Format: "{{InvalidFunction}}"}, + expected: `template parsing error: template: :1: function "InvalidFunction" not defined`, }, { - formatter.Context{Format: "{{nil}}"}, - `template parsing error: template: :1:2: executing "" at : nil is not a command`, + doc: "nil template", + context: formatter.Context{Format: "{{nil}}"}, + expected: `template parsing error: template: :1:2: executing "" at : nil is not a command`, }, - // Table format { - formatter.Context{Format: NewFormat("table", false)}, - `ID NAME DESCRIPTION ENABLED + doc: "table format", + context: formatter.Context{Format: newFormat("table", false)}, + expected: `ID NAME DESCRIPTION ENABLED pluginID1 foobar_baz description 1 true pluginID2 foobar_bar description 2 false `, }, { - formatter.Context{Format: NewFormat("table", true)}, - `pluginID1 + doc: "table format, quiet", + context: formatter.Context{Format: newFormat("table", true)}, + expected: `pluginID1 pluginID2 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", false)}, - `NAME + doc: "table format name col", + context: formatter.Context{Format: newFormat("table {{.Name}}", false)}, + expected: `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, - `NAME + doc: "table format name col, quiet", + context: formatter.Context{Format: newFormat("table {{.Name}}", true)}, + expected: `NAME foobar_baz foobar_bar `, }, - // Raw Format { - formatter.Context{Format: NewFormat("raw", false)}, - `plugin_id: pluginID1 + doc: "raw format", + context: formatter.Context{Format: newFormat("raw", false)}, + expected: `plugin_id: pluginID1 name: foobar_baz description: description 1 enabled: true @@ -110,31 +132,34 @@ enabled: false `, }, { - formatter.Context{Format: NewFormat("raw", true)}, - `plugin_id: pluginID1 + doc: "raw format, quiet", + context: formatter.Context{Format: newFormat("raw", true)}, + expected: `plugin_id: pluginID1 plugin_id: pluginID2 `, }, - // Custom Format { - formatter.Context{Format: NewFormat("{{.Name}}", false)}, - `foobar_baz + doc: "custom format", + context: formatter.Context{Format: newFormat("{{.Name}}", false)}, + expected: `foobar_baz foobar_bar `, }, } - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true}, - {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, + plugins := client.PluginListResult{ + Items: []plugin.Plugin{ + {ID: "pluginID1", Name: "foobar_baz", Config: plugin.Config{Description: "description 1"}, Enabled: true}, + {ID: "pluginID2", Name: "foobar_bar", Config: plugin.Config{Description: "description 2"}, Enabled: false}, + }, } - for _, tc := range cases { - t.Run(string(tc.context.Format), func(t *testing.T) { + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, plugins) + err := formatWrite(tc.context, plugins) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -145,9 +170,11 @@ foobar_bar } func TestPluginContextWriteJSON(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, + plugins := client.PluginListResult{ + Items: []plugin.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + }, } expectedJSONs := []map[string]any{ {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""}, @@ -155,7 +182,7 @@ func TestPluginContextWriteJSON(t *testing.T) { } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, plugins) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, plugins) if err != nil { t.Fatal(err) } @@ -169,12 +196,14 @@ func TestPluginContextWriteJSON(t *testing.T) { } func TestPluginContextWriteJSONField(t *testing.T) { - plugins := []*types.Plugin{ - {ID: "pluginID1", Name: "foobar_baz"}, - {ID: "pluginID2", Name: "foobar_bar"}, + plugins := client.PluginListResult{ + Items: []plugin.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + }, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, plugins) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, plugins) if err != nil { t.Fatal(err) } @@ -183,6 +212,6 @@ func TestPluginContextWriteJSONField(t *testing.T) { if err := json.Unmarshal([]byte(line), &s); err != nil { t.Fatal(err) } - assert.Check(t, is.Equal(plugins[i].ID, s)) + assert.Check(t, is.Equal(plugins.Items[i].ID, s)) } } diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go index 9b6a453c9336..17d7b89169e4 100644 --- a/cli/command/plugin/inspect.go +++ b/cli/command/plugin/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package plugin @@ -10,6 +10,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,7 +19,7 @@ type inspectOptions struct { format string } -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -27,8 +28,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.pluginNames = args - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI, stateAny), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -36,11 +39,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - getRef := func(ref string) (any, []byte, error) { - return client.PluginInspectWithRaw(ctx, ref) - } - - return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() + return inspect.Inspect(dockerCLI.Out(), opts.pluginNames, opts.format, func(ref string) (any, []byte, error) { + res, err := apiClient.PluginInspect(ctx, ref, client.PluginInspectOptions{}) + return res.Plugin, res.Raw, err + }) } diff --git a/cli/command/plugin/inspect_test.go b/cli/command/plugin/inspect_test.go index 414ff479a0c2..1b3d7f44668f 100644 --- a/cli/command/plugin/inspect_test.go +++ b/cli/command/plugin/inspect_test.go @@ -7,29 +7,31 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) -var pluginFoo = &types.Plugin{ - ID: "id-foo", - Name: "name-foo", - Config: types.PluginConfig{ - Description: "plugin foo description", - DockerVersion: "17.12.1-ce", - Documentation: "plugin foo documentation", - Entrypoint: []string{"/foo"}, - Interface: types.PluginConfigInterface{ - Socket: "pluginfoo.sock", - }, - Linux: types.PluginConfigLinux{ - Capabilities: []string{"CAP_SYS_ADMIN"}, - }, - WorkDir: "workdir-foo", - Rootfs: &types.PluginConfigRootfs{ - DiffIds: []string{"sha256:8603eedd4ea52cebb2f22b45405a3dc8f78ba3e31bf18f27b4547a9ff930e0bd"}, - Type: "layers", +var pluginFoo = client.PluginInspectResult{ + Plugin: plugin.Plugin{ + ID: "id-foo", + Name: "name-foo", + Config: plugin.Config{ + Description: "plugin foo description", + Documentation: "plugin foo documentation", + Entrypoint: []string{"/foo"}, + Interface: plugin.Interface{ + Socket: "plugin-foo.sock", + }, + Linux: plugin.LinuxConfig{ + Capabilities: []string{"CAP_SYS_ADMIN"}, + }, + WorkDir: "workdir-foo", + Rootfs: &plugin.RootFS{ + DiffIds: []string{"sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, + Type: "layers", + }, }, }, } @@ -40,7 +42,7 @@ func TestInspectErrors(t *testing.T) { args []string flags map[string]string expectedError string - inspectFunc func(name string) (*types.Plugin, []byte, error) + inspectFunc func(name string) (client.PluginInspectResult, error) }{ { description: "too few arguments", @@ -51,8 +53,8 @@ func TestInspectErrors(t *testing.T) { description: "error inspecting plugin", args: []string{"foo"}, expectedError: "error inspecting plugin", - inspectFunc: func(name string) (*types.Plugin, []byte, error) { - return nil, nil, errors.New("error inspecting plugin") + inspectFunc: func(string) (client.PluginInspectResult, error) { + return client.PluginInspectResult{}, errors.New("error inspecting plugin") }, }, { @@ -62,6 +64,9 @@ func TestInspectErrors(t *testing.T) { "format": "{{invalid format}}", }, expectedError: "template parsing error", + inspectFunc: func(string) (client.PluginInspectResult, error) { + return client.PluginInspectResult{}, errors.New("this function should not be called in this test") + }, }, } @@ -71,7 +76,7 @@ func TestInspectErrors(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -86,7 +91,7 @@ func TestInspect(t *testing.T) { args []string flags map[string]string golden string - inspectFunc func(name string) (*types.Plugin, []byte, error) + inspectFunc func(name string) (client.PluginInspectResult, error) }{ { description: "inspect single plugin with format", @@ -95,19 +100,21 @@ func TestInspect(t *testing.T) { "format": "{{ .Name }}", }, golden: "plugin-inspect-single-with-format.golden", - inspectFunc: func(name string) (*types.Plugin, []byte, error) { - return &types.Plugin{ - ID: "id-foo", - Name: "name-foo", - }, []byte{}, nil + inspectFunc: func(name string) (client.PluginInspectResult, error) { + return client.PluginInspectResult{ + Plugin: plugin.Plugin{ + ID: "id-foo", + Name: "name-foo", + }, + }, nil }, }, { description: "inspect single plugin without format", args: []string{"foo"}, golden: "plugin-inspect-single-without-format.golden", - inspectFunc: func(name string) (*types.Plugin, []byte, error) { - return pluginFoo, nil, nil + inspectFunc: func(name string) (client.PluginInspectResult, error) { + return pluginFoo, nil }, }, { @@ -117,20 +124,24 @@ func TestInspect(t *testing.T) { "format": "{{ .Name }}", }, golden: "plugin-inspect-multiple-with-format.golden", - inspectFunc: func(name string) (*types.Plugin, []byte, error) { + inspectFunc: func(name string) (client.PluginInspectResult, error) { switch name { case "foo": - return &types.Plugin{ - ID: "id-foo", - Name: "name-foo", - }, []byte{}, nil + return client.PluginInspectResult{ + Plugin: plugin.Plugin{ + ID: "id-foo", + Name: "name-foo", + }, + }, nil case "bar": - return &types.Plugin{ - ID: "id-bar", - Name: "name-bar", - }, []byte{}, nil + return client.PluginInspectResult{ + Plugin: plugin.Plugin{ + ID: "id-bar", + Name: "name-bar", + }, + }, nil default: - return nil, nil, fmt.Errorf("unexpected plugin name: %s", name) + return client.PluginInspectResult{}, fmt.Errorf("unexpected plugin name: %s", name) } }, }, @@ -142,7 +153,7 @@ func TestInspect(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), tc.golden) diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index 027478c0fc33..7d2d9aafaeb0 100644 --- a/cli/command/plugin/install.go +++ b/cli/command/plugin/install.go @@ -3,20 +3,15 @@ package plugin import ( "context" "fmt" - "strings" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/prompt" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type pluginOptions struct { @@ -26,15 +21,9 @@ type pluginOptions struct { disable bool args []string skipRemoteCheck bool - untrusted bool } -func loadPullFlags(dockerCli command.Cli, opts *pluginOptions, flags *pflag.FlagSet) { - flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") - command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) -} - -func newInstallCommand(dockerCli command.Cli) *cobra.Command { +func newInstallCommand(dockerCLI command.Cli) *cobra.Command { var options pluginOptions cmd := &cobra.Command{ Use: "install [OPTIONS] PLUGIN [KEY=VALUE...]", @@ -45,66 +34,46 @@ func newInstallCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { options.args = args[1:] } - return runInstall(cmd.Context(), dockerCli, options) + return runInstall(cmd.Context(), dockerCLI, options) }, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - loadPullFlags(dockerCli, &options, flags) + flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") flags.StringVar(&options.localName, "alias", "", "Local name for plugin") + + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") return cmd } -func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) { +func buildPullConfig(dockerCLI command.Cli, opts pluginOptions) (client.PluginInstallOptions, error) { // Names with both tag and digest will be treated by the daemon // as a pull by digest with a local name for the tag // (if no local name is provided). ref, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { - return types.PluginInstallOptions{}, err + return client.PluginInstallOptions{}, err } - repoInfo, _ := registry.ParseRepositoryInfo(ref) - - remote := ref.String() - - _, isCanonical := ref.(reference.Canonical) - if !opts.untrusted && !isCanonical { - ref = reference.TagNameOnly(ref) - nt, ok := ref.(reference.NamedTagged) - if !ok { - return types.PluginInstallOptions{}, errors.Errorf("invalid name: %s", ref.String()) - } - - trusted, err := image.TrustedReference(ctx, dockerCli, nt) - if err != nil { - return types.PluginInstallOptions{}, err - } - remote = reference.FamiliarString(trusted) - } - - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), ref.String()) if err != nil { - return types.PluginInstallOptions{}, err + return client.PluginInstallOptions{}, err } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName) - } - - options := types.PluginInstallOptions{ + return client.PluginInstallOptions{ RegistryAuth: encodedAuth, - RemoteRef: remote, + RemoteRef: ref.String(), Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, - AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote), - PrivilegeFunc: requestPrivilege, + AcceptPermissionsFunc: acceptPrivileges(dockerCLI, opts.remote), + PrivilegeFunc: nil, Args: opts.args, - } - return options, nil + }, nil } func runInstall(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) error { @@ -115,23 +84,22 @@ func runInstall(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) return err } if _, ok := aref.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", opts.localName) + return fmt.Errorf("invalid name: %s", opts.localName) } localName = reference.FamiliarString(reference.TagNameOnly(aref)) } - options, err := buildPullConfig(ctx, dockerCLI, opts, "plugin install") + options, err := buildPullConfig(dockerCLI, opts) if err != nil { return err } responseBody, err := dockerCLI.Client().PluginInstall(ctx, localName, options) if err != nil { - if strings.Contains(err.Error(), "(image) when fetching") { - return errors.New(err.Error() + " - Use \"docker image pull\"") - } return err } - defer responseBody.Close() + defer func() { + _ = responseBody.Close() + }() if err := jsonstream.Display(ctx, responseBody, dockerCLI.Out()); err != nil { return err } @@ -139,8 +107,8 @@ func runInstall(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) return nil } -func acceptPrivileges(dockerCLI command.Streams, name string) func(ctx context.Context, privileges types.PluginPrivileges) (bool, error) { - return func(ctx context.Context, privileges types.PluginPrivileges) (bool, error) { +func acceptPrivileges(dockerCLI command.Streams, name string) func(ctx context.Context, privileges plugin.Privileges) (bool, error) { + return func(ctx context.Context, privileges plugin.Privileges) (bool, error) { _, _ = fmt.Fprintf(dockerCLI.Out(), "Plugin %q is requesting the following privileges:\n", name) for _, privilege := range privileges { _, _ = fmt.Fprintf(dockerCLI.Out(), " - %s: %v\n", privilege.Name, privilege.Value) diff --git a/cli/command/plugin/install_test.go b/cli/command/plugin/install_test.go index b59ed98fcbee..c1c9025037e3 100644 --- a/cli/command/plugin/install_test.go +++ b/cli/command/plugin/install_test.go @@ -7,8 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -18,7 +17,7 @@ func TestInstallErrors(t *testing.T) { description string args []string expectedError string - installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) + installFunc func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) }{ { description: "insufficient number of arguments", @@ -32,23 +31,15 @@ func TestInstallErrors(t *testing.T) { }, { description: "invalid plugin name", - args: []string{"UPPERCASE_REPONAME"}, + args: []string{"UPPERCASE_REPO_NAME"}, expectedError: "invalid", }, { description: "installation error", args: []string{"foo"}, expectedError: "error installing plugin", - installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return nil, errors.New("error installing plugin") - }, - }, - { - description: "installation error due to missing image", - args: []string{"foo"}, - expectedError: "docker image pull", - installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return nil, errors.New("(image) when fetching") + installFunc: func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) { + return client.PluginInstallResult{}, errors.New("error installing plugin") }, }, } @@ -65,72 +56,28 @@ func TestInstallErrors(t *testing.T) { } } -func TestInstallContentTrustErrors(t *testing.T) { - testCases := []struct { - description string - args []string - expectedError string - notaryFunc test.NotaryClientFuncType - }{ - { - description: "install plugin, offline notary server", - args: []string{"plugin:tag"}, - expectedError: "client is offline", - notaryFunc: notary.GetOfflineNotaryRepository, - }, - { - description: "install plugin, uninitialized notary server", - args: []string{"plugin:tag"}, - expectedError: "remote trust data does not exist", - notaryFunc: notary.GetUninitializedNotaryRepository, - }, - { - description: "install plugin, empty notary server", - args: []string{"plugin:tag"}, - expectedError: "No valid trust data for tag", - notaryFunc: notary.GetEmptyTargetsNotaryRepository, - }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{ - pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return nil, errors.New("should not try to install plugin") - }, - }, test.EnableContentTrust) - cli.SetNotaryClient(tc.notaryFunc) - cmd := newInstallCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) - }) - } -} - func TestInstall(t *testing.T) { testCases := []struct { description string args []string expectedOutput string - installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) + installFunc func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) }{ { description: "install with no additional flags", args: []string{"foo"}, expectedOutput: "Installed plugin foo\n", - installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return io.NopCloser(strings.NewReader("")), nil + installFunc: func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) { + return client.PluginInstallResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil }, }, { description: "install with disable flag", args: []string{"--disable", "foo"}, expectedOutput: "Installed plugin foo\n", - installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { + installFunc: func(name string, options client.PluginInstallOptions) (client.PluginInstallResult, error) { assert.Check(t, options.Disabled) - return io.NopCloser(strings.NewReader("")), nil + return client.PluginInstallResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil }, }, } diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go index 0c7234a2c3b0..fb651ab25abe 100644 --- a/cli/command/plugin/list.go +++ b/cli/command/plugin/list.go @@ -6,11 +6,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -21,7 +21,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { options := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -30,9 +30,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Aliases: []string{"list"}, Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), dockerCli, options) + return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -46,13 +47,15 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { } func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - plugins, err := dockerCli.Client().PluginList(ctx, options.filter.Value()) + resp, err := dockerCli.Client().PluginList(ctx, client.PluginListOptions{ + Filters: options.filter.Value(), + }) if err != nil { return err } - sort.Slice(plugins, func(i, j int) bool { - return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name) + sort.Slice(resp.Items, func(i, j int) bool { + return sortorder.NaturalLess(resp.Items[i].Name, resp.Items[j].Name) }) format := options.format @@ -66,8 +69,8 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er pluginsCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Format: newFormat(format, options.quiet), Trunc: !options.noTrunc, } - return FormatWrite(pluginsCtx, plugins) + return formatWrite(pluginsCtx, resp) } diff --git a/cli/command/plugin/list_test.go b/cli/command/plugin/list_test.go index 91665da654f1..cad31ad80dd8 100644 --- a/cli/command/plugin/list_test.go +++ b/cli/command/plugin/list_test.go @@ -6,11 +6,10 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" ) @@ -20,7 +19,7 @@ func TestListErrors(t *testing.T) { args []string flags map[string]string expectedError string - listFunc func(filter filters.Args) (types.PluginsListResponse, error) + listFunc func(client.PluginListOptions) (client.PluginListResult, error) }{ { description: "too many arguments", @@ -31,8 +30,8 @@ func TestListErrors(t *testing.T) { description: "error listing plugins", args: []string{}, expectedError: "error listing plugins", - listFunc: func(filter filters.Args) (types.PluginsListResponse, error) { - return types.PluginsListResponse{}, errors.New("error listing plugins") + listFunc: func(client.PluginListOptions) (client.PluginListResult, error) { + return client.PluginListResult{}, errors.New("error listing plugins") }, }, { @@ -51,7 +50,7 @@ func TestListErrors(t *testing.T) { cmd := newListCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -61,14 +60,16 @@ func TestListErrors(t *testing.T) { } func TestList(t *testing.T) { - singlePluginListFunc := func(_ filters.Args) (types.PluginsListResponse, error) { - return types.PluginsListResponse{ - { - ID: "id-foo", - Name: "name-foo", - Enabled: true, - Config: types.PluginConfig{ - Description: "desc-bar", + singlePluginListFunc := func(client.PluginListOptions) (client.PluginListResult, error) { + return client.PluginListResult{ + Items: plugin.ListResponse{ + { + ID: "id-foo", + Name: "name-foo", + Enabled: true, + Config: plugin.Config{ + Description: "desc-bar", + }, }, }, }, nil @@ -79,7 +80,7 @@ func TestList(t *testing.T) { args []string flags map[string]string golden string - listFunc func(filter filters.Args) (types.PluginsListResponse, error) + listFunc func(client.PluginListOptions) (client.PluginListResult, error) }{ { description: "list with no additional flags", @@ -94,9 +95,9 @@ func TestList(t *testing.T) { "filter": "foo=bar", }, golden: "plugin-list-without-format.golden", - listFunc: func(filter filters.Args) (types.PluginsListResponse, error) { - assert.Check(t, is.Equal("bar", filter.Get("foo")[0])) - return singlePluginListFunc(filter) + listFunc: func(opts client.PluginListOptions) (client.PluginListResult, error) { + assert.Check(t, opts.Filters["foo"]["bar"]) + return singlePluginListFunc(opts) }, }, { @@ -116,16 +117,16 @@ func TestList(t *testing.T) { "format": "{{ .ID }}", }, golden: "plugin-list-with-no-trunc-option.golden", - listFunc: func(_ filters.Args) (types.PluginsListResponse, error) { - return types.PluginsListResponse{ - { + listFunc: func(opts client.PluginListOptions) (client.PluginListResult, error) { + return client.PluginListResult{ + Items: []plugin.Plugin{{ ID: "xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd", Name: "name-foo", Enabled: true, - Config: types.PluginConfig{ + Config: plugin.Config{ Description: "desc-bar", }, - }, + }}, }, nil }, }, @@ -145,19 +146,21 @@ func TestList(t *testing.T) { "format": "{{ .Name }}", }, golden: "plugin-list-sort.golden", - listFunc: func(_ filters.Args) (types.PluginsListResponse, error) { - return types.PluginsListResponse{ - { - ID: "id-1", - Name: "plugin-1-foo", - }, - { - ID: "id-2", - Name: "plugin-10-foo", - }, - { - ID: "id-3", - Name: "plugin-2-foo", + listFunc: func(client.PluginListOptions) (client.PluginListResult, error) { + return client.PluginListResult{ + Items: []plugin.Plugin{ + { + ID: "id-1", + Name: "plugin-1-foo", + }, + { + ID: "id-2", + Name: "plugin-10-foo", + }, + { + ID: "id-3", + Name: "plugin-2-foo", + }, }, }, nil }, @@ -170,7 +173,7 @@ func TestList(t *testing.T) { cmd := newListCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), tc.golden) diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index 402068034797..af76db49ebef 100644 --- a/cli/command/plugin/push.go +++ b/cli/command/plugin/push.go @@ -2,69 +2,59 @@ package plugin import ( "context" + "fmt" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -type pushOptions struct { - name string - untrusted bool -} - -func newPushCommand(dockerCli command.Cli) *cobra.Command { - var opts pushOptions +func newPushCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "push [OPTIONS] PLUGIN[:TAG]", Short: "Push a plugin to a registry", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runPush(cmd.Context(), dockerCli, opts) + name := args[0] + return runPush(cmd.Context(), dockerCLI, name) }, + ValidArgsFunction: completeNames(dockerCLI, stateAny), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - - command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) - + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") return cmd } -func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error { - named, err := reference.ParseNormalizedNamed(opts.name) +func runPush(ctx context.Context, dockerCli command.Cli, name string) error { + named, err := reference.ParseNormalizedNamed(name) if err != nil { return err } if _, ok := named.(reference.Canonical); ok { - return errors.Errorf("invalid name: %s", opts.name) + return fmt.Errorf("invalid name: %s", name) } named = reference.TagNameOnly(named) - - repoInfo, _ := registry.ParseRepositoryInfo(named) - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), named.String()) if err != nil { return err } - responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(named), encodedAuth) + responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(named), client.PluginPushOptions{ + RegistryAuth: encodedAuth, + }) if err != nil { return err } - defer responseBody.Close() - - if !opts.untrusted { - return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent()) - } - + defer func() { + _ = responseBody.Close() + }() return jsonstream.Display(ctx, responseBody, dockerCli.Out()) } diff --git a/cli/command/plugin/remove.go b/cli/command/plugin/remove.go index 59ea19793e87..c10636616ef9 100644 --- a/cli/command/plugin/remove.go +++ b/cli/command/plugin/remove.go @@ -7,7 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,7 +17,7 @@ type rmOptions struct { plugins []string } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var opts rmOptions cmd := &cobra.Command{ @@ -27,8 +27,10 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.plugins = args - return runRemove(cmd.Context(), dockerCli, &opts) + return runRemove(cmd.Context(), dockerCLI, &opts) }, + ValidArgsFunction: completeNames(dockerCLI, stateAny), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -41,7 +43,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) erro var errs []error for _, name := range opts.plugins { - if err := apiClient.PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { + if _, err := apiClient.PluginRemove(ctx, name, client.PluginRemoveOptions{Force: opts.force}); err != nil { errs = append(errs, err) continue } diff --git a/cli/command/plugin/remove_test.go b/cli/command/plugin/remove_test.go index de40a28ade52..61e4c82afbb5 100644 --- a/cli/command/plugin/remove_test.go +++ b/cli/command/plugin/remove_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -14,7 +14,7 @@ import ( func TestRemoveErrors(t *testing.T) { testCases := []struct { args []string - pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error + pluginRemoveFunc func(name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) expectedError string }{ { @@ -23,8 +23,8 @@ func TestRemoveErrors(t *testing.T) { }, { args: []string{"plugin-foo"}, - pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error { - return errors.New("error removing plugin") + pluginRemoveFunc: func(name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) { + return client.PluginRemoveResult{}, errors.New("error removing plugin") }, expectedError: "error removing plugin", }, @@ -43,11 +43,7 @@ func TestRemoveErrors(t *testing.T) { } func TestRemove(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{ - pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error { - return nil - }, - }) + cli := test.NewFakeCli(&fakeClient{}) cmd := newRemoveCommand(cli) cmd.SetArgs([]string{"plugin-foo"}) assert.NilError(t, cmd.Execute()) @@ -57,14 +53,14 @@ func TestRemove(t *testing.T) { func TestRemoveWithForceOption(t *testing.T) { force := false cli := test.NewFakeCli(&fakeClient{ - pluginRemoveFunc: func(name string, options types.PluginRemoveOptions) error { + pluginRemoveFunc: func(name string, options client.PluginRemoveOptions) (client.PluginRemoveResult, error) { force = options.Force - return nil + return client.PluginRemoveResult{}, nil }, }) cmd := newRemoveCommand(cli) cmd.SetArgs([]string{"plugin-foo"}) - cmd.Flags().Set("force", "true") + assert.NilError(t, cmd.Flags().Set("force", "true")) assert.NilError(t, cmd.Execute()) assert.Check(t, force) assert.Check(t, is.Equal("plugin-foo\n", cli.OutBuffer().String())) diff --git a/cli/command/plugin/set.go b/cli/command/plugin/set.go index 64442097b3aa..21b517d8a301 100644 --- a/cli/command/plugin/set.go +++ b/cli/command/plugin/set.go @@ -3,18 +3,22 @@ package plugin import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newSetCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ +func newSetCommand(dockerCLI command.Cli) *cobra.Command { + return &cobra.Command{ Use: "set PLUGIN KEY=VALUE [KEY=VALUE...]", Short: "Change settings for a plugin", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return dockerCli.Client().PluginSet(cmd.Context(), args[0], args[1:]) + _, err := dockerCLI.Client().PluginSet(cmd.Context(), args[0], client.PluginSetOptions{ + Args: args[1:], + }) + return err }, + ValidArgsFunction: completeNames(dockerCLI, stateAny), // TODO(thaJeztah): should only complete for the first arg + DisableFlagsInUseLine: true, } - - return cmd } diff --git a/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden b/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden index 65c8d39ce995..1c871a0c4206 100644 --- a/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden +++ b/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden @@ -8,14 +8,13 @@ "Value": null }, "Description": "plugin foo description", - "DockerVersion": "17.12.1-ce", "Documentation": "plugin foo documentation", "Entrypoint": [ "/foo" ], "Env": null, "Interface": { - "Socket": "pluginfoo.sock", + "Socket": "plugin-foo.sock", "Types": null }, "IpcHost": false, @@ -36,7 +35,7 @@ "WorkDir": "workdir-foo", "rootfs": { "diff_ids": [ - "sha256:8603eedd4ea52cebb2f22b45405a3dc8f78ba3e31bf18f27b4547a9ff930e0bd" + "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" ], "type": "layers" } diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go index da9ea4076d84..9053ce6e8980 100644 --- a/cli/command/plugin/upgrade.go +++ b/cli/command/plugin/upgrade.go @@ -2,19 +2,19 @@ package plugin import ( "context" + "errors" "fmt" - "strings" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/prompt" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newUpgradeCommand(dockerCli command.Cli) *cobra.Command { +func newUpgradeCommand(dockerCLI command.Cli) *cobra.Command { var options pluginOptions cmd := &cobra.Command{ Use: "upgrade [OPTIONS] PLUGIN [REMOTE]", @@ -25,44 +25,49 @@ func newUpgradeCommand(dockerCli command.Cli) *cobra.Command { if len(args) == 2 { options.remote = args[1] } - return runUpgrade(cmd.Context(), dockerCli, options) + return runUpgrade(cmd.Context(), dockerCLI, options) }, - Annotations: map[string]string{"version": "1.26"}, + Annotations: map[string]string{"version": "1.26"}, + ValidArgsFunction: completeNames(dockerCLI, stateAny), // TODO(thaJeztah): should only complete for the first arg + DisableFlagsInUseLine: true, } flags := cmd.Flags() - loadPullFlags(dockerCli, &options, flags) + flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") + // TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30 + flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)") + _ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed") flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") return cmd } func runUpgrade(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) error { - p, _, err := dockerCLI.Client().PluginInspectWithRaw(ctx, opts.localName) + res, err := dockerCLI.Client().PluginInspect(ctx, opts.localName, client.PluginInspectOptions{}) if err != nil { - return errors.Errorf("error reading plugin data: %v", err) + return fmt.Errorf("error reading plugin data: %w", err) } - if p.Enabled { - return errors.Errorf("the plugin must be disabled before upgrading") + if res.Plugin.Enabled { + return errors.New("the plugin must be disabled before upgrading") } - opts.localName = p.Name + opts.localName = res.Plugin.Name if opts.remote == "" { - opts.remote = p.PluginReference + opts.remote = res.Plugin.PluginReference } remote, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { - return errors.Wrap(err, "error parsing remote upgrade image reference") + return fmt.Errorf("error parsing remote upgrade image reference: %w", err) } remote = reference.TagNameOnly(remote) - old, err := reference.ParseNormalizedNamed(p.PluginReference) + old, err := reference.ParseNormalizedNamed(res.Plugin.PluginReference) if err != nil { - return errors.Wrap(err, "error parsing current image reference") + return fmt.Errorf("error parsing current image reference: %w", err) } old = reference.TagNameOnly(old) - _, _ = fmt.Fprintf(dockerCLI.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote)) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Upgrading plugin %s from %s to %s\n", res.Plugin.Name, reference.FamiliarString(old), reference.FamiliarString(remote)) if !opts.skipRemoteCheck && remote.String() != old.String() { r, err := prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), "Plugin images do not match, are you sure?") if err != nil { @@ -73,19 +78,18 @@ func runUpgrade(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) } } - options, err := buildPullConfig(ctx, dockerCLI, opts, "plugin upgrade") + options, err := buildPullConfig(dockerCLI, opts) if err != nil { return err } - responseBody, err := dockerCLI.Client().PluginUpgrade(ctx, opts.localName, options) + responseBody, err := dockerCLI.Client().PluginUpgrade(ctx, opts.localName, client.PluginUpgradeOptions(options)) if err != nil { - if strings.Contains(err.Error(), "target is image") { - return errors.New(err.Error() + " - Use `docker image pull`") - } return err } - defer responseBody.Close() + defer func() { + _ = responseBody.Close() + }() if err := jsonstream.Display(ctx, responseBody, dockerCLI.Out()); err != nil { return err } diff --git a/cli/command/plugin/upgrade_test.go b/cli/command/plugin/upgrade_test.go index 21986cdceffc..0eb8f98bb712 100644 --- a/cli/command/plugin/upgrade_test.go +++ b/cli/command/plugin/upgrade_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/plugin" + "github.com/moby/moby/client" "gotest.tools/v3/golden" ) @@ -16,16 +17,18 @@ func TestUpgradePromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - pluginUpgradeFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { + pluginUpgradeFunc: func(name string, options client.PluginUpgradeOptions) (client.PluginUpgradeResult, error) { return nil, errors.New("should not be called") }, - pluginInspectFunc: func(name string) (*types.Plugin, []byte, error) { - return &types.Plugin{ - ID: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078", - Name: "foo/bar", - Enabled: false, - PluginReference: "localhost:5000/foo/bar:v0.1.0", - }, []byte{}, nil + pluginInspectFunc: func(name string) (client.PluginInspectResult, error) { + return client.PluginInspectResult{ + Plugin: plugin.Plugin{ + ID: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078", + Name: "foo/bar", + Enabled: false, + PluginReference: "localhost:5000/foo/bar:v0.1.0", + }, + }, nil }, }) cmd := newUpgradeCommand(cli) diff --git a/cli/command/registry.go b/cli/command/registry.go index be49d85b6bb8..87c81c6da5da 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -2,6 +2,7 @@ package command import ( "context" + "errors" "fmt" "os" "runtime" @@ -15,9 +16,9 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/internal/tui" - registrytypes "github.com/docker/docker/api/types/registry" + "github.com/moby/moby/api/pkg/authconfig" + registrytypes "github.com/moby/moby/api/types/registry" "github.com/morikuni/aec" - "github.com/pkg/errors" ) const ( @@ -31,41 +32,14 @@ const ( // authConfigKey is the key used to store credentials for Docker Hub. It is // a copy of [registry.IndexServer]. // -// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer const authConfigKey = "https://index.docker.io/v1/" -// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info -// for the given command to prompt the user for username and password. -func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig { - configKey := getAuthConfigKey(index.Name) - isDefaultRegistry := configKey == authConfigKey || index.Official - return func(ctx context.Context) (string, error) { - _, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName) - authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry) - if err != nil { - _, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", configKey, err) - } - - select { - case <-ctx.Done(): - return "", ctx.Err() - default: - } - - authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, configKey) - if err != nil { - return "", err - } - return registrytypes.EncodeAuthConfig(authConfig) - } -} - // ResolveAuthConfig returns auth-config for the given registry from the // credential-store. It returns an empty AuthConfig if no credentials were // found. // -// It is similar to [registry.ResolveAuthConfig], but uses the credentials- -// store, instead of looking up credentials from a map. +// Deprecated: this function is no longer used, and will be removed in the next release. func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig { configKey := index.Name if index.Official { @@ -73,7 +47,16 @@ func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInf } a, _ := cfg.GetAuthConfig(configKey) - return registrytypes.AuthConfig(a) + return registrytypes.AuthConfig{ + Username: a.Username, + Password: a.Password, + ServerAddress: a.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: a.Auth, + IdentityToken: a.IdentityToken, + RegistryToken: a.RegistryToken, + } } // GetDefaultAuthConfig gets the default auth config given a serverAddress @@ -82,36 +65,27 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve if !isDefaultRegistry { serverAddress = credentials.ConvertToHostname(serverAddress) } - authconfig := configtypes.AuthConfig{} + authCfg := configtypes.AuthConfig{} var err error if checkCredStore { - authconfig, err = cfg.GetAuthConfig(serverAddress) + authCfg, err = cfg.GetAuthConfig(serverAddress) if err != nil { return registrytypes.AuthConfig{ ServerAddress: serverAddress, }, err } } - authconfig.ServerAddress = serverAddress - authconfig.IdentityToken = "" - return registrytypes.AuthConfig(authconfig), nil -} - -// ConfigureAuth handles prompting of user's username and password if needed. -// -// Deprecated: use [PromptUserForCredentials] instead. -func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { - defaultUsername := authConfig.Username - serverAddress := authConfig.ServerAddress - newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress) - if err != nil { - return err - } + return registrytypes.AuthConfig{ + Username: authCfg.Username, + Password: authCfg.Password, + ServerAddress: serverAddress, - authConfig.Username = newAuthConfig.Username - authConfig.Password = newAuthConfig.Password - return nil + // TODO(thaJeztah): Are these expected to be included? + Auth: authCfg.Auth, + IdentityToken: "", + RegistryToken: authCfg.RegistryToken, + }, nil } // PromptUserForCredentials handles the CLI prompt for the user to input @@ -166,7 +140,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword argUser = defaultUsername } if argUser == "" { - return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required") + return registrytypes.AuthConfig{}, errors.New("error: username is required") } } @@ -198,7 +172,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword } _, _ = fmt.Fprintln(cli.Out()) if argPassword == "" { - return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required") + return registrytypes.AuthConfig{}, errors.New("error: password is required") } } @@ -209,37 +183,37 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword }, nil } -// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete -// image. The auth configuration is serialized as a base64url encoded RFC4648, -// section 5) JSON string for sending through the X-Registry-Auth header. +// RetrieveAuthTokenFromImage retrieves an encoded auth token given a +// complete image reference. The auth configuration is serialized as a +// base64url encoded ([RFC 4648, Section 5]) JSON string for sending through +// the "X-Registry-Auth" header. // -// For details on base64url encoding, see: -// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5 +// [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5 func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) { - // Retrieve encoded auth token from the image reference - authConfig, err := resolveAuthConfigFromImage(cfg, image) + registryRef, err := reference.ParseNormalizedNamed(image) if err != nil { return "", err } - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + configKey := getAuthConfigKey(reference.Domain(registryRef)) + authConfig, err := cfg.GetAuthConfig(configKey) if err != nil { return "", err } - return encodedAuth, nil -} -// resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (registrytypes.AuthConfig, error) { - registryRef, err := reference.ParseNormalizedNamed(image) - if err != nil { - return registrytypes.AuthConfig{}, err - } - configKey := getAuthConfigKey(reference.Domain(registryRef)) - a, err := cfg.GetAuthConfig(configKey) + encodedAuth, err := authconfig.Encode(registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { - return registrytypes.AuthConfig{}, err + return "", err } - return registrytypes.AuthConfig(a), nil + return encodedAuth, nil } // getAuthConfigKey special-cases using the full index address of the official @@ -248,8 +222,8 @@ func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (regis // It is similar to [registry.GetAuthConfigKey], but does not require on // [registrytypes.IndexInfo] as intermediate. // -// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker/registry#GetAuthConfigKey -// [registrytypes.IndexInfo]:https://pkg.go.dev/github.com/docker/docker/api/types/registry#IndexInfo +// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey +// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo func getAuthConfigKey(domainName string) string { if domainName == "docker.io" || domainName == "index.docker.io" { return authConfigKey diff --git a/cli/command/registry/formatter_search.go b/cli/command/registry/formatter_search.go index 23f56ac69e63..6be72269e382 100644 --- a/cli/command/registry/formatter_search.go +++ b/cli/command/registry/formatter_search.go @@ -5,7 +5,8 @@ import ( "strings" "github.com/docker/cli/cli/command/formatter" - registrytypes "github.com/docker/docker/api/types/registry" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" ) const ( @@ -16,8 +17,8 @@ const ( automatedHeader = "AUTOMATED" ) -// NewSearchFormat returns a Format for rendering using a network Context -func NewSearchFormat(source string) formatter.Format { +// newFormat returns a Format for rendering using a searchContext. +func newFormat(source string) formatter.Format { switch source { case "", formatter.TableFormatKey: return defaultSearchTableFormat @@ -25,25 +26,29 @@ func NewSearchFormat(source string) formatter.Format { return formatter.Format(source) } -// SearchWrite writes the context -func SearchWrite(ctx formatter.Context, results []registrytypes.SearchResult) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, result := range results { - searchCtx := &searchContext{trunc: ctx.Trunc, s: result} - if err := format(searchCtx); err != nil { +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, results client.ImageSearchResult) error { + searchCtx := &searchContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Name": formatter.NameHeader, + "Description": formatter.DescriptionHeader, + "StarCount": starsHeader, + "IsOfficial": officialHeader, + }, + }, + } + return fmtCtx.Write(searchCtx, func(format func(subContext formatter.SubContext) error) error { + for _, result := range results.Items { + if err := format(&searchContext{ + trunc: fmtCtx.Trunc, + s: result, + }); err != nil { return err } } return nil - } - searchCtx := searchContext{} - searchCtx.Header = formatter.SubHeaderContext{ - "Name": formatter.NameHeader, - "Description": formatter.DescriptionHeader, - "StarCount": starsHeader, - "IsOfficial": officialHeader, - } - return ctx.Write(&searchCtx, render) + }) } type searchContext struct { diff --git a/cli/command/registry/formatter_search_test.go b/cli/command/registry/formatter_search_test.go index 082b0377e030..70787c9e90c6 100644 --- a/cli/command/registry/formatter_search_test.go +++ b/cli/command/registry/formatter_search_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - registrytypes "github.com/docker/docker/api/types/registry" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -157,12 +158,12 @@ func TestSearchContextWrite(t *testing.T) { }, { doc: "Table format", - format: NewSearchFormat("table"), + format: newFormat("table"), expected: string(golden.Get(t, "search-context-write-table.golden")), }, { doc: "Table format, single column", - format: NewSearchFormat("table {{.Name}}"), + format: newFormat("table {{.Name}}"), expected: `NAME result1 result2 @@ -170,29 +171,31 @@ result2 }, { doc: "Custom format, single field", - format: NewSearchFormat("{{.Name}}"), + format: newFormat("{{.Name}}"), expected: `result1 result2 `, }, { doc: "Custom Format, two columns", - format: NewSearchFormat("{{.Name}} {{.StarCount}}"), + format: newFormat("{{.Name}} {{.StarCount}}"), expected: `result1 5000 result2 5 `, }, } - results := []registrytypes.SearchResult{ - {Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true}, - {Name: "result2", Description: "Not official", StarCount: 5}, + results := client.ImageSearchResult{ + Items: []registrytypes.SearchResult{ + {Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true}, + {Name: "result2", Description: "Not official", StarCount: 5}, + }, } for _, tc := range cases { t.Run(tc.doc, func(t *testing.T) { var out bytes.Buffer - err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results) + err := formatWrite(formatter.Context{Format: tc.format, Output: &out}, results) if tc.expectedErr != "" { assert.Check(t, is.Error(err, tc.expectedErr)) } else { diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 3679e51eddd5..17d0d883ba1d 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -2,28 +2,30 @@ package registry import ( "context" + "errors" "fmt" "io" - "os" - "strconv" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/config/configfile" configtypes "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/internal/commands" "github.com/docker/cli/internal/oauth/manager" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/tui" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/docker/docker/registry" - "github.com/pkg/errors" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) +func init() { + commands.Register(newLoginCommand) +} + type loginOptions struct { serverAddress string user string @@ -31,8 +33,8 @@ type loginOptions struct { passwordStdin bool } -// NewLoginCommand creates a new `docker login` command -func NewLoginCommand(dockerCLI command.Cli) *cobra.Command { +// newLoginCommand creates a new `docker login` command +func newLoginCommand(dockerCLI command.Cli) *cobra.Command { var opts loginOptions cmd := &cobra.Command{ @@ -52,7 +54,8 @@ func NewLoginCommand(dockerCLI command.Cli) *cobra.Command { Annotations: map[string]string{ "category-top": "8", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -157,38 +160,32 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth _, _ = fmt.Fprint(dockerCLI.Err(), "\n\n") - response, err := dockerCLI.Client().RegistryLogin(ctx, authConfig) + resp, err := dockerCLI.Client().RegistryLogin(ctx, client.RegistryLoginOptions{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { - if cerrdefs.IsUnauthorized(err) { + if errdefs.IsUnauthorized(err) { _, _ = fmt.Fprintln(dockerCLI.Err(), "Stored credentials invalid or expired") } else { _, _ = fmt.Fprintln(dockerCLI.Err(), "Login did not succeed, error:", err) } + // TODO(thaJeztah): should this return the error here, or is there a reason for continuing? } - if response.IdentityToken != "" { + if resp.Auth.IdentityToken != "" { authConfig.Password = "" - authConfig.IdentityToken = response.IdentityToken + authConfig.IdentityToken = resp.Auth.IdentityToken } if err := storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil { return "", err } - return response.Status, err -} - -const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN" - -func isOauthLoginDisabled() bool { - if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" { - enabled, err := strconv.ParseBool(v) - if err != nil { - return false - } - return enabled - } - return false + return resp.Auth.Status, err } func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, defaultUsername, serverAddress string) (msg string, _ error) { @@ -200,11 +197,11 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de // will hit this if you attempt docker login from mintty where stdin // is a pipe, not a character based console. if (opts.user == "" || opts.password == "") && !dockerCLI.In().IsTerminal() { - return "", errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") + return "", errors.New("error: cannot perform an interactive login from a non TTY device") } // If we're logging into the index server and the user didn't provide a username or password, use the device flow - if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() { + if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" { var err error msg, err = loginWithDeviceCodeFlow(ctx, dockerCLI) // if the error represents a failure to initiate the device-code flow, @@ -225,20 +222,26 @@ func loginWithUsernameAndPassword(ctx context.Context, dockerCLI command.Cli, op return "", err } - response, err := loginWithRegistry(ctx, dockerCLI.Client(), authConfig) + res, err := loginWithRegistry(ctx, dockerCLI.Client(), client.RegistryLoginOptions{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { return "", err } - if response.IdentityToken != "" { + if res.Auth.IdentityToken != "" { authConfig.Password = "" - authConfig.IdentityToken = response.IdentityToken + authConfig.IdentityToken = res.Auth.IdentityToken } if err = storeCredentials(dockerCLI.ConfigFile(), authConfig); err != nil { return "", err } - return response.Status, nil + return res.Auth.Status, nil } func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg string, _ error) { @@ -248,53 +251,90 @@ func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg st return "", err } - response, err := loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig(*authConfig)) + response, err := loginWithRegistry(ctx, dockerCLI.Client(), client.RegistryLoginOptions{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + // Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { return "", err } - if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil { + if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }); err != nil { return "", err } - return response.Status, nil + return response.Auth.Status, nil } func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error { creds := cfg.GetCredentialsStore(authConfig.ServerAddress) - if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil { - return errors.Errorf("Error saving credentials: %v", err) + if err := creds.Store(configtypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }); err != nil { + return fmt.Errorf("error saving credentials: %v", err) } return nil } -func loginWithRegistry(ctx context.Context, apiClient client.SystemAPIClient, authConfig registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) { - response, err := apiClient.RegistryLogin(ctx, authConfig) +func loginWithRegistry(ctx context.Context, apiClient client.SystemAPIClient, options client.RegistryLoginOptions) (client.RegistryLoginResult, error) { + res, err := apiClient.RegistryLogin(ctx, options) if err != nil { if client.IsErrConnectionFailed(err) { // daemon isn't responding; attempt to login client side. - return loginClientSide(ctx, authConfig) + return loginClientSide(ctx, options) } - return nil, err + return client.RegistryLoginResult{}, err } - return &response, nil + return res, nil } -func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*registrytypes.AuthenticateOKBody, error) { +func loginClientSide(ctx context.Context, options client.RegistryLoginOptions) (client.RegistryLoginResult, error) { svc, err := registry.NewService(registry.ServiceOptions{}) if err != nil { - return nil, err + return client.RegistryLoginResult{}, err } - _, token, err := svc.Auth(ctx, &auth, command.UserAgent()) + auth := registrytypes.AuthConfig{ + Username: options.Username, + Password: options.Password, + ServerAddress: options.ServerAddress, + IdentityToken: options.IdentityToken, + RegistryToken: options.RegistryToken, + } + + token, err := svc.Auth(ctx, &auth, command.UserAgent()) if err != nil { - return nil, err + return client.RegistryLoginResult{}, err } - return ®istrytypes.AuthenticateOKBody{ - Status: "Login Succeeded", - IdentityToken: token, + return client.RegistryLoginResult{ + Auth: registrytypes.AuthResponse{ + Status: "Login Succeeded", + IdentityToken: token, + }, }, nil } diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 58ca73c6a6cf..41f833483503 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -13,11 +13,10 @@ import ( configtypes "github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/prompt" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/test" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" - "github.com/docker/docker/registry" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -34,23 +33,23 @@ type fakeClient struct { client.Client } -func (*fakeClient) Info(context.Context) (system.Info, error) { - return system.Info{}, nil +func (*fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, nil } -func (*fakeClient) RegistryLogin(_ context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) { - if auth.Password == expiredPassword { - return registrytypes.AuthenticateOKBody{}, errors.New("Invalid Username or Password") +func (*fakeClient) RegistryLogin(_ context.Context, options client.RegistryLoginOptions) (client.RegistryLoginResult, error) { + if options.Password == expiredPassword { + return client.RegistryLoginResult{}, errors.New("invalid Username or Password") } - if auth.Password == useToken { - return registrytypes.AuthenticateOKBody{ - IdentityToken: auth.Password, + if options.Password == useToken { + return client.RegistryLoginResult{ + Auth: registrytypes.AuthResponse{IdentityToken: options.Password}, }, nil } - if auth.Username == unknownUser { - return registrytypes.AuthenticateOKBody{}, errors.New(errUnknownUser) + if options.Username == unknownUser { + return client.RegistryLoginResult{}, errors.New(errUnknownUser) } - return registrytypes.AuthenticateOKBody{}, nil + return client.RegistryLoginResult{}, nil } func TestLoginWithCredStoreCreds(t *testing.T) { @@ -128,7 +127,7 @@ func TestRunLogin(t *testing.T) { input: loginOptions{ serverAddress: "reg1", }, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, { doc: "store valid username and password", @@ -335,19 +334,19 @@ func TestLoginNonInteractive(t *testing.T) { doc: "error - w/o user w/o pass ", username: false, password: false, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, { doc: "error - w/ user w/o pass", username: true, password: false, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, { doc: "error - w/o user w/ pass", username: false, password: true, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, } @@ -404,13 +403,13 @@ func TestLoginNonInteractive(t *testing.T) { doc: "error - w/ user w/o pass", username: true, password: false, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, { doc: "error - w/o user w/ pass", username: false, password: true, - expectedErr: "Error: Cannot perform an interactive login from a non TTY device", + expectedErr: "error: cannot perform an interactive login from a non TTY device", }, } @@ -497,50 +496,6 @@ func TestLoginTermination(t *testing.T) { } } -func TestIsOauthLoginDisabled(t *testing.T) { - testCases := []struct { - envVar string - disabled bool - }{ - { - envVar: "", - disabled: false, - }, - { - envVar: "bork", - disabled: false, - }, - { - envVar: "0", - disabled: false, - }, - { - envVar: "false", - disabled: false, - }, - { - envVar: "true", - disabled: true, - }, - { - envVar: "TRUE", - disabled: true, - }, - { - envVar: "1", - disabled: true, - }, - } - - for _, tc := range testCases { - t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar) - - disabled := isOauthLoginDisabled() - - assert.Equal(t, disabled, tc.disabled) - } -} - func TestLoginValidateFlags(t *testing.T) { for _, tc := range []struct { name string @@ -584,7 +539,7 @@ func TestLoginValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewLoginCommand(test.NewFakeCli(&fakeClient{})) + cmd := newLoginCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 2af2cdad3f3f..16218f67d9a7 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -7,13 +7,18 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/internal/commands" "github.com/docker/cli/internal/oauth/manager" - "github.com/docker/docker/registry" + "github.com/docker/cli/internal/registry" "github.com/spf13/cobra" ) -// NewLogoutCommand creates a new `docker logout` command -func NewLogoutCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newLogoutCommand) +} + +// newLogoutCommand creates a new `docker logout` command +func newLogoutCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "logout [SERVER]", Short: "Log out from a registry", @@ -24,11 +29,12 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 0 { serverAddress = args[0] } - return runLogout(cmd.Context(), dockerCli, serverAddress) + return runLogout(cmd.Context(), dockerCLI, serverAddress) }, Annotations: map[string]string{ "category-top": "9", }, + DisableFlagsInUseLine: true, // TODO (thaJeztah) add completion for registries we have authentication stored for } diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go index 5007322fc86b..a99631758ad2 100644 --- a/cli/command/registry/search.go +++ b/cli/command/registry/search.go @@ -3,16 +3,23 @@ package registry import ( "context" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/internal/commands" "github.com/docker/cli/opts" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" + "github.com/moby/moby/api/pkg/authconfig" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) +func init() { + commands.Register(newSearchCommand) +} + type searchOptions struct { format string term string @@ -21,8 +28,8 @@ type searchOptions struct { filter opts.FilterOpt } -// NewSearchCommand creates a new `docker search` command -func NewSearchCommand(dockerCli command.Cli) *cobra.Command { +// newSearchCommand creates a new `docker search` command +func newSearchCommand(dockerCLI command.Cli) *cobra.Command { options := searchOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -31,11 +38,12 @@ func NewSearchCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.term = args[0] - return runSearch(cmd.Context(), dockerCli, options) + return runSearch(cmd.Context(), dockerCLI, options) }, Annotations: map[string]string{ "category-top": "10", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -49,27 +57,17 @@ func NewSearchCommand(dockerCli command.Cli) *cobra.Command { } func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions) error { - if options.filter.Value().Contains("is-automated") { + if _, ok := options.filter.Value()["is-automated"]; ok { _, _ = fmt.Fprintln(dockerCli.Err(), `WARNING: the "is-automated" filter is deprecated, and searching for "is-automated=true" will not yield any results in future.`) } - indexInfo, err := registry.ParseSearchIndexInfo(options.term) + encodedAuth, err := getAuth(dockerCli, options.term) if err != nil { return err } - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) - if err != nil { - return err - } - - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, indexInfo, "search") - } - results, err := dockerCli.Client().ImageSearch(ctx, options.term, registrytypes.SearchOptions{ + results, err := dockerCli.Client().ImageSearch(ctx, options.term, client.ImageSearchOptions{ RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, Filters: options.filter.Value(), Limit: options.limit, }) @@ -79,8 +77,51 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions searchCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewSearchFormat(options.format), + Format: newFormat(options.format), Trunc: !options.noTrunc, } - return SearchWrite(searchCtx, results) + return formatWrite(searchCtx, results) +} + +// authConfigKey is the key used to store credentials for Docker Hub. It is +// a copy of [registry.IndexServer]. +// +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer +const authConfigKey = "https://index.docker.io/v1/" + +// getAuth will use fetch auth based on the given search-term. If the search +// does not contain a hostname for the registry, it assumes Docker Hub is used, +// and resolves authentication for Docker Hub, otherwise it resolves authentication +// for the given registry. +func getAuth(dockerCLI command.Cli, reposName string) (encodedAuth string, err error) { + authCfgKey := splitReposSearchTerm(reposName) + if authCfgKey == "docker.io" || authCfgKey == "index.docker.io" { + authCfgKey = authConfigKey + } + + // Ignoring errors here, which was the existing behavior (likely + // "no credentials found"). We'll get an error when search failed, + // so fine to ignore in most situations. + authConfig, _ := dockerCLI.ConfigFile().GetAuthConfig(authCfgKey) + return authconfig.Encode(registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) +} + +// splitReposSearchTerm breaks a search term into an index name and remote name +func splitReposSearchTerm(reposName string) string { + nameParts := strings.SplitN(reposName, "/", 2) + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), + // use the default Docker Hub registry (docker.io) + return "docker.io" + } + return nameParts[0] } diff --git a/cli/command/registry_test.go b/cli/command/registry_test.go index 4c6bb9f89abb..91372ad56eb1 100644 --- a/cli/command/registry_test.go +++ b/cli/command/registry_test.go @@ -8,7 +8,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/configfile" configtypes "github.com/docker/cli/cli/config/types" - "github.com/docker/docker/api/types/registry" + "github.com/moby/moby/api/pkg/authconfig" + "github.com/moby/moby/api/types/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -58,14 +59,23 @@ func TestGetDefaultAuthConfig(t *testing.T) { }, } cfg := configfile.New("filename") - for _, authconfig := range testAuthConfigs { - assert.Check(t, cfg.GetCredentialsStore(authconfig.ServerAddress).Store(configtypes.AuthConfig(authconfig))) + for _, authConfig := range testAuthConfigs { + assert.Check(t, cfg.GetCredentialsStore(authConfig.ServerAddress).Store(configtypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + })) } for _, tc := range testCases { serverAddress := tc.inputServerAddress - authconfig, err := command.GetDefaultAuthConfig(cfg, tc.checkCredStore, serverAddress, serverAddress == "https://index.docker.io/v1/") + authCfg, err := command.GetDefaultAuthConfig(cfg, tc.checkCredStore, serverAddress, serverAddress == "https://index.docker.io/v1/") assert.NilError(t, err) - assert.Check(t, is.DeepEqual(tc.expectedAuthConfig, authconfig)) + assert.Check(t, is.DeepEqual(tc.expectedAuthConfig, authCfg)) } } @@ -78,8 +88,8 @@ func TestGetDefaultAuthConfig_HelperError(t *testing.T) { ServerAddress: serverAddress, } const isDefaultRegistry = false // registry is not "https://index.docker.io/v1/" - authconfig, err := command.GetDefaultAuthConfig(cfg, true, serverAddress, isDefaultRegistry) - assert.Check(t, is.DeepEqual(expectedAuthConfig, authconfig)) + authCfg, err := command.GetDefaultAuthConfig(cfg, true, serverAddress, isDefaultRegistry) + assert.Check(t, is.DeepEqual(expectedAuthConfig, authCfg)) assert.Check(t, is.ErrorContains(err, "docker-credential-fake-does-not-exist")) } @@ -185,9 +195,9 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) { imageRef := path.Join(tc.prefix, remoteRef) actual, err := command.RetrieveAuthTokenFromImage(&cfg, imageRef) assert.NilError(t, err) - ac, err := registry.DecodeAuthConfig(actual) + expectedAuthCfg, err := authconfig.Encode(tc.expectedAuthCfg) assert.NilError(t, err) - assert.Check(t, is.DeepEqual(*ac, tc.expectedAuthCfg)) + assert.Equal(t, actual, expectedAuthCfg) } }) } diff --git a/cli/command/secret/client_test.go b/cli/command/secret/client_test.go index 1b5a04c605fc..bd59383dfc95 100644 --- a/cli/command/secret/client_test.go +++ b/cli/command/secret/client_test.go @@ -3,42 +3,41 @@ package secret import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - secretCreateFunc func(context.Context, swarm.SecretSpec) (swarm.SecretCreateResponse, error) - secretInspectFunc func(context.Context, string) (swarm.Secret, []byte, error) - secretListFunc func(context.Context, swarm.SecretListOptions) ([]swarm.Secret, error) - secretRemoveFunc func(context.Context, string) error + secretCreateFunc func(context.Context, client.SecretCreateOptions) (client.SecretCreateResult, error) + secretInspectFunc func(context.Context, string, client.SecretInspectOptions) (client.SecretInspectResult, error) + secretListFunc func(context.Context, client.SecretListOptions) (client.SecretListResult, error) + secretRemoveFunc func(context.Context, string, client.SecretRemoveOptions) (client.SecretRemoveResult, error) } -func (c *fakeClient) SecretCreate(ctx context.Context, spec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { +func (c *fakeClient) SecretCreate(ctx context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) { if c.secretCreateFunc != nil { - return c.secretCreateFunc(ctx, spec) + return c.secretCreateFunc(ctx, options) } - return swarm.SecretCreateResponse{}, nil + return client.SecretCreateResult{}, nil } -func (c *fakeClient) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { +func (c *fakeClient) SecretInspect(ctx context.Context, id string, options client.SecretInspectOptions) (client.SecretInspectResult, error) { if c.secretInspectFunc != nil { - return c.secretInspectFunc(ctx, id) + return c.secretInspectFunc(ctx, id, options) } - return swarm.Secret{}, nil, nil + return client.SecretInspectResult{}, nil } -func (c *fakeClient) SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { +func (c *fakeClient) SecretList(ctx context.Context, options client.SecretListOptions) (client.SecretListResult, error) { if c.secretListFunc != nil { return c.secretListFunc(ctx, options) } - return []swarm.Secret{}, nil + return client.SecretListResult{}, nil } -func (c *fakeClient) SecretRemove(ctx context.Context, name string) error { +func (c *fakeClient) SecretRemove(ctx context.Context, name string, options client.SecretRemoveOptions) (client.SecretRemoveResult, error) { if c.secretRemoveFunc != nil { - return c.secretRemoveFunc(ctx, name) + return c.secretRemoveFunc(ctx, name, options) } - return nil + return client.SecretRemoveResult{}, nil } diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go index f31348a7e87f..7ff0e369489d 100644 --- a/cli/command/secret/cmd.go +++ b/cli/command/secret/cmd.go @@ -4,27 +4,33 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" + "github.com/docker/cli/internal/commands" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -// NewSecretCommand returns a cobra command for `secret` subcommands -func NewSecretCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newSecretCommand) +} + +// newSecretCommand returns a cobra command for `secret` subcommands. +func newSecretCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "secret", Short: "Manage Swarm secrets", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.25", "swarm": "manager", }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newSecretListCommand(dockerCli), - newSecretCreateCommand(dockerCli), - newSecretInspectCommand(dockerCli), - newSecretRemoveCommand(dockerCli), + newSecretListCommand(dockerCLI), + newSecretCreateCommand(dockerCLI), + newSecretInspectCommand(dockerCLI), + newSecretRemoveCommand(dockerCLI), ) return cmd } @@ -32,13 +38,13 @@ func NewSecretCommand(dockerCli command.Cli) *cobra.Command { // completeNames offers completion for swarm secrets func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().SecretList(cmd.Context(), swarm.SecretListOptions{}) + res, err := dockerCLI.Client().SecretList(cmd.Context(), client.SecretListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } var names []string - for _, secret := range list { - names = append(names, secret.ID) + for _, secret := range res.Items { + names = append(names, secret.Spec.Name) } return names, cobra.ShellCompDirectiveNoFileComp } diff --git a/cli/command/secret/create.go b/cli/command/secret/create.go index 07975247010d..c6ecfa23eec1 100644 --- a/cli/command/secret/create.go +++ b/cli/command/secret/create.go @@ -2,15 +2,16 @@ package secret import ( "context" + "errors" "fmt" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/moby/sys/sequential" - "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -22,7 +23,7 @@ type createOptions struct { labels opts.ListOpts } -func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { +func newSecretCreateCommand(dockerCLI command.Cli) *cobra.Command { options := createOptions{ labels: opts.NewListOpts(opts.ValidateLabel), } @@ -36,8 +37,26 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { if len(args) == 2 { options.file = args[1] } - return runSecretCreate(cmd.Context(), dockerCli, options) + return runSecretCreate(cmd.Context(), dockerCLI, options) }, + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + switch len(args) { + case 0: + // No completion for the first argument, which is the name for + // the new secret, 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.VarP(&options.labels, "label", "l", "Secret labels") @@ -49,17 +68,17 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error { - client := dockerCli.Client() +func runSecretCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { + apiClient := dockerCLI.Client() var secretData []byte if options.driver != "" { if options.file != "" { - return errors.Errorf("When using secret driver secret data must be empty") + return errors.New("when using secret driver secret data must be empty") } } else { var err error - secretData, err = readSecretData(dockerCli.In(), options.file) + secretData, err = readSecretData(dockerCLI.In(), options.file) if err != nil { return err } @@ -82,12 +101,14 @@ func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createO Name: options.templateDriver, } } - r, err := client.SecretCreate(ctx, spec) + r, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{ + Spec: spec, + }) if err != nil { return err } - _, _ = fmt.Fprintln(dockerCli.Out(), r.ID) + _, _ = fmt.Fprintln(dockerCLI.Out(), r.ID) return nil } diff --git a/cli/command/secret/create_test.go b/cli/command/secret/create_test.go index cb5b897a545b..4456b0f7f354 100644 --- a/cli/command/secret/create_test.go +++ b/cli/command/secret/create_test.go @@ -12,7 +12,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -22,7 +23,7 @@ const secretDataFile = "secret-create-with-name.golden" func TestSecretCreateErrors(t *testing.T) { testCases := []struct { args []string - secretCreateFunc func(context.Context, swarm.SecretSpec) (swarm.SecretCreateResponse, error) + secretCreateFunc func(context.Context, client.SecretCreateOptions) (client.SecretCreateResult, error) expectedError string }{ { @@ -35,8 +36,8 @@ func TestSecretCreateErrors(t *testing.T) { }, { args: []string{"name", filepath.Join("testdata", secretDataFile)}, - secretCreateFunc: func(_ context.Context, secretSpec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - return swarm.SecretCreateResponse{}, errors.New("error creating secret") + secretCreateFunc: func(context.Context, client.SecretCreateOptions) (client.SecretCreateResult, error) { + return client.SecretCreateResult{}, errors.New("error creating secret") }, expectedError: "error creating secret", }, @@ -68,12 +69,12 @@ func TestSecretCreateWithName(t *testing.T) { } cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - if !reflect.DeepEqual(spec, expected) { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec) + secretCreateFunc: func(_ context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) { + if !reflect.DeepEqual(options.Spec, expected) { + return client.SecretCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec) } - return swarm.SecretCreateResponse{ - ID: "ID-" + spec.Name, + return client.SecretCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) @@ -91,17 +92,17 @@ func TestSecretCreateWithDriver(t *testing.T) { const name = "secret-with-driver" cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - if spec.Name != name { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) + secretCreateFunc: func(_ context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) { + if options.Spec.Name != name { + return client.SecretCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) } - if spec.Driver.Name != expectedDriver.Name { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels) + if options.Spec.Driver.Name != expectedDriver.Name { + return client.SecretCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels) } - return swarm.SecretCreateResponse{ - ID: "ID-" + spec.Name, + return client.SecretCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) @@ -120,17 +121,17 @@ func TestSecretCreateWithTemplatingDriver(t *testing.T) { const name = "secret-with-template-driver" cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - if spec.Name != name { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) + secretCreateFunc: func(_ context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) { + if options.Spec.Name != name { + return client.SecretCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) } - if spec.Templating.Name != expectedDriver.Name { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels) + if options.Spec.Templating.Name != expectedDriver.Name { + return client.SecretCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels) } - return swarm.SecretCreateResponse{ - ID: "ID-" + spec.Name, + return client.SecretCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) @@ -150,17 +151,17 @@ func TestSecretCreateWithLabels(t *testing.T) { const name = "secret-with-labels" cli := test.NewFakeCli(&fakeClient{ - secretCreateFunc: func(_ context.Context, spec swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - if spec.Name != name { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name) + secretCreateFunc: func(_ context.Context, options client.SecretCreateOptions) (client.SecretCreateResult, error) { + if options.Spec.Name != name { + return client.SecretCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) } - if !reflect.DeepEqual(spec.Labels, expectedLabels) { - return swarm.SecretCreateResponse{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels) + if !reflect.DeepEqual(options.Spec.Labels, expectedLabels) { + return client.SecretCreateResult{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, options.Spec.Labels) } - return swarm.SecretCreateResponse{ - ID: "ID-" + spec.Name, + return client.SecretCreateResult{ + ID: "ID-" + options.Spec.Name, }, nil }, }) diff --git a/cli/command/secret/formatter.go b/cli/command/secret/formatter.go index e30547628665..5762165261f7 100644 --- a/cli/command/secret/formatter.go +++ b/cli/command/secret/formatter.go @@ -7,8 +7,9 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-units" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -28,8 +29,8 @@ Created at: {{.CreatedAt}} Updated at: {{.UpdatedAt}}` ) -// NewFormat returns a Format for rendering using a secret Context -func NewFormat(source string, quiet bool) formatter.Format { +// newFormat returns a Format for rendering using a secretContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return secretInspectPrettyTemplate @@ -42,32 +43,29 @@ func NewFormat(source string, quiet bool) formatter.Format { return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, secrets []swarm.Secret) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, secret := range secrets { +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, secrets client.SecretListResult) error { + sCtx := &secretContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": secretIDHeader, + "Name": formatter.NameHeader, + "Driver": formatter.DriverHeader, + "CreatedAt": secretCreatedHeader, + "UpdatedAt": secretUpdatedHeader, + "Labels": formatter.LabelsHeader, + }, + }, + } + return fmtCtx.Write(sCtx, func(format func(subContext formatter.SubContext) error) error { + for _, secret := range secrets.Items { secretCtx := &secretContext{s: secret} if err := format(secretCtx); err != nil { return err } } return nil - } - return ctx.Write(newSecretContext(), render) -} - -func newSecretContext() *secretContext { - sCtx := &secretContext{} - - sCtx.Header = formatter.SubHeaderContext{ - "ID": secretIDHeader, - "Name": formatter.NameHeader, - "Driver": formatter.DriverHeader, - "CreatedAt": secretCreatedHeader, - "UpdatedAt": secretUpdatedHeader, - "Labels": formatter.LabelsHeader, - } - return sCtx + }) } type secretContext struct { @@ -121,12 +119,12 @@ func (c *secretContext) Label(name string) string { return c.s.Spec.Annotations.Labels[name] } -// InspectFormatWrite renders the context for a list of secrets -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != secretInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// inspectFormatWrite renders the context for a list of secrets. +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != secretInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } - render := func(format func(subContext formatter.SubContext) error) error { + return fmtCtx.Write(&secretInspectContext{}, func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { secretI, _, err := getRef(ref) if err != nil { @@ -141,8 +139,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } } return nil - } - return ctx.Write(&secretInspectContext{}, render) + }) } type secretInspectContext struct { diff --git a/cli/command/secret/formatter_test.go b/cli/command/secret/formatter_test.go index 8ede765a57be..4a3ac1daa6c5 100644 --- a/cli/command/secret/formatter_test.go +++ b/cli/command/secret/formatter_test.go @@ -6,7 +6,8 @@ import ( "time" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -27,21 +28,21 @@ func TestSecretContextFormatWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `ID NAME DRIVER CREATED UPDATED 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 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME passwords id_rsa `, }, { - formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)}, `1-passwords 2-id_rsa `, @@ -65,7 +66,7 @@ id_rsa var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, secrets); err != nil { + if err := formatWrite(tc.context, client.SecretListResult{Items: secrets}); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go index 22fed4b6e7a1..4c2718cfe347 100644 --- a/cli/command/secret/inspect.go +++ b/cli/command/secret/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package secret @@ -12,6 +12,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -21,7 +22,7 @@ type inspectOptions struct { pretty bool } -func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { +func newSecretInspectCommand(dockerCLI command.Cli) *cobra.Command { opts := inspectOptions{} cmd := &cobra.Command{ Use: "inspect [OPTIONS] SECRET [SECRET...]", @@ -29,11 +30,10 @@ func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.names = args - return runSecretInspect(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runSecretInspect(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -41,30 +41,30 @@ func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runSecretInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" } getRef := func(id string) (any, []byte, error) { - return client.SecretInspectWithRaw(ctx, id) + res, err := apiClient.SecretInspect(ctx, id, client.SecretInspectOptions{}) + return res.Secret, res.Raw, err } - f := opts.format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f, false), + Output: dockerCLI.Out(), + Format: newFormat(opts.format, false), } - if err := InspectFormatWrite(secretCtx, opts.names, getRef); err != nil { + if err := inspectFormatWrite(secretCtx, opts.names, getRef); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/secret/inspect_test.go b/cli/command/secret/inspect_test.go index cd440558398b..a46d02230f65 100644 --- a/cli/command/secret/inspect_test.go +++ b/cli/command/secret/inspect_test.go @@ -10,7 +10,7 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -19,7 +19,7 @@ func TestSecretInspectErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - secretInspectFunc func(ctx context.Context, secretID string) (swarm.Secret, []byte, error) + secretInspectFunc func(ctx context.Context, secretID string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) expectedError string }{ { @@ -27,8 +27,8 @@ func TestSecretInspectErrors(t *testing.T) { }, { args: []string{"foo"}, - secretInspectFunc: func(_ context.Context, secretID string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, nil, errors.New("error while inspecting the secret") + secretInspectFunc: func(_ context.Context, secretID string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { + return client.SecretInspectResult{}, errors.New("error while inspecting the secret") }, expectedError: "error while inspecting the secret", }, @@ -41,11 +41,13 @@ func TestSecretInspectErrors(t *testing.T) { }, { args: []string{"foo", "bar"}, - secretInspectFunc: func(_ context.Context, secretID string) (swarm.Secret, []byte, error) { + secretInspectFunc: func(_ context.Context, secretID string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { if secretID == "foo" { - return *builders.Secret(builders.SecretName("foo")), nil, nil + return client.SecretInspectResult{ + Secret: *builders.Secret(builders.SecretName("foo")), + }, nil } - return swarm.Secret{}, nil, errors.New("error while inspecting the secret") + return client.SecretInspectResult{}, errors.New("error while inspecting the secret") }, expectedError: "error while inspecting the secret", }, @@ -70,25 +72,29 @@ func TestSecretInspectWithoutFormat(t *testing.T) { testCases := []struct { name string args []string - secretInspectFunc func(ctx context.Context, secretID string) (swarm.Secret, []byte, error) + secretInspectFunc func(ctx context.Context, secretID string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) }{ { name: "single-secret", args: []string{"foo"}, - secretInspectFunc: func(_ context.Context, name string) (swarm.Secret, []byte, error) { + secretInspectFunc: func(_ context.Context, name string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { if name != "foo" { - return swarm.Secret{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) + return client.SecretInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) } - return *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), nil, nil + return client.SecretInspectResult{ + Secret: *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), + }, nil }, }, { name: "multiple-secrets-with-labels", args: []string{"foo", "bar"}, - secretInspectFunc: func(_ context.Context, name string) (swarm.Secret, []byte, error) { - return *builders.Secret(builders.SecretID("ID-"+name), builders.SecretName(name), builders.SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil + secretInspectFunc: func(_ context.Context, name string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { + return client.SecretInspectResult{ + Secret: *builders.Secret(builders.SecretID("ID-"+name), builders.SecretName(name), builders.SecretLabels(map[string]string{ + "label1": "label-foo", + })), + }, nil }, }, } @@ -106,16 +112,21 @@ func TestSecretInspectWithoutFormat(t *testing.T) { } func TestSecretInspectWithFormat(t *testing.T) { - secretInspectFunc := func(_ context.Context, name string) (swarm.Secret, []byte, error) { - return *builders.Secret(builders.SecretName("foo"), builders.SecretLabels(map[string]string{ - "label1": "label-foo", - })), nil, nil + secretInspectFunc := func(_ context.Context, name string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { + return client.SecretInspectResult{ + Secret: *builders.Secret( + builders.SecretName("foo"), + builders.SecretLabels(map[string]string{ + "label1": "label-foo", + }), + ), + }, nil } testCases := []struct { name string format string args []string - secretInspectFunc func(_ context.Context, name string) (swarm.Secret, []byte, error) + secretInspectFunc func(_ context.Context, name string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) }{ { name: "simple-template", @@ -147,21 +158,23 @@ func TestSecretInspectWithFormat(t *testing.T) { func TestSecretInspectPretty(t *testing.T) { testCases := []struct { name string - secretInspectFunc func(context.Context, string) (swarm.Secret, []byte, error) + secretInspectFunc func(context.Context, string, client.SecretInspectOptions) (client.SecretInspectResult, error) }{ { name: "simple", - secretInspectFunc: func(_ context.Context, id string) (swarm.Secret, []byte, error) { - return *builders.Secret( - builders.SecretLabels(map[string]string{ - "lbl1": "value1", - }), - builders.SecretID("secretID"), - builders.SecretName("secretName"), - builders.SecretDriver("driver"), - builders.SecretCreatedAt(time.Time{}), - builders.SecretUpdatedAt(time.Time{}), - ), []byte{}, nil + secretInspectFunc: func(_ context.Context, id string, _ client.SecretInspectOptions) (client.SecretInspectResult, error) { + return client.SecretInspectResult{ + Secret: *builders.Secret( + builders.SecretLabels(map[string]string{ + "lbl1": "value1", + }), + builders.SecretID("secretID"), + builders.SecretName("secretName"), + builders.SecretDriver("driver"), + builders.SecretCreatedAt(time.Time{}), + builders.SecretUpdatedAt(time.Time{}), + ), + }, nil }, }, } diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go index 65d8f23e0e8c..d68b52165c25 100644 --- a/cli/command/secret/ls.go +++ b/cli/command/secret/ls.go @@ -9,8 +9,8 @@ import ( "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -20,7 +20,7 @@ type listOptions struct { filter opts.FilterOpt } -func newSecretListCommand(dockerCli command.Cli) *cobra.Command { +func newSecretListCommand(dockerCLI command.Cli) *cobra.Command { options := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -29,11 +29,10 @@ func newSecretListCommand(dockerCli command.Cli) *cobra.Command { Short: "List secrets", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runSecretList(cmd.Context(), dockerCli, options) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runSecretList(cmd.Context(), dockerCLI, options) }, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -44,29 +43,29 @@ func newSecretListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() +func runSecretList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() - secrets, err := client.SecretList(ctx, swarm.SecretListOptions{Filters: options.filter.Value()}) + res, err := apiClient.SecretList(ctx, client.SecretListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 { - if len(dockerCli.ConfigFile().SecretFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().SecretFormat + if len(dockerCLI.ConfigFile().SecretFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().SecretFormat } else { format = formatter.TableFormatKey } } - sort.Slice(secrets, func(i, j int) bool { - return sortorder.NaturalLess(secrets[i].Spec.Name, secrets[j].Spec.Name) + sort.Slice(res.Items, func(i, j int) bool { + return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name) }) secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), } - return FormatWrite(secretCtx, secrets) + return formatWrite(secretCtx, res) } diff --git a/cli/command/secret/ls_test.go b/cli/command/secret/ls_test.go index bd21b8c9451a..d8476894791a 100644 --- a/cli/command/secret/ls_test.go +++ b/cli/command/secret/ls_test.go @@ -10,16 +10,16 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" ) func TestSecretListErrors(t *testing.T) { testCases := []struct { args []string - secretListFunc func(context.Context, swarm.SecretListOptions) ([]swarm.Secret, error) + secretListFunc func(context.Context, client.SecretListOptions) (client.SecretListResult, error) expectedError string }{ { @@ -27,8 +27,8 @@ func TestSecretListErrors(t *testing.T) { expectedError: "accepts no argument", }, { - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{}, errors.New("error listing secrets") + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + return client.SecretListResult{}, errors.New("error listing secrets") }, expectedError: "error listing secrets", }, @@ -48,28 +48,30 @@ func TestSecretListErrors(t *testing.T) { func TestSecretList(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *builders.Secret(builders.SecretID("ID-1-foo"), - builders.SecretName("1-foo"), - builders.SecretVersion(swarm.Version{Index: 10}), - builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), - builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *builders.Secret(builders.SecretID("ID-10-foo"), - builders.SecretName("10-foo"), - builders.SecretVersion(swarm.Version{Index: 11}), - builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), - builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - builders.SecretDriver("driver"), - ), - *builders.Secret(builders.SecretID("ID-2-foo"), - builders.SecretName("2-foo"), - builders.SecretVersion(swarm.Version{Index: 11}), - builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), - builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - builders.SecretDriver("driver"), - ), + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + return client.SecretListResult{ + Items: []swarm.Secret{ + *builders.Secret(builders.SecretID("ID-1-foo"), + builders.SecretName("1-foo"), + builders.SecretVersion(swarm.Version{Index: 10}), + builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), + builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *builders.Secret(builders.SecretID("ID-10-foo"), + builders.SecretName("10-foo"), + builders.SecretVersion(swarm.Version{Index: 11}), + builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), + builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + builders.SecretDriver("driver"), + ), + *builders.Secret(builders.SecretID("ID-2-foo"), + builders.SecretName("2-foo"), + builders.SecretVersion(swarm.Version{Index: 11}), + builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), + builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + builders.SecretDriver("driver"), + ), + }, }, nil }, }) @@ -80,12 +82,14 @@ func TestSecretList(t *testing.T) { func TestSecretListWithQuietOption(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), - *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ - "label": "label-bar", - })), + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + return client.SecretListResult{ + Items: []swarm.Secret{ + *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), + *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -97,12 +101,14 @@ func TestSecretListWithQuietOption(t *testing.T) { func TestSecretListWithConfigFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), - *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ - "label": "label-bar", - })), + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + return client.SecretListResult{ + Items: []swarm.Secret{ + *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), + *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -116,12 +122,14 @@ func TestSecretListWithConfigFormat(t *testing.T) { func TestSecretListWithFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - return []swarm.Secret{ - *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), - *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ - "label": "label-bar", - })), + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + return client.SecretListResult{ + Items: []swarm.Secret{ + *builders.Secret(builders.SecretID("ID-foo"), builders.SecretName("foo")), + *builders.Secret(builders.SecretID("ID-bar"), builders.SecretName("bar"), builders.SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, }, nil }, }) @@ -133,22 +141,24 @@ func TestSecretListWithFormat(t *testing.T) { func TestSecretListWithFilter(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - secretListFunc: func(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]), "foo") - assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0])) - return []swarm.Secret{ - *builders.Secret(builders.SecretID("ID-foo"), - builders.SecretName("foo"), - builders.SecretVersion(swarm.Version{Index: 10}), - builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), - builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), - *builders.Secret(builders.SecretID("ID-bar"), - builders.SecretName("bar"), - builders.SecretVersion(swarm.Version{Index: 11}), - builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), - builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), - ), + secretListFunc: func(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { + assert.Check(t, options.Filters["name"]["foo"]) + assert.Check(t, options.Filters["label"]["lbl1=Label-bar"]) + return client.SecretListResult{ + Items: []swarm.Secret{ + *builders.Secret(builders.SecretID("ID-foo"), + builders.SecretName("foo"), + builders.SecretVersion(swarm.Version{Index: 10}), + builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), + builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *builders.Secret(builders.SecretID("ID-bar"), + builders.SecretName("bar"), + builders.SecretVersion(swarm.Version{Index: 11}), + builders.SecretCreatedAt(time.Now().Add(-2*time.Hour)), + builders.SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + }, }, nil }, }) diff --git a/cli/command/secret/remove.go b/cli/command/secret/remove.go index 2a2764a8e318..24df99cfc973 100644 --- a/cli/command/secret/remove.go +++ b/cli/command/secret/remove.go @@ -7,6 +7,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -26,9 +27,8 @@ func newSecretRemoveCommand(dockerCLI command.Cli) *cobra.Command { } return runRemove(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, } } @@ -37,7 +37,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, opts removeOptions) e var errs []error for _, name := range opts.names { - if err := apiClient.SecretRemove(ctx, name); err != nil { + if _, err := apiClient.SecretRemove(ctx, name, client.SecretRemoveOptions{}); err != nil { errs = append(errs, err) continue } diff --git a/cli/command/secret/remove_test.go b/cli/command/secret/remove_test.go index ef343d9b7b89..84535b756419 100644 --- a/cli/command/secret/remove_test.go +++ b/cli/command/secret/remove_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,7 +16,7 @@ import ( func TestSecretRemoveErrors(t *testing.T) { testCases := []struct { args []string - secretRemoveFunc func(context.Context, string) error + secretRemoveFunc func(context.Context, string, client.SecretRemoveOptions) (client.SecretRemoveResult, error) expectedError string }{ { @@ -24,8 +25,8 @@ func TestSecretRemoveErrors(t *testing.T) { }, { args: []string{"foo"}, - secretRemoveFunc: func(_ context.Context, name string) error { - return errors.New("error removing secret") + secretRemoveFunc: func(_ context.Context, name string, _ client.SecretRemoveOptions) (client.SecretRemoveResult, error) { + return client.SecretRemoveResult{}, errors.New("error removing secret") }, expectedError: "error removing secret", }, @@ -47,9 +48,9 @@ func TestSecretRemoveWithName(t *testing.T) { names := []string{"foo", "bar"} var removedSecrets []string cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(_ context.Context, name string) error { + secretRemoveFunc: func(_ context.Context, name string, _ client.SecretRemoveOptions) (client.SecretRemoveResult, error) { removedSecrets = append(removedSecrets, name) - return nil + return client.SecretRemoveResult{}, nil }, }) cmd := newSecretRemoveCommand(cli) @@ -64,12 +65,12 @@ func TestSecretRemoveContinueAfterError(t *testing.T) { var removedSecrets []string cli := test.NewFakeCli(&fakeClient{ - secretRemoveFunc: func(_ context.Context, name string) error { + secretRemoveFunc: func(_ context.Context, name string, _ client.SecretRemoveOptions) (client.SecretRemoveResult, error) { removedSecrets = append(removedSecrets, name) if name == "foo" { - return errors.New("error removing secret: " + name) + return client.SecretRemoveResult{}, errors.New("error removing secret: " + name) } - return nil + return client.SecretRemoveResult{}, nil }, }) diff --git a/cli/command/service/client_test.go b/cli/command/service/client_test.go index 8853fa21d378..6e73940a8887 100644 --- a/cli/command/service/client_test.go +++ b/cli/command/service/client_test.go @@ -4,73 +4,73 @@ import ( "context" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) - serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) - serviceListFunc func(context.Context, swarm.ServiceListOptions) ([]swarm.Service, error) - taskListFunc func(context.Context, swarm.TaskListOptions) ([]swarm.Task, error) - infoFunc func(ctx context.Context) (system.Info, error) - networkInspectFunc func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) - nodeListFunc func(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) + serviceInspectFunc func(ctx context.Context, serviceID string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) + serviceUpdateFunc func(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) + serviceListFunc func(context.Context, client.ServiceListOptions) (client.ServiceListResult, error) + taskListFunc func(context.Context, client.TaskListOptions) (client.TaskListResult, error) + infoFunc func(ctx context.Context) (client.SystemInfoResult, error) + networkInspectFunc func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) + nodeListFunc func(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) } -func (f *fakeClient) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { +func (f *fakeClient) NodeList(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) { if f.nodeListFunc != nil { return f.nodeListFunc(ctx, options) } - return nil, nil + return client.NodeListResult{}, nil } -func (f *fakeClient) TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { +func (f *fakeClient) TaskList(ctx context.Context, options client.TaskListOptions) (client.TaskListResult, error) { if f.taskListFunc != nil { return f.taskListFunc(ctx, options) } - return nil, nil + return client.TaskListResult{}, nil } -func (f *fakeClient) ServiceInspectWithRaw(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - if f.serviceInspectWithRawFunc != nil { - return f.serviceInspectWithRawFunc(ctx, serviceID, options) +func (f *fakeClient) ServiceInspect(ctx context.Context, serviceID string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + if f.serviceInspectFunc != nil { + return f.serviceInspectFunc(ctx, serviceID, options) } - return *builders.Service(builders.ServiceID(serviceID)), []byte{}, nil + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceID(serviceID)), + }, nil } -func (f *fakeClient) ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { +func (f *fakeClient) ServiceList(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { if f.serviceListFunc != nil { return f.serviceListFunc(ctx, options) } - return nil, nil + return client.ServiceListResult{}, nil } -func (f *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { +func (f *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) { if f.serviceUpdateFunc != nil { - return f.serviceUpdateFunc(ctx, serviceID, version, service, options) + return f.serviceUpdateFunc(ctx, serviceID, options) } - return swarm.ServiceUpdateResponse{}, nil + return client.ServiceUpdateResult{}, nil } -func (f *fakeClient) Info(ctx context.Context) (system.Info, error) { - if f.infoFunc == nil { - return system.Info{}, nil +func (f *fakeClient) Info(ctx context.Context, _ client.InfoOptions) (client.SystemInfoResult, error) { + if f.infoFunc != nil { + return f.infoFunc(ctx) } - return f.infoFunc(ctx) + return client.SystemInfoResult{}, nil } -func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { +func (f *fakeClient) NetworkInspect(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { if f.networkInspectFunc != nil { return f.networkInspectFunc(ctx, networkID, options) } - return network.Inspect{}, nil + return client.NetworkInspectResult{}, nil } func newService(id string, name string) swarm.Service { diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index 6b6626cc1443..1620edc19f43 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -3,31 +3,37 @@ package service import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewServiceCommand returns a cobra command for `service` subcommands -func NewServiceCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newServiceCommand) +} + +// newServiceCommand returns a cobra command for `service` subcommands +func newServiceCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "service", Short: "Manage Swarm services", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newPsCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newScaleCommand(dockerCli), - newUpdateCommand(dockerCli), - newLogsCommand(dockerCli), - newRollbackCommand(dockerCli), + newCreateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newPsCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newScaleCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newLogsCommand(dockerCLI), + newRollbackCommand(dockerCLI), ) return cmd } diff --git a/cli/command/service/completion.go b/cli/command/service/completion.go index 613be94192ca..9e244ac41019 100644 --- a/cli/command/service/completion.go +++ b/cli/command/service/completion.go @@ -4,7 +4,7 @@ import ( "os" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -15,13 +15,13 @@ func completeServiceNames(dockerCLI completion.APIClientProvider) cobra.Completi // https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43 showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_SERVICE_IDS") == "yes" return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := dockerCLI.Client().ServiceList(cmd.Context(), swarm.ServiceListOptions{}) + res, err := dockerCLI.Client().ServiceList(cmd.Context(), client.ServiceListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } - names := make([]string, 0, len(list)) - for _, service := range list { + names := make([]string, 0, len(res.Items)) + for _, service := range res.Items { if showIDs { names = append(names, service.Spec.Name, service.ID) } else { diff --git a/cli/command/service/create.go b/cli/command/service/create.go index f63f65aefe24..45406d7b2633 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -8,9 +8,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" cliopts "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -29,7 +28,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { } return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVar(&opts.mode, flagMode, "replicated", `Service mode ("replicated", "global", "replicated-job", "global-job")`) @@ -81,8 +81,8 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { // _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals) _ = cmd.RegisterFlagCompletionFunc(flagMode, completion.FromList("replicated", "global", "replicated-job", "global-job")) - _ = cmd.RegisterFlagCompletionFunc(flagEnv, completion.EnvVarNames) // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service) - _ = cmd.RegisterFlagCompletionFunc(flagEnvFile, completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc(flagEnv, completion.EnvVarNames()) // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service) + _ = cmd.RegisterFlagCompletionFunc(flagEnvFile, completion.FileNames()) _ = cmd.RegisterFlagCompletionFunc(flagNetwork, completion.NetworkNames(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc(flagRestartCondition, completion.FromList("none", "on-failure", "any")) _ = cmd.RegisterFlagCompletionFunc(flagRollbackOrder, completion.FromList("start-first", "stop-first")) @@ -90,18 +90,11 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { _ = cmd.RegisterFlagCompletionFunc(flagUpdateOrder, completion.FromList("start-first", "stop-first")) _ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback")) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *serviceOptions) error { apiClient := dockerCLI.Client() - createOpts := swarm.ServiceCreateOptions{} service, err := opts.ToService(ctx, apiClient, flags) if err != nil { @@ -122,26 +115,23 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, return err } - if err := resolveServiceImageDigestContentTrust(dockerCLI, &service); err != nil { - return err - } - // only send auth if flag was set + var encodedAuth string if opts.registryAuth { // Retrieve encoded auth token from the image reference - encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), opts.image) + var err error + encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), opts.image) if err != nil { return err } - createOpts.EncodedRegistryAuth = encodedAuth } - // query registry if flag disabling it was not set - if !opts.noResolveImage && versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") { - createOpts.QueryRegistry = true - } + response, err := apiClient.ServiceCreate(ctx, client.ServiceCreateOptions{ + Spec: service, - response, err := apiClient.ServiceCreate(ctx, service, createOpts) + EncodedRegistryAuth: encodedAuth, + QueryRegistry: !opts.noResolveImage, // query registry if flag disabling it was not set. + }) if err != nil { return err } @@ -152,7 +142,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, _, _ = fmt.Fprintln(dockerCLI.Out(), response.ID) - if opts.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") { + if opts.detach { return nil } diff --git a/cli/command/service/create_test.go b/cli/command/service/create_test.go index 1a478c930c83..9b4e9ddd83ea 100644 --- a/cli/command/service/create_test.go +++ b/cli/command/service/create_test.go @@ -5,7 +5,8 @@ import ( "testing" "github.com/docker/cli/opts/swarmopts" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -13,26 +14,26 @@ import ( // fakeConfigAPIClientList is used to let us pass a closure as a // ConfigAPIClient, to use as ConfigList. for all the other methods in the // interface, it does nothing, not even return an error, so don't use them -type fakeConfigAPIClientList func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error) +type fakeConfigAPIClientList func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) -func (f fakeConfigAPIClientList) ConfigList(ctx context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { +func (f fakeConfigAPIClientList) ConfigList(ctx context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { return f(ctx, opts) } -func (fakeConfigAPIClientList) ConfigCreate(_ context.Context, _ swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - return swarm.ConfigCreateResponse{}, nil +func (fakeConfigAPIClientList) ConfigCreate(_ context.Context, _ client.ConfigCreateOptions) (client.ConfigCreateResult, error) { + return client.ConfigCreateResult{}, nil } -func (fakeConfigAPIClientList) ConfigRemove(_ context.Context, _ string) error { - return nil +func (fakeConfigAPIClientList) ConfigRemove(_ context.Context, _ string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { + return client.ConfigRemoveResult{}, nil } -func (fakeConfigAPIClientList) ConfigInspectWithRaw(_ context.Context, _ string) (swarm.Config, []byte, error) { - return swarm.Config{}, nil, nil +func (fakeConfigAPIClientList) ConfigInspect(_ context.Context, _ string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { + return client.ConfigInspectResult{}, nil } -func (fakeConfigAPIClientList) ConfigUpdate(_ context.Context, _ string, _ swarm.Version, _ swarm.ConfigSpec) error { - return nil +func (fakeConfigAPIClientList) ConfigUpdate(_ context.Context, _ string, _ client.ConfigUpdateOptions) (client.ConfigUpdateResult, error) { + return client.ConfigUpdateResult{}, nil } // TestSetConfigsWithCredSpecAndConfigs tests that the setConfigs function for @@ -66,28 +67,25 @@ func TestSetConfigsWithCredSpecAndConfigs(t *testing.T) { } // set up a function to use as the list function - var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { - f := opts.Filters - + var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { // we're expecting the filter to have names "foo" and "bar" - names := f.Get("name") - assert.Equal(t, len(names), 2) - assert.Assert(t, is.Contains(names, "foo")) - assert.Assert(t, is.Contains(names, "bar")) - - return []swarm.Config{ - { - ID: "fooID", - Spec: swarm.ConfigSpec{ - Annotations: swarm.Annotations{ - Name: "foo", + expected := make(client.Filters).Add("name", "foo", "bar") + assert.Assert(t, is.DeepEqual(opts.Filters, expected)) + return client.ConfigListResult{ + Items: []swarm.Config{ + { + ID: "fooID", + Spec: swarm.ConfigSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, }, - }, - }, { - ID: "barID", - Spec: swarm.ConfigSpec{ - Annotations: swarm.Annotations{ - Name: "bar", + }, { + ID: "barID", + Spec: swarm.ConfigSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, }, }, }, @@ -146,24 +144,23 @@ func TestSetConfigsOnlyCredSpec(t *testing.T) { } // set up a function to use as the list function - var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { - f := opts.Filters - - names := f.Get("name") - assert.Equal(t, len(names), 1) - assert.Assert(t, is.Contains(names, "foo")) - - return []swarm.Config{ - { - ID: "fooID", - Spec: swarm.ConfigSpec{ - Annotations: swarm.Annotations{ - Name: "foo", + fakeClient := fakeConfigAPIClientList(func(_ context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { + expected := make(client.Filters).Add("name", "foo") + assert.Assert(t, is.DeepEqual(opts.Filters, expected)) + + return client.ConfigListResult{ + Items: []swarm.Config{ + { + ID: "fooID", + Spec: swarm.ConfigSpec{ + Annotations: swarm.Annotations{ + Name: "foo", + }, }, }, }, }, nil - } + }) // now call setConfigs ctx := context.Background() @@ -197,28 +194,26 @@ func TestSetConfigsOnlyConfigs(t *testing.T) { }, } - var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { - f := opts.Filters - - names := f.Get("name") - assert.Equal(t, len(names), 1) - assert.Assert(t, is.Contains(names, "bar")) - - return []swarm.Config{ - { - ID: "barID", - Spec: swarm.ConfigSpec{ - Annotations: swarm.Annotations{ - Name: "bar", + fakeConfigClient := fakeConfigAPIClientList(func(_ context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { + expected := make(client.Filters).Add("name", "bar") + assert.Assert(t, is.DeepEqual(opts.Filters, expected)) + return client.ConfigListResult{ + Items: []swarm.Config{ + { + ID: "barID", + Spec: swarm.ConfigSpec{ + Annotations: swarm.Annotations{ + Name: "bar", + }, }, }, }, }, nil - } + }) // now call setConfigs ctx := context.Background() - err := setConfigs(ctx, fakeClient, service, opts) + err := setConfigs(ctx, fakeConfigClient, service, opts) // verify no error is returned assert.NilError(t, err) @@ -258,17 +253,17 @@ func TestSetConfigsNoConfigs(t *testing.T) { }, } - var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { + fakeConfigClient := fakeConfigAPIClientList(func(_ context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { // assert false -- we should never call this function assert.Assert(t, false, "we should not be listing configs") - return nil, nil - } + return client.ConfigListResult{}, nil + }) ctx := context.Background() - err := setConfigs(ctx, fakeClient, service, opts) + err := setConfigs(ctx, fakeConfigClient, service, opts) assert.NilError(t, err) - // ensure that the value of the credentialspec has not changed + // ensure that the value of the credential-spec has not changed assert.Equal(t, service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.File, "foo") assert.Equal(t, service.TaskTemplate.ContainerSpec.Privileges.CredentialSpec.Config, "") } diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index aee5453f10f9..1ec5263debe8 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "sort" "strconv" @@ -10,13 +11,13 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-units" "github.com/fvbommel/sortorder" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const serviceInspectPrettyTemplate formatter.Format = ` @@ -195,8 +196,8 @@ Ports: {{- end }} ` -// NewFormat returns a Format for rendering using a Context -func NewFormat(source string) formatter.Format { +// newFormat returns a Format for rendering using a Context. +func newFormat(source string) formatter.Format { switch source { case formatter.PrettyFormatKey: return serviceInspectPrettyTemplate @@ -217,12 +218,13 @@ func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[s return networkNames } -// InspectFormatWrite renders the context for a list of services -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { - if ctx.Format != serviceInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// inspectFormatWrite renders the context for a list of services +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { + if fmtCtx.Format != serviceInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } - render := func(format func(subContext formatter.SubContext) error) error { + + return fmtCtx.Write(&serviceInspectContext{}, func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { serviceI, _, err := getRef(ref) if err != nil { @@ -230,15 +232,17 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef, getNetwork } service, ok := serviceI.(swarm.Service) if !ok { - return errors.Errorf("got wrong object to inspect") + return errors.New("got wrong object to inspect") } - if err := format(&serviceInspectContext{Service: service, networkNames: resolveNetworks(service, getNetwork)}); err != nil { + if err := format(&serviceInspectContext{ + Service: service, + networkNames: resolveNetworks(service, getNetwork), + }); err != nil { return err } } return nil - } - return ctx.Write(&serviceInspectContext{}, render) + }) } type serviceInspectContext struct { @@ -378,11 +382,11 @@ func (ctx *serviceInspectContext) UpdateDelay() time.Duration { } func (ctx *serviceInspectContext) UpdateOnFailure() string { - return ctx.Service.Spec.UpdateConfig.FailureAction + return string(ctx.Service.Spec.UpdateConfig.FailureAction) } func (ctx *serviceInspectContext) UpdateOrder() string { - return ctx.Service.Spec.UpdateConfig.Order + return string(ctx.Service.Spec.UpdateConfig.Order) } func (ctx *serviceInspectContext) HasUpdateMonitor() bool { @@ -414,7 +418,7 @@ func (ctx *serviceInspectContext) RollbackDelay() time.Duration { } func (ctx *serviceInspectContext) RollbackOnFailure() string { - return ctx.Service.Spec.RollbackConfig.FailureAction + return string(ctx.Service.Spec.RollbackConfig.FailureAction) } func (ctx *serviceInspectContext) HasRollbackMonitor() bool { @@ -430,7 +434,7 @@ func (ctx *serviceInspectContext) RollbackMaxFailureRatio() float32 { } func (ctx *serviceInspectContext) RollbackOrder() string { - return ctx.Service.Spec.RollbackConfig.Order + return string(ctx.Service.Spec.RollbackConfig.Order) } func (ctx *serviceInspectContext) ContainerImage() string { @@ -461,7 +465,7 @@ func (ctx *serviceInspectContext) ContainerInit() bool { return *ctx.Service.Spec.TaskTemplate.ContainerSpec.Init } -func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount { +func (ctx *serviceInspectContext) ContainerMounts() []mount.Mount { return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts } @@ -609,12 +613,12 @@ func NewListFormat(source string, quiet bool) formatter.Format { } // ListFormatWrite writes the context -func ListFormatWrite(ctx formatter.Context, services []swarm.Service) error { +func ListFormatWrite(ctx formatter.Context, services client.ServiceListResult) error { render := func(format func(subContext formatter.SubContext) error) error { - sort.Slice(services, func(i, j int) bool { - return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name) + sort.Slice(services.Items, func(i, j int) bool { + return sortorder.NaturalLess(services.Items[i].Spec.Name, services.Items[j].Spec.Name) }) - for _, service := range services { + for _, service := range services.Items { serviceCtx := &serviceContext{service: service} if err := format(serviceCtx); err != nil { return err @@ -732,7 +736,7 @@ type portRange struct { pEnd uint32 tStart uint32 tEnd uint32 - protocol swarm.PortConfigProtocol + protocol network.IPProtocol } func (pr portRange) String() string { diff --git a/cli/command/service/formatter_test.go b/cli/command/service/formatter_test.go index fcbd6d4ecff0..dc21f7b8a07a 100644 --- a/cli/command/service/formatter_test.go +++ b/cli/command/service/formatter_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package service @@ -11,7 +11,8 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -110,114 +111,116 @@ zarp2 }, } - services := []swarm.Service{ - { - ID: "01_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - Mode: swarm.ServiceMode{ - Global: &swarm.GlobalService{}, + services := client.ServiceListResult{ + Items: []swarm.Service{ + { + ID: "01_baz", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "baz"}, + Mode: swarm.ServiceMode{ + Global: &swarm.GlobalService{}, + }, }, - }, - Endpoint: swarm.Endpoint{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", + Endpoint: swarm.Endpoint{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, }, }, - }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 1, - DesiredTasks: 3, - }, - }, - { - ID: "02_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{}, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 1, + DesiredTasks: 3, }, }, - Endpoint: swarm.Endpoint{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8090, - Protocol: "udp", + { + ID: "02_bar", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "bar"}, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{}, }, }, - }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 4, - }, - }, - { - ID: "03_qux10", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "qux10"}, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{}, + Endpoint: swarm.Endpoint{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8090, + Protocol: "udp", + }, + }, }, - TaskTemplate: swarm.TaskSpec{ - Placement: &swarm.Placement{MaxReplicas: 1}, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 4, }, }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 3, - }, - }, - { - ID: "04_qux2", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "qux2"}, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{}, + { + ID: "03_qux10", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "qux10"}, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{}, + }, + TaskTemplate: swarm.TaskSpec{ + Placement: &swarm.Placement{MaxReplicas: 1}, + }, }, - TaskTemplate: swarm.TaskSpec{ - Placement: &swarm.Placement{MaxReplicas: 2}, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 3, }, }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 3, - DesiredTasks: 3, - }, - }, - { - ID: "05_job1", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "zarp1"}, - Mode: swarm.ServiceMode{ - ReplicatedJob: &swarm.ReplicatedJob{ - MaxConcurrent: &varThree, - TotalCompletions: &varTen, + { + ID: "04_qux2", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "qux2"}, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{}, + }, + TaskTemplate: swarm.TaskSpec{ + Placement: &swarm.Placement{MaxReplicas: 2}, }, }, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 3, + DesiredTasks: 3, + }, }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 3, - CompletedTasks: 5, - }, - }, - { - ID: "06_job2", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "zarp2"}, - Mode: swarm.ServiceMode{ - GlobalJob: &swarm.GlobalJob{}, + { + ID: "05_job1", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "zarp1"}, + Mode: swarm.ServiceMode{ + ReplicatedJob: &swarm.ReplicatedJob{ + MaxConcurrent: &varThree, + TotalCompletions: &varTen, + }, + }, + }, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 3, + CompletedTasks: 5, }, }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 1, - DesiredTasks: 1, - CompletedTasks: 3, + { + ID: "06_job2", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "zarp2"}, + Mode: swarm.ServiceMode{ + GlobalJob: &swarm.GlobalJob{}, + }, + }, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 1, + DesiredTasks: 1, + CompletedTasks: 3, + }, }, }, } @@ -237,51 +240,53 @@ zarp2 } func TestServiceContextWriteJSON(t *testing.T) { - services := []swarm.Service{ - { - ID: "01_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - Mode: swarm.ServiceMode{ - Global: &swarm.GlobalService{}, + services := client.ServiceListResult{ + Items: []swarm.Service{ + { + ID: "01_baz", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "baz"}, + Mode: swarm.ServiceMode{ + Global: &swarm.GlobalService{}, + }, }, - }, - Endpoint: swarm.Endpoint{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", + Endpoint: swarm.Endpoint{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, }, }, - }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 1, - DesiredTasks: 3, - }, - }, - { - ID: "02_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{}, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 1, + DesiredTasks: 3, }, }, - Endpoint: swarm.Endpoint{ - Ports: []swarm.PortConfig{ - { - PublishMode: "ingress", - PublishedPort: 80, - TargetPort: 8080, - Protocol: "tcp", + { + ID: "02_bar", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "bar"}, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{}, }, }, - }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 4, + Endpoint: swarm.Endpoint{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, + }, + }, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 4, + }, }, }, } @@ -305,31 +310,33 @@ func TestServiceContextWriteJSON(t *testing.T) { } func TestServiceContextWriteJSONField(t *testing.T) { - services := []swarm.Service{ - { - ID: "01_baz", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "baz"}, - Mode: swarm.ServiceMode{ - Global: &swarm.GlobalService{}, + services := client.ServiceListResult{ + Items: []swarm.Service{ + { + ID: "01_baz", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "baz"}, + Mode: swarm.ServiceMode{ + Global: &swarm.GlobalService{}, + }, }, - }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 4, - }, - }, - { - ID: "24_bar", - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: "bar"}, - Mode: swarm.ServiceMode{ - Replicated: &swarm.ReplicatedService{}, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 4, }, }, - ServiceStatus: &swarm.ServiceStatus{ - RunningTasks: 2, - DesiredTasks: 4, + { + ID: "24_bar", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "bar"}, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{}, + }, + }, + ServiceStatus: &swarm.ServiceStatus{ + RunningTasks: 2, + DesiredTasks: 4, + }, }, }, } @@ -343,7 +350,7 @@ func TestServiceContextWriteJSONField(t *testing.T) { var s string err := json.Unmarshal([]byte(line), &s) assert.NilError(t, err, msg) - assert.Check(t, is.Equal(services[i].Spec.Name, s), msg) + assert.Check(t, is.Equal(services.Items[i].Spec.Name, s), msg) } } diff --git a/cli/command/service/generic_resource_opts.go b/cli/command/service/generic_resource_opts.go index ba5cf6aa5be1..426b44d1581b 100644 --- a/cli/command/service/generic_resource_opts.go +++ b/cli/command/service/generic_resource_opts.go @@ -4,11 +4,8 @@ import ( "fmt" "strings" - "github.com/pkg/errors" - - "github.com/docker/docker/api/types/swarm" - swarmapi "github.com/moby/swarmkit/v2/api" - "github.com/moby/swarmkit/v2/api/genericresource" + "github.com/docker/cli/cli/command/service/internal/genericresource" + "github.com/moby/moby/api/types/swarm" ) // GenericResource is a concept that a user can use to advertise user-defined @@ -35,12 +32,11 @@ func ParseGenericResources(value []string) ([]swarm.GenericResource, error) { return nil, nil } - resources, err := genericresource.Parse(value) + swarmResources, err := genericresource.Parse(value) if err != nil { - return nil, errors.Wrapf(err, "invalid generic resource specification") + return nil, fmt.Errorf("invalid generic resource specification: %w", err) } - swarmResources := genericResourcesFromGRPC(resources) for _, res := range swarmResources { if res.NamedResourceSpec != nil { return nil, fmt.Errorf("invalid generic-resource request `%s=%s`, Named Generic Resources is not supported for service create or update", @@ -52,31 +48,6 @@ func ParseGenericResources(value []string) ([]swarm.GenericResource, error) { return swarmResources, nil } -// genericResourcesFromGRPC converts a GRPC GenericResource to a GenericResource -func genericResourcesFromGRPC(genericRes []*swarmapi.GenericResource) []swarm.GenericResource { - generic := make([]swarm.GenericResource, 0, len(genericRes)) - for _, res := range genericRes { - var current swarm.GenericResource - - switch r := res.Resource.(type) { - case *swarmapi.GenericResource_DiscreteResourceSpec: - current.DiscreteResourceSpec = &swarm.DiscreteGenericResource{ - Kind: r.DiscreteResourceSpec.Kind, - Value: r.DiscreteResourceSpec.Value, - } - case *swarmapi.GenericResource_NamedResourceSpec: - current.NamedResourceSpec = &swarm.NamedGenericResource{ - Kind: r.NamedResourceSpec.Kind, - Value: r.NamedResourceSpec.Value, - } - } - - generic = append(generic, current) - } - - return generic -} - func buildGenericResourceMap(genericRes []swarm.GenericResource) (map[string]swarm.GenericResource, error) { m := make(map[string]swarm.GenericResource) diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go index e3ae937125bc..9896eabbb475 100644 --- a/cli/command/service/inspect.go +++ b/cli/command/service/inspect.go @@ -1,23 +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.23 +//go:build go1.24 package service import ( "context" + "errors" + "fmt" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type inspectOptions struct { @@ -26,7 +24,7 @@ type inspectOptions struct { pretty bool } -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -37,28 +35,23 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { opts.refs = args if opts.pretty && len(opts.format) > 0 { - return errors.Errorf("--format is incompatible with human friendly format") + return errors.New("--format is incompatible with human friendly format") } - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completeServiceNames(dockerCli), + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" @@ -66,41 +59,41 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) getRef := func(ref string) (any, []byte, error) { // Service inspect shows defaults values in empty fields. - service, _, err := client.ServiceInspectWithRaw(ctx, ref, swarm.ServiceInspectOptions{InsertDefaults: true}) - if err == nil || !cerrdefs.IsNotFound(err) { - return service, nil, err + res, err := apiClient.ServiceInspect(ctx, ref, client.ServiceInspectOptions{InsertDefaults: true}) + if err == nil || !errdefs.IsNotFound(err) { + return res.Service, res.Raw, err } - return nil, nil, errors.Errorf("Error: no such service: %s", ref) + return nil, nil, fmt.Errorf("no such service: %s", ref) } getNetwork := func(ref string) (any, []byte, error) { - nw, _, err := client.NetworkInspectWithRaw(ctx, ref, network.InspectOptions{Scope: "swarm"}) - if err == nil || !cerrdefs.IsNotFound(err) { - return nw, nil, err + res, err := apiClient.NetworkInspect(ctx, ref, client.NetworkInspectOptions{Scope: "swarm"}) + if err == nil || !errdefs.IsNotFound(err) { + return res.Network, res.Raw, err } - return nil, nil, errors.Errorf("Error: no such network: %s", ref) + return nil, nil, fmt.Errorf("no such network: %s", ref) } f := opts.format if len(f) == 0 { f = "raw" - if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { - f = dockerCli.ConfigFile().ServiceInspectFormat + if len(dockerCLI.ConfigFile().ServiceInspectFormat) > 0 { + f = dockerCLI.ConfigFile().ServiceInspectFormat } } // check if the user is trying to apply a template to the pretty format, which // is not supported if strings.HasPrefix(f, "pretty") && f != "pretty" { - return errors.Errorf("Cannot supply extra formatting options to the pretty template") + return errors.New("cannot supply extra formatting options to the pretty template") } serviceCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f), + Output: dockerCLI.Out(), + Format: newFormat(f), } - if err := InspectFormatWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { + if err := inspectFormatWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go index d7aa42e8f9bd..129998bd2b3a 100644 --- a/cli/command/service/inspect_test.go +++ b/cli/command/service/inspect_test.go @@ -1,19 +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.23 +//go:build go1.24 package service import ( "bytes" "encoding/json" + "net/netip" "strings" "testing" "time" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -27,7 +28,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Mode: "vip", Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, }, }, @@ -108,7 +109,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Spec: *endpointSpec, Ports: []swarm.PortConfig{ { - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, TargetPort: 5000, PublishedPort: 30000, }, @@ -116,7 +117,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) VirtualIPs: []swarm.EndpointVirtualIP{ { NetworkID: "6o4107cj2jx9tihgb0jyts6pj", - Addr: "10.255.0.4/16", + Addr: netip.MustParsePrefix("10.255.0.4/16"), // FIXME(thaJeztah): this was testing with "10.255.0.4/16" }, }, }, @@ -131,14 +132,16 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Format: format, } - err := InspectFormatWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, + err := inspectFormatWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (any, []byte, error) { return s, nil, nil }, func(ref string) (any, []byte, error) { - return network.Inspect{ - ID: "5vpyomhb6ievnk0i0o60gcnei", - Name: "mynetwork", + return network.Summary{ + Network: network.Network{ + ID: "5vpyomhb6ievnk0i0o60gcnei", + Name: "mynetwork", + }, }, nil, nil }, ) @@ -149,12 +152,12 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) } func TestPrettyPrint(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) golden.Assert(t, s, "service-inspect-pretty.golden") } func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) if strings.Contains(s, "UpdateStatus") { t.Fatal("Pretty print failed before parsing UpdateStatus") } @@ -167,8 +170,8 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) { now := time.Now() // s1: [{"ID":..}] // s2: {"ID":..} - s1 := formatServiceInspect(t, NewFormat(""), now) - s2 := formatServiceInspect(t, NewFormat("{{json .}}"), now) + s1 := formatServiceInspect(t, newFormat(""), now) + s2 := formatServiceInspect(t, newFormat("{{json .}}"), now) var m1Wrap []map[string]any if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { t.Fatal(err) @@ -185,7 +188,7 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) { } func TestPrettyPrintWithConfigsAndSecrets(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) assert.Check(t, is.Contains(s, "Log Driver:"), "Pretty print missing Log Driver") assert.Check(t, is.Contains(s, "Configs:"), "Pretty print missing configs") assert.Check(t, is.Contains(s, "Secrets:"), "Pretty print missing secrets") diff --git a/cli/command/service/internal/genericresource/helpers.go b/cli/command/service/internal/genericresource/helpers.go new file mode 100644 index 000000000000..4cece4ce744f --- /dev/null +++ b/cli/command/service/internal/genericresource/helpers.go @@ -0,0 +1,48 @@ +package genericresource + +import ( + api "github.com/moby/moby/api/types/swarm" +) + +// NewSet creates a set object +func NewSet(key string, vals ...string) []api.GenericResource { + rs := make([]api.GenericResource, 0, len(vals)) + for _, v := range vals { + rs = append(rs, NewString(key, v)) + } + return rs +} + +// NewString creates a String resource +func NewString(kind, value string) api.GenericResource { + return api.GenericResource{ + NamedResourceSpec: &api.NamedGenericResource{ + Kind: kind, + Value: value, + }, + } +} + +// NewDiscrete creates a Discrete resource +func NewDiscrete(key string, val int64) api.GenericResource { + return api.GenericResource{ + DiscreteResourceSpec: &api.DiscreteGenericResource{ + Kind: key, + Value: val, + }, + } +} + +// GetResource returns resources from the "resources" parameter matching the kind key +func GetResource(kind string, resources []api.GenericResource) []api.GenericResource { + var res []api.GenericResource + for _, r := range resources { + switch { + case r.DiscreteResourceSpec != nil && r.DiscreteResourceSpec.Kind == kind: + res = append(res, r) + case r.NamedResourceSpec != nil && r.NamedResourceSpec.Kind == kind: + res = append(res, r) + } + } + return res +} diff --git a/cli/command/service/internal/genericresource/parse.go b/cli/command/service/internal/genericresource/parse.go new file mode 100644 index 000000000000..e06ed95c1910 --- /dev/null +++ b/cli/command/service/internal/genericresource/parse.go @@ -0,0 +1,116 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +// Package genericresource is a local fork of SwarmKit's [genericresource] package, +// without protobuf dependencies. +// +// [genericresource]: https://github.com/moby/swarmkit/blob/v2.1.1/api/genericresource/parse.go +package genericresource + +import ( + "encoding/csv" + "fmt" + "strconv" + "strings" + + api "github.com/moby/moby/api/types/swarm" +) + +func newParseError(format string, args ...any) error { + return fmt.Errorf("could not parse GenericResource: "+format, args...) +} + +// discreteResourceVal returns an int64 if the string is a discreteResource +// and an error if it isn't +func discreteResourceVal(res string) (int64, error) { + return strconv.ParseInt(res, 10, 64) +} + +// allNamedResources returns true if the array of resources are all namedResources +// e.g: res = [red, orange, green] +func allNamedResources(res []string) bool { + for _, v := range res { + if _, err := discreteResourceVal(v); err == nil { + return false + } + } + + return true +} + +// ParseCmd parses the Generic Resource command line argument +// and returns a list of api.GenericResource +func ParseCmd(cmd string) ([]api.GenericResource, error) { + if strings.Contains(cmd, "\n") { + return nil, newParseError("unexpected '\\n' character") + } + + r := csv.NewReader(strings.NewReader(cmd)) + records, err := r.ReadAll() + if err != nil { + return nil, newParseError("%v", err) + } + + if len(records) != 1 { + return nil, newParseError("found multiple records while parsing cmd %v", records) + } + + return Parse(records[0]) +} + +// Parse parses a table of GenericResource resources +func Parse(cmds []string) ([]api.GenericResource, error) { + tokens := make(map[string][]string) + + for _, term := range cmds { + kva := strings.Split(term, "=") + if len(kva) != 2 { + return nil, newParseError("incorrect term %s, missing"+ + " '=' or malformed expression", term) + } + + key := strings.TrimSpace(kva[0]) + val := strings.TrimSpace(kva[1]) + + tokens[key] = append(tokens[key], val) + } + + var rs []api.GenericResource + for k, v := range tokens { + if u, ok := isDiscreteResource(v); ok { + if u < 0 { + return nil, newParseError("cannot ask for"+ + " negative resource %s", k) + } + + rs = append(rs, NewDiscrete(k, u)) + continue + } + + if allNamedResources(v) { + rs = append(rs, NewSet(k, v...)...) + continue + } + + return nil, newParseError("mixed discrete and named resources"+ + " in expression '%s=%s'", k, v) + } + + return rs, nil +} + +// isDiscreteResource returns true if the array of resources is a +// Discrete Resource. +// e.g: res = [1] +func isDiscreteResource(values []string) (int64, bool) { + if len(values) != 1 { + return int64(0), false + } + + u, err := discreteResourceVal(values[0]) + if err != nil { + return int64(0), false + } + + return u, true +} diff --git a/cli/command/service/internal/genericresource/parse_test.go b/cli/command/service/internal/genericresource/parse_test.go new file mode 100644 index 000000000000..3a3390ac997e --- /dev/null +++ b/cli/command/service/internal/genericresource/parse_test.go @@ -0,0 +1,65 @@ +package genericresource + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestParseDiscrete(t *testing.T) { + res, err := ParseCmd("apple=3") + assert.NilError(t, err) + assert.Equal(t, len(res), 1) + + apples := GetResource("apple", res) + assert.Equal(t, len(apples), 1) + if apples[0].DiscreteResourceSpec == nil { + t.Fatalf("expected discrete resource spec, got nil") + } + assert.Equal(t, apples[0].DiscreteResourceSpec.Value, int64(3)) + + _, err = ParseCmd("apple=3\napple=4") + assert.Assert(t, err != nil) + + _, err = ParseCmd("apple=3,apple=4") + assert.Assert(t, err != nil) + + _, err = ParseCmd("apple=-3") + assert.Assert(t, err != nil) +} + +func TestParseStr(t *testing.T) { + res, err := ParseCmd("orange=red,orange=green,orange=blue") + assert.NilError(t, err) + assert.Equal(t, len(res), 3) + + oranges := GetResource("orange", res) + assert.Equal(t, len(oranges), 3) + for _, k := range []string{"red", "green", "blue"} { + assert.Assert(t, HasResource(NewString("orange", k), oranges)) + } +} + +func TestParseDiscreteAndStr(t *testing.T) { + res, err := ParseCmd("orange=red,orange=green,orange=blue,apple=3") + assert.NilError(t, err) + assert.Equal(t, len(res), 4) + + oranges := GetResource("orange", res) + assert.Equal(t, len(oranges), 3) + for _, k := range []string{"red", "green", "blue"} { + assert.Assert(t, HasResource(NewString("orange", k), oranges)) + } + + apples := GetResource("apple", res) + assert.Equal(t, len(apples), 1) + if apples[0].DiscreteResourceSpec == nil { + t.Fatalf("expected discrete resource spec, got nil") + } + assert.Equal(t, apples[0].DiscreteResourceSpec.Value, int64(3)) +} + +func TestParseMixedForSameKindFails(t *testing.T) { + _, err := ParseCmd("gpu=fast,gpu=slow,gpu=2") + assert.Assert(t, err != nil) +} diff --git a/cli/command/service/internal/genericresource/validate.go b/cli/command/service/internal/genericresource/validate.go new file mode 100644 index 000000000000..40ce4ec79830 --- /dev/null +++ b/cli/command/service/internal/genericresource/validate.go @@ -0,0 +1,29 @@ +package genericresource + +import ( + api "github.com/moby/moby/api/types/swarm" +) + +// HasResource checks if there is enough "res" in the "resources" argument +func HasResource(res api.GenericResource, resources []api.GenericResource) bool { + for _, r := range resources { + if equalResource(r, res) { + return true + } + } + return false +} + +// equalResource matches the resource *type* (named vs discrete), and then kind+value. +func equalResource(a, b api.GenericResource) bool { + switch { + case a.NamedResourceSpec != nil && b.NamedResourceSpec != nil: + return a.NamedResourceSpec.Kind == b.NamedResourceSpec.Kind && + a.NamedResourceSpec.Value == b.NamedResourceSpec.Value + + case a.DiscreteResourceSpec != nil && b.DiscreteResourceSpec != nil: + return a.DiscreteResourceSpec.Kind == b.DiscreteResourceSpec.Kind && + a.DiscreteResourceSpec.Value == b.DiscreteResourceSpec.Value + } + return false +} diff --git a/cli/command/service/list.go b/cli/command/service/list.go index d2768cc0e197..d2e2fe6a1003 100644 --- a/cli/command/service/list.go +++ b/cli/command/service/list.go @@ -5,15 +5,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type listOptions struct { @@ -33,7 +29,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -41,54 +38,23 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { - var ( - apiClient = dockerCLI.Client() - err error - ) - - listOpts := swarm.ServiceListOptions{ + apiClient := dockerCLI.Client() + res, err := apiClient.ServiceList(ctx, client.ServiceListOptions{ Filters: options.filter.Value(), // When not running "quiet", also get service status (number of running // and desired tasks). Note that this is only supported on API v1.41 and // up; older API versions ignore this option, and we will have to collect // the information manually below. Status: !options.quiet, - } - - services, err := apiClient.ServiceList(ctx, listOpts) + }) if err != nil { return err } - if listOpts.Status { - // Now that a request was made, we know what API version was used (either - // through configuration, or after client and daemon negotiated a version). - // If API version v1.41 or up was used; the daemon should already have done - // the legwork for us, and we don't have to calculate the number of desired - // and running tasks. On older API versions, we need to do some extra requests - // to get that information. - // - // So theoretically, this step can be skipped based on API version, however, - // some of our unit tests don't set the API version, and there may be other - // situations where the client uses the "default" version. To account for - // these situations, we do a quick check for services that do not have - // a ServiceStatus set, and perform a lookup for those. - services, err = AppendServiceStatus(ctx, apiClient, services) - if err != nil { - return err - } - } - format := options.format if len(format) == 0 { if len(dockerCLI.ConfigFile().ServicesFormat) > 0 && !options.quiet { @@ -102,96 +68,5 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er Output: dockerCLI.Out(), Format: NewListFormat(format, options.quiet), } - return ListFormatWrite(servicesCtx, services) -} - -// AppendServiceStatus propagates the ServiceStatus field for "services". -// -// If API version v1.41 or up is used, this information is already set by the -// daemon. On older API versions, we need to do some extra requests to get -// that information. Theoretically, this function can be skipped based on API -// version, however, some of our unit tests don't set the API version, and -// there may be other situations where the client uses the "default" version. -// To take these situations into account, we do a quick check for services -// that don't have ServiceStatus set, and perform a lookup for those. -func AppendServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) { - status := map[string]*swarm.ServiceStatus{} - taskFilter := filters.NewArgs() - for i, s := range services { - // there is no need in this switch to check for job modes. jobs are not - // supported until after ServiceStatus was introduced. - switch { - case s.ServiceStatus != nil: - // Server already returned service-status, so we don't - // have to look-up tasks for this service. - continue - case s.Spec.Mode.Replicated != nil: - // For replicated services, set the desired number of tasks; - // that way we can present this information in case we're unable - // to get a list of tasks from the server. - services[i].ServiceStatus = &swarm.ServiceStatus{DesiredTasks: *s.Spec.Mode.Replicated.Replicas} - status[s.ID] = &swarm.ServiceStatus{} - taskFilter.Add("service", s.ID) - case s.Spec.Mode.Global != nil: - // No such thing as number of desired tasks for global services - services[i].ServiceStatus = &swarm.ServiceStatus{} - status[s.ID] = &swarm.ServiceStatus{} - taskFilter.Add("service", s.ID) - default: - // Unknown task type - } - } - if len(status) == 0 { - // All services have their ServiceStatus set, so we're done - return services, nil - } - - tasks, err := c.TaskList(ctx, swarm.TaskListOptions{Filters: taskFilter}) - if err != nil { - return nil, err - } - if len(tasks) == 0 { - return services, nil - } - activeNodes, err := getActiveNodes(ctx, c) - if err != nil { - return nil, err - } - - for _, task := range tasks { - if status[task.ServiceID] == nil { - // This should not happen in practice; either all services have - // a ServiceStatus set, or none of them. - continue - } - // TODO: this should only be needed for "global" services. Replicated - // services have `Spec.Mode.Replicated.Replicas`, which should give this value. - if task.DesiredState != swarm.TaskStateShutdown { - status[task.ServiceID].DesiredTasks++ - } - if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning { - status[task.ServiceID].RunningTasks++ - } - } - - for i, service := range services { - if s := status[service.ID]; s != nil { - services[i].ServiceStatus = s - } - } - return services, nil -} - -func getActiveNodes(ctx context.Context, c client.NodeAPIClient) (map[string]struct{}, error) { - nodes, err := c.NodeList(ctx, swarm.NodeListOptions{}) - if err != nil { - return nil, err - } - activeNodes := make(map[string]struct{}) - for _, n := range nodes { - if n.Status.State != swarm.NodeStateDown { - activeNodes[n.ID] = struct{}{} - } - } - return activeNodes, nil + return ListFormatWrite(servicesCtx, res) } diff --git a/cli/command/service/list_test.go b/cli/command/service/list_test.go index c1defc768e91..be60eb4f77d7 100644 --- a/cli/command/service/list_test.go +++ b/cli/command/service/list_test.go @@ -9,8 +9,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -18,11 +18,13 @@ import ( func TestServiceListOrder(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - newService("a57dbe8", "service-1-foo"), - newService("a57dbdd", "service-10-foo"), - newService("aaaaaaa", "service-2-foo"), + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{ + newService("a57dbe8", "service-1-foo"), + newService("a57dbdd", "service-10-foo"), + newService("aaaaaaa", "service-2-foo"), + }, }, nil }, }) @@ -62,59 +64,12 @@ func TestServiceListServiceStatus(t *testing.T) { cluster: &cluster{}, // force an empty cluster expected: []listResponse{}, }, - { - // Services are running, but no active nodes were found. On API v1.40 - // and below, this will cause looking up the "running" tasks to fail, - // as well as looking up "desired" tasks for global services. - doc: "API v1.40 no active nodes", - opts: clusterOpts{ - apiVersion: "1.40", - activeNodes: 0, - runningTasks: 2, - desiredTasks: 4, - }, - expected: []listResponse{ - {ID: "replicated", Replicas: "0/4"}, - {ID: "global", Replicas: "0/0"}, - {ID: "none-id", Replicas: "0/0"}, - }, - }, - { - doc: "API v1.40 3 active nodes, 1 task running", - opts: clusterOpts{ - apiVersion: "1.40", - activeNodes: 3, - runningTasks: 1, - desiredTasks: 2, - }, - expected: []listResponse{ - {ID: "replicated", Replicas: "1/2"}, - {ID: "global", Replicas: "1/3"}, - {ID: "none-id", Replicas: "0/0"}, - }, - }, - { - doc: "API v1.40 3 active nodes, all tasks running", - opts: clusterOpts{ - apiVersion: "1.40", - activeNodes: 3, - runningTasks: 3, - desiredTasks: 3, - }, - expected: []listResponse{ - {ID: "replicated", Replicas: "3/3"}, - {ID: "global", Replicas: "3/3"}, - {ID: "none-id", Replicas: "0/0"}, - }, - }, - { // Services are running, but no active nodes were found. On API v1.41 // and up, the ServiceStatus is sent by the daemon, so this should not // affect the results. - doc: "API v1.41 no active nodes", + doc: "no active nodes", opts: clusterOpts{ - apiVersion: "1.41", activeNodes: 0, runningTasks: 2, desiredTasks: 4, @@ -126,9 +81,8 @@ func TestServiceListServiceStatus(t *testing.T) { }, }, { - doc: "API v1.41 3 active nodes, 1 task running", + doc: "active nodes, 1 task running", opts: clusterOpts{ - apiVersion: "1.41", activeNodes: 3, runningTasks: 1, desiredTasks: 2, @@ -140,9 +94,8 @@ func TestServiceListServiceStatus(t *testing.T) { }, }, { - doc: "API v1.41 3 active nodes, all tasks running", + doc: "active nodes, all tasks running", opts: clusterOpts{ - apiVersion: "1.41", activeNodes: 3, runningTasks: 3, desiredTasks: 3, @@ -172,19 +125,19 @@ func TestServiceListServiceStatus(t *testing.T) { tc.cluster = generateCluster(t, tc.opts) } cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - if !options.Status || versions.LessThan(tc.opts.apiVersion, "1.41") { + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + if !options.Status { // Don't return "ServiceStatus" if not requested, or on older API versions - for i := range tc.cluster.services { - tc.cluster.services[i].ServiceStatus = nil + for i := range tc.cluster.services.Items { + tc.cluster.services.Items[i].ServiceStatus = nil } } return tc.cluster.services, nil }, - taskListFunc: func(context.Context, swarm.TaskListOptions) ([]swarm.Task, error) { + taskListFunc: func(context.Context, client.TaskListOptions) (client.TaskListResult, error) { return tc.cluster.tasks, nil }, - nodeListFunc: func(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { + nodeListFunc: func(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) { return tc.cluster.nodes, nil }, }) @@ -213,16 +166,15 @@ func TestServiceListServiceStatus(t *testing.T) { } type clusterOpts struct { - apiVersion string activeNodes uint64 desiredTasks uint64 runningTasks uint64 } type cluster struct { - services []swarm.Service - tasks []swarm.Task - nodes []swarm.Node + services client.ServiceListResult + tasks client.TaskListResult + nodes client.NodeListResult } func generateCluster(t *testing.T, opts clusterOpts) *cluster { @@ -235,7 +187,7 @@ func generateCluster(t *testing.T, opts clusterOpts) *cluster { return &c } -func generateServices(t *testing.T, opts clusterOpts) []swarm.Service { +func generateServices(t *testing.T, opts clusterOpts) client.ServiceListResult { t.Helper() // Can't have more global tasks than nodes @@ -243,32 +195,33 @@ func generateServices(t *testing.T, opts clusterOpts) []swarm.Service { if globalTasks > opts.activeNodes { globalTasks = opts.activeNodes } - - return []swarm.Service{ - *builders.Service( - builders.ServiceID("replicated"), - builders.ServiceName("01-replicated-service"), - builders.ReplicatedService(opts.desiredTasks), - builders.ServiceStatus(opts.desiredTasks, opts.runningTasks), - ), - *builders.Service( - builders.ServiceID("global"), - builders.ServiceName("02-global-service"), - builders.GlobalService(), - builders.ServiceStatus(opts.activeNodes, globalTasks), - ), - *builders.Service( - builders.ServiceID("none-id"), - builders.ServiceName("03-none-service"), - ), + return client.ServiceListResult{ + Items: []swarm.Service{ + *builders.Service( + builders.ServiceID("replicated"), + builders.ServiceName("01-replicated-service"), + builders.ReplicatedService(opts.desiredTasks), + builders.ServiceStatus(opts.desiredTasks, opts.runningTasks), + ), + *builders.Service( + builders.ServiceID("global"), + builders.ServiceName("02-global-service"), + builders.GlobalService(), + builders.ServiceStatus(opts.activeNodes, globalTasks), + ), + *builders.Service( + builders.ServiceID("none-id"), + builders.ServiceName("03-none-service"), + ), + }, } } -func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, opts clusterOpts) []swarm.Task { +func generateTasks(t *testing.T, services client.ServiceListResult, nodes client.NodeListResult, opts clusterOpts) client.TaskListResult { t.Helper() - tasks := make([]swarm.Task, 0) + tasks := client.TaskListResult{} - for _, s := range services { + for _, s := range services.Items { if s.Spec.Mode.Replicated == nil && s.Spec.Mode.Global == nil { continue } @@ -281,9 +234,9 @@ func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, o desiredTasks = opts.activeNodes } - for _, n := range nodes { + for _, n := range nodes.Items { if runningTasks < opts.runningTasks && n.Status.State != swarm.NodeStateDown { - tasks = append(tasks, swarm.Task{ + tasks.Items = append(tasks.Items, swarm.Task{ NodeID: n.ID, ServiceID: s.ID, Status: swarm.TaskStatus{State: swarm.TaskStateRunning}, @@ -298,7 +251,7 @@ func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, o // and thus will be included when calculating the "desired" tasks // for services. if failedTasks < (desiredTasks - opts.runningTasks) { - tasks = append(tasks, swarm.Task{ + tasks.Items = append(tasks.Items, swarm.Task{ NodeID: n.ID, ServiceID: s.ID, Status: swarm.TaskStatus{State: swarm.TaskStateFailed}, @@ -309,7 +262,7 @@ func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, o // Also add tasks with DesiredState: Shutdown. These should not be // counted as running or desired tasks. - tasks = append(tasks, swarm.Task{ + tasks.Items = append(tasks.Items, swarm.Task{ NodeID: n.ID, ServiceID: s.ID, Status: swarm.TaskStatus{State: swarm.TaskStateShutdown}, @@ -322,16 +275,16 @@ func generateTasks(t *testing.T, services []swarm.Service, nodes []swarm.Node, o // generateNodes generates a "nodes" endpoint API response with the requested // number of "ready" nodes. In addition, a "down" node is generated. -func generateNodes(t *testing.T, activeNodes uint64) []swarm.Node { +func generateNodes(t *testing.T, activeNodes uint64) client.NodeListResult { t.Helper() - nodes := make([]swarm.Node, 0) + nodes := client.NodeListResult{} var i uint64 for i = 0; i < activeNodes; i++ { - nodes = append(nodes, swarm.Node{ + nodes.Items = append(nodes.Items, swarm.Node{ ID: fmt.Sprintf("node-ready-%d", i), Status: swarm.NodeStatus{State: swarm.NodeStateReady}, }) - nodes = append(nodes, swarm.Node{ + nodes.Items = append(nodes.Items, swarm.Node{ ID: fmt.Sprintf("node-down-%d", i), Status: swarm.NodeStatus{State: swarm.NodeStateDown}, }) diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index 7c651d859bb2..347442332546 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -3,26 +3,23 @@ package service import ( "bytes" "context" + "errors" "fmt" "io" "sort" "strconv" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/internal/logdetails" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stdcopy" - "github.com/pkg/errors" + "github.com/moby/moby/api/pkg/stdcopy" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type logsOptions struct { @@ -39,7 +36,7 @@ type logsOptions struct { target string } -func newLogsCommand(dockerCli command.Cli) *cobra.Command { +func newLogsCommand(dockerCLI command.Cli) *cobra.Command { var opts logsOptions cmd := &cobra.Command{ @@ -48,10 +45,11 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.target = args[0] - return runLogs(cmd.Context(), dockerCli, &opts) + return runLogs(cmd.Context(), dockerCLI, &opts) }, - Annotations: map[string]string{"version": "1.29"}, - ValidArgsFunction: completeServiceNames(dockerCli), + Annotations: map[string]string{"version": "1.29"}, + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -59,46 +57,37 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.raw, "raw", false, "Do not neatly format logs") - flags.SetAnnotation("raw", "version", []string{"1.30"}) + _ = flags.SetAnnotation("raw", "version", []string{"1.30"}) flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") // options identical to container logs flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.StringVar(&opts.since, "since", "", `Show logs since timestamp (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes)`) flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") - flags.SetAnnotation("details", "version", []string{"1.30"}) + _ = flags.SetAnnotation("details", "version", []string{"1.30"}) flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } -func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error { +func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) error { //nolint:gocyclo apiClient := dockerCli.Client() var ( maxLength = 1 responseBody io.ReadCloser tty bool - // logfunc is used to delay the call to logs so that we can do some - // processing before we actually get the logs - logfunc func(context.Context, string, container.LogsOptions) (io.ReadCloser, error) ) - service, _, err := apiClient.ServiceInspectWithRaw(ctx, opts.target, swarm.ServiceInspectOptions{}) + service, err := apiClient.ServiceInspect(ctx, opts.target, client.ServiceInspectOptions{}) if err != nil { // if it's any error other than service not found, it's Real - if !cerrdefs.IsNotFound(err) { + if !errdefs.IsNotFound(err) { return err } - task, _, err := apiClient.TaskInspectWithRaw(ctx, opts.target) + res, err := apiClient.TaskInspect(ctx, opts.target, client.TaskInspectOptions{}) if err != nil { - if cerrdefs.IsNotFound(err) { + if errdefs.IsNotFound(err) { // if the task isn't found, rewrite the error to be clear // that we looked for services AND tasks and found none err = fmt.Errorf("no such task or service: %v", opts.target) @@ -106,46 +95,66 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro return err } - tty = task.Spec.ContainerSpec.TTY - maxLength = getMaxLength(task.Slot) + tty = res.Task.Spec.ContainerSpec.TTY + maxLength = getMaxLength(res.Task.Slot) - // use the TaskLogs api function - logfunc = apiClient.TaskLogs + // we can't prettify tty logs. tell the user that this is the case. + // this is why we assign the logs function to a variable and delay calling + // it. we want to check this before we make the call and checking twice in + // each branch is even sloppier than this CLI disaster already is + if tty && !opts.raw { + return errors.New("tty service logs only supported with --raw") + } + + // now get the logs + responseBody, err = apiClient.TaskLogs(ctx, opts.target, client.TaskLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Since: opts.since, + Timestamps: opts.timestamps, + Follow: opts.follow, + Tail: opts.tail, + // get the details if we request it OR if we're not doing raw mode + // (we need them for the context to pretty print) + Details: opts.details || !opts.raw, + }) + if err != nil { + return err + } + defer responseBody.Close() } else { - // use ServiceLogs api function - logfunc = apiClient.ServiceLogs - tty = service.Spec.TaskTemplate.ContainerSpec.TTY - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + tty = service.Service.Spec.TaskTemplate.ContainerSpec.TTY + if service.Service.Spec.Mode.Replicated != nil && service.Service.Spec.Mode.Replicated.Replicas != nil { // if replicas are initialized, figure out if we need to pad them - replicas := *service.Spec.Mode.Replicated.Replicas + replicas := *service.Service.Spec.Mode.Replicated.Replicas maxLength = getMaxLength(int(replicas)) } - } - // we can't prettify tty logs. tell the user that this is the case. - // this is why we assign the logs function to a variable and delay calling - // it. we want to check this before we make the call and checking twice in - // each branch is even sloppier than this CLI disaster already is - if tty && !opts.raw { - return errors.New("tty service logs only supported with --raw") - } + // we can't prettify tty logs. tell the user that this is the case. + // this is why we assign the logs function to a variable and delay calling + // it. we want to check this before we make the call and checking twice in + // each branch is even sloppier than this CLI disaster already is + if tty && !opts.raw { + return errors.New("tty service logs only supported with --raw") + } - // now get the logs - responseBody, err = logfunc(ctx, opts.target, container.LogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: opts.since, - Timestamps: opts.timestamps, - Follow: opts.follow, - Tail: opts.tail, - // get the details if we request it OR if we're not doing raw mode - // (we need them for the context to pretty print) - Details: opts.details || !opts.raw, - }) - if err != nil { - return err + // now get the logs + responseBody, err = apiClient.ServiceLogs(ctx, opts.target, client.ServiceLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Since: opts.since, + Timestamps: opts.timestamps, + Follow: opts.follow, + Tail: opts.tail, + // get the details if we request it OR if we're not doing raw mode + // (we need them for the context to pretty print) + Details: opts.details || !opts.raw, + }) + if err != nil { + return err + } + defer responseBody.Close() } - defer responseBody.Close() // tty logs get straight copied. they're not muxed with stdcopy if tty { @@ -210,21 +219,21 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, return "", err } - task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID) + res, err := f.client.TaskInspect(ctx, logCtx.taskID, client.TaskInspectOptions{}) if err != nil { return "", err } - taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) + taskName := fmt.Sprintf("%s.%d", serviceName, res.Task.Slot) if !f.opts.noTaskIDs { if f.opts.noTrunc { - taskName += "." + task.ID + taskName += "." + res.Task.ID } else { - taskName += "." + formatter.TruncateID(task.ID) + taskName += "." + formatter.TruncateID(res.Task.ID) } } - paddingCount := f.padding - getMaxLength(task.Slot) + paddingCount := f.padding - getMaxLength(res.Task.Slot) padding := "" if paddingCount > 0 { padding = strings.Repeat(" ", paddingCount) @@ -261,7 +270,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) { // break up the log line into parts. parts := bytes.SplitN(buf, []byte(" "), numParts) if len(parts) != numParts { - return 0, errors.Errorf("invalid context in log message: %v", string(buf)) + return 0, fmt.Errorf("invalid context in log message: %v", string(buf)) } // parse the details out details, err := logdetails.Parse(string(parts[detailsIndex])) @@ -326,19 +335,19 @@ func (lw *logWriter) Write(buf []byte) (int, error) { func parseContext(details map[string]string) (logContext, error) { nodeID, ok := details["com.docker.swarm.node.id"] if !ok { - return logContext{}, errors.Errorf("missing node id in details: %v", details) + return logContext{}, fmt.Errorf("missing node id in details: %v", details) } delete(details, "com.docker.swarm.node.id") serviceID, ok := details["com.docker.swarm.service.id"] if !ok { - return logContext{}, errors.Errorf("missing service id in details: %v", details) + return logContext{}, fmt.Errorf("missing service id in details: %v", details) } delete(details, "com.docker.swarm.service.id") taskID, ok := details["com.docker.swarm.task.id"] if !ok { - return logContext{}, errors.Errorf("missing task id in details: %s", details) + return logContext{}, fmt.Errorf("missing task id in details: %s", details) } delete(details, "com.docker.swarm.task.id") diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index b4346cf0805a..742db95ee90e 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -1,11 +1,13 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package service import ( "context" + "errors" "fmt" + "net/netip" "sort" "strconv" "strings" @@ -13,15 +15,13 @@ import ( "github.com/docker/cli/opts" "github.com/docker/cli/opts/swarmopts" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" gogotypes "github.com/gogo/protobuf/types" "github.com/google/shlex" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/moby/swarmkit/v2/api" "github.com/moby/swarmkit/v2/api/defaults" - "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -101,7 +101,7 @@ func (o *placementPrefOpts) Set(value string) error { return errors.New(`placement preference must be of the format "="`) } if strategy != "spread" { - return errors.Errorf("unsupported placement preference %s (only spread is supported)", strategy) + return fmt.Errorf("unsupported placement preference %s (only spread is supported)", strategy) } o.prefs = append(o.prefs, swarm.PlacementPreference{ @@ -164,9 +164,9 @@ func updateConfigFromDefaults(defaultUpdateConfig *api.UpdateConfig) *swarm.Upda Parallelism: defaultUpdateConfig.Parallelism, Delay: defaultUpdateConfig.Delay, Monitor: defaultMonitor, - FailureAction: defaultFailureAction, + FailureAction: swarm.FailureAction(defaultFailureAction), MaxFailureRatio: defaultUpdateConfig.MaxFailureRatio, - Order: defaultOrder(defaultUpdateConfig.Order), + Order: swarm.UpdateOrder(defaultOrder(defaultUpdateConfig.Order)), } } @@ -187,13 +187,13 @@ func (o updateOptions) updateConfig(flags *pflag.FlagSet) *swarm.UpdateConfig { updateConfig.Monitor = o.monitor } if flags.Changed(flagUpdateFailureAction) { - updateConfig.FailureAction = o.onFailure + updateConfig.FailureAction = swarm.FailureAction(o.onFailure) } if flags.Changed(flagUpdateMaxFailureRatio) { updateConfig.MaxFailureRatio = o.maxFailureRatio.Value() } if flags.Changed(flagUpdateOrder) { - updateConfig.Order = o.order + updateConfig.Order = swarm.UpdateOrder(o.order) } return updateConfig @@ -216,13 +216,13 @@ func (o updateOptions) rollbackConfig(flags *pflag.FlagSet) *swarm.UpdateConfig updateConfig.Monitor = o.monitor } if flags.Changed(flagRollbackFailureAction) { - updateConfig.FailureAction = o.onFailure + updateConfig.FailureAction = swarm.FailureAction(o.onFailure) } if flags.Changed(flagRollbackMaxFailureRatio) { updateConfig.MaxFailureRatio = o.maxFailureRatio.Value() } if flags.Changed(flagRollbackOrder) { - updateConfig.Order = o.order + updateConfig.Order = swarm.UpdateOrder(o.order) } return updateConfig @@ -235,15 +235,17 @@ type resourceOptions struct { resCPU opts.NanoCPUs resMemBytes opts.MemBytes resGenericResources []string + swapBytes opts.MemBytes + memSwappiness int64 } -func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, error) { +func (r *resourceOptions) ToResourceRequirements(flags *pflag.FlagSet) (*swarm.ResourceRequirements, error) { generic, err := ParseGenericResources(r.resGenericResources) if err != nil { return nil, err } - return &swarm.ResourceRequirements{ + resreq := &swarm.ResourceRequirements{ Limits: &swarm.Limit{ NanoCPUs: r.limitCPU.Value(), MemoryBytes: r.limitMemBytes.Value(), @@ -254,7 +256,20 @@ func (r *resourceOptions) ToResourceRequirements() (*swarm.ResourceRequirements, MemoryBytes: r.resMemBytes.Value(), GenericResources: generic, }, - }, nil + } + + // SwapBytes and MemorySwappiness are *int64 (pointers), so we need to have + // a variable we can take a pointer to. Additionally, we need to ensure + // that these values are only set if they are set as options. + if flags.Changed(flagSwapBytes) { + swapBytes := r.swapBytes.Value() + resreq.SwapBytes = &swapBytes + } + if flags.Changed(flagMemSwappiness) { + resreq.MemorySwappiness = &r.memSwappiness + } + + return resreq, nil } type restartPolicyOptions struct { @@ -286,11 +301,11 @@ func defaultRestartPolicy() *swarm.RestartPolicy { func defaultRestartCondition() swarm.RestartPolicyCondition { switch defaults.Service.Task.Restart.Condition { case api.RestartOnNone: - return "none" + return swarm.RestartPolicyConditionNone case api.RestartOnFailure: - return "on-failure" + return swarm.RestartPolicyConditionOnFailure case api.RestartOnAny: - return "any" + return swarm.RestartPolicyConditionAny default: return "" } @@ -299,9 +314,9 @@ func defaultRestartCondition() swarm.RestartPolicyCondition { func defaultOrder(order api.UpdateConfig_UpdateOrder) string { switch order { case api.UpdateConfig_STOP_FIRST: - return "stop-first" + return string(swarm.UpdateOrderStopFirst) case api.UpdateConfig_START_FIRST: - return "start-first" + return string(swarm.UpdateOrderStartFirst) default: return "" } @@ -335,11 +350,31 @@ type credentialSpecOpt struct { source string } +type credentialSpecType string + +const ( + credentialSpecConfig credentialSpecType = "config" + credentialSpecFile credentialSpecType = "file" + credentialSpecRegistry credentialSpecType = "registry" +) + func (c *credentialSpecOpt) Set(value string) error { + // TODO(thaJeztah): should c.source always be set, even if we may error further down? c.source = value - c.value = &swarm.CredentialSpec{} - switch { - case strings.HasPrefix(value, "config://"): + if value == "" { + // if the value of the flag is an empty string, that means there is no + // CredentialSpec needed. This is useful for removing a CredentialSpec + // during a service update. + c.value = &swarm.CredentialSpec{} + return nil + } + + scheme, val, ok := strings.Cut(value, "://") + if !ok { + scheme = "" + } + switch credentialSpecType(scheme) { + case credentialSpecConfig: // NOTE(dperny): we allow the user to specify the value of // CredentialSpec Config using the Name of the config, but the API // requires the ID of the config. For simplicity, we will parse @@ -347,20 +382,24 @@ func (c *credentialSpecOpt) Set(value string) error { // making API calls, we may need to swap the Config Name for the ID. // Therefore, this isn't the definitive location for the value of // Config that is passed to the API. - c.value.Config = strings.TrimPrefix(value, "config://") - case strings.HasPrefix(value, "file://"): - c.value.File = strings.TrimPrefix(value, "file://") - case strings.HasPrefix(value, "registry://"): - c.value.Registry = strings.TrimPrefix(value, "registry://") - case value == "": - // if the value of the flag is an empty string, that means there is no - // CredentialSpec needed. This is useful for removing a CredentialSpec - // during a service update. + c.value = &swarm.CredentialSpec{ + Config: val, + } + return nil + case credentialSpecFile: + c.value = &swarm.CredentialSpec{ + File: val, + } + return nil + case credentialSpecRegistry: + c.value = &swarm.CredentialSpec{ + Registry: val, + } + return nil default: + c.value = &swarm.CredentialSpec{} return errors.New(`invalid credential spec: value must be prefixed with "config://", "file://", or "registry://"`) } - - return nil } func (*credentialSpecOpt) Type() string { @@ -376,8 +415,11 @@ func (c *credentialSpecOpt) Value() *swarm.CredentialSpec { } func resolveNetworkID(ctx context.Context, apiClient client.NetworkAPIClient, networkIDOrName string) (string, error) { - nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, network.InspectOptions{Scope: "swarm"}) - return nw.ID, err + res, err := apiClient.NetworkInspect(ctx, networkIDOrName, client.NetworkInspectOptions{Scope: "swarm"}) + if err != nil { + return "", err + } + return res.Network.ID, nil } func convertNetworks(networks opts.NetworkOpt) []swarm.NetworkAttachmentConfig { @@ -446,7 +488,7 @@ func (o *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { o.retries != 0 if o.noHealthcheck { if haveHealthSettings { - return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) } healthConfig = &container.HealthConfig{Test: []string{"NONE"}} } else if haveHealthSettings { @@ -584,7 +626,7 @@ func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { switch options.mode { case "global": if options.replicas.Value() != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") + return serviceMode, errors.New("replicas can only be used with replicated or replicated-job mode") } if options.maxReplicas > 0 { @@ -620,11 +662,11 @@ func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { return serviceMode, errors.New("max-concurrent can only be used with replicated-job mode") } if options.replicas.Value() != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") + return serviceMode, errors.New("replicas can only be used with replicated or replicated-job mode") } serviceMode.GlobalJob = &swarm.GlobalJob{} default: - return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", options.mode) + return serviceMode, fmt.Errorf("unknown mode: %s, only replicated and global supported", options.mode) } return serviceMode, nil } @@ -692,7 +734,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N // flags are not set, then the values will be nil. If they are non-nil, // then return an error. if (serviceMode.ReplicatedJob != nil || serviceMode.GlobalJob != nil) && (updateConfig != nil || rollbackConfig != nil) { - return service, errors.Errorf("update and rollback configuration is not supported for jobs") + return service, errors.New("update and rollback configuration is not supported for jobs") } networks := convertNetworks(options.networks) @@ -707,7 +749,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N return networks[i].Target < networks[j].Target }) - resources, err := options.resources.ToResourceRequirements() + resources, err := options.resources.ToResourceRequirements(flags) if err != nil { return service, err } @@ -736,7 +778,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N Mounts: options.mounts.Value(), Init: &options.init, DNSConfig: &swarm.DNSConfig{ - Nameservers: options.dns.GetSlice(), + Nameservers: toNetipAddrSlice(options.dns.GetSlice()), Search: options.dnsSearch.GetSlice(), Options: options.dnsOption.GetSlice(), }, @@ -862,6 +904,10 @@ func addServiceFlags(flags *pflag.FlagSet, options *serviceOptions, defaultFlagV flags.Var(&options.resources.resMemBytes, flagReserveMemory, "Reserve Memory") flags.Int64Var(&options.resources.limitPids, flagLimitPids, 0, "Limit maximum number of processes (default 0 = unlimited)") flags.SetAnnotation(flagLimitPids, "version", []string{"1.41"}) + flags.Var(&options.resources.swapBytes, flagSwapBytes, "Swap Bytes (-1 for unlimited)") + flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"}) + flags.Int64Var(&options.resources.memSwappiness, flagMemSwappiness, -1, "Tune memory swappiness (0-100), -1 to reset to default") + flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"}) flags.Var(&options.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) flags.Var(&options.replicas, flagReplicas, "Number of tasks") @@ -1046,4 +1092,21 @@ const ( flagUlimitAdd = "ulimit-add" flagUlimitRemove = "ulimit-rm" flagOomScoreAdj = "oom-score-adj" + flagSwapBytes = "memory-swap" + flagMemSwappiness = "memory-swappiness" ) + +func toNetipAddrSlice(ips []string) []netip.Addr { + if len(ips) == 0 { + return nil + } + netIPs := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + netIPs = append(netIPs, addr) + } + return netIPs +} diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go index 799d32cebd2d..d11d9a2e1aa9 100644 --- a/cli/command/service/opts_test.go +++ b/cli/command/service/opts_test.go @@ -7,9 +7,10 @@ import ( "time" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -162,8 +163,10 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) { }, } + flags := newCreateCommand(nil).Flags() + for _, opt := range incorrectOptions { - _, err := opt.ToResourceRequirements() + _, err := opt.ToResourceRequirements(flags) assert.Check(t, is.ErrorContains(err, "")) } @@ -177,27 +180,71 @@ func TestResourceOptionsToResourceRequirements(t *testing.T) { } for _, opt := range correctOptions { - r, err := opt.ToResourceRequirements() + r, err := opt.ToResourceRequirements(flags) assert.NilError(t, err) assert.Check(t, is.Len(r.Reservations.GenericResources, len(opt.resGenericResources))) } } +func TestResourceOptionsToResourceRequirementsSwap(t *testing.T) { + // first, check that no flag set means no field set in the return + flags := newCreateCommand(nil).Flags() + + // These should be the default values of the field. + swapOptions := resourceOptions{ + swapBytes: 0, + memSwappiness: -1, + } + + r, err := swapOptions.ToResourceRequirements(flags) + assert.NilError(t, err) + assert.Check(t, is.Nil(r.SwapBytes)) + assert.Check(t, is.Nil(r.MemorySwappiness)) + + // now set the flags and some values + flags.Set(flagSwapBytes, "86000") + flags.Set(flagMemSwappiness, "23") + swapOptions.swapBytes = 86000 + swapOptions.memSwappiness = 23 + + r, err = swapOptions.ToResourceRequirements(flags) + assert.NilError(t, err) + assert.Check(t, r.SwapBytes != nil) + assert.Check(t, is.Equal(*(r.SwapBytes), int64(86000))) + assert.Check(t, r.MemorySwappiness != nil) + assert.Check(t, is.Equal(*(r.MemorySwappiness), int64(23))) +} + func TestToServiceNetwork(t *testing.T) { nws := []network.Inspect{ - {Name: "aaa-network", ID: "id555"}, - {Name: "mmm-network", ID: "id999"}, - {Name: "zzz-network", ID: "id111"}, + { + Network: network.Network{ + Name: "aaa-network", + ID: "id555", + }, + }, + { + Network: network.Network{ + Name: "mmm-network", + ID: "id999", + }, + }, + { + Network: network.Network{ + Name: "zzz-network", + ID: "id111", + }, + }, } - client := &fakeClient{ - networkInspectFunc: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { + apiClient := &fakeClient{ + networkInspectFunc: func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { for _, nw := range nws { if nw.ID == networkID || nw.Name == networkID { - return nw, nil + return client.NetworkInspectResult{Network: nw}, nil } } - return network.Inspect{}, fmt.Errorf("network not found: %s", networkID) + return client.NetworkInspectResult{}, fmt.Errorf("network not found: %s", networkID) }, } @@ -212,7 +259,7 @@ func TestToServiceNetwork(t *testing.T) { ctx := context.Background() flags := newCreateCommand(nil).Flags() - service, err := o.ToService(ctx, client, flags) + service, err := o.ToService(ctx, apiClient, flags) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)) } @@ -233,17 +280,17 @@ func TestToServiceUpdateRollback(t *testing.T) { Parallelism: 23, Delay: 34 * time.Second, Monitor: 54321 * time.Nanosecond, - FailureAction: "pause", + FailureAction: swarm.UpdateFailureActionPause, MaxFailureRatio: 0.6, - Order: "stop-first", + Order: swarm.UpdateOrderStopFirst, }, RollbackConfig: &swarm.UpdateConfig{ Parallelism: 12, Delay: 23 * time.Second, Monitor: 12345 * time.Nanosecond, - FailureAction: "continue", + FailureAction: swarm.UpdateFailureActionContinue, MaxFailureRatio: 0.5, - Order: "start-first", + Order: swarm.UpdateOrderStartFirst, }, } @@ -253,16 +300,16 @@ func TestToServiceUpdateRollback(t *testing.T) { flags.Set("update-parallelism", "23") flags.Set("update-delay", "34s") flags.Set("update-monitor", "54321ns") - flags.Set("update-failure-action", "pause") + flags.Set("update-failure-action", string(swarm.UpdateFailureActionPause)) flags.Set("update-max-failure-ratio", "0.6") - flags.Set("update-order", "stop-first") + flags.Set("update-order", string(swarm.UpdateOrderStopFirst)) flags.Set("rollback-parallelism", "12") flags.Set("rollback-delay", "23s") flags.Set("rollback-monitor", "12345ns") - flags.Set("rollback-failure-action", "continue") + flags.Set("rollback-failure-action", string(swarm.UpdateFailureActionContinue)) flags.Set("rollback-max-failure-ratio", "0.5") - flags.Set("rollback-order", "start-first") + flags.Set("rollback-order", string(swarm.UpdateOrderStartFirst)) o := newServiceOptions() o.mode = "replicated" @@ -270,17 +317,17 @@ func TestToServiceUpdateRollback(t *testing.T) { parallelism: 23, delay: 34 * time.Second, monitor: 54321 * time.Nanosecond, - onFailure: "pause", + onFailure: string(swarm.UpdateFailureActionPause), maxFailureRatio: 0.6, - order: "stop-first", + order: string(swarm.UpdateOrderStopFirst), } o.rollback = updateOptions{ parallelism: 12, delay: 23 * time.Second, monitor: 12345 * time.Nanosecond, - onFailure: "continue", + onFailure: string(swarm.UpdateFailureActionContinue), maxFailureRatio: 0.5, - order: "start-first", + order: string(swarm.UpdateOrderStartFirst), } service, err := o.ToService(context.Background(), &fakeClient{}, flags) @@ -291,18 +338,18 @@ func TestToServiceUpdateRollback(t *testing.T) { func TestToServiceUpdateRollbackOrder(t *testing.T) { flags := newCreateCommand(nil).Flags() - flags.Set("update-order", "start-first") - flags.Set("rollback-order", "start-first") + flags.Set("update-order", string(swarm.UpdateOrderStartFirst)) + flags.Set("rollback-order", string(swarm.UpdateOrderStartFirst)) o := newServiceOptions() o.mode = "replicated" - o.update = updateOptions{order: "start-first"} - o.rollback = updateOptions{order: "start-first"} + o.update = updateOptions{order: string(swarm.UpdateOrderStartFirst)} + o.rollback = updateOptions{order: string(swarm.UpdateOrderStartFirst)} service, err := o.ToService(context.Background(), &fakeClient{}, flags) assert.NilError(t, err) - assert.Check(t, is.Equal(service.UpdateConfig.Order, o.update.order)) - assert.Check(t, is.Equal(service.RollbackConfig.Order, o.rollback.order)) + assert.Check(t, is.Equal(string(service.UpdateConfig.Order), o.update.order)) + assert.Check(t, is.Equal(string(service.RollbackConfig.Order), o.rollback.order)) } func TestToServiceMaxReplicasGlobalModeConflict(t *testing.T) { diff --git a/cli/command/service/parse.go b/cli/command/service/parse.go index dcd43621713b..e0086985d289 100644 --- a/cli/command/service/parse.go +++ b/cli/command/service/parse.go @@ -2,37 +2,36 @@ package service import ( "context" + "fmt" - "github.com/docker/docker/api/types/filters" - swarmtypes "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) // ParseSecrets retrieves the secrets with the requested names and fills // secret IDs into the secret references. -func ParseSecrets(ctx context.Context, apiClient client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) { +func ParseSecrets(ctx context.Context, apiClient client.SecretAPIClient, requestedSecrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { if len(requestedSecrets) == 0 { - return []*swarmtypes.SecretReference{}, nil + return []*swarm.SecretReference{}, nil } - secretRefs := make(map[string]*swarmtypes.SecretReference) + secretRefs := make(map[string]*swarm.SecretReference) for _, secret := range requestedSecrets { if _, exists := secretRefs[secret.File.Name]; exists { - return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName) + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.SecretName) } - secretRef := new(swarmtypes.SecretReference) + secretRef := new(swarm.SecretReference) *secretRef = *secret secretRefs[secret.File.Name] = secretRef } - args := filters.NewArgs() + args := make(client.Filters) for _, s := range secretRefs { args.Add("name", s.SecretName) } - secrets, err := apiClient.SecretList(ctx, swarmtypes.SecretListOptions{ + res, err := apiClient.SecretList(ctx, client.SecretListOptions{ Filters: args, }) if err != nil { @@ -40,16 +39,15 @@ func ParseSecrets(ctx context.Context, apiClient client.SecretAPIClient, request } foundSecrets := make(map[string]string) - for _, secret := range secrets { + for _, secret := range res.Items { foundSecrets[secret.Spec.Annotations.Name] = secret.ID } - addedSecrets := []*swarmtypes.SecretReference{} - + addedSecrets := make([]*swarm.SecretReference, 0, len(secretRefs)) for _, ref := range secretRefs { id, ok := foundSecrets[ref.SecretName] if !ok { - return nil, errors.Errorf("secret not found: %s", ref.SecretName) + return nil, fmt.Errorf("secret not found: %s", ref.SecretName) } // set the id for the ref to properly assign in swarm @@ -63,9 +61,9 @@ func ParseSecrets(ctx context.Context, apiClient client.SecretAPIClient, request // ParseConfigs retrieves the configs from the requested names and converts // them to config references to use with the spec -func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, requestedConfigs []*swarmtypes.ConfigReference) ([]*swarmtypes.ConfigReference, error) { +func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, requestedConfigs []*swarm.ConfigReference) ([]*swarm.ConfigReference, error) { if len(requestedConfigs) == 0 { - return []*swarmtypes.ConfigReference{}, nil + return []*swarm.ConfigReference{}, nil } // the configRefs map has two purposes: it prevents duplication of config @@ -79,12 +77,12 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request // are in use for the same Config, and we should deduplicate // such ConfigReferences, as no matter how many times the Config is used, // it is only needed to be referenced once. - configRefs := make(map[string]*swarmtypes.ConfigReference) - runtimeRefs := make(map[string]*swarmtypes.ConfigReference) + configRefs := make(map[string]*swarm.ConfigReference) + runtimeRefs := make(map[string]*swarm.ConfigReference) for _, config := range requestedConfigs { // copy the config, so we don't mutate the args - configRef := new(swarmtypes.ConfigReference) + configRef := new(swarm.ConfigReference) *configRef = *config if config.Runtime != nil { @@ -98,13 +96,13 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request } if _, exists := configRefs[config.File.Name]; exists { - return nil, errors.Errorf("duplicate config target for %s not allowed", config.ConfigName) + return nil, fmt.Errorf("duplicate config target for %s not allowed", config.ConfigName) } configRefs[config.File.Name] = configRef } - args := filters.NewArgs() + args := make(client.Filters) for _, s := range configRefs { args.Add("name", s.ConfigName) } @@ -112,7 +110,7 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request args.Add("name", s.ConfigName) } - configs, err := apiClient.ConfigList(ctx, swarmtypes.ConfigListOptions{ + res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{ Filters: args, }) if err != nil { @@ -120,16 +118,15 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request } foundConfigs := make(map[string]string) - for _, config := range configs { + for _, config := range res.Items { foundConfigs[config.Spec.Annotations.Name] = config.ID } - addedConfigs := []*swarmtypes.ConfigReference{} - + addedConfigs := make([]*swarm.ConfigReference, 0, len(configRefs)) for _, ref := range configRefs { id, ok := foundConfigs[ref.ConfigName] if !ok { - return nil, errors.Errorf("config not found: %s", ref.ConfigName) + return nil, fmt.Errorf("config not found: %s", ref.ConfigName) } // set the id for the ref to properly assign in swarm @@ -144,7 +141,7 @@ func ParseConfigs(ctx context.Context, apiClient client.ConfigAPIClient, request for _, ref := range runtimeRefs { id, ok := foundConfigs[ref.ConfigName] if !ok { - return nil, errors.Errorf("config not found: %s", ref.ConfigName) + return nil, fmt.Errorf("config not found: %s", ref.ConfigName) } ref.ConfigID = id diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go index 5f87e29125e7..a4de39b8fbdf 100644 --- a/cli/command/service/progress/progress.go +++ b/cli/command/service/progress/progress.go @@ -12,11 +12,10 @@ import ( "time" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/progress" + "github.com/moby/moby/client/pkg/streamformatter" ) var ( @@ -88,24 +87,24 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID ) for { - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{}) + res, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{}) if err != nil { return err } - if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 { - monitor = service.Spec.UpdateConfig.Monitor + if res.Service.Spec.UpdateConfig != nil && res.Service.Spec.UpdateConfig.Monitor != 0 { + monitor = res.Service.Spec.UpdateConfig.Monitor } if updater == nil { - updater, err = initializeUpdater(service, progressOut) + updater, err = initializeUpdater(res.Service, progressOut) if err != nil { return err } } - if service.UpdateStatus != nil { - switch service.UpdateStatus.State { + if res.Service.UpdateStatus != nil { + switch res.Service.UpdateStatus.State { case swarm.UpdateStateUpdating: rollback = false case swarm.UpdateStateCompleted: @@ -113,39 +112,38 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID return nil } case swarm.UpdateStatePaused: - return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message) + return fmt.Errorf("service update paused: %s", res.Service.UpdateStatus.Message) case swarm.UpdateStateRollbackStarted: - if !rollback && service.UpdateStatus.Message != "" { + if !rollback && res.Service.UpdateStatus.Message != "" { progressOut.WriteProgress(progress.Progress{ ID: "rollback", - Action: service.UpdateStatus.Message, + Action: res.Service.UpdateStatus.Message, }) } rollback = true case swarm.UpdateStateRollbackPaused: - return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message) + return fmt.Errorf("service rollback paused: %s", res.Service.UpdateStatus.Message) case swarm.UpdateStateRollbackCompleted: if !converged { - message = &progress.Progress{ID: "rollback", Message: service.UpdateStatus.Message} + message = &progress.Progress{ID: "rollback", Message: res.Service.UpdateStatus.Message} } rollback = true } } if converged && time.Since(convergedAt) >= monitor { - progressOut.WriteProgress(progress.Progress{ + _ = progressOut.WriteProgress(progress.Progress{ ID: "verify", Action: fmt.Sprintf("Service %s converged", serviceID), }) if message != nil { - progressOut.WriteProgress(*message) + _ = progressOut.WriteProgress(*message) } return nil } - tasks, err := apiClient.TaskList(ctx, swarm.TaskListOptions{Filters: filters.NewArgs( - filters.KeyValuePair{Key: "service", Value: service.ID}, - filters.KeyValuePair{Key: "_up-to-date", Value: "true"}, - )}) + tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{ + Filters: make(client.Filters).Add("service", res.Service.ID).Add("_up-to-date", "true"), + }) if err != nil { return err } @@ -155,7 +153,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID return err } - converged, err = updater.update(service, tasks, activeNodes, rollback) + converged, err = updater.update(res.Service, tasks.Items, activeNodes, rollback) if err != nil { return err } @@ -167,7 +165,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID // only job services have a non-nil job status, which means we can // use the presence of this field to check if the service is a job // here. - if service.JobStatus != nil { + if res.Service.JobStatus != nil { progress.Message(progressOut, "", "job complete") return nil } @@ -177,7 +175,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID } wait := monitor - time.Since(convergedAt) if wait >= 0 { - progressOut.WriteProgress(progress.Progress{ + _ = progressOut.WriteProgress(progress.Progress{ // Ideally this would have no ID, but // the progress rendering code behaves // poorly on an "action" with no ID. It @@ -192,7 +190,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID } } else { if !convergedAt.IsZero() { - progressOut.WriteProgress(progress.Progress{ + _ = progressOut.WriteProgress(progress.Progress{ ID: "verify", Action: "Detected task failure", }) @@ -216,13 +214,13 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID // // TODO(thaJeztah): this should really be a filter on [apiClient.NodeList] instead of being filtered on the client side. func getActiveNodes(ctx context.Context, apiClient client.NodeAPIClient) (map[string]struct{}, error) { - nodes, err := apiClient.NodeList(ctx, swarm.NodeListOptions{}) + res, err := apiClient.NodeList(ctx, client.NodeListOptions{}) if err != nil { return nil, err } activeNodes := make(map[string]struct{}) - for _, n := range nodes { + for _, n := range res.Items { if n.Status.State != swarm.NodeStateDown { activeNodes[n.ID] = struct{}{} } @@ -652,7 +650,7 @@ func (u *replicatedJobProgressUpdater) update(_ swarm.Service, tasks []swarm.Tas } func (u *replicatedJobProgressUpdater) writeOverallProgress(active, completed int) { - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: "job progress", Action: fmt.Sprintf( // * means "use the next positional arg to compute padding" @@ -669,7 +667,7 @@ func (u *replicatedJobProgressUpdater) writeOverallProgress(active, completed in actualDesired = u.concurrent } - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: "active tasks", Action: fmt.Sprintf( // [n] notation lets us select a specific argument, 1-indexed @@ -692,14 +690,14 @@ func (u *replicatedJobProgressUpdater) writeTaskProgress(task swarm.Task) { } if task.Status.Err != "" { - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: fmt.Sprintf("%d/%d", task.Slot+1, u.total), Action: truncError(task.Status.Err), }) return } - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: fmt.Sprintf("%d/%d", task.Slot+1, u.total), Action: fmt.Sprintf("%-*s", longestState, task.Status.State), Current: numberedStates[task.Status.State], @@ -732,7 +730,7 @@ func (u *globalJobProgressUpdater) update(service swarm.Service, tasks []swarm.T if !u.initialized { // if there are not yet tasks, then return early. if len(tasks) == 0 && len(activeNodes) != 0 { - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: "job progress", Action: "waiting for tasks", }) @@ -810,14 +808,14 @@ func (u *globalJobProgressUpdater) writeTaskProgress(task swarm.Task) { } if task.Status.Err != "" { - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: task.NodeID, Action: truncError(task.Status.Err), }) return } - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ ID: task.NodeID, Action: fmt.Sprintf("%-*s", longestState, task.Status.State), Current: numberedStates[task.Status.State], @@ -829,7 +827,7 @@ func (u *globalJobProgressUpdater) writeTaskProgress(task swarm.Task) { func (u *globalJobProgressUpdater) writeOverallProgress(complete int) { // all tasks for a global job are active at once, so we only write out the // total progress. - u.progressOut.WriteProgress(progress.Progress{ + _ = u.progressOut.WriteProgress(progress.Progress{ // see (*replicatedJobProgressUpdater).writeOverallProgress for an // explanation of the advanced fmt use in this function. ID: "job progress", diff --git a/cli/command/service/progress/progress_test.go b/cli/command/service/progress/progress_test.go index 7e5ed2643ba2..7cd7664e3545 100644 --- a/cli/command/service/progress/progress_test.go +++ b/cli/command/service/progress/progress_test.go @@ -5,8 +5,8 @@ import ( "strconv" "testing" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/progress" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client/pkg/progress" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go index 601014054d0d..4591f1cf0a5e 100644 --- a/cli/command/service/ps.go +++ b/cli/command/service/ps.go @@ -2,21 +2,17 @@ package service import ( "context" + "errors" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/cli/command/node" "github.com/docker/cli/cli/command/task" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type psOptions struct { @@ -28,7 +24,7 @@ type psOptions struct { filter opts.FilterOpt } -func newPsCommand(dockerCli command.Cli) *cobra.Command { +func newPsCommand(dockerCLI command.Cli) *cobra.Command { options := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -37,9 +33,10 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.services = args - return runPS(cmd.Context(), dockerCli, options) + return runPS(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completeServiceNames(dockerCli), + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs") @@ -48,12 +45,6 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } @@ -68,7 +59,7 @@ func runPS(ctx context.Context, dockerCli command.Cli, options psOptions) error return err } - tasks, err := apiClient.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) + tasks, err := apiClient.TaskList(ctx, client.TaskListOptions{Filters: filter}) if err != nil { return err } @@ -89,20 +80,20 @@ func runPS(ctx context.Context, dockerCli command.Cli, options psOptions) error return nil } -func createFilter(ctx context.Context, apiClient client.APIClient, options psOptions) (filters.Args, []string, error) { +func createFilter(ctx context.Context, apiClient client.APIClient, options psOptions) (client.Filters, []string, error) { filter := options.filter.Value() - serviceIDFilter := filters.NewArgs() - serviceNameFilter := filters.NewArgs() + serviceIDFilter := make(client.Filters) + serviceNameFilter := make(client.Filters) for _, service := range options.services { serviceIDFilter.Add("id", service) serviceNameFilter.Add("name", service) } - serviceByIDList, err := apiClient.ServiceList(ctx, swarm.ServiceListOptions{Filters: serviceIDFilter}) + serviceByID, err := apiClient.ServiceList(ctx, client.ServiceListOptions{Filters: serviceIDFilter}) if err != nil { return filter, nil, err } - serviceByNameList, err := apiClient.ServiceList(ctx, swarm.ServiceListOptions{Filters: serviceNameFilter}) + serviceByName, err := apiClient.ServiceList(ctx, client.ServiceListOptions{Filters: serviceNameFilter}) if err != nil { return filter, nil, err } @@ -112,14 +103,14 @@ func createFilter(ctx context.Context, apiClient client.APIClient, options psOpt loop: // Match services by 1. Full ID, 2. Full name, 3. ID prefix. An error is returned if the ID-prefix match is ambiguous for _, service := range options.services { - for _, s := range serviceByIDList { + for _, s := range serviceByID.Items { if s.ID == service { filter.Add("service", s.ID) serviceCount++ continue loop } } - for _, s := range serviceByNameList { + for _, s := range serviceByName.Items { if s.Spec.Annotations.Name == service { filter.Add("service", s.ID) serviceCount++ @@ -127,7 +118,7 @@ loop: } } found := false - for _, s := range serviceByIDList { + for _, s := range serviceByID.Items { if strings.HasPrefix(s.ID, service) { if found { return filter, nil, errors.New("multiple services found with provided prefix: " + service) @@ -147,15 +138,15 @@ loop: return filter, notfound, err } -func updateNodeFilter(ctx context.Context, apiClient client.APIClient, filter filters.Args) error { - if filter.Contains("node") { - nodeFilters := filter.Get("node") - for _, nodeFilter := range nodeFilters { +func updateNodeFilter(ctx context.Context, apiClient client.APIClient, filter client.Filters) error { + if nodeFilters, ok := filter["node"]; ok { + for nodeFilter := range nodeFilters { nodeReference, err := node.Reference(ctx, apiClient, nodeFilter) if err != nil { return err } - filter.Del("node", nodeFilter) + // TODO(thaJeztah): add utility to remove? + delete(filter["node"], nodeFilter) filter.Add("node", nodeReference) } } diff --git a/cli/command/service/ps_test.go b/cli/command/service/ps_test.go index 5301846a551a..0833d64a768e 100644 --- a/cli/command/service/ps_test.go +++ b/cli/command/service/ps_test.go @@ -6,22 +6,23 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/google/go-cmp/cmp" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestCreateFilter(t *testing.T) { - client := &fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - {ID: "idmatch"}, - {ID: "idprefixmatch"}, - newService("cccccccc", "namematch"), - newService("01010101", "notfoundprefix"), + apiClient := &fakeClient{ + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{ + {ID: "idmatch"}, + {ID: "idprefixmatch"}, + newService("cccccccc", "namematch"), + newService("01010101", "notfoundprefix"), + }, }, nil }, } @@ -33,56 +34,52 @@ func TestCreateFilter(t *testing.T) { filter: filter, } - actual, notfound, err := createFilter(context.Background(), client, options) + actual, notfound, err := createFilter(context.Background(), apiClient, options) assert.NilError(t, err) assert.Check(t, is.DeepEqual(notfound, []string{"no such service: notfound"})) - expected := filters.NewArgs( - filters.Arg("service", "idmatch"), - filters.Arg("service", "idprefixmatch"), - filters.Arg("service", "cccccccc"), - filters.Arg("node", "somenode"), - ) - assert.DeepEqual(t, expected, actual, cmpFilters) + expected := make(client.Filters).Add("service", "idmatch").Add("service", "idprefixmatch").Add("service", "cccccccc").Add("node", "somenode") + assert.DeepEqual(t, expected, actual) } func TestCreateFilterWithAmbiguousIDPrefixError(t *testing.T) { - client := &fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - {ID: "aaaone"}, - {ID: "aaatwo"}, + apiClient := &fakeClient{ + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{ + {ID: "aaaone"}, + {ID: "aaatwo"}, + }, }, nil }, } - options := psOptions{ + _, _, err := createFilter(context.Background(), apiClient, psOptions{ services: []string{"aaa"}, filter: opts.NewFilterOpt(), - } - _, _, err := createFilter(context.Background(), client, options) + }) assert.Error(t, err, "multiple services found with provided prefix: aaa") } func TestCreateFilterNoneFound(t *testing.T) { - client := &fakeClient{} + apiClient := &fakeClient{} options := psOptions{ services: []string{"foo", "notfound"}, filter: opts.NewFilterOpt(), } - _, _, err := createFilter(context.Background(), client, options) + _, _, err := createFilter(context.Background(), apiClient, options) assert.Error(t, err, "no such service: foo\nno such service: notfound") } func TestRunPSWarnsOnNotFound(t *testing.T) { - client := &fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - {ID: "foo"}, + apiClient := &fakeClient{ + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{{ID: "foo"}}, }, nil }, } - cli := test.NewFakeCli(client) + cli := test.NewFakeCli(apiClient) options := psOptions{ services: []string{"foo", "bar"}, filter: opts.NewFilterOpt(), @@ -95,16 +92,20 @@ func TestRunPSWarnsOnNotFound(t *testing.T) { } func TestRunPSQuiet(t *testing.T) { - client := &fakeClient{ - serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{{ID: "foo"}}, nil + apiClient := &fakeClient{ + serviceListFunc: func(ctx context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{{ID: "foo"}}, + }, nil }, - taskListFunc: func(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{{ID: "sxabyp0obqokwekpun4rjo0b3"}}, nil + taskListFunc: func(ctx context.Context, options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{{ID: "sxabyp0obqokwekpun4rjo0b3"}}, + }, nil }, } - cli := test.NewFakeCli(client) + cli := test.NewFakeCli(apiClient) ctx := context.Background() err := runPS(ctx, cli, psOptions{services: []string{"foo"}, quiet: true, filter: opts.NewFilterOpt()}) assert.NilError(t, err) @@ -113,26 +114,21 @@ func TestRunPSQuiet(t *testing.T) { func TestUpdateNodeFilter(t *testing.T) { selfNodeID := "foofoo" - filter := filters.NewArgs( - filters.Arg("node", "one"), - filters.Arg("node", "two"), - filters.Arg("node", "self"), - ) - - client := &fakeClient{ - infoFunc: func(_ context.Context) (system.Info, error) { - return system.Info{Swarm: swarm.Info{NodeID: selfNodeID}}, nil + filter := make(client.Filters).Add("node", "one", "two", "self") + + apiClient := &fakeClient{ + infoFunc: func(_ context.Context) (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + Swarm: swarm.Info{NodeID: selfNodeID}, + }, + }, nil }, } - updateNodeFilter(context.Background(), client, filter) + err := updateNodeFilter(context.Background(), apiClient, filter) + assert.NilError(t, err) - expected := filters.NewArgs( - filters.Arg("node", "one"), - filters.Arg("node", "two"), - filters.Arg("node", selfNodeID), - ) - assert.DeepEqual(t, expected, filter, cmpFilters) + expected := make(client.Filters).Add("node", "one", "two", selfNodeID) + assert.DeepEqual(t, expected, filter) } - -var cmpFilters = cmp.AllowUnexported(filters.Args{}) diff --git a/cli/command/service/remove.go b/cli/command/service/remove.go index 522dd91cd532..56ca7482cd91 100644 --- a/cli/command/service/remove.go +++ b/cli/command/service/remove.go @@ -7,19 +7,21 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "rm SERVICE [SERVICE...]", Aliases: []string{"remove"}, Short: "Remove one or more services", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(cmd.Context(), dockerCli, args) + return runRemove(cmd.Context(), dockerCLI, args) }, - ValidArgsFunction: completeServiceNames(dockerCli), + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } cmd.Flags() @@ -31,7 +33,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, serviceIDs []string) var errs []error for _, id := range serviceIDs { - if err := apiClient.ServiceRemove(ctx, id); err != nil { + if _, err := apiClient.ServiceRemove(ctx, id, client.ServiceRemoveOptions{}); err != nil { errs = append(errs, err) continue } diff --git a/cli/command/service/rollback.go b/cli/command/service/rollback.go index 3b4a445f2e23..e3ca98e2385a 100644 --- a/cli/command/service/rollback.go +++ b/cli/command/service/rollback.go @@ -6,14 +6,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) -func newRollbackCommand(dockerCli command.Cli) *cobra.Command { +func newRollbackCommand(dockerCLI command.Cli) *cobra.Command { options := newServiceOptions() cmd := &cobra.Command{ @@ -21,34 +18,31 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command { Short: "Revert changes to a service's configuration", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRollback(cmd.Context(), dockerCli, options, args[0]) + return runRollback(cmd.Context(), dockerCLI, options, args[0]) }, - Annotations: map[string]string{"version": "1.31"}, - ValidArgsFunction: completeServiceNames(dockerCli), + Annotations: map[string]string{"version": "1.31"}, + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output") addDetachFlag(flags, &options.detach) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } func runRollback(ctx context.Context, dockerCLI command.Cli, options *serviceOptions, serviceID string) error { apiClient := dockerCLI.Client() - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{}) + res, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{}) if err != nil { return err } - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, swarm.ServiceUpdateOptions{ + response, err := apiClient.ServiceUpdate(ctx, res.Service.ID, client.ServiceUpdateOptions{ + Version: res.Service.Version, + Spec: res.Service.Spec, Rollback: "previous", // TODO(thaJeztah): this should have a const defined }) if err != nil { @@ -61,7 +55,7 @@ func runRollback(ctx context.Context, dockerCLI command.Cli, options *serviceOpt _, _ = fmt.Fprintln(dockerCLI.Out(), serviceID) - if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") { + if options.detach { return nil } diff --git a/cli/command/service/rollback_test.go b/cli/command/service/rollback_test.go index a308584344e3..19842888bad0 100644 --- a/cli/command/service/rollback_test.go +++ b/cli/command/service/rollback_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -17,7 +17,7 @@ func TestRollback(t *testing.T) { testCases := []struct { name string args []string - serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) + serviceUpdateFunc func(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) expectedDockerCliErr string }{ { @@ -27,15 +27,13 @@ func TestRollback(t *testing.T) { { name: "rollback-service-with-warnings", args: []string{"service-id"}, - serviceUpdateFunc: func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { - response := swarm.ServiceUpdateResponse{} - - response.Warnings = []string{ - "- warning 1", - "- warning 2", - } - - return response, nil + serviceUpdateFunc: func(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) { + return client.ServiceUpdateResult{ + Warnings: []string{ + "- warning 1", + "- warning 2", + }, + }, nil }, expectedDockerCliErr: "- warning 1\n- warning 2", }, @@ -47,7 +45,8 @@ func TestRollback(t *testing.T) { }) cmd := newRollbackCommand(cli) cmd.SetArgs(tc.args) - cmd.Flags().Set("quiet", "true") + assert.NilError(t, cmd.Flags().Set("quiet", "true")) + assert.NilError(t, cmd.Flags().Set("detach", "true")) cmd.SetOut(io.Discard) assert.NilError(t, cmd.Execute()) assert.Check(t, is.Equal(strings.TrimSpace(cli.ErrBuffer().String()), tc.expectedDockerCliErr)) @@ -56,11 +55,11 @@ func TestRollback(t *testing.T) { func TestRollbackWithErrors(t *testing.T) { testCases := []struct { - name string - args []string - serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) - serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) - expectedError string + name string + args []string + serviceInspectFunc func(ctx context.Context, serviceID string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) + serviceUpdateFunc func(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) + expectedError string }{ { name: "not-enough-args", @@ -74,16 +73,16 @@ func TestRollbackWithErrors(t *testing.T) { { name: "service-does-not-exists", args: []string{"service-id"}, - serviceInspectWithRawFunc: func(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return swarm.Service{}, []byte{}, fmt.Errorf("no such services: %s", serviceID) + serviceInspectFunc: func(ctx context.Context, serviceID string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{}, fmt.Errorf("no such services: %s", serviceID) }, expectedError: "no such services: service-id", }, { name: "service-update-failed", args: []string{"service-id"}, - serviceUpdateFunc: func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { - return swarm.ServiceUpdateResponse{}, fmt.Errorf("no such services: %s", serviceID) + serviceUpdateFunc: func(ctx context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) { + return client.ServiceUpdateResult{}, fmt.Errorf("no such services: %s", serviceID) }, expectedError: "no such services: service-id", }, @@ -93,11 +92,11 @@ func TestRollbackWithErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cmd := newRollbackCommand( test.NewFakeCli(&fakeClient{ - serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc, - serviceUpdateFunc: tc.serviceUpdateFunc, + serviceInspectFunc: tc.serviceInspectFunc, + serviceUpdateFunc: tc.serviceUpdateFunc, })) cmd.SetArgs(tc.args) - cmd.Flags().Set("quiet", "true") + assert.NilError(t, cmd.Flags().Set("quiet", "true")) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) diff --git a/cli/command/service/scale.go b/cli/command/service/scale.go index 2d1acf3d89e0..a2555ab98f7f 100644 --- a/cli/command/service/scale.go +++ b/cli/command/service/scale.go @@ -9,9 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -19,7 +17,7 @@ type scaleOptions struct { detach bool } -func newScaleCommand(dockerCli command.Cli) *cobra.Command { +func newScaleCommand(dockerCLI command.Cli) *cobra.Command { options := &scaleOptions{} cmd := &cobra.Command{ @@ -27,9 +25,10 @@ func newScaleCommand(dockerCli command.Cli) *cobra.Command { Short: "Scale one or multiple replicated services", Args: scaleArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runScale(cmd.Context(), dockerCli, options, args) + return runScale(cmd.Context(), dockerCLI, options, args) }, - ValidArgsFunction: completeScaleArgs(dockerCli), + ValidArgsFunction: completeScaleArgs(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -83,7 +82,7 @@ func runScale(ctx context.Context, dockerCLI command.Cli, options *scaleOptions, serviceIDs = append(serviceIDs, serviceID) } - if len(serviceIDs) > 0 && !options.detach && versions.GreaterThanOrEqualTo(dockerCLI.Client().ClientVersion(), "1.29") { + if len(serviceIDs) > 0 && !options.detach { for _, serviceID := range serviceIDs { if err := WaitOnService(ctx, dockerCLI, serviceID, false); err != nil { errs = append(errs, fmt.Errorf("%s: %v", serviceID, err)) @@ -94,12 +93,12 @@ func runScale(ctx context.Context, dockerCLI command.Cli, options *scaleOptions, } func runServiceScale(ctx context.Context, apiClient client.ServiceAPIClient, serviceID string, scale uint64) (warnings []string, _ error) { - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{}) + res, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{}) if err != nil { return nil, err } - serviceMode := &service.Spec.Mode + serviceMode := &res.Service.Spec.Mode switch { case serviceMode.Replicated != nil: serviceMode.Replicated.Replicas = &scale @@ -109,7 +108,10 @@ func runServiceScale(ctx context.Context, apiClient client.ServiceAPIClient, ser return nil, errors.New("scale can only be used with replicated or replicated-job mode") } - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, swarm.ServiceUpdateOptions{}) + response, err := apiClient.ServiceUpdate(ctx, res.Service.ID, client.ServiceUpdateOptions{ + Version: res.Service.Version, + Spec: res.Service.Spec, + }) if err != nil { return nil, err } diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go deleted file mode 100644 index 6fb4d129a5ff..000000000000 --- a/cli/command/service/trust.go +++ /dev/null @@ -1,82 +0,0 @@ -package service - -import ( - "encoding/hex" - - "github.com/distribution/reference" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/trust" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/registry" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/theupdateframework/notary/tuf/data" -) - -func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm.ServiceSpec) error { - if !dockerCli.ContentTrustEnabled() { - // When not using content trust, digest resolution happens later when - // contacting the registry to retrieve image information. - return nil - } - - ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image) - if err != nil { - return errors.Wrapf(err, "invalid reference %s", service.TaskTemplate.ContainerSpec.Image) - } - - // If reference does not have digest (is not canonical nor image id) - if _, ok := ref.(reference.Digested); !ok { - namedRef, ok := ref.(reference.Named) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not named") - } - namedRef = reference.TagNameOnly(namedRef) - taggedRef, ok := namedRef.(reference.NamedTagged) - if !ok { - return errors.New("failed to resolve image digest using content trust: reference is not tagged") - } - - resolvedImage, err := trustedResolveDigest(dockerCli, taggedRef) - if err != nil { - return errors.Wrap(err, "failed to resolve image digest using content trust") - } - resolvedFamiliar := reference.FamiliarString(resolvedImage) - logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar) - service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar - } - - return nil -} - -func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index) - - notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") - if err != nil { - return nil, errors.Wrap(err, "error establishing connection to trust repository") - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, trust.NotaryError(repoInfo.Name.Name(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref))) - } - - logrus.Debugf("retrieving target for %s role", t.Role) - h, ok := t.Hashes["sha256"] - if !ok { - return nil, errors.New("no valid hash, expecting sha256") - } - - dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) - - // Allow returning canonical reference with tag - return reference.WithDigest(ref, dgst) -} diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 2b75b6b59b18..54e83d442ea7 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -1,8 +1,14 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + package service import ( "context" + "errors" "fmt" + "net/netip" + "slices" "sort" "strings" "time" @@ -12,14 +18,12 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/cli/opts/swarmopts" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/moby/swarmkit/v2/api/defaults" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -34,21 +38,22 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), options, args[0]) }, - ValidArgsFunction: completeServiceNames(dockerCLI), + ValidArgsFunction: completeServiceNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.String("image", "", "Service image tag") flags.Var(&ShlexOpt{}, "args", "Service command args") flags.Bool(flagRollback, false, "Rollback to previous specification") - flags.SetAnnotation(flagRollback, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagRollback, "version", []string{"1.25"}) flags.Bool("force", false, "Force update even if no changes require it") - flags.SetAnnotation("force", "version", []string{"1.25"}) + _ = flags.SetAnnotation("force", "version", []string{"1.25"}) addServiceFlags(flags, options, nil) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") - flags.SetAnnotation(flagGroupRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagGroupRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") @@ -56,72 +61,72 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { flags.Var(&swarmopts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") - flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") - flags.SetAnnotation(flagDNSOptionRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSOptionRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") - flags.SetAnnotation(flagDNSSearchRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSSearchRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagHostRemove, `Remove a custom host-to-IP mapping ("host:ip")`) - flags.SetAnnotation(flagHostRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagHostRemove, "version", []string{"1.25"}) flags.Var(&options.labels, flagLabelAdd, "Add or update a service label") flags.Var(&options.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&options.env, flagEnvAdd, "Add or update an environment variable") flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) flags.Var(&options.secrets, flagSecretAdd, "Add or update a secret on a service") - flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagConfigRemove, "Remove a configuration file") - flags.SetAnnotation(flagConfigRemove, "version", []string{"1.30"}) + _ = flags.SetAnnotation(flagConfigRemove, "version", []string{"1.30"}) flags.Var(&options.configs, flagConfigAdd, "Add or update a config file on a service") - flags.SetAnnotation(flagConfigAdd, "version", []string{"1.30"}) + _ = flags.SetAnnotation(flagConfigAdd, "version", []string{"1.30"}) flags.Var(&options.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&options.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&options.placementPrefs, flagPlacementPrefAdd, "Add a placement preference") - flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) + _ = flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") - flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) + _ = flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) flags.Var(&options.networks, flagNetworkAdd, "Add a network") - flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"}) + _ = flags.SetAnnotation(flagNetworkAdd, "version", []string{"1.29"}) flags.Var(newListOptsVar(), flagNetworkRemove, "Remove a network") - flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"}) + _ = flags.SetAnnotation(flagNetworkRemove, "version", []string{"1.29"}) flags.Var(&options.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") flags.Var(&options.groups, flagGroupAdd, "Add an additional supplementary user group to the container") - flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) flags.Var(&options.dns, flagDNSAdd, "Add or update a custom DNS server") - flags.SetAnnotation(flagDNSAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSAdd, "version", []string{"1.25"}) flags.Var(&options.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") - flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) flags.Var(&options.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") - flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) flags.Var(&options.hosts, flagHostAdd, `Add a custom host-to-IP mapping ("host:ip")`) - flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) + _ = flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) flags.BoolVar(&options.init, flagInit, false, "Use an init inside each service container to forward signals and reap processes") - flags.SetAnnotation(flagInit, "version", []string{"1.37"}) + _ = flags.SetAnnotation(flagInit, "version", []string{"1.37"}) flags.Var(&options.sysctls, flagSysCtlAdd, "Add or update a Sysctl option") - flags.SetAnnotation(flagSysCtlAdd, "version", []string{"1.40"}) + _ = flags.SetAnnotation(flagSysCtlAdd, "version", []string{"1.40"}) flags.Var(newListOptsVar(), flagSysCtlRemove, "Remove a Sysctl option") - flags.SetAnnotation(flagSysCtlRemove, "version", []string{"1.40"}) + _ = flags.SetAnnotation(flagSysCtlRemove, "version", []string{"1.40"}) flags.Var(&options.ulimits, flagUlimitAdd, "Add or update a ulimit option") - flags.SetAnnotation(flagUlimitAdd, "version", []string{"1.41"}) + _ = flags.SetAnnotation(flagUlimitAdd, "version", []string{"1.41"}) flags.Var(newListOptsVar(), flagUlimitRemove, "Remove a ulimit option") - flags.SetAnnotation(flagUlimitRemove, "version", []string{"1.41"}) + _ = flags.SetAnnotation(flagUlimitRemove, "version", []string{"1.41"}) flags.Int64Var(&options.oomScoreAdj, flagOomScoreAdj, 0, "Tune host's OOM preferences (-1000 to 1000) ") - flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"}) + _ = flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"}) // Add needs parsing, Remove only needs the key flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + _ = flags.SetAnnotation(flagGenericResourcesRemove, "version", []string{"1.32"}) flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + _ = flags.SetAnnotation(flagGenericResourcesAdd, "version", []string{"1.32"}) // TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package) // _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames) // _ = cmd.RegisterFlagCompletionFunc(flagCapDrop, completeLinuxCapabilityNames) // _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals) - _ = cmd.RegisterFlagCompletionFunc(flagEnvAdd, completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc(flagEnvAdd, completion.EnvVarNames()) // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service) _ = cmd.RegisterFlagCompletionFunc("image", completion.ImageNames(dockerCLI, -1)) _ = cmd.RegisterFlagCompletionFunc(flagNetworkAdd, completion.NetworkNames(dockerCLI)) @@ -133,12 +138,6 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { _ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback")) completion.ImageNames(dockerCLI, -1) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) return cmd } @@ -155,7 +154,7 @@ func newListOptsVarWithValidator(validator opts.ValidatorFctType) *opts.ListOpts func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error { apiClient := dockerCLI.Client() - service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID, swarm.ServiceInspectOptions{}) + res, err := apiClient.ServiceInspect(ctx, serviceID, client.ServiceInspectOptions{}) if err != nil { return err } @@ -165,15 +164,6 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, return err } - // There are two ways to do user-requested rollback. The old way is - // client-side, but with a sufficiently recent daemon we prefer - // server-side, because it will honor the rollback parameters. - var ( - clientSideRollback bool - serverSideRollback bool - ) - - spec := &service.Spec if rollback { // Rollback can't be combined with other flags. otherFlagsPassed := false @@ -188,49 +178,34 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, if otherFlagsPassed { return errors.New("other flags may not be combined with --rollback") } - - if versions.LessThan(apiClient.ClientVersion(), "1.28") { - clientSideRollback = true - spec = service.PreviousSpec - if spec == nil { - return errors.Errorf("service does not have a previous specification to roll back to") - } - } else { - serverSideRollback = true - } } - updateOpts := swarm.ServiceUpdateOptions{} - if serverSideRollback { - updateOpts.Rollback = "previous" + updateOpts := client.ServiceUpdateOptions{} + rollbackAction := "none" + if rollback { + rollbackAction = "previous" } + spec := &res.Service.Spec err = updateService(ctx, apiClient, flags, spec) if err != nil { return err } if flags.Changed("image") { - if err := resolveServiceImageDigestContentTrust(dockerCLI, spec); err != nil { - return err - } - if !options.noResolveImage && versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") { - updateOpts.QueryRegistry = true - } + updateOpts.QueryRegistry = !options.noResolveImage } updatedSecrets, err := getUpdatedSecrets(ctx, apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) if err != nil { return err } - spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets updatedConfigs, err := getUpdatedConfigs(ctx, apiClient, flags, spec.TaskTemplate.ContainerSpec) if err != nil { return err } - spec.TaskTemplate.ContainerSpec.Configs = updatedConfigs // set the credential spec value after get the updated configs, because we @@ -239,27 +214,31 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, updateCredSpecConfig(flags, spec.TaskTemplate.ContainerSpec) // only send auth if flag was set - sendAuth, err := flags.GetBool(flagRegistryAuth) - if err != nil { + var encodedAuth string + var registryAuthFrom string + if ok, err := flags.GetBool(flagRegistryAuth); err != nil { return err - } - switch { - case sendAuth: + } else if ok { // Retrieve encoded auth token from the image reference // This would be the old image if it didn't change in this update - image := spec.TaskTemplate.ContainerSpec.Image - encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image) + var err error + encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), spec.TaskTemplate.ContainerSpec.Image) if err != nil { return err } updateOpts.EncodedRegistryAuth = encodedAuth - case clientSideRollback: - updateOpts.RegistryAuthFrom = swarm.RegistryAuthFromPreviousSpec - default: - updateOpts.RegistryAuthFrom = swarm.RegistryAuthFromSpec + } else { + registryAuthFrom = string(swarm.RegistryAuthFromSpec) } - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) + response, err := apiClient.ServiceUpdate(ctx, res.Service.ID, client.ServiceUpdateOptions{ + Version: res.Service.Version, + Spec: *spec, + + EncodedRegistryAuth: encodedAuth, + RegistryAuthFrom: swarm.RegistryAuthSource(registryAuthFrom), + Rollback: rollbackAction, + }) if err != nil { return err } @@ -270,7 +249,7 @@ func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, _, _ = fmt.Fprintln(dockerCLI.Out(), serviceID) - if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") { + if options.detach { return nil } @@ -454,9 +433,15 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor) - updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) + if flags.Changed(flagUpdateFailureAction) { + value, _ := flags.GetString(flagUpdateFailureAction) + spec.UpdateConfig.FailureAction = swarm.FailureAction(value) + } updateFloatValue(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) - updateString(flagUpdateOrder, &spec.UpdateConfig.Order) + if flags.Changed(flagUpdateOrder) { + value, _ := flags.GetString(flagUpdateOrder) + spec.UpdateConfig.Order = swarm.UpdateOrder(value) + } } if anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio, flagRollbackOrder) { @@ -466,9 +451,15 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags updateUint64(flagRollbackParallelism, &spec.RollbackConfig.Parallelism) updateDuration(flagRollbackDelay, &spec.RollbackConfig.Delay) updateDuration(flagRollbackMonitor, &spec.RollbackConfig.Monitor) - updateString(flagRollbackFailureAction, &spec.RollbackConfig.FailureAction) + if flags.Changed(flagRollbackFailureAction) { + value, _ := flags.GetString(flagRollbackFailureAction) + spec.RollbackConfig.FailureAction = swarm.FailureAction(value) + } updateFloatValue(flagRollbackMaxFailureRatio, &spec.RollbackConfig.MaxFailureRatio) - updateString(flagRollbackOrder, &spec.RollbackConfig.Order) + if flags.Changed(flagRollbackOrder) { + value, _ := flags.GetString(flagRollbackOrder) + spec.RollbackConfig.Order = swarm.UpdateOrder(value) + } } if flags.Changed(flagEndpointMode) { @@ -943,33 +934,33 @@ func removeItems( return newSeq } -func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { - mountsByTarget := map[string]mounttypes.Mount{} +func updateMounts(flags *pflag.FlagSet, mounts *[]mount.Mount) error { + mountsByTarget := map[string]mount.Mount{} if flags.Changed(flagMountAdd) { values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() - for _, mount := range values { - if _, ok := mountsByTarget[mount.Target]; ok { - return errors.Errorf("duplicate mount target") + for _, mnt := range values { + if _, ok := mountsByTarget[mnt.Target]; ok { + return errors.New("duplicate mount target") } - mountsByTarget[mount.Target] = mount + mountsByTarget[mnt.Target] = mnt } } // Add old list of mount points minus updated one. - for _, mount := range *mounts { - if _, ok := mountsByTarget[mount.Target]; !ok { - mountsByTarget[mount.Target] = mount + for _, mnt := range *mounts { + if _, ok := mountsByTarget[mnt.Target]; !ok { + mountsByTarget[mnt.Target] = mnt } } - newMounts := []mounttypes.Mount{} + newMounts := make([]mount.Mount, 0, len(mountsByTarget)) toRemove := buildToRemoveSet(flags, flagMountRemove) - for _, mount := range mountsByTarget { - if _, exists := toRemove[mount.Target]; !exists { - newMounts = append(newMounts, mount) + for _, mnt := range mountsByTarget { + if _, exists := toRemove[mnt.Target]; !exists { + newMounts = append(newMounts, mnt) } } sort.Slice(newMounts, func(i, j int) bool { @@ -1005,9 +996,9 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error { return nil } -func removeDuplicates(entries []string) []string { - hit := map[string]bool{} - newEntries := []string{} +func removeDuplicates[T comparable](entries []T) []T { + hit := map[T]bool{} + newEntries := []T{} for _, v := range entries { if !hit[v] { newEntries = append(newEntries, v) @@ -1023,24 +1014,34 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { nameservers := (*config).Nameservers if flags.Changed(flagDNSAdd) { values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetSlice() - nameservers = append(nameservers, values...) + var ips []netip.Addr + for _, ip := range values { + a, err := netip.ParseAddr(ip) + if err != nil { + return err + } + ips = append(ips, a) + } + nameservers = append(nameservers, ips...) } nameservers = removeDuplicates(nameservers) toRemove := buildToRemoveSet(flags, flagDNSRemove) for _, nameserver := range nameservers { - if _, exists := toRemove[nameserver]; !exists { + if _, exists := toRemove[nameserver.String()]; !exists { newConfig.Nameservers = append(newConfig.Nameservers, nameserver) } } // Sort so that result is predictable. - sort.Strings(newConfig.Nameservers) + slices.SortFunc(newConfig.Nameservers, func(a, b netip.Addr) int { + return a.Compare(b) + }) search := (*config).Search if flags.Changed(flagDNSSearchAdd) { values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetSlice() search = append(search, values...) } - search = removeDuplicates(search) + search = slices.Compact(search) toRemove = buildToRemoveSet(flags, flagDNSSearchRemove) for _, entry := range search { if _, exists := toRemove[entry]; !exists { @@ -1055,7 +1056,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { values := flags.Lookup(flagDNSOptionAdd).Value.(*opts.ListOpts).GetSlice() options = append(options, values...) } - options = removeDuplicates(options) + options = slices.Compact(options) toRemove = buildToRemoveSet(flags, flagDNSOptionRemove) for _, option := range options { if _, exists := toRemove[option]; !exists { @@ -1126,16 +1127,16 @@ portLoop: return nil } -func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool { +func equalProtocol(prot1, prot2 network.IPProtocol) bool { return prot1 == prot2 || - (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) || - (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP) + (prot1 == "" && prot2 == network.TCP) || + (prot2 == "" && prot1 == network.TCP) } func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool { return mode1 == mode2 || - (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) || - (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress) + (mode1 == "" && mode2 == swarm.PortConfigPublishModeIngress) || + (mode2 == "" && mode1 == swarm.PortConfigPublishModeIngress) } func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error { @@ -1144,7 +1145,7 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error } if serviceMode == nil || serviceMode.Replicated == nil { - return errors.Errorf("replicas can only be used with replicated mode") + return errors.New("replicas can only be used with replicated mode") } serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value() return nil @@ -1284,7 +1285,7 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) } return nil } - return errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + return fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) } if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" { containerSpec.Healthcheck.Test = nil @@ -1320,19 +1321,15 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag // spec.Networks field. If spec.Network is in use, we'll migrate those // values to spec.TaskTemplate.Networks. specNetworks := spec.TaskTemplate.Networks - if len(specNetworks) == 0 { - specNetworks = spec.Networks //nolint:staticcheck // ignore SA1019: field is deprecated. - } - spec.Networks = nil //nolint:staticcheck // ignore SA1019: field is deprecated. toRemove := buildToRemoveSet(flags, flagNetworkRemove) idsToRemove := make(map[string]struct{}) for networkIDOrName := range toRemove { - nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, network.InspectOptions{Scope: "swarm"}) + nw, err := apiClient.NetworkInspect(ctx, networkIDOrName, client.NetworkInspectOptions{Scope: "swarm"}) if err != nil { return err } - idsToRemove[nw.ID] = struct{}{} + idsToRemove[nw.Network.ID] = struct{}{} } existingNetworks := make(map[string]struct{}) @@ -1355,7 +1352,7 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag return err } if _, exists := existingNetworks[nwID]; exists { - return errors.Errorf("service is already attached to network %s", nw.Target) + return fmt.Errorf("service is already attached to network %s", nw.Target) } nw.Target = nwID newNetworks = append(newNetworks, nw) diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index d022b0c3d507..b2731a3aba47 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -3,15 +3,17 @@ package service import ( "context" "fmt" + "net/netip" "reflect" "sort" "testing" "time" - "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -212,7 +214,7 @@ func TestUpdateDNSConfig(t *testing.T) { flags.Set("dns-option-rm", "timeout:3") config := &swarm.DNSConfig{ - Nameservers: []string{"3.3.3.3", "5.5.5.5"}, + Nameservers: []netip.Addr{netip.MustParseAddr("3.3.3.3"), netip.MustParseAddr("5.5.5.5")}, Search: []string{"localdomain"}, Options: []string{"timeout:3"}, } @@ -220,9 +222,9 @@ func TestUpdateDNSConfig(t *testing.T) { updateDNSConfig(flags, &config) assert.Assert(t, is.Len(config.Nameservers, 3)) - assert.Check(t, is.Equal("1.1.1.1", config.Nameservers[0])) - assert.Check(t, is.Equal("2001:db8:abc8::1", config.Nameservers[1])) - assert.Check(t, is.Equal("5.5.5.5", config.Nameservers[2])) + assert.Check(t, is.Equal(netip.MustParseAddr("1.1.1.1"), config.Nameservers[0])) + assert.Check(t, is.Equal(netip.MustParseAddr("5.5.5.5"), config.Nameservers[1])) + assert.Check(t, is.Equal(netip.MustParseAddr("2001:db8:abc8::1"), config.Nameservers[2])) assert.Assert(t, is.Len(config.Search, 2)) assert.Check(t, is.Equal("example.com", config.Search[0])) @@ -237,9 +239,9 @@ func TestUpdateMounts(t *testing.T) { flags.Set("mount-add", "type=volume,source=vol2,target=/toadd") flags.Set("mount-rm", "/toremove") - mounts := []mounttypes.Mount{ - {Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind}, + mounts := []mount.Mount{ + {Target: "/toremove", Source: "vol1", Type: mount.TypeBind}, + {Target: "/tokeep", Source: "vol3", Type: mount.TypeBind}, } updateMounts(flags, &mounts) @@ -252,10 +254,10 @@ func TestUpdateMountsWithDuplicateMounts(t *testing.T) { flags := newUpdateCommand(nil).Flags() flags.Set("mount-add", "type=volume,source=vol4,target=/toadd") - mounts := []mounttypes.Mount{ - {Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind}, - {Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind}, - {Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind}, + mounts := []mount.Mount{ + {Target: "/tokeep1", Source: "vol1", Type: mount.TypeBind}, + {Target: "/toadd", Source: "vol2", Type: mount.TypeBind}, + {Target: "/tokeep2", Source: "vol3", Type: mount.TypeBind}, } updateMounts(flags, &mounts) @@ -271,7 +273,7 @@ func TestUpdatePorts(t *testing.T) { flags.Set("publish-rm", "333/udp") portConfigs := []swarm.PortConfig{ - {TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP}, + {TargetPort: 333, Protocol: network.UDP}, {TargetPort: 555}, } @@ -294,7 +296,7 @@ func TestUpdatePortsDuplicate(t *testing.T) { { TargetPort: 80, PublishedPort: 80, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, PublishMode: swarm.PortConfigPublishModeIngress, }, } @@ -487,7 +489,7 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { { TargetPort: 80, PublishedPort: 8080, - Protocol: swarm.PortConfigProtocolTCP, + Protocol: network.TCP, PublishMode: swarm.PortConfigPublishModeIngress, }, } @@ -500,27 +502,27 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { } type secretAPIClientMock struct { - listResult []swarm.Secret + listResult client.SecretListResult } -func (s secretAPIClientMock) SecretList(context.Context, swarm.SecretListOptions) ([]swarm.Secret, error) { +func (s secretAPIClientMock) SecretList(context.Context, client.SecretListOptions) (client.SecretListResult, error) { return s.listResult, nil } -func (secretAPIClientMock) SecretCreate(context.Context, swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - return swarm.SecretCreateResponse{}, nil +func (secretAPIClientMock) SecretCreate(context.Context, client.SecretCreateOptions) (client.SecretCreateResult, error) { + return client.SecretCreateResult{}, nil } -func (secretAPIClientMock) SecretRemove(context.Context, string) error { - return nil +func (secretAPIClientMock) SecretRemove(context.Context, string, client.SecretRemoveOptions) (client.SecretRemoveResult, error) { + return client.SecretRemoveResult{}, nil } -func (secretAPIClientMock) SecretInspectWithRaw(context.Context, string) (swarm.Secret, []byte, error) { - return swarm.Secret{}, []byte{}, nil +func (secretAPIClientMock) SecretInspect(context.Context, string, client.SecretInspectOptions) (client.SecretInspectResult, error) { + return client.SecretInspectResult{}, nil } -func (secretAPIClientMock) SecretUpdate(context.Context, string, swarm.Version, swarm.SecretSpec) error { - return nil +func (secretAPIClientMock) SecretUpdate(context.Context, string, client.SecretUpdateOptions) (client.SecretUpdateResult, error) { + return client.SecretUpdateResult{}, nil } // TestUpdateSecretUpdateInPlace tests the ability to update the "target" of a @@ -528,11 +530,11 @@ func (secretAPIClientMock) SecretUpdate(context.Context, string, swarm.Version, // "--secret-add" for the same secret. func TestUpdateSecretUpdateInPlace(t *testing.T) { apiClient := secretAPIClientMock{ - listResult: []swarm.Secret{ - { + listResult: client.SecretListResult{ + Items: []swarm.Secret{{ ID: "tn9qiblgnuuut11eufquw5dev", Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}}, - }, + }}, }, } @@ -846,19 +848,36 @@ func TestRemoveGenericResources(t *testing.T) { func TestUpdateNetworks(t *testing.T) { ctx := context.Background() nws := []network.Summary{ - {Name: "aaa-network", ID: "id555"}, - {Name: "mmm-network", ID: "id999"}, - {Name: "zzz-network", ID: "id111"}, + { + Network: network.Network{ + Name: "aaa-network", + ID: "id555", + }, + }, + { + Network: network.Network{ + Name: "mmm-network", + ID: "id999", + }, + }, + { + Network: network.Network{ + Name: "zzz-network", + ID: "id111", + }, + }, } - client := &fakeClient{ - networkInspectFunc: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { + apiClient := &fakeClient{ + networkInspectFunc: func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { for _, nw := range nws { if nw.ID == networkID || nw.Name == networkID { - return nw, nil + return client.NetworkInspectResult{ + Network: network.Inspect{Network: nw.Network}, + }, nil } } - return network.Inspect{}, fmt.Errorf("network not found: %s", networkID) + return client.NetworkInspectResult{}, fmt.Errorf("network not found: %s", networkID) }, } @@ -874,28 +893,28 @@ func TestUpdateNetworks(t *testing.T) { flags := newUpdateCommand(nil).Flags() err := flags.Set(flagNetworkAdd, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkAdd, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.Error(t, err, "service is already attached to network aaa-network") assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkAdd, "id555") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.Error(t, err, "service is already attached to network id555") assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkRemove, "id999") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)) @@ -904,7 +923,7 @@ func TestUpdateNetworks(t *testing.T) { assert.NilError(t, err) err = flags.Set(flagNetworkRemove, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)) } @@ -1202,16 +1221,16 @@ func TestUpdateGetUpdatedConfigs(t *testing.T) { // fakeConfigAPIClientList is actually defined in create_test.go, // but we'll use it here as well - var fakeClient fakeConfigAPIClientList = func(_ context.Context, opts swarm.ConfigListOptions) ([]swarm.Config, error) { - names := opts.Filters.Get("name") + var fakeConfigClient fakeConfigAPIClientList = func(_ context.Context, opts client.ConfigListOptions) (client.ConfigListResult, error) { + names := opts.Filters["name"] assert.Equal(t, len(names), len(tc.lookupConfigs)) - configs := []swarm.Config{} + configs := client.ConfigListResult{} for _, lookup := range tc.lookupConfigs { - assert.Assert(t, is.Contains(names, lookup)) + assert.Assert(t, names[lookup]) cfg, ok := cannedConfigs[lookup] assert.Assert(t, ok) - configs = append(configs, *cfg) + configs.Items = append(configs.Items, *cfg) } return configs, nil } @@ -1232,10 +1251,10 @@ func TestUpdateGetUpdatedConfigs(t *testing.T) { } ctx := context.Background() - finalConfigs, err := getUpdatedConfigs(ctx, fakeClient, flags, containerSpec) + finalConfigs, err := getUpdatedConfigs(ctx, fakeConfigClient, flags, containerSpec) assert.NilError(t, err) - // ensure that the finalConfigs consists of all of the expected + // ensure that the finalConfigs consists of all the expected // configs assert.Equal(t, len(finalConfigs), len(tc.expected), "%v final configs, %v expected", diff --git a/cli/command/stack/client_test.go b/cli/command/stack/client_test.go index a3b4bcdd05bd..9c308c700776 100644 --- a/cli/command/stack/client_test.go +++ b/cli/command/stack/client_test.go @@ -5,19 +5,14 @@ import ( "strings" "github.com/docker/cli/cli/compose/convert" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - version string - services []string networks []string secrets []string @@ -28,167 +23,166 @@ type fakeClient struct { removedSecrets []string removedConfigs []string - serviceListFunc func(options swarm.ServiceListOptions) ([]swarm.Service, error) - networkListFunc func(options network.ListOptions) ([]network.Summary, error) - secretListFunc func(options swarm.SecretListOptions) ([]swarm.Secret, error) - configListFunc func(options swarm.ConfigListOptions) ([]swarm.Config, error) - nodeListFunc func(options swarm.NodeListOptions) ([]swarm.Node, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) - nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) - - serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) - - serviceRemoveFunc func(serviceID string) error + serviceListFunc func(options client.ServiceListOptions) (client.ServiceListResult, error) + networkListFunc func(options client.NetworkListOptions) (client.NetworkListResult, error) + secretListFunc func(options client.SecretListOptions) (client.SecretListResult, error) + configListFunc func(options client.ConfigListOptions) (client.ConfigListResult, error) + nodeListFunc func(options client.NodeListOptions) (client.NodeListResult, error) + taskListFunc func(options client.TaskListOptions) (client.TaskListResult, error) + nodeInspectFunc func(ref string) (client.NodeInspectResult, error) + serviceUpdateFunc func(serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) + serviceRemoveFunc func(serviceID string) (client.ServiceRemoveResult, error) networkRemoveFunc func(networkID string) error - secretRemoveFunc func(secretID string) error - configRemoveFunc func(configID string) error + secretRemoveFunc func(secretID string) (client.SecretRemoveResult, error) + configRemoveFunc func(configID string) (client.ConfigRemoveResult, error) } -func (*fakeClient) ServerVersion(context.Context) (types.Version, error) { - return types.Version{ - Version: "docker-dev", - APIVersion: api.DefaultVersion, +func (*fakeClient) ServerVersion(context.Context, client.ServerVersionOptions) (client.ServerVersionResult, error) { + return client.ServerVersionResult{ + APIVersion: client.MaxAPIVersion, }, nil } -func (cli *fakeClient) ClientVersion() string { - return cli.version +func (*fakeClient) ClientVersion() string { + return client.MaxAPIVersion } -func (cli *fakeClient) ServiceList(_ context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { +func (cli *fakeClient) ServiceList(_ context.Context, options client.ServiceListOptions) (client.ServiceListResult, error) { if cli.serviceListFunc != nil { return cli.serviceListFunc(options) } namespace := namespaceFromFilters(options.Filters) - servicesList := []swarm.Service{} + servicesList := client.ServiceListResult{} for _, name := range cli.services { if belongToNamespace(name, namespace) { - servicesList = append(servicesList, serviceFromName(name)) + servicesList.Items = append(servicesList.Items, serviceFromName(name)) } } return servicesList, nil } -func (cli *fakeClient) NetworkList(_ context.Context, options network.ListOptions) ([]network.Summary, error) { +func (cli *fakeClient) NetworkList(_ context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { if cli.networkListFunc != nil { return cli.networkListFunc(options) } namespace := namespaceFromFilters(options.Filters) - networksList := []network.Summary{} + networksList := client.NetworkListResult{} for _, name := range cli.networks { if belongToNamespace(name, namespace) { - networksList = append(networksList, networkFromName(name)) + networksList.Items = append(networksList.Items, networkFromName(name)) } } return networksList, nil } -func (cli *fakeClient) SecretList(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { +func (cli *fakeClient) SecretList(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { if cli.secretListFunc != nil { return cli.secretListFunc(options) } namespace := namespaceFromFilters(options.Filters) - secretsList := []swarm.Secret{} + secretsList := client.SecretListResult{} for _, name := range cli.secrets { if belongToNamespace(name, namespace) { - secretsList = append(secretsList, secretFromName(name)) + secretsList.Items = append(secretsList.Items, secretFromName(name)) } } return secretsList, nil } -func (cli *fakeClient) ConfigList(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { +func (cli *fakeClient) ConfigList(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { if cli.configListFunc != nil { return cli.configListFunc(options) } namespace := namespaceFromFilters(options.Filters) - configsList := []swarm.Config{} + configsList := client.ConfigListResult{} for _, name := range cli.configs { if belongToNamespace(name, namespace) { - configsList = append(configsList, configFromName(name)) + configsList.Items = append(configsList.Items, configFromName(name)) } } return configsList, nil } -func (cli *fakeClient) TaskList(_ context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { +func (cli *fakeClient) TaskList(_ context.Context, options client.TaskListOptions) (client.TaskListResult, error) { if cli.taskListFunc != nil { return cli.taskListFunc(options) } - return []swarm.Task{}, nil + return client.TaskListResult{}, nil } -func (cli *fakeClient) NodeList(_ context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { +func (cli *fakeClient) NodeList(_ context.Context, options client.NodeListOptions) (client.NodeListResult, error) { if cli.nodeListFunc != nil { return cli.nodeListFunc(options) } - return []swarm.Node{}, nil + return client.NodeListResult{}, nil } -func (cli *fakeClient) NodeInspectWithRaw(_ context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectWithRaw != nil { - return cli.nodeInspectWithRaw(ref) +func (cli *fakeClient) NodeInspect(_ context.Context, ref string, _ client.NodeInspectOptions) (client.NodeInspectResult, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc(ref) } - return swarm.Node{}, nil, nil + return client.NodeInspectResult{}, nil } -func (cli *fakeClient) ServiceUpdate(_ context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { +func (cli *fakeClient) ServiceUpdate(_ context.Context, serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) { if cli.serviceUpdateFunc != nil { - return cli.serviceUpdateFunc(serviceID, version, service, options) + return cli.serviceUpdateFunc(serviceID, options) } - return swarm.ServiceUpdateResponse{}, nil + return client.ServiceUpdateResult{}, nil } -func (cli *fakeClient) ServiceRemove(_ context.Context, serviceID string) error { +func (cli *fakeClient) ServiceRemove(_ context.Context, serviceID string, _ client.ServiceRemoveOptions) (client.ServiceRemoveResult, error) { if cli.serviceRemoveFunc != nil { return cli.serviceRemoveFunc(serviceID) } cli.removedServices = append(cli.removedServices, serviceID) - return nil + return client.ServiceRemoveResult{}, nil } -func (cli *fakeClient) NetworkRemove(_ context.Context, networkID string) error { +func (cli *fakeClient) NetworkRemove(_ context.Context, networkID string, _ client.NetworkRemoveOptions) (client.NetworkRemoveResult, error) { if cli.networkRemoveFunc != nil { - return cli.networkRemoveFunc(networkID) + return client.NetworkRemoveResult{}, cli.networkRemoveFunc(networkID) } cli.removedNetworks = append(cli.removedNetworks, networkID) - return nil + return client.NetworkRemoveResult{}, nil } -func (cli *fakeClient) SecretRemove(_ context.Context, secretID string) error { +func (cli *fakeClient) SecretRemove(_ context.Context, secretID string, _ client.SecretRemoveOptions) (client.SecretRemoveResult, error) { if cli.secretRemoveFunc != nil { return cli.secretRemoveFunc(secretID) } cli.removedSecrets = append(cli.removedSecrets, secretID) - return nil + return client.SecretRemoveResult{}, nil } -func (cli *fakeClient) ConfigRemove(_ context.Context, configID string) error { +func (cli *fakeClient) ConfigRemove(_ context.Context, configID string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { if cli.configRemoveFunc != nil { return cli.configRemoveFunc(configID) } cli.removedConfigs = append(cli.removedConfigs, configID) - return nil + return client.ConfigRemoveResult{}, nil } -func (*fakeClient) ServiceInspectWithRaw(_ context.Context, serviceID string, _ swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return swarm.Service{ - ID: serviceID, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: serviceID, +func (*fakeClient) ServiceInspect(_ context.Context, serviceID string, _ client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: swarm.Service{ + ID: serviceID, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: serviceID, + }, }, }, - }, []byte{}, nil + }, nil } func serviceFromName(name string) swarm.Service { @@ -202,8 +196,10 @@ func serviceFromName(name string) swarm.Service { func networkFromName(name string) network.Summary { return network.Summary{ - ID: "ID-" + name, - Name: name, + Network: network.Network{ + ID: "ID-" + name, + Name: name, + }, } } @@ -225,8 +221,13 @@ func configFromName(name string) swarm.Config { } } -func namespaceFromFilters(fltrs filters.Args) string { - label := fltrs.Get("label")[0] +func namespaceFromFilters(fltrs client.Filters) string { + // FIXME(thaJeztah): more elegant way for this? Should we have a utility for this? + var label string + for fltr := range fltrs["label"] { + label = fltr + break + } return strings.TrimPrefix(label, convert.LabelNamespace+"=") } diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index e46f49ad9740..45c0e8803eba 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -6,37 +6,42 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/stack/swarm" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newStackCommand) +} + +// newStackCommand returns a cobra command for `stack` subcommands +func newStackCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "stack [OPTIONS]", Short: "Manage Swarm stacks", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.25", "swarm": "manager", }, + DisableFlagsInUseLine: true, } defaultHelpFunc := cmd.HelpFunc() cmd.SetHelpFunc(func(c *cobra.Command, args []string) { if err := cmd.Root().PersistentPreRunE(c, args); err != nil { - fmt.Fprintln(dockerCli.Err(), err) + fmt.Fprintln(dockerCLI.Err(), err) return } defaultHelpFunc(c, args) }) cmd.AddCommand( - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newPsCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newConfigCommand(dockerCli), + newDeployCommand(dockerCLI), + newListCommand(dockerCLI), + newPsCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newServicesCommand(dockerCLI), + newConfigCommand(dockerCLI), ) flags := cmd.PersistentFlags() flags.String("orchestrator", "", "Orchestrator to use (swarm|all)") @@ -48,7 +53,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command { // completeNames offers completion for swarm stacks func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - list, err := swarm.GetStacks(cmd.Context(), dockerCLI.Client()) + list, err := getStacks(cmd.Context(), dockerCLI.Client()) if err != nil { return nil, cobra.ShellCompDirectiveError } diff --git a/cli/command/stack/common.go b/cli/command/stack/common.go index 6de410da84c3..29f41acd631d 100644 --- a/cli/command/stack/common.go +++ b/cli/command/stack/common.go @@ -1,9 +1,14 @@ package stack import ( + "context" "fmt" "strings" "unicode" + + "github.com/docker/cli/cli/compose/convert" + "github.com/docker/cli/opts" + "github.com/moby/moby/client" ) // validateStackName checks if the provided string is a valid stack name (namespace). @@ -29,3 +34,37 @@ func validateStackNames(namespaces []string) error { func quotesOrWhitespace(r rune) bool { return unicode.IsSpace(r) || r == '"' || r == '\'' } + +func getStackFilter(namespace string) client.Filters { + return make(client.Filters).Add("label", convert.LabelNamespace+"="+namespace) +} + +func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) client.Filters { + filter := opt.Value() + filter.Add("label", convert.LabelNamespace+"="+namespace) + return filter +} + +func getAllStacksFilter() client.Filters { + return make(client.Filters).Add("label", convert.LabelNamespace) +} + +func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) (client.ServiceListResult, error) { + return apiclient.ServiceList(ctx, client.ServiceListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) (client.NetworkListResult, error) { + return apiclient.NetworkList(ctx, client.NetworkListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) (client.SecretListResult, error) { + return apiclient.SecretList(ctx, client.SecretListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) (client.ConfigListResult, error) { + return apiclient.ConfigList(ctx, client.ConfigListOptions{Filters: getStackFilter(namespace)}) +} + +func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) (client.TaskListResult, error) { + return apiclient.TaskList(ctx, client.TaskListOptions{Filters: getStackFilter(namespace)}) +} diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index 045db77b5fa6..ebcbb3f2511e 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -6,42 +6,46 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/stack/loader" - "github.com/docker/cli/cli/command/stack/options" composeLoader "github.com/docker/cli/cli/compose/loader" composetypes "github.com/docker/cli/cli/compose/types" "github.com/spf13/cobra" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) -func newConfigCommand(dockerCli command.Cli) *cobra.Command { - var opts options.Config +// configOptions holds docker stack config options +type configOptions struct { + composeFiles []string + skipInterpolation bool +} + +func newConfigCommand(dockerCLI command.Cli) *cobra.Command { + var opts configOptions cmd := &cobra.Command{ Use: "config [OPTIONS]", Short: "Outputs the final config file, after doing merges and interpolations", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - configDetails, err := loader.GetConfigDetails(opts.Composefiles, dockerCli.In()) + configDetails, err := getConfigDetails(opts.composeFiles, dockerCLI.In()) if err != nil { return err } - cfg, err := outputConfig(configDetails, opts.SkipInterpolation) + cfg, err := outputConfig(configDetails, opts.skipInterpolation) if err != nil { return err } - _, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg) + _, err = fmt.Fprintf(dockerCLI.Out(), "%s", cfg) return err }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) - flags.BoolVar(&opts.SkipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config") + flags.StringSliceVarP(&opts.composeFiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) + flags.BoolVar(&opts.skipInterpolation, "skip-interpolation", false, "Skip interpolation and output only merged config") return cmd } diff --git a/cli/command/stack/config_test.go b/cli/command/stack/config_test.go index f745ec23c25f..b4243567e709 100644 --- a/cli/command/stack/config_test.go +++ b/cli/command/stack/config_test.go @@ -15,7 +15,7 @@ func TestConfigWithEmptyComposeFile(t *testing.T) { cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), `Specify a Compose file`) + assert.ErrorContains(t, cmd.Execute(), `specify a Compose file`) } func TestConfigMergeInterpolation(t *testing.T) { diff --git a/cli/command/stack/deploy.go b/cli/command/stack/deploy.go index bb7058dd34f7..2e8a2b09530a 100644 --- a/cli/command/stack/deploy.go +++ b/cli/command/stack/deploy.go @@ -1,16 +1,33 @@ package stack import ( + "context" + "errors" + "fmt" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/loader" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/stack/swarm" + "github.com/docker/cli/cli/compose/convert" + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) -func newDeployCommand(dockerCli command.Cli) *cobra.Command { - var opts options.Deploy +// deployOptions holds docker stack deploy options +type deployOptions struct { + composefiles []string + namespace string + resolveImage string + sendRegistryAuth bool + prune bool + detach bool + quiet bool +} + +func newDeployCommand(dockerCLI command.Cli) *cobra.Command { + var opts deployOptions cmd := &cobra.Command{ Use: "deploy [OPTIONS] STACK", @@ -18,31 +35,94 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command { Short: "Deploy a new stack or update an existing stack", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Namespace = args[0] - if err := validateStackName(opts.Namespace); err != nil { + opts.namespace = args[0] + if err := validateStackName(opts.namespace); err != nil { return err } - config, err := loader.LoadComposefile(dockerCli, opts) + config, err := loadComposeFile(dockerCLI, opts) if err != nil { return err } - return swarm.RunDeploy(cmd.Context(), dockerCli, cmd.Flags(), &opts, config) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runDeploy(cmd.Context(), dockerCLI, cmd.Flags(), &opts, config) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringSliceVarP(&opts.Composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) - flags.SetAnnotation("compose-file", "version", []string{"1.25"}) - flags.BoolVar(&opts.SendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents") - flags.BoolVar(&opts.Prune, "prune", false, "Prune services that are no longer referenced") + flags.StringSliceVarP(&opts.composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`) + _ = flags.SetAnnotation("compose-file", "version", []string{"1.25"}) + // Provide tab-completion for filenames. On Bash, this is constrained to the + // ".yaml" and ".yml" file-extensions, but this doesn't appear to be supported + // by other shells. + _ = cmd.MarkFlagFilename("compose-file", "yaml", "yml") + + flags.BoolVar(&opts.sendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents") + flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") flags.SetAnnotation("prune", "version", []string{"1.27"}) - flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways, - `Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`", "`+swarm.ResolveImageChanged+`", "`+swarm.ResolveImageNever+`")`) + flags.StringVar(&opts.resolveImage, "resolve-image", resolveImageAlways, + `Query the registry to resolve image digest and supported platforms ("`+resolveImageAlways+`", "`+resolveImageChanged+`", "`+resolveImageNever+`")`) flags.SetAnnotation("resolve-image", "version", []string{"1.30"}) - flags.BoolVarP(&opts.Detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge") - flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Suppress progress output") + flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output") return cmd } + +// Resolve image constants +const ( + resolveImageAlways = "always" + resolveImageChanged = "changed" + resolveImageNever = "never" +) + +const defaultNetworkDriver = "overlay" + +// runDeploy is the swarm implementation of docker stack deploy +func runDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *deployOptions, cfg *composetypes.Config) error { + switch opts.resolveImage { + case resolveImageAlways, resolveImageChanged, resolveImageNever: + // valid options. + default: + return fmt.Errorf("invalid option %s for flag --resolve-image", opts.resolveImage) + } + + if opts.detach && !flags.Changed("detach") { + _, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ + "In a future release, --detach=false will become the default.") + } + + return deployCompose(ctx, dockerCLI, opts, cfg) +} + +// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is +// a swarm manager. This is necessary because we must create networks before we +// create services, but the API call for creating a network does not return a +// proper status code when it can't create a network in the "global" scope. +func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error { + res, err := dockerCli.Client().Info(ctx, client.InfoOptions{}) + if err != nil { + return err + } + if !res.Info.Swarm.ControlAvailable { + return errors.New(`this node is not a swarm manager. Use "docker swarm init" or "docker swarm join" to connect this node to swarm and try again`) + } + return nil +} + +// pruneServices removes services that are no longer referenced in the source +func pruneServices(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, services map[string]struct{}) { + apiClient := dockerCLI.Client() + + oldServices, err := getStackServices(ctx, apiClient, namespace.Name()) + if err != nil { + _, _ = fmt.Fprintln(dockerCLI.Err(), "Failed to list services:", err) + } + + toRemove := make([]swarm.Service, 0, len(oldServices.Items)) + for _, service := range oldServices.Items { + if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists { + toRemove = append(toRemove, service) + } + } + removeServices(ctx, dockerCLI, toRemove) +} diff --git a/cli/command/stack/deploy_composefile.go b/cli/command/stack/deploy_composefile.go new file mode 100644 index 000000000000..ad85765133cb --- /dev/null +++ b/cli/command/stack/deploy_composefile.go @@ -0,0 +1,312 @@ +package stack + +import ( + "context" + "errors" + "fmt" + + "github.com/containerd/errdefs" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/service" + "github.com/docker/cli/cli/compose/convert" + composetypes "github.com/docker/cli/cli/compose/types" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" +) + +func deployCompose(ctx context.Context, dockerCli command.Cli, opts *deployOptions, config *composetypes.Config) error { + if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { + return err + } + + namespace := convert.NewNamespace(opts.namespace) + + if opts.prune { + services := map[string]struct{}{} + for _, svc := range config.Services { + services[svc.Name] = struct{}{} + } + pruneServices(ctx, dockerCli, namespace, services) + } + + serviceNetworks := getServicesDeclaredNetworks(config.Services) + networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) + if err := validateExternalNetworks(ctx, dockerCli.Client(), externalNetworks); err != nil { + return err + } + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + + secrets, err := convert.Secrets(namespace, config.Secrets) + if err != nil { + return err + } + if err := createSecrets(ctx, dockerCli, secrets); err != nil { + return err + } + + configs, err := convert.Configs(namespace, config.Configs) + if err != nil { + return err + } + if err := createConfigs(ctx, dockerCli, configs); err != nil { + return err + } + + services, err := convert.Services(ctx, namespace, config, dockerCli.Client()) + if err != nil { + return err + } + + serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth, opts.resolveImage) + if err != nil { + return err + } + + if opts.detach { + return nil + } + + return waitOnServices(ctx, dockerCli, serviceIDs, opts.quiet) +} + +func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { + serviceNetworks := map[string]struct{}{} + for _, serviceConfig := range serviceConfigs { + if len(serviceConfig.Networks) == 0 { + serviceNetworks["default"] = struct{}{} + continue + } + for nw := range serviceConfig.Networks { + serviceNetworks[nw] = struct{}{} + } + } + return serviceNetworks +} + +func validateExternalNetworks(ctx context.Context, apiClient client.NetworkAPIClient, externalNetworks []string) error { + for _, networkName := range externalNetworks { + if !container.NetworkMode(networkName).IsUserDefined() { + // Networks that are not user defined always exist on all nodes as + // local-scoped networks, so there's no need to inspect them. + continue + } + res, err := apiClient.NetworkInspect(ctx, networkName, client.NetworkInspectOptions{}) + switch { + case errdefs.IsNotFound(err): + return fmt.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName) + case err != nil: + return err + case res.Network.Scope != "swarm": + return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, res.Network.Scope) + } + } + return nil +} + +func createSecrets(ctx context.Context, dockerCLI command.Cli, secrets []swarm.SecretSpec) error { + apiClient := dockerCLI.Client() + + for _, secretSpec := range secrets { + res, err := apiClient.SecretInspect(ctx, secretSpec.Name, client.SecretInspectOptions{}) + switch { + case err == nil: + // secret already exists, then we update that + _, err := apiClient.SecretUpdate(ctx, res.Secret.ID, client.SecretUpdateOptions{ + Version: res.Secret.Meta.Version, + Spec: secretSpec, + }) + if err != nil { + return fmt.Errorf("failed to update secret %s: %w", secretSpec.Name, err) + } + case errdefs.IsNotFound(err): + // secret does not exist, then we create a new one. + _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating secret", secretSpec.Name) + _, err := apiClient.SecretCreate(ctx, client.SecretCreateOptions{ + Spec: secretSpec, + }) + if err != nil { + return fmt.Errorf("failed to create secret %s: %w", secretSpec.Name, err) + } + default: + return err + } + } + return nil +} + +func createConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.ConfigSpec) error { + apiClient := dockerCLI.Client() + + for _, configSpec := range configs { + res, err := apiClient.ConfigInspect(ctx, configSpec.Name, client.ConfigInspectOptions{}) + switch { + case err == nil: + // config already exists, then we update that + _, err := apiClient.ConfigUpdate(ctx, res.Config.ID, client.ConfigUpdateOptions{ + Version: res.Config.Meta.Version, + Spec: configSpec, + }) + if err != nil { + return fmt.Errorf("failed to update config %s: %w", configSpec.Name, err) + } + case errdefs.IsNotFound(err): + // config does not exist, then we create a new one. + _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating config", configSpec.Name) + _, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{ + Spec: configSpec, + }) + if err != nil { + return fmt.Errorf("failed to create config %s: %w", configSpec.Name, err) + } + default: + return err + } + } + return nil +} + +func createNetworks(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, networks map[string]client.NetworkCreateOptions) error { + apiClient := dockerCLI.Client() + + existingNetworks, err := getStackNetworks(ctx, apiClient, namespace.Name()) + if err != nil { + return err + } + + existingNetworkMap := make(map[string]network.Summary) + for _, nw := range existingNetworks.Items { + existingNetworkMap[nw.Name] = nw + } + + for name, createOpts := range networks { + if _, exists := existingNetworkMap[name]; exists { + continue + } + + if createOpts.Driver == "" { + createOpts.Driver = defaultNetworkDriver + } + + _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating network", name) + if _, err := apiClient.NetworkCreate(ctx, name, createOpts); err != nil { + return fmt.Errorf("failed to create network %s: %w", name, err) + } + } + return nil +} + +func deployServices(ctx context.Context, dockerCLI command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) ([]string, error) { + apiClient := dockerCLI.Client() + out := dockerCLI.Out() + + existingServices, err := getStackServices(ctx, apiClient, namespace.Name()) + if err != nil { + return nil, err + } + + existingServiceMap := make(map[string]swarm.Service) + for _, svc := range existingServices.Items { + existingServiceMap[svc.Spec.Name] = svc + } + + var serviceIDs []string + + for internalName, serviceSpec := range services { + var ( + name = namespace.Scope(internalName) + image = serviceSpec.TaskTemplate.ContainerSpec.Image + encodedAuth string + ) + + if sendAuth { + // Retrieve encoded auth token from the image reference + encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image) + if err != nil { + return nil, err + } + } + + if svc, exists := existingServiceMap[name]; exists { + _, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, svc.ID) + + updateOpts := client.ServiceUpdateOptions{ + Version: svc.Version, + EncodedRegistryAuth: encodedAuth, + } + + switch resolveImage { + case resolveImageAlways: + // image should be updated by the server using QueryRegistry + updateOpts.QueryRegistry = true + case resolveImageChanged: + if image != svc.Spec.Labels[convert.LabelImage] { + // Query the registry to resolve digest for the updated image + updateOpts.QueryRegistry = true + } else { + // image has not changed; update the serviceSpec with the + // existing information that was set by QueryRegistry on the + // previous deploy. Otherwise this will trigger an incorrect + // service update. + serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image + } + default: + if image == svc.Spec.Labels[convert.LabelImage] { + // image has not changed; update the serviceSpec with the + // existing information that was set by QueryRegistry on the + // previous deploy. Otherwise this will trigger an incorrect + // service update. + serviceSpec.TaskTemplate.ContainerSpec.Image = svc.Spec.TaskTemplate.ContainerSpec.Image + } + } + + // Stack deploy does not have a `--force` option. Preserve existing + // ForceUpdate value so that tasks are not re-deployed if not updated. + // TODO move this to API client? + serviceSpec.TaskTemplate.ForceUpdate = svc.Spec.TaskTemplate.ForceUpdate + + updateOpts.Spec = serviceSpec + response, err := apiClient.ServiceUpdate(ctx, svc.ID, updateOpts) + if err != nil { + return nil, fmt.Errorf("failed to update service %s: %w", name, err) + } + + for _, warning := range response.Warnings { + _, _ = fmt.Fprintln(dockerCLI.Err(), warning) + } + + serviceIDs = append(serviceIDs, svc.ID) + } else { + _, _ = fmt.Fprintln(out, "Creating service", name) + + // query registry if flag disabling it was not set + queryRegistry := resolveImage == resolveImageAlways || resolveImage == resolveImageChanged + + response, err := apiClient.ServiceCreate(ctx, client.ServiceCreateOptions{ + Spec: serviceSpec, + EncodedRegistryAuth: encodedAuth, + QueryRegistry: queryRegistry, + }) + if err != nil { + return nil, fmt.Errorf("failed to create service %s: %w", name, err) + } + + serviceIDs = append(serviceIDs, response.ID) + } + } + + return serviceIDs, nil +} + +func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error { + var errs []error + for _, serviceID := range serviceIDs { + if err := service.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil { + errs = append(errs, fmt.Errorf("%s: %w", serviceID, err)) + } + } + return errors.Join(errs...) +} diff --git a/cli/command/stack/deploy_composefile_test.go b/cli/command/stack/deploy_composefile_test.go new file mode 100644 index 000000000000..4a5d9963c621 --- /dev/null +++ b/cli/command/stack/deploy_composefile_test.go @@ -0,0 +1,72 @@ +package stack + +import ( + "context" + "errors" + "testing" + + "github.com/docker/cli/internal/test/network" + networktypes "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" + "gotest.tools/v3/assert" +) + +type notFound struct { + error +} + +func (notFound) NotFound() {} + +func TestValidateExternalNetworks(t *testing.T) { + testcases := []struct { + inspectResponse client.NetworkInspectResult + inspectError error + expectedMsg string + network string + }{ + { + inspectError: notFound{}, + expectedMsg: "could not be found. You need to create a swarm-scoped network", + }, + { + inspectError: errors.New("unexpected"), + expectedMsg: "unexpected", + }, + // FIXME(vdemeester) that doesn't work under windows, the check needs to be smarter + /* + { + inspectError: errors.New("host net does not exist on swarm classic"), + network: "host", + }, + */ + { + network: "user", + expectedMsg: "is not in the right scope", + }, + { + network: "user", + inspectResponse: client.NetworkInspectResult{ + Network: networktypes.Inspect{ + Network: networktypes.Network{ + Scope: "swarm", + }, + }, + }, + }, + } + + for _, testcase := range testcases { + fakeAPIClient := &network.FakeClient{ + NetworkInspectFunc: func(_ context.Context, _ string, _ client.NetworkInspectOptions) (client.NetworkInspectResult, error) { + return testcase.inspectResponse, testcase.inspectError + }, + } + networks := []string{testcase.network} + err := validateExternalNetworks(context.Background(), fakeAPIClient, networks) + if testcase.expectedMsg == "" { + assert.NilError(t, err) + } else { + assert.ErrorContains(t, err, testcase.expectedMsg) + } + } +} diff --git a/cli/command/stack/deploy_test.go b/cli/command/stack/deploy_test.go index df731256bde5..a0fb1fd0c4c2 100644 --- a/cli/command/stack/deploy_test.go +++ b/cli/command/stack/deploy_test.go @@ -1,11 +1,16 @@ package stack import ( + "context" "io" "testing" + "github.com/docker/cli/cli/compose/convert" "github.com/docker/cli/internal/test" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" ) func TestDeployWithEmptyName(t *testing.T) { @@ -16,3 +21,98 @@ func TestDeployWithEmptyName(t *testing.T) { assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`) } + +func TestPruneServices(t *testing.T) { + ctx := context.Background() + namespace := convert.NewNamespace("foo") + services := map[string]struct{}{ + "new": {}, + "keep": {}, + } + apiClient := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} + dockerCli := test.NewFakeCli(apiClient) + + pruneServices(ctx, dockerCli, namespace, services) + assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), apiClient.removedServices)) +} + +// TestServiceUpdateResolveImageChanged tests that the service's +// image digest, and "ForceUpdate" is preserved if the image did not change in +// the compose file +func TestServiceUpdateResolveImageChanged(t *testing.T) { + namespace := convert.NewNamespace("mystack") + + var receivedOptions client.ServiceUpdateOptions + + fakeCli := test.NewFakeCli(&fakeClient{ + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{ + { + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: namespace.Name() + "_myservice", + Labels: map[string]string{"com.docker.stack.image": "foobar:1.2.3"}, + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{ + Image: "foobar:1.2.3@sha256:deadbeef", + }, + ForceUpdate: 123, + }, + }, + }, + }, + }, nil + }, + serviceUpdateFunc: func(serviceID string, options client.ServiceUpdateOptions) (client.ServiceUpdateResult, error) { + receivedOptions = options + return client.ServiceUpdateResult{}, nil + }, + }) + + testcases := []struct { + image string + expectedQueryRegistry bool + expectedImage string + expectedForceUpdate uint64 + }{ + // Image not changed + { + image: "foobar:1.2.3", + expectedQueryRegistry: false, + expectedImage: "foobar:1.2.3@sha256:deadbeef", + expectedForceUpdate: 123, + }, + // Image changed + { + image: "foobar:1.2.4", + expectedQueryRegistry: true, + expectedImage: "foobar:1.2.4", + expectedForceUpdate: 123, + }, + } + + ctx := context.Background() + + for _, tc := range testcases { + t.Run(tc.image, func(t *testing.T) { + spec := map[string]swarm.ServiceSpec{ + "myservice": { + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: &swarm.ContainerSpec{ + Image: tc.image, + }, + }, + }, + } + _, err := deployServices(ctx, fakeCli, spec, namespace, false, resolveImageChanged) + assert.NilError(t, err) + assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry)) + assert.Check(t, is.Equal(receivedOptions.Spec.TaskTemplate.ContainerSpec.Image, tc.expectedImage)) + assert.Check(t, is.Equal(receivedOptions.Spec.TaskTemplate.ForceUpdate, tc.expectedForceUpdate)) + + receivedOptions = client.ServiceUpdateOptions{} + }) + } +} diff --git a/cli/command/stack/formatter/formatter.go b/cli/command/stack/formatter/formatter.go deleted file mode 100644 index 07b322f0616a..000000000000 --- a/cli/command/stack/formatter/formatter.go +++ /dev/null @@ -1,70 +0,0 @@ -package formatter - -import ( - "strconv" - - "github.com/docker/cli/cli/command/formatter" -) - -const ( - // SwarmStackTableFormat is the default Swarm stack format - SwarmStackTableFormat formatter.Format = "table {{.Name}}\t{{.Services}}" - - stackServicesHeader = "SERVICES" - - // TableFormatKey is an alias for formatter.TableFormatKey - TableFormatKey = formatter.TableFormatKey -) - -// Context is an alias for formatter.Context -type Context = formatter.Context - -// Format is an alias for formatter.Format -type Format = formatter.Format - -// Stack contains deployed stack information. -type Stack struct { - // Name is the name of the stack - Name string - // Services is the number of the services - Services int -} - -// StackWrite writes formatted stacks using the Context -func StackWrite(ctx formatter.Context, stacks []*Stack) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, stack := range stacks { - if err := format(&stackContext{s: stack}); err != nil { - return err - } - } - return nil - } - return ctx.Write(newStackContext(), render) -} - -type stackContext struct { - formatter.HeaderContext - s *Stack -} - -func newStackContext() *stackContext { - stackCtx := stackContext{} - stackCtx.Header = formatter.SubHeaderContext{ - "Name": formatter.NameHeader, - "Services": stackServicesHeader, - } - return &stackCtx -} - -func (s *stackContext) MarshalJSON() ([]byte, error) { - return formatter.MarshalJSON(s) -} - -func (s *stackContext) Name() string { - return s.s.Name -} - -func (s *stackContext) Services() string { - return strconv.Itoa(s.s.Services) -} diff --git a/cli/command/stack/formatter/formatter_test.go b/cli/command/stack/formatter/formatter_test.go deleted file mode 100644 index fdb70f9d425a..000000000000 --- a/cli/command/stack/formatter/formatter_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package formatter - -import ( - "bytes" - "testing" - - "github.com/docker/cli/cli/command/formatter" - "gotest.tools/v3/assert" -) - -func TestStackContextWrite(t *testing.T) { - cases := []struct { - context formatter.Context - expected string - }{ - // Errors - { - formatter.Context{Format: "{{InvalidFunction}}"}, - `template parsing error: template: :1: function "InvalidFunction" not defined`, - }, - { - formatter.Context{Format: "{{nil}}"}, - `template parsing error: template: :1:2: executing "" at : nil is not a command`, - }, - // Table format - { - formatter.Context{Format: SwarmStackTableFormat}, - `NAME SERVICES -baz 2 -bar 1 -`, - }, - { - formatter.Context{Format: formatter.Format("table {{.Name}}")}, - `NAME -baz -bar -`, - }, - // Custom Format - { - formatter.Context{Format: formatter.Format("{{.Name}}")}, - `baz -bar -`, - }, - } - - stacks := []*Stack{ - {Name: "baz", Services: 2}, - {Name: "bar", Services: 1}, - } - for _, tc := range cases { - t.Run(string(tc.context.Format), func(t *testing.T) { - var out bytes.Buffer - tc.context.Output = &out - - if err := StackWrite(tc.context, stacks); err != nil { - assert.Error(t, err, tc.expected) - } else { - assert.Equal(t, out.String(), tc.expected) - } - }) - } -} diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index 4ec1b30085d4..6461b0902076 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -2,22 +2,23 @@ package stack import ( "context" - "io" "sort" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/stack/formatter" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/stack/swarm" + "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/fvbommel/sortorder" "github.com/spf13/cobra" ) -func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := options.List{} +// listOptions holds docker stack ls options +type listOptions struct { + format string +} + +func newListCommand(dockerCLI command.Cli) *cobra.Command { + opts := listOptions{} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -25,38 +26,34 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List stacks", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return RunList(cmd.Context(), dockerCli, opts) + return runList(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.StringVar(&opts.Format, "format", "", flagsHelper.FormatHelp) + flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) return cmd } -// RunList performs a stack list against the specified swarm cluster -func RunList(ctx context.Context, dockerCli command.Cli, opts options.List) error { - ss, err := swarm.GetStacks(ctx, dockerCli.Client()) +// runList performs a stack list against the specified swarm cluster +func runList(ctx context.Context, dockerCLI command.Cli, opts listOptions) error { + stacks, err := getStacks(ctx, dockerCLI.Client()) if err != nil { return err } - stacks := make([]*formatter.Stack, 0, len(ss)) - stacks = append(stacks, ss...) - return format(dockerCli.Out(), opts, stacks) -} -func format(out io.Writer, opts options.List, stacks []*formatter.Stack) error { - fmt := formatter.Format(opts.Format) - if fmt == "" || fmt == formatter.TableFormatKey { - fmt = formatter.SwarmStackTableFormat + format := formatter.Format(opts.format) + if format == "" || format == formatter.TableFormatKey { + format = stackTableFormat } stackCtx := formatter.Context{ - Output: out, - Format: fmt, + Output: dockerCLI.Out(), + Format: format, } sort.Slice(stacks, func(i, j int) bool { return sortorder.NaturalLess(stacks[i].Name, stacks[j].Name) }) - return formatter.StackWrite(stackCtx, stacks) + return stackWrite(stackCtx, stacks) } diff --git a/cli/command/stack/list_formatter.go b/cli/command/stack/list_formatter.go new file mode 100644 index 000000000000..1aa4f97e07b4 --- /dev/null +++ b/cli/command/stack/list_formatter.go @@ -0,0 +1,53 @@ +package stack + +import ( + "strconv" + + "github.com/docker/cli/cli/command/formatter" +) + +// stackTableFormat is the default Swarm stack format +const stackTableFormat formatter.Format = "table {{.Name}}\t{{.Services}}" + +// stackSummary contains deployed stack information. +type stackSummary struct { + Name string // Name is the name of the stack. + Services int // Services is the number services in the stack. +} + +// stackWrite writes formatted stacks using the Context +func stackWrite(fmtCtx formatter.Context, stacks []stackSummary) error { + stackCtx := &stackContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Name": formatter.NameHeader, + "Services": "SERVICES", + }, + }, + } + return fmtCtx.Write(stackCtx, func(format func(subContext formatter.SubContext) error) error { + for _, stack := range stacks { + if err := format(&stackContext{s: stack}); err != nil { + return err + } + } + return nil + }) +} + +type stackContext struct { + formatter.HeaderContext + s stackSummary +} + +func (s *stackContext) MarshalJSON() ([]byte, error) { + return formatter.MarshalJSON(s) +} + +func (s *stackContext) Name() string { + return s.s.Name +} + +func (s *stackContext) Services() string { + return strconv.Itoa(s.s.Services) +} diff --git a/cli/command/stack/list_formatter_test.go b/cli/command/stack/list_formatter_test.go new file mode 100644 index 000000000000..99dcd43d4bc4 --- /dev/null +++ b/cli/command/stack/list_formatter_test.go @@ -0,0 +1,69 @@ +package stack + +import ( + "bytes" + "testing" + + "github.com/docker/cli/cli/command/formatter" + "gotest.tools/v3/assert" +) + +func TestStackContextWrite(t *testing.T) { + tests := []struct { + name string + format formatter.Format + expected string + }{ + { + name: "invalid function", + format: `{{InvalidFunction}}`, + expected: `template parsing error: template: :1: function "InvalidFunction" not defined`, + }, + { + name: "invalid placeholder", + format: `{{nil}}`, + expected: `template parsing error: template: :1:2: executing "" at : nil is not a command`, + }, + { + name: "table format", + format: stackTableFormat, + expected: `NAME SERVICES +baz 2 +bar 1 +`, + }, + { + name: "custom table format", + format: `table {{.Name}}`, + expected: `NAME +baz +bar +`, + }, + { + name: "custom format", + format: `{{.Name}}`, + expected: `baz +bar +`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var out bytes.Buffer + fmtCtx := formatter.Context{ + Format: tc.format, + Output: &out, + } + if err := stackWrite(fmtCtx, []stackSummary{ + {Name: "baz", Services: 2}, + {Name: "bar", Services: 1}, + }); err != nil { + assert.Error(t, err, tc.expected) + } else { + assert.Equal(t, out.String(), tc.expected) + } + }) + } +} diff --git a/cli/command/stack/list_test.go b/cli/command/stack/list_test.go index cc0979418741..e194a8acc477 100644 --- a/cli/command/stack/list_test.go +++ b/cli/command/stack/list_test.go @@ -7,7 +7,8 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -16,7 +17,7 @@ func TestListErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - serviceListFunc func(options swarm.ServiceListOptions) ([]swarm.Service, error) + serviceListFunc func(options client.ServiceListOptions) (client.ServiceListResult, error) expectedError string }{ { @@ -32,15 +33,17 @@ func TestListErrors(t *testing.T) { }, { args: []string{}, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{}, errors.New("error getting services") + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{}, errors.New("error getting services") }, expectedError: "error getting services", }, { args: []string{}, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service()}, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service()}, + }, nil }, expectedError: "cannot get label", }, @@ -114,8 +117,10 @@ func TestStackList(t *testing.T) { ) } cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return services, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: services, + }, nil }, }) cmd := newListCommand(cli) diff --git a/cli/command/stack/list_utils.go b/cli/command/stack/list_utils.go new file mode 100644 index 000000000000..cbf42b3dab50 --- /dev/null +++ b/cli/command/stack/list_utils.go @@ -0,0 +1,36 @@ +package stack + +import ( + "context" + "errors" + + "github.com/docker/cli/cli/compose/convert" + "github.com/moby/moby/client" +) + +// getStacks lists the swarm stacks with the number of services they contain. +func getStacks(ctx context.Context, apiClient client.ServiceAPIClient) ([]stackSummary, error) { + res, err := apiClient.ServiceList(ctx, client.ServiceListOptions{ + Filters: getAllStacksFilter(), + }) + if err != nil { + return nil, err + } + + idx := make(map[string]int, len(res.Items)) + out := make([]stackSummary, 0, len(res.Items)) + + for _, svc := range res.Items { + name, ok := svc.Spec.Labels[convert.LabelNamespace] + if !ok { + return nil, errors.New("cannot get label " + convert.LabelNamespace + " for service " + svc.ID) + } + if i, ok := idx[name]; ok { + out[i].Services++ + continue + } + idx[name] = len(out) + out = append(out, stackSummary{Name: name, Services: 1}) + } + return out, nil +} diff --git a/cli/command/stack/loader/loader.go b/cli/command/stack/loader.go similarity index 77% rename from cli/command/stack/loader/loader.go rename to cli/command/stack/loader.go index 75d485b01464..647a9f6b3c04 100644 --- a/cli/command/stack/loader/loader.go +++ b/cli/command/stack/loader.go @@ -1,9 +1,10 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 -package loader +package stack import ( + "errors" "fmt" "io" "os" @@ -14,16 +15,14 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/schema" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/pkg/errors" ) -// LoadComposefile parse the composefile specified in the cli and returns its Config and version. -func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) { - configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In()) +// loadComposeFile parse the composefile specified in the cli and returns its configOptions and version. +func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes.Config, error) { + configDetails, err := getConfigDetails(opts.composefiles, streams.In()) if err != nil { return nil, err } @@ -31,9 +30,10 @@ func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes. dicts := getDictsFrom(configDetails.ConfigFiles) config, err := loader.Load(configDetails) if err != nil { - if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { + var fpe *loader.ForbiddenPropertiesError + if errors.As(err, &fpe) { // this error is intentionally formatted multi-line - return nil, errors.Errorf("Compose file contains unsupported options:\n\n%s\n", propertyWarnings(fpe.Properties)) + return nil, fmt.Errorf("compose file contains unsupported options:\n\n%s\n", propertyWarnings(fpe.Properties)) //nolint:staticcheck // ignore ST1005 } return nil, err @@ -41,23 +41,23 @@ func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes. unsupportedProperties := loader.GetUnsupportedProperties(dicts...) if len(unsupportedProperties) > 0 { - _, _ = fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", + _, _ = fmt.Fprintf(streams.Err(), "Ignoring unsupported options: %s\n\n", strings.Join(unsupportedProperties, ", ")) } deprecatedProperties := loader.GetDeprecatedProperties(dicts...) if len(deprecatedProperties) > 0 { - _, _ = fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", + _, _ = fmt.Fprintf(streams.Err(), "Ignoring deprecated options:\n\n%s\n\n", propertyWarnings(deprecatedProperties)) } // Validate if each service has a valid image-reference. for _, svc := range config.Services { if svc.Image == "" { - return nil, errors.Errorf("invalid image reference for service %s: no image specified", svc.Name) + return nil, fmt.Errorf("invalid image reference for service %s: no image specified", svc.Name) } if _, err := reference.ParseAnyReference(svc.Image); err != nil { - return nil, errors.Wrapf(err, "invalid image reference for service %s", svc.Name) + return nil, fmt.Errorf("invalid image reference for service %s: %w", svc.Name, err) } } @@ -83,12 +83,12 @@ func propertyWarnings(properties map[string]string) string { return strings.Join(msgs, "\n\n") } -// GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails -func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { +// getConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails +func getConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails if len(composefiles) == 0 { - return details, errors.New("Specify a Compose file (with --compose-file)") + return details, errors.New("specify a Compose file (with --compose-file)") } if composefiles[0] == "-" && len(composefiles) == 1 { @@ -134,7 +134,7 @@ func buildEnvironment(env []string) (map[string]string, error) { k, v, ok := strings.Cut(s, "=") if !ok || k == "" { - return result, errors.Errorf("unexpected environment variable '%s'", s) + return result, fmt.Errorf("unexpected environment variable '%s'", s) } // value may be set, but empty if "s" is like "K=", not "K". result[k] = v diff --git a/cli/command/stack/loader/loader_test.go b/cli/command/stack/loader_test.go similarity index 92% rename from cli/command/stack/loader/loader_test.go rename to cli/command/stack/loader_test.go index 6c0da17aa339..823440074887 100644 --- a/cli/command/stack/loader/loader_test.go +++ b/cli/command/stack/loader_test.go @@ -1,4 +1,4 @@ -package loader +package stack import ( "os" @@ -22,7 +22,7 @@ services: file := fs.NewFile(t, "test-get-config-details", fs.WithContent(content)) defer file.Remove() - details, err := GetConfigDetails([]string{file.Path()}, nil) + details, err := getConfigDetails([]string{file.Path()}, nil) assert.NilError(t, err) assert.Check(t, is.Equal(filepath.Dir(file.Path()), details.WorkingDir)) assert.Assert(t, is.Len(details.ConfigFiles, 1)) @@ -37,7 +37,7 @@ services: foo: image: alpine:3.5 ` - details, err := GetConfigDetails([]string{"-"}, strings.NewReader(content)) + details, err := getConfigDetails([]string{"-"}, strings.NewReader(content)) assert.NilError(t, err) cwd, err := os.Getwd() assert.NilError(t, err) diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go deleted file mode 100644 index 28d4c3262207..000000000000 --- a/cli/command/stack/options/opts.go +++ /dev/null @@ -1,50 +0,0 @@ -package options - -import "github.com/docker/cli/opts" - -// Deploy holds docker stack deploy options -type Deploy struct { - Composefiles []string - Namespace string - ResolveImage string - SendRegistryAuth bool - Prune bool - Detach bool - Quiet bool -} - -// Config holds docker stack config options -type Config struct { - Composefiles []string - SkipInterpolation bool -} - -// List holds docker stack ls options -type List struct { - Format string - AllNamespaces bool -} - -// PS holds docker stack ps options -type PS struct { - Filter opts.FilterOpt - NoTrunc bool - Namespace string - NoResolve bool - Quiet bool - Format string -} - -// Remove holds docker stack remove options -type Remove struct { - Namespaces []string - Detach bool -} - -// Services holds docker stack services options -type Services struct { - Quiet bool - Format string - Filter opts.FilterOpt - Namespace string -} diff --git a/cli/command/stack/ps.go b/cli/command/stack/ps.go index 8a91cc75f8e6..7fb21af72e17 100644 --- a/cli/command/stack/ps.go +++ b/cli/command/stack/ps.go @@ -1,38 +1,72 @@ package stack import ( + "context" + "fmt" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/stack/swarm" + "github.com/docker/cli/cli/command/idresolver" + "github.com/docker/cli/cli/command/task" flagsHelper "github.com/docker/cli/cli/flags" cliopts "github.com/docker/cli/opts" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newPsCommand(dockerCli command.Cli) *cobra.Command { - opts := options.PS{Filter: cliopts.NewFilterOpt()} +// psOptions holds docker stack ps options +type psOptions struct { + filter cliopts.FilterOpt + noTrunc bool + namespace string + noResolve bool + quiet bool + format string +} + +func newPsCommand(dockerCLI command.Cli) *cobra.Command { + opts := psOptions{filter: cliopts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ps [OPTIONS] STACK", Short: "List the tasks in the stack", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Namespace = args[0] - if err := validateStackName(opts.Namespace); err != nil { + opts.namespace = args[0] + if err := validateStackName(opts.namespace); err != nil { return err } - return swarm.RunPS(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runPS(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVar(&opts.NoTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.NoResolve, "no-resolve", false, "Do not map IDs to Names") - flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display task IDs") - flags.StringVar(&opts.Format, "format", "", flagsHelper.FormatHelp) + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") + flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) return cmd } + +// runPS is the swarm implementation of docker stack ps +func runPS(ctx context.Context, dockerCLI command.Cli, opts psOptions) error { + apiClient := dockerCLI.Client() + res, err := apiClient.TaskList(ctx, client.TaskListOptions{ + Filters: getStackFilterFromOpt(opts.namespace, opts.filter), + }) + if err != nil { + return err + } + + if len(res.Items) == 0 { + return fmt.Errorf("nothing found in stack: %s", opts.namespace) + } + + if opts.format == "" { + opts.format = task.DefaultFormat(dockerCLI.ConfigFile(), opts.quiet) + } + + return task.Print(ctx, dockerCLI, res, idresolver.New(apiClient, opts.noResolve), !opts.noTrunc, opts.quiet, opts.format) +} diff --git a/cli/command/stack/ps_test.go b/cli/command/stack/ps_test.go index 2994bbf3545f..d3098a907468 100644 --- a/cli/command/stack/ps_test.go +++ b/cli/command/stack/ps_test.go @@ -9,7 +9,8 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -18,7 +19,7 @@ import ( func TestStackPsErrors(t *testing.T) { testCases := []struct { args []string - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) + taskListFunc func(options client.TaskListOptions) (client.TaskListResult, error) expectedError string }{ { @@ -31,8 +32,8 @@ func TestStackPsErrors(t *testing.T) { }, { args: []string{"foo"}, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return nil, errors.New("error getting tasks") + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{}, errors.New("error getting tasks") }, expectedError: "error getting tasks", }, @@ -53,14 +54,14 @@ func TestStackPsErrors(t *testing.T) { func TestStackPs(t *testing.T) { testCases := []struct { - doc string - taskListFunc func(swarm.TaskListOptions) ([]swarm.Task, error) - nodeInspectWithRaw func(string) (swarm.Node, []byte, error) - config configfile.ConfigFile - args []string - flags map[string]string - expectedErr string - golden string + doc string + taskListFunc func(client.TaskListOptions) (client.TaskListResult, error) + nodeInspectFunc func(ref string) (client.NodeInspectResult, error) + config configfile.ConfigFile + args []string + flags map[string]string + expectedErr string + golden string }{ { doc: "WithEmptyName", @@ -69,16 +70,20 @@ func TestStackPs(t *testing.T) { }, { doc: "WithEmptyStack", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{}, nil }, args: []string{"foo"}, expectedErr: "nothing found in stack: foo", }, { doc: "WithQuietOption", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task(builders.TaskID("id-foo"))}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskID("id-foo")), + }, + }, nil }, args: []string{"foo"}, flags: map[string]string{ @@ -88,8 +93,12 @@ func TestStackPs(t *testing.T) { }, { doc: "WithNoTruncOption", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task(builders.TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskID("xn4cypcov06f2w8gsbaf2lst3")), + }, + }, nil }, args: []string{"foo"}, flags: map[string]string{ @@ -100,13 +109,17 @@ func TestStackPs(t *testing.T) { }, { doc: "WithNoResolveOption", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task( - builders.TaskNodeID("id-node-foo"), - )}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{*builders.Task( + builders.TaskNodeID("id-node-foo"), + )}, + }, nil }, - nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName("node-name-bar")), nil, nil + nodeInspectFunc: func(ref string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-name-bar")), + }, nil }, args: []string{"foo"}, flags: map[string]string{ @@ -117,8 +130,10 @@ func TestStackPs(t *testing.T) { }, { doc: "WithFormat", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task(builders.TaskServiceID("service-id-foo"))}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{*builders.Task(builders.TaskServiceID("service-id-foo"))}, + }, nil }, args: []string{"foo"}, flags: map[string]string{ @@ -128,8 +143,10 @@ func TestStackPs(t *testing.T) { }, { doc: "WithConfigFormat", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task(builders.TaskServiceID("service-id-foo"))}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{*builders.Task(builders.TaskServiceID("service-id-foo"))}, + }, nil }, config: configfile.ConfigFile{ TasksFormat: "{{ .Name }}", @@ -139,18 +156,22 @@ func TestStackPs(t *testing.T) { }, { doc: "WithoutFormat", - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task( - builders.TaskID("id-foo"), - builders.TaskServiceID("service-id-foo"), - builders.TaskNodeID("id-node"), - builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), - builders.TaskDesiredState(swarm.TaskStateReady), - builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), - )}, nil + taskListFunc: func(options client.TaskListOptions) (client.TaskListResult, error) { + return client.TaskListResult{ + Items: []swarm.Task{*builders.Task( + builders.TaskID("id-foo"), + builders.TaskServiceID("service-id-foo"), + builders.TaskNodeID("id-node"), + builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), + builders.TaskDesiredState(swarm.TaskStateReady), + builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), + )}, + }, nil }, - nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName("node-name-bar")), nil, nil + nodeInspectFunc: func(ref string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-name-bar")), + }, nil }, args: []string{"foo"}, golden: "stack-ps-without-format.golden", @@ -160,8 +181,8 @@ func TestStackPs(t *testing.T) { for _, tc := range testCases { t.Run(tc.doc, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - taskListFunc: tc.taskListFunc, - nodeInspectWithRaw: tc.nodeInspectWithRaw, + taskListFunc: tc.taskListFunc, + nodeInspectFunc: tc.nodeInspectFunc, }) cli.SetConfigFile(&tc.config) diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index 18ad5a25bf7b..90ab2df29bbb 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -1,15 +1,27 @@ package stack import ( + "context" + "errors" + "fmt" + "sort" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/stack/swarm" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - var opts options.Remove +// removeOptions holds docker stack remove options +type removeOptions struct { + namespaces []string + detach bool +} + +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { + var opts removeOptions cmd := &cobra.Command{ Use: "rm [OPTIONS] STACK [STACK...]", @@ -17,18 +29,166 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove one or more stacks", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Namespaces = args - if err := validateStackNames(opts.Namespaces); err != nil { + opts.namespaces = args + if err := validateStackNames(opts.namespaces); err != nil { return err } - return swarm.RunRemove(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runRemove(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVarP(&opts.Detach, "detach", "d", true, "Do not wait for stack removal") + flags.BoolVarP(&opts.detach, "detach", "d", true, "Do not wait for stack removal") return cmd } + +// runRemove is the swarm implementation of docker stack remove. +func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions) error { + apiClient := dockerCli.Client() + + var errs []error + for _, namespace := range opts.namespaces { + services, err := getStackServices(ctx, apiClient, namespace) + if err != nil { + return err + } + + networks, err := getStackNetworks(ctx, apiClient, namespace) + if err != nil { + return err + } + + secrets, err := getStackSecrets(ctx, apiClient, namespace) + if err != nil { + return err + } + + configs, err := getStackConfigs(ctx, apiClient, namespace) + if err != nil { + return err + } + + if len(services.Items)+len(networks.Items)+len(secrets.Items)+len(configs.Items) == 0 { + _, _ = fmt.Fprintln(dockerCli.Err(), "Nothing found in stack:", namespace) + continue + } + + // TODO(thaJeztah): change this "hasError" boolean to return a (multi-)error for each of these functions instead. + hasError := removeServices(ctx, dockerCli, services.Items) + hasError = removeSecrets(ctx, dockerCli, secrets.Items) || hasError + hasError = removeConfigs(ctx, dockerCli, configs.Items) || hasError + hasError = removeNetworks(ctx, dockerCli, networks.Items) || hasError + + if hasError { + errs = append(errs, errors.New("failed to remove some resources from stack: "+namespace)) + continue + } + + if !opts.detach { + err = waitOnTasks(ctx, apiClient, namespace) + if err != nil { + errs = append(errs, fmt.Errorf("failed to wait on tasks of stack: %s: %w", namespace, err)) + } + } + } + return errors.Join(errs...) +} + +func sortServiceByName(services []swarm.Service) func(i, j int) bool { + return func(i, j int) bool { + return services[i].Spec.Name < services[j].Spec.Name + } +} + +func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool { + var hasError bool + sort.Slice(services, sortServiceByName(services)) + for _, service := range services { + _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name) + if _, err := dockerCLI.Client().ServiceRemove(ctx, service.ID, client.ServiceRemoveOptions{}); err != nil { + hasError = true + _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove service %s: %s", service.ID, err) + } + } + return hasError +} + +func removeNetworks(ctx context.Context, dockerCLI command.Cli, networks []network.Summary) bool { + var hasError bool + for _, nw := range networks { + _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing network", nw.Name) + if _, err := dockerCLI.Client().NetworkRemove(ctx, nw.ID, client.NetworkRemoveOptions{}); err != nil { + hasError = true + _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove network %s: %s", nw.ID, err) + } + } + return hasError +} + +func removeSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.Secret) bool { + var hasError bool + for _, secret := range secrets { + _, _ = fmt.Fprintln(dockerCli.Out(), "Removing secret", secret.Spec.Name) + if _, err := dockerCli.Client().SecretRemove(ctx, secret.ID, client.SecretRemoveOptions{}); err != nil { + hasError = true + _, _ = fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err) + } + } + return hasError +} + +func removeConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.Config) bool { + var hasError bool + for _, config := range configs { + _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing config", config.Spec.Name) + if _, err := dockerCLI.Client().ConfigRemove(ctx, config.ID, client.ConfigRemoveOptions{}); err != nil { + hasError = true + _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove config %s: %s", config.ID, err) + } + } + return hasError +} + +var numberedStates = map[swarm.TaskState]int64{ + swarm.TaskStateNew: 1, + swarm.TaskStateAllocated: 2, + swarm.TaskStatePending: 3, + swarm.TaskStateAssigned: 4, + swarm.TaskStateAccepted: 5, + swarm.TaskStatePreparing: 6, + swarm.TaskStateReady: 7, + swarm.TaskStateStarting: 8, + swarm.TaskStateRunning: 9, + swarm.TaskStateComplete: 10, + swarm.TaskStateShutdown: 11, + swarm.TaskStateFailed: 12, + swarm.TaskStateRejected: 13, +} + +func terminalState(state swarm.TaskState) bool { + return numberedStates[state] > numberedStates[swarm.TaskStateRunning] +} + +func waitOnTasks(ctx context.Context, apiClient client.APIClient, namespace string) error { + terminalStatesReached := 0 + for { + res, err := getStackTasks(ctx, apiClient, namespace) + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + + for _, task := range res.Items { + if terminalState(task.Status.State) { + terminalStatesReached++ + break + } + } + + if terminalStatesReached == len(res.Items) { + break + } + } + return nil +} diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go index 1a6923b747d7..16c6675e4c0d 100644 --- a/cli/command/stack/remove_test.go +++ b/cli/command/stack/remove_test.go @@ -7,11 +7,12 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) -func fakeClientForRemoveStackTest(version string) *fakeClient { +func fakeClientForRemoveStackTest() *fakeClient { allServices := []string{ objectName("foo", "service1"), objectName("foo", "service2"), @@ -33,7 +34,6 @@ func fakeClientForRemoveStackTest(version string) *fakeClient { objectName("bar", "config1"), } return &fakeClient{ - version: version, services: allServices, networks: allNetworks, secrets: allSecrets, @@ -50,40 +50,16 @@ func TestRemoveWithEmptyName(t *testing.T) { assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`) } -func TestRemoveStackVersion124DoesNotRemoveConfigsOrSecrets(t *testing.T) { - client := fakeClientForRemoveStackTest("1.24") - cmd := newRemoveCommand(test.NewFakeCli(client)) +func TestRemoveStackRemovesEverything(t *testing.T) { + apiClient := fakeClientForRemoveStackTest() + cmd := newRemoveCommand(test.NewFakeCli(apiClient)) cmd.SetArgs([]string{"foo", "bar"}) assert.NilError(t, cmd.Execute()) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks)) - assert.Check(t, is.Len(client.removedSecrets, 0)) - assert.Check(t, is.Len(client.removedConfigs, 0)) -} - -func TestRemoveStackVersion125DoesNotRemoveConfigs(t *testing.T) { - client := fakeClientForRemoveStackTest("1.25") - cmd := newRemoveCommand(test.NewFakeCli(client)) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NilError(t, cmd.Execute()) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.secrets), client.removedSecrets)) - assert.Check(t, is.Len(client.removedConfigs, 0)) -} - -func TestRemoveStackVersion130RemovesEverything(t *testing.T) { - client := fakeClientForRemoveStackTest("1.30") - cmd := newRemoveCommand(test.NewFakeCli(client)) - cmd.SetArgs([]string{"foo", "bar"}) - - assert.NilError(t, cmd.Execute()) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.services), client.removedServices)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.networks), client.removedNetworks)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.secrets), client.removedSecrets)) - assert.Check(t, is.DeepEqual(buildObjectIDs(client.configs), client.removedConfigs)) + assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.services), apiClient.removedServices)) + assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.networks), apiClient.removedNetworks)) + assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.secrets), apiClient.removedSecrets)) + assert.Check(t, is.DeepEqual(buildObjectIDs(apiClient.configs), apiClient.removedConfigs)) } func TestRemoveStackSkipEmpty(t *testing.T) { @@ -99,14 +75,13 @@ func TestRemoveStackSkipEmpty(t *testing.T) { allConfigs := []string{objectName("bar", "config1")} allConfigIDs := buildObjectIDs(allConfigs) - fakeClient := &fakeClient{ - version: "1.30", + apiClient := &fakeClient{ services: allServices, networks: allNetworks, secrets: allSecrets, configs: allConfigs, } - fakeCli := test.NewFakeCli(fakeClient) + fakeCli := test.NewFakeCli(apiClient) cmd := newRemoveCommand(fakeCli) cmd.SetArgs([]string{"foo", "bar"}) @@ -120,10 +95,10 @@ func TestRemoveStackSkipEmpty(t *testing.T) { } assert.Check(t, is.Equal(strings.Join(expectedList, "\n"), fakeCli.OutBuffer().String())) assert.Check(t, is.Contains(fakeCli.ErrBuffer().String(), "Nothing found in stack: foo\n")) - assert.Check(t, is.DeepEqual(allServiceIDs, fakeClient.removedServices)) - assert.Check(t, is.DeepEqual(allNetworkIDs, fakeClient.removedNetworks)) - assert.Check(t, is.DeepEqual(allSecretIDs, fakeClient.removedSecrets)) - assert.Check(t, is.DeepEqual(allConfigIDs, fakeClient.removedConfigs)) + assert.Check(t, is.DeepEqual(allServiceIDs, apiClient.removedServices)) + assert.Check(t, is.DeepEqual(allNetworkIDs, apiClient.removedNetworks)) + assert.Check(t, is.DeepEqual(allSecretIDs, apiClient.removedSecrets)) + assert.Check(t, is.DeepEqual(allConfigIDs, apiClient.removedConfigs)) } func TestRemoveContinueAfterError(t *testing.T) { @@ -139,31 +114,30 @@ func TestRemoveContinueAfterError(t *testing.T) { allConfigs := []string{objectName("foo", "config1"), objectName("bar", "config1")} allConfigIDs := buildObjectIDs(allConfigs) - removedServices := []string{} - cli := &fakeClient{ - version: "1.30", + var removedServices []string + apiClient := &fakeClient{ services: allServices, networks: allNetworks, secrets: allSecrets, configs: allConfigs, - serviceRemoveFunc: func(serviceID string) error { + serviceRemoveFunc: func(serviceID string) (client.ServiceRemoveResult, error) { removedServices = append(removedServices, serviceID) if strings.Contains(serviceID, "foo") { - return errors.New("") + return client.ServiceRemoveResult{}, errors.New("") } - return nil + return client.ServiceRemoveResult{}, nil }, } - cmd := newRemoveCommand(test.NewFakeCli(cli)) + cmd := newRemoveCommand(test.NewFakeCli(apiClient)) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"foo", "bar"}) assert.Error(t, cmd.Execute(), "failed to remove some resources from stack: foo") assert.Check(t, is.DeepEqual(allServiceIDs, removedServices)) - assert.Check(t, is.DeepEqual(allNetworkIDs, cli.removedNetworks)) - assert.Check(t, is.DeepEqual(allSecretIDs, cli.removedSecrets)) - assert.Check(t, is.DeepEqual(allConfigIDs, cli.removedConfigs)) + assert.Check(t, is.DeepEqual(allNetworkIDs, apiClient.removedNetworks)) + assert.Check(t, is.DeepEqual(allSecretIDs, apiClient.removedSecrets)) + assert.Check(t, is.DeepEqual(allConfigIDs, apiClient.removedConfigs)) } diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go index f91ba71edfa5..26d252d98e93 100644 --- a/cli/command/stack/services.go +++ b/cli/command/stack/services.go @@ -7,64 +7,74 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/service" - "github.com/docker/cli/cli/command/stack/formatter" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/stack/swarm" flagsHelper "github.com/docker/cli/cli/flags" cliopts "github.com/docker/cli/opts" - swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -func newServicesCommand(dockerCli command.Cli) *cobra.Command { - opts := options.Services{Filter: cliopts.NewFilterOpt()} +// serviceListOptions holds docker stack services options +type serviceListOptions = struct { + quiet bool + format string + filter cliopts.FilterOpt + namespace string +} + +func newServicesCommand(dockerCLI command.Cli) *cobra.Command { + opts := serviceListOptions{filter: cliopts.NewFilterOpt()} cmd := &cobra.Command{ Use: "services [OPTIONS] STACK", Short: "List the services in the stack", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Namespace = args[0] - if err := validateStackName(opts.Namespace); err != nil { + opts.namespace = args[0] + if err := validateStackName(opts.namespace); err != nil { return err } - return RunServices(cmd.Context(), dockerCli, opts) - }, - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return runServices(cmd.Context(), dockerCLI, opts) }, + ValidArgsFunction: completeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&opts.Format, "format", "", flagsHelper.FormatHelp) - flags.VarP(&opts.Filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp) + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } -// RunServices performs a stack services against the specified swarm cluster -func RunServices(ctx context.Context, dockerCli command.Cli, opts options.Services) error { - services, err := swarm.GetServices(ctx, dockerCli, opts) +// runServices performs a stack services against the specified swarm cluster +func runServices(ctx context.Context, dockerCLI command.Cli, opts serviceListOptions) error { + res, err := dockerCLI.Client().ServiceList(ctx, client.ServiceListOptions{ + Filters: getStackFilterFromOpt(opts.namespace, opts.filter), + // When not running "quiet", also get service status (number of running + // and desired tasks). + Status: !opts.quiet, + }) if err != nil { return err } - return formatWrite(dockerCli, services, opts) + return formatWrite(dockerCLI, res, opts) } -func formatWrite(dockerCLI command.Cli, services []swarmtypes.Service, opts options.Services) error { +func formatWrite(dockerCLI command.Cli, services client.ServiceListResult, opts serviceListOptions) error { // if no services in the stack, print message and exit 0 - if len(services) == 0 { - _, _ = fmt.Fprintln(dockerCLI.Err(), "Nothing found in stack:", opts.Namespace) + if len(services.Items) == 0 { + _, _ = fmt.Fprintln(dockerCLI.Err(), "Nothing found in stack:", opts.namespace) return nil } - sort.Slice(services, func(i, j int) bool { - return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name) + sort.Slice(services.Items, func(i, j int) bool { + return sortorder.NaturalLess(services.Items[i].Spec.Name, services.Items[j].Spec.Name) }) - f := opts.Format + f := opts.format if len(f) == 0 { - if len(dockerCLI.ConfigFile().ServicesFormat) > 0 && !opts.Quiet { + if len(dockerCLI.ConfigFile().ServicesFormat) > 0 && !opts.quiet { f = dockerCLI.ConfigFile().ServicesFormat } else { f = formatter.TableFormatKey @@ -73,7 +83,7 @@ func formatWrite(dockerCLI command.Cli, services []swarmtypes.Service, opts opti servicesCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: service.NewListFormat(f, opts.Quiet), + Format: service.NewListFormat(f, opts.quiet), } return service.ListFormatWrite(servicesCtx, services) } diff --git a/cli/command/stack/services_test.go b/cli/command/stack/services_test.go index 8d79dbeaddb3..c7ee85df06ac 100644 --- a/cli/command/stack/services_test.go +++ b/cli/command/stack/services_test.go @@ -8,7 +8,9 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -18,48 +20,25 @@ func TestStackServicesErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - serviceListFunc func(options swarm.ServiceListOptions) ([]swarm.Service, error) - nodeListFunc func(options swarm.NodeListOptions) ([]swarm.Node, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) + serviceListFunc func(options client.ServiceListOptions) (client.ServiceListResult, error) expectedError string }{ { args: []string{"foo"}, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return nil, errors.New("error getting services") + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{}, errors.New("error getting services") }, expectedError: "error getting services", }, - { - args: []string{"foo"}, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service(builders.GlobalService())}, nil - }, - nodeListFunc: func(options swarm.NodeListOptions) ([]swarm.Node, error) { - return nil, errors.New("error getting nodes") - }, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{*builders.Task()}, nil - }, - expectedError: "error getting nodes", - }, - { - args: []string{"foo"}, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service(builders.GlobalService())}, nil - }, - taskListFunc: func(options swarm.TaskListOptions) ([]swarm.Task, error) { - return nil, errors.New("error getting tasks") - }, - expectedError: "error getting tasks", - }, { args: []string{"foo"}, flags: map[string]string{ "format": "{{invalid format}}", }, - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service()}, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service()}, + }, nil }, expectedError: "template parsing error", }, @@ -69,8 +48,6 @@ func TestStackServicesErrors(t *testing.T) { t.Run(tc.expectedError, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ serviceListFunc: tc.serviceListFunc, - nodeListFunc: tc.nodeListFunc, - taskListFunc: tc.taskListFunc, }) cmd := newServicesCommand(cli) cmd.SetArgs(tc.args) @@ -95,8 +72,8 @@ func TestRunServicesWithEmptyName(t *testing.T) { func TestStackServicesEmptyServiceList(t *testing.T) { fakeCli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{}, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{}, nil }, }) cmd := newServicesCommand(fakeCli) @@ -108,8 +85,10 @@ func TestStackServicesEmptyServiceList(t *testing.T) { func TestStackServicesWithQuietOption(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service(builders.ServiceID("id-foo"))}, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service(builders.ServiceID("id-foo"))}, + }, nil }, }) cmd := newServicesCommand(cli) @@ -121,9 +100,9 @@ func TestStackServicesWithQuietOption(t *testing.T) { func TestStackServicesWithFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - *builders.Service(builders.ServiceName("service-name-foo")), + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service(builders.ServiceName("service-name-foo"))}, }, nil }, }) @@ -136,9 +115,9 @@ func TestStackServicesWithFormat(t *testing.T) { func TestStackServicesWithConfigFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - *builders.Service(builders.ServiceName("service-name-foo")), + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service(builders.ServiceName("service-name-foo"))}, }, nil }, }) @@ -153,19 +132,21 @@ func TestStackServicesWithConfigFormat(t *testing.T) { func TestStackServicesWithoutFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{*builders.Service( - builders.ServiceName("name-foo"), - builders.ServiceID("id-foo"), - builders.ReplicatedService(2), - builders.ServiceImage("busybox:latest"), - builders.ServicePort(swarm.PortConfig{ - PublishMode: swarm.PortConfigPublishModeIngress, - PublishedPort: 0, - TargetPort: 3232, - Protocol: swarm.PortConfigProtocolTCP, - }), - )}, nil + serviceListFunc: func(options client.ServiceListOptions) (client.ServiceListResult, error) { + return client.ServiceListResult{ + Items: []swarm.Service{*builders.Service( + builders.ServiceName("name-foo"), + builders.ServiceID("id-foo"), + builders.ReplicatedService(2), + builders.ServiceImage("busybox:latest"), + builders.ServicePort(swarm.PortConfig{ + PublishMode: swarm.PortConfigPublishModeIngress, + PublishedPort: 0, + TargetPort: 3232, + Protocol: network.TCP, + }), + )}, + }, nil }, }) cmd := newServicesCommand(cli) diff --git a/cli/command/stack/swarm/client_test.go b/cli/command/stack/swarm/client_test.go deleted file mode 100644 index b072b35c5e00..000000000000 --- a/cli/command/stack/swarm/client_test.go +++ /dev/null @@ -1,240 +0,0 @@ -package swarm - -import ( - "context" - "strings" - - "github.com/docker/cli/cli/compose/convert" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" -) - -type fakeClient struct { - client.Client - - version string - - services []string - networks []string - secrets []string - configs []string - - removedServices []string - removedNetworks []string - removedSecrets []string - removedConfigs []string - - serviceListFunc func(options swarm.ServiceListOptions) ([]swarm.Service, error) - networkListFunc func(options network.ListOptions) ([]network.Summary, error) - secretListFunc func(options swarm.SecretListOptions) ([]swarm.Secret, error) - configListFunc func(options swarm.ConfigListOptions) ([]swarm.Config, error) - nodeListFunc func(options swarm.NodeListOptions) ([]swarm.Node, error) - taskListFunc func(options swarm.TaskListOptions) ([]swarm.Task, error) - nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) - - serviceUpdateFunc func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) - - serviceRemoveFunc func(serviceID string) error - networkRemoveFunc func(networkID string) error - secretRemoveFunc func(secretID string) error - configRemoveFunc func(configID string) error -} - -func (*fakeClient) ServerVersion(context.Context) (types.Version, error) { - return types.Version{ - Version: "docker-dev", - APIVersion: api.DefaultVersion, - }, nil -} - -func (cli *fakeClient) ClientVersion() string { - return cli.version -} - -func (cli *fakeClient) ServiceList(_ context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - if cli.serviceListFunc != nil { - return cli.serviceListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - servicesList := []swarm.Service{} - for _, name := range cli.services { - if belongToNamespace(name, namespace) { - servicesList = append(servicesList, serviceFromName(name)) - } - } - return servicesList, nil -} - -func (cli *fakeClient) NetworkList(_ context.Context, options network.ListOptions) ([]network.Summary, error) { - if cli.networkListFunc != nil { - return cli.networkListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - networksList := []network.Summary{} - for _, name := range cli.networks { - if belongToNamespace(name, namespace) { - networksList = append(networksList, networkFromName(name)) - } - } - return networksList, nil -} - -func (cli *fakeClient) SecretList(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - if cli.secretListFunc != nil { - return cli.secretListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - secretsList := []swarm.Secret{} - for _, name := range cli.secrets { - if belongToNamespace(name, namespace) { - secretsList = append(secretsList, secretFromName(name)) - } - } - return secretsList, nil -} - -func (cli *fakeClient) ConfigList(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - if cli.configListFunc != nil { - return cli.configListFunc(options) - } - - namespace := namespaceFromFilters(options.Filters) - configsList := []swarm.Config{} - for _, name := range cli.configs { - if belongToNamespace(name, namespace) { - configsList = append(configsList, configFromName(name)) - } - } - return configsList, nil -} - -func (cli *fakeClient) TaskList(_ context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { - if cli.taskListFunc != nil { - return cli.taskListFunc(options) - } - return []swarm.Task{}, nil -} - -func (cli *fakeClient) NodeList(_ context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { - if cli.nodeListFunc != nil { - return cli.nodeListFunc(options) - } - return []swarm.Node{}, nil -} - -func (cli *fakeClient) NodeInspectWithRaw(_ context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectWithRaw != nil { - return cli.nodeInspectWithRaw(ref) - } - return swarm.Node{}, nil, nil -} - -func (cli *fakeClient) ServiceUpdate(_ context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { - if cli.serviceUpdateFunc != nil { - return cli.serviceUpdateFunc(serviceID, version, service, options) - } - - return swarm.ServiceUpdateResponse{}, nil -} - -func (cli *fakeClient) ServiceRemove(_ context.Context, serviceID string) error { - if cli.serviceRemoveFunc != nil { - return cli.serviceRemoveFunc(serviceID) - } - - cli.removedServices = append(cli.removedServices, serviceID) - return nil -} - -func (cli *fakeClient) NetworkRemove(_ context.Context, networkID string) error { - if cli.networkRemoveFunc != nil { - return cli.networkRemoveFunc(networkID) - } - - cli.removedNetworks = append(cli.removedNetworks, networkID) - return nil -} - -func (cli *fakeClient) SecretRemove(_ context.Context, secretID string) error { - if cli.secretRemoveFunc != nil { - return cli.secretRemoveFunc(secretID) - } - - cli.removedSecrets = append(cli.removedSecrets, secretID) - return nil -} - -func (cli *fakeClient) ConfigRemove(_ context.Context, configID string) error { - if cli.configRemoveFunc != nil { - return cli.configRemoveFunc(configID) - } - - cli.removedConfigs = append(cli.removedConfigs, configID) - return nil -} - -func serviceFromName(name string) swarm.Service { - return swarm.Service{ - ID: "ID-" + name, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func networkFromName(name string) network.Summary { - return network.Summary{ - ID: "ID-" + name, - Name: name, - } -} - -func secretFromName(name string) swarm.Secret { - return swarm.Secret{ - ID: "ID-" + name, - Spec: swarm.SecretSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func configFromName(name string) swarm.Config { - return swarm.Config{ - ID: "ID-" + name, - Spec: swarm.ConfigSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - } -} - -func namespaceFromFilters(fltrs filters.Args) string { - label := fltrs.Get("label")[0] - return strings.TrimPrefix(label, convert.LabelNamespace+"=") -} - -func belongToNamespace(id, namespace string) bool { - return strings.HasPrefix(id, namespace+"_") -} - -func objectName(namespace, name string) string { - return namespace + "_" + name -} - -func objectID(name string) string { - return "ID-" + name -} - -func buildObjectIDs(objectNames []string) []string { - IDs := make([]string, len(objectNames)) - for i, name := range objectNames { - IDs[i] = objectID(name) - } - return IDs -} diff --git a/cli/command/stack/swarm/common.go b/cli/command/stack/swarm/common.go deleted file mode 100644 index d9a1e097f502..000000000000 --- a/cli/command/stack/swarm/common.go +++ /dev/null @@ -1,50 +0,0 @@ -package swarm - -import ( - "context" - - "github.com/docker/cli/cli/compose/convert" - "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" -) - -func getStackFilter(namespace string) filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { - filter := opt.Value() - filter.Add("label", convert.LabelNamespace+"="+namespace) - return filter -} - -func getAllStacksFilter() filters.Args { - filter := filters.NewArgs() - filter.Add("label", convert.LabelNamespace) - return filter -} - -func getStackServices(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Service, error) { - return apiclient.ServiceList(ctx, swarm.ServiceListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackNetworks(ctx context.Context, apiclient client.APIClient, namespace string) ([]network.Summary, error) { - return apiclient.NetworkList(ctx, network.ListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Secret, error) { - return apiclient.SecretList(ctx, swarm.SecretListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) { - return apiclient.ConfigList(ctx, swarm.ConfigListOptions{Filters: getStackFilter(namespace)}) -} - -func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) { - return apiclient.TaskList(ctx, swarm.TaskListOptions{Filters: getStackFilter(namespace)}) -} diff --git a/cli/command/stack/swarm/deploy.go b/cli/command/stack/swarm/deploy.go deleted file mode 100644 index bfd71f5f6566..000000000000 --- a/cli/command/stack/swarm/deploy.go +++ /dev/null @@ -1,85 +0,0 @@ -package swarm - -import ( - "context" - "fmt" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/compose/convert" - composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/pkg/errors" - "github.com/spf13/pflag" -) - -// Resolve image constants -const ( - defaultNetworkDriver = "overlay" - ResolveImageAlways = "always" - ResolveImageChanged = "changed" - ResolveImageNever = "never" -) - -// RunDeploy is the swarm implementation of docker stack deploy -func RunDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *options.Deploy, cfg *composetypes.Config) error { - if err := validateResolveImageFlag(opts); err != nil { - return err - } - // client side image resolution should not be done when the supported - // server version is older than 1.30 - if versions.LessThan(dockerCLI.Client().ClientVersion(), "1.30") { - opts.ResolveImage = ResolveImageNever - } - - if opts.Detach && !flags.Changed("detach") { - _, _ = fmt.Fprintln(dockerCLI.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ - "In a future release, --detach=false will become the default.") - } - - return deployCompose(ctx, dockerCLI, opts, cfg) -} - -// validateResolveImageFlag validates the opts.resolveImage command line option -func validateResolveImageFlag(opts *options.Deploy) error { - switch opts.ResolveImage { - case ResolveImageAlways, ResolveImageChanged, ResolveImageNever: - return nil - default: - return errors.Errorf("Invalid option %s for flag --resolve-image", opts.ResolveImage) - } -} - -// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is -// a swarm manager. This is necessary because we must create networks before we -// create services, but the API call for creating a network does not return a -// proper status code when it can't create a network in the "global" scope. -func checkDaemonIsSwarmManager(ctx context.Context, dockerCli command.Cli) error { - info, err := dockerCli.Client().Info(ctx) - if err != nil { - return err - } - if !info.Swarm.ControlAvailable { - return errors.New("this node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again") - } - return nil -} - -// pruneServices removes services that are no longer referenced in the source -func pruneServices(ctx context.Context, dockerCCLI command.Cli, namespace convert.Namespace, services map[string]struct{}) { - apiClient := dockerCCLI.Client() - - oldServices, err := getStackServices(ctx, apiClient, namespace.Name()) - if err != nil { - _, _ = fmt.Fprintln(dockerCCLI.Err(), "Failed to list services:", err) - } - - pruneServices := []swarm.Service{} - for _, service := range oldServices { - if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists { - pruneServices = append(pruneServices, service) - } - } - removeServices(ctx, dockerCCLI, pruneServices) -} diff --git a/cli/command/stack/swarm/deploy_composefile.go b/cli/command/stack/swarm/deploy_composefile.go deleted file mode 100644 index edde88962a9d..000000000000 --- a/cli/command/stack/swarm/deploy_composefile.go +++ /dev/null @@ -1,295 +0,0 @@ -package swarm - -import ( - "context" - "errors" - "fmt" - - cerrdefs "github.com/containerd/errdefs" - "github.com/docker/cli/cli/command" - servicecli "github.com/docker/cli/cli/command/service" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/compose/convert" - composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" -) - -func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Deploy, config *composetypes.Config) error { - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.Namespace) - - if opts.Prune { - services := map[string]struct{}{} - for _, service := range config.Services { - services[service.Name] = struct{}{} - } - pruneServices(ctx, dockerCli, namespace, services) - } - - serviceNetworks := getServicesDeclaredNetworks(config.Services) - networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) - if err := validateExternalNetworks(ctx, dockerCli.Client(), externalNetworks); err != nil { - return err - } - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - - secrets, err := convert.Secrets(namespace, config.Secrets) - if err != nil { - return err - } - if err := createSecrets(ctx, dockerCli, secrets); err != nil { - return err - } - - configs, err := convert.Configs(namespace, config.Configs) - if err != nil { - return err - } - if err := createConfigs(ctx, dockerCli, configs); err != nil { - return err - } - - services, err := convert.Services(ctx, namespace, config, dockerCli.Client()) - if err != nil { - return err - } - - serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage) - if err != nil { - return err - } - - if opts.Detach { - return nil - } - - return waitOnServices(ctx, dockerCli, serviceIDs, opts.Quiet) -} - -func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { - serviceNetworks := map[string]struct{}{} - for _, serviceConfig := range serviceConfigs { - if len(serviceConfig.Networks) == 0 { - serviceNetworks["default"] = struct{}{} - continue - } - for nw := range serviceConfig.Networks { - serviceNetworks[nw] = struct{}{} - } - } - return serviceNetworks -} - -func validateExternalNetworks(ctx context.Context, apiClient client.NetworkAPIClient, externalNetworks []string) error { - for _, networkName := range externalNetworks { - if !container.NetworkMode(networkName).IsUserDefined() { - // Networks that are not user defined always exist on all nodes as - // local-scoped networks, so there's no need to inspect them. - continue - } - nw, err := apiClient.NetworkInspect(ctx, networkName, network.InspectOptions{}) - switch { - case cerrdefs.IsNotFound(err): - return fmt.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName) - case err != nil: - return err - case nw.Scope != "swarm": - return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, nw.Scope) - } - } - return nil -} - -func createSecrets(ctx context.Context, dockerCLI command.Cli, secrets []swarm.SecretSpec) error { - apiClient := dockerCLI.Client() - - for _, secretSpec := range secrets { - secret, _, err := apiClient.SecretInspectWithRaw(ctx, secretSpec.Name) - switch { - case err == nil: - // secret already exists, then we update that - if err := apiClient.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { - return fmt.Errorf("failed to update secret %s: %w", secretSpec.Name, err) - } - case cerrdefs.IsNotFound(err): - // secret does not exist, then we create a new one. - _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating secret", secretSpec.Name) - if _, err := apiClient.SecretCreate(ctx, secretSpec); err != nil { - return fmt.Errorf("failed to create secret %s: %w", secretSpec.Name, err) - } - default: - return err - } - } - return nil -} - -func createConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.ConfigSpec) error { - apiClient := dockerCLI.Client() - - for _, configSpec := range configs { - config, _, err := apiClient.ConfigInspectWithRaw(ctx, configSpec.Name) - switch { - case err == nil: - // config already exists, then we update that - if err := apiClient.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil { - return fmt.Errorf("failed to update config %s: %w", configSpec.Name, err) - } - case cerrdefs.IsNotFound(err): - // config does not exist, then we create a new one. - _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating config", configSpec.Name) - if _, err := apiClient.ConfigCreate(ctx, configSpec); err != nil { - return fmt.Errorf("failed to create config %s: %w", configSpec.Name, err) - } - default: - return err - } - } - return nil -} - -func createNetworks(ctx context.Context, dockerCLI command.Cli, namespace convert.Namespace, networks map[string]network.CreateOptions) error { - apiClient := dockerCLI.Client() - - existingNetworks, err := getStackNetworks(ctx, apiClient, namespace.Name()) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]network.Summary) - for _, nw := range existingNetworks { - existingNetworkMap[nw.Name] = nw - } - - for name, createOpts := range networks { - if _, exists := existingNetworkMap[name]; exists { - continue - } - - if createOpts.Driver == "" { - createOpts.Driver = defaultNetworkDriver - } - - _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating network", name) - if _, err := apiClient.NetworkCreate(ctx, name, createOpts); err != nil { - return fmt.Errorf("failed to create network %s: %w", name, err) - } - } - return nil -} - -func deployServices(ctx context.Context, dockerCLI command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) ([]string, error) { - apiClient := dockerCLI.Client() - out := dockerCLI.Out() - - existingServices, err := getStackServices(ctx, apiClient, namespace.Name()) - if err != nil { - return nil, err - } - - existingServiceMap := make(map[string]swarm.Service) - for _, service := range existingServices { - existingServiceMap[service.Spec.Name] = service - } - - var serviceIDs []string - - for internalName, serviceSpec := range services { - var ( - name = namespace.Scope(internalName) - image = serviceSpec.TaskTemplate.ContainerSpec.Image - encodedAuth string - ) - - if sendAuth { - // Retrieve encoded auth token from the image reference - encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), image) - if err != nil { - return nil, err - } - } - - if service, exists := existingServiceMap[name]; exists { - _, _ = fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) - - updateOpts := swarm.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth} - - switch resolveImage { - case ResolveImageAlways: - // image should be updated by the server using QueryRegistry - updateOpts.QueryRegistry = true - case ResolveImageChanged: - if image != service.Spec.Labels[convert.LabelImage] { - // Query the registry to resolve digest for the updated image - updateOpts.QueryRegistry = true - } else { - // image has not changed; update the serviceSpec with the - // existing information that was set by QueryRegistry on the - // previous deploy. Otherwise this will trigger an incorrect - // service update. - serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image - } - default: - if image == service.Spec.Labels[convert.LabelImage] { - // image has not changed; update the serviceSpec with the - // existing information that was set by QueryRegistry on the - // previous deploy. Otherwise this will trigger an incorrect - // service update. - serviceSpec.TaskTemplate.ContainerSpec.Image = service.Spec.TaskTemplate.ContainerSpec.Image - } - } - - // Stack deploy does not have a `--force` option. Preserve existing - // ForceUpdate value so that tasks are not re-deployed if not updated. - // TODO move this to API client? - serviceSpec.TaskTemplate.ForceUpdate = service.Spec.TaskTemplate.ForceUpdate - - response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts) - if err != nil { - return nil, fmt.Errorf("failed to update service %s: %w", name, err) - } - - for _, warning := range response.Warnings { - _, _ = fmt.Fprintln(dockerCLI.Err(), warning) - } - - serviceIDs = append(serviceIDs, service.ID) - } else { - _, _ = fmt.Fprintln(out, "Creating service", name) - - createOpts := swarm.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth} - - // query registry if flag disabling it was not set - if resolveImage == ResolveImageAlways || resolveImage == ResolveImageChanged { - createOpts.QueryRegistry = true - } - - response, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts) - if err != nil { - return nil, fmt.Errorf("failed to create service %s: %w", name, err) - } - - serviceIDs = append(serviceIDs, response.ID) - } - } - - return serviceIDs, nil -} - -func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error { - var errs []error - for _, serviceID := range serviceIDs { - if err := servicecli.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil { - errs = append(errs, fmt.Errorf("%s: %w", serviceID, err)) - } - } - return errors.Join(errs...) -} diff --git a/cli/command/stack/swarm/deploy_composefile_test.go b/cli/command/stack/swarm/deploy_composefile_test.go deleted file mode 100644 index 7df4b4551c81..000000000000 --- a/cli/command/stack/swarm/deploy_composefile_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package swarm - -import ( - "context" - "errors" - "testing" - - "github.com/docker/cli/internal/test/network" - networktypes "github.com/docker/docker/api/types/network" - "gotest.tools/v3/assert" -) - -type notFound struct { - error -} - -func (notFound) NotFound() {} - -func TestValidateExternalNetworks(t *testing.T) { - testcases := []struct { - inspectResponse networktypes.Inspect - inspectError error - expectedMsg string - network string - }{ - { - inspectError: notFound{}, - expectedMsg: "could not be found. You need to create a swarm-scoped network", - }, - { - inspectError: errors.New("unexpected"), - expectedMsg: "unexpected", - }, - // FIXME(vdemeester) that doesn't work under windows, the check needs to be smarter - /* - { - inspectError: errors.New("host net does not exist on swarm classic"), - network: "host", - }, - */ - { - network: "user", - expectedMsg: "is not in the right scope", - }, - { - network: "user", - inspectResponse: networktypes.Inspect{Scope: "swarm"}, - }, - } - - for _, testcase := range testcases { - client := &network.FakeClient{ - NetworkInspectFunc: func(_ context.Context, _ string, _ networktypes.InspectOptions) (networktypes.Inspect, error) { - return testcase.inspectResponse, testcase.inspectError - }, - } - networks := []string{testcase.network} - err := validateExternalNetworks(context.Background(), client, networks) - if testcase.expectedMsg == "" { - assert.NilError(t, err) - } else { - assert.ErrorContains(t, err, testcase.expectedMsg) - } - } -} diff --git a/cli/command/stack/swarm/deploy_test.go b/cli/command/stack/swarm/deploy_test.go deleted file mode 100644 index 1c62eb3e9832..000000000000 --- a/cli/command/stack/swarm/deploy_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package swarm - -import ( - "context" - "testing" - - "github.com/docker/cli/cli/compose/convert" - "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestPruneServices(t *testing.T) { - ctx := context.Background() - namespace := convert.NewNamespace("foo") - services := map[string]struct{}{ - "new": {}, - "keep": {}, - } - client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} - dockerCli := test.NewFakeCli(client) - - pruneServices(ctx, dockerCli, namespace, services) - assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices)) -} - -// TestServiceUpdateResolveImageChanged tests that the service's -// image digest, and "ForceUpdate" is preserved if the image did not change in -// the compose file -func TestServiceUpdateResolveImageChanged(t *testing.T) { - namespace := convert.NewNamespace("mystack") - - var ( - receivedOptions swarm.ServiceUpdateOptions - receivedService swarm.ServiceSpec - ) - - client := test.NewFakeCli(&fakeClient{ - serviceListFunc: func(options swarm.ServiceListOptions) ([]swarm.Service, error) { - return []swarm.Service{ - { - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: namespace.Name() + "_myservice", - Labels: map[string]string{"com.docker.stack.image": "foobar:1.2.3"}, - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: &swarm.ContainerSpec{ - Image: "foobar:1.2.3@sha256:deadbeef", - }, - ForceUpdate: 123, - }, - }, - }, - }, nil - }, - serviceUpdateFunc: func(serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { - receivedOptions = options - receivedService = service - return swarm.ServiceUpdateResponse{}, nil - }, - }) - - testcases := []struct { - image string - expectedQueryRegistry bool - expectedImage string - expectedForceUpdate uint64 - }{ - // Image not changed - { - image: "foobar:1.2.3", - expectedQueryRegistry: false, - expectedImage: "foobar:1.2.3@sha256:deadbeef", - expectedForceUpdate: 123, - }, - // Image changed - { - image: "foobar:1.2.4", - expectedQueryRegistry: true, - expectedImage: "foobar:1.2.4", - expectedForceUpdate: 123, - }, - } - - ctx := context.Background() - - for _, tc := range testcases { - t.Run(tc.image, func(t *testing.T) { - spec := map[string]swarm.ServiceSpec{ - "myservice": { - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: &swarm.ContainerSpec{ - Image: tc.image, - }, - }, - }, - } - _, err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged) - assert.NilError(t, err) - assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry)) - assert.Check(t, is.Equal(receivedService.TaskTemplate.ContainerSpec.Image, tc.expectedImage)) - assert.Check(t, is.Equal(receivedService.TaskTemplate.ForceUpdate, tc.expectedForceUpdate)) - - receivedService = swarm.ServiceSpec{} - receivedOptions = swarm.ServiceUpdateOptions{} - }) - } -} diff --git a/cli/command/stack/swarm/list.go b/cli/command/stack/swarm/list.go deleted file mode 100644 index ec951d71d395..000000000000 --- a/cli/command/stack/swarm/list.go +++ /dev/null @@ -1,44 +0,0 @@ -package swarm - -import ( - "context" - - "github.com/docker/cli/cli/command/stack/formatter" - "github.com/docker/cli/cli/compose/convert" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" -) - -// GetStacks lists the swarm stacks. -func GetStacks(ctx context.Context, apiClient client.ServiceAPIClient) ([]*formatter.Stack, error) { - services, err := apiClient.ServiceList( - ctx, - swarm.ServiceListOptions{Filters: getAllStacksFilter()}) - if err != nil { - return nil, err - } - m := make(map[string]*formatter.Stack) - for _, service := range services { - labels := service.Spec.Labels - name, ok := labels[convert.LabelNamespace] - if !ok { - return nil, errors.Errorf("cannot get label %s for service %s", - convert.LabelNamespace, service.ID) - } - ztack, ok := m[name] - if !ok { - m[name] = &formatter.Stack{ - Name: name, - Services: 1, - } - } else { - ztack.Services++ - } - } - stacks := make([]*formatter.Stack, 0, len(m)) - for _, stack := range m { - stacks = append(stacks, stack) - } - return stacks, nil -} diff --git a/cli/command/stack/swarm/ps.go b/cli/command/stack/swarm/ps.go deleted file mode 100644 index d213d525d6e7..000000000000 --- a/cli/command/stack/swarm/ps.go +++ /dev/null @@ -1,34 +0,0 @@ -package swarm - -import ( - "context" - "fmt" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/idresolver" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/cli/cli/command/task" - "github.com/docker/docker/api/types/swarm" -) - -// RunPS is the swarm implementation of docker stack ps -func RunPS(ctx context.Context, dockerCli command.Cli, opts options.PS) error { - filter := getStackFilterFromOpt(opts.Namespace, opts.Filter) - - client := dockerCli.Client() - tasks, err := client.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) - if err != nil { - return err - } - - if len(tasks) == 0 { - return fmt.Errorf("nothing found in stack: %s", opts.Namespace) - } - - format := opts.Format - if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), opts.Quiet) - } - - return task.Print(ctx, dockerCli, tasks, idresolver.New(client, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format) -} diff --git a/cli/command/stack/swarm/remove.go b/cli/command/stack/swarm/remove.go deleted file mode 100644 index cd426d5111d5..000000000000 --- a/cli/command/stack/swarm/remove.go +++ /dev/null @@ -1,170 +0,0 @@ -package swarm - -import ( - "context" - "errors" - "fmt" - "sort" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" -) - -// RunRemove is the swarm implementation of docker stack remove -func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) error { - apiClient := dockerCli.Client() - - var errs []error - for _, namespace := range opts.Namespaces { - services, err := getStackServices(ctx, apiClient, namespace) - if err != nil { - return err - } - - networks, err := getStackNetworks(ctx, apiClient, namespace) - if err != nil { - return err - } - - var secrets []swarm.Secret - if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.25") { - secrets, err = getStackSecrets(ctx, apiClient, namespace) - if err != nil { - return err - } - } - - var configs []swarm.Config - if versions.GreaterThanOrEqualTo(apiClient.ClientVersion(), "1.30") { - configs, err = getStackConfigs(ctx, apiClient, namespace) - if err != nil { - return err - } - } - - if len(services)+len(networks)+len(secrets)+len(configs) == 0 { - _, _ = fmt.Fprintln(dockerCli.Err(), "Nothing found in stack:", namespace) - continue - } - - // TODO(thaJeztah): change this "hasError" boolean to return a (multi-)error for each of these functions instead. - hasError := removeServices(ctx, dockerCli, services) - hasError = removeSecrets(ctx, dockerCli, secrets) || hasError - hasError = removeConfigs(ctx, dockerCli, configs) || hasError - hasError = removeNetworks(ctx, dockerCli, networks) || hasError - - if hasError { - errs = append(errs, errors.New("failed to remove some resources from stack: "+namespace)) - continue - } - - if !opts.Detach { - err = waitOnTasks(ctx, apiClient, namespace) - if err != nil { - errs = append(errs, fmt.Errorf("failed to wait on tasks of stack: %s: %w", namespace, err)) - } - } - } - return errors.Join(errs...) -} - -func sortServiceByName(services []swarm.Service) func(i, j int) bool { - return func(i, j int) bool { - return services[i].Spec.Name < services[j].Spec.Name - } -} - -func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool { - var hasError bool - sort.Slice(services, sortServiceByName(services)) - for _, service := range services { - _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name) - if err := dockerCLI.Client().ServiceRemove(ctx, service.ID); err != nil { - hasError = true - _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove service %s: %s", service.ID, err) - } - } - return hasError -} - -func removeNetworks(ctx context.Context, dockerCLI command.Cli, networks []network.Summary) bool { - var hasError bool - for _, nw := range networks { - _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing network", nw.Name) - if err := dockerCLI.Client().NetworkRemove(ctx, nw.ID); err != nil { - hasError = true - _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove network %s: %s", nw.ID, err) - } - } - return hasError -} - -func removeSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.Secret) bool { - var hasError bool - for _, secret := range secrets { - _, _ = fmt.Fprintln(dockerCli.Out(), "Removing secret", secret.Spec.Name) - if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil { - hasError = true - _, _ = fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err) - } - } - return hasError -} - -func removeConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.Config) bool { - var hasError bool - for _, config := range configs { - _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing config", config.Spec.Name) - if err := dockerCLI.Client().ConfigRemove(ctx, config.ID); err != nil { - hasError = true - _, _ = fmt.Fprintf(dockerCLI.Err(), "Failed to remove config %s: %s", config.ID, err) - } - } - return hasError -} - -var numberedStates = map[swarm.TaskState]int64{ - swarm.TaskStateNew: 1, - swarm.TaskStateAllocated: 2, - swarm.TaskStatePending: 3, - swarm.TaskStateAssigned: 4, - swarm.TaskStateAccepted: 5, - swarm.TaskStatePreparing: 6, - swarm.TaskStateReady: 7, - swarm.TaskStateStarting: 8, - swarm.TaskStateRunning: 9, - swarm.TaskStateComplete: 10, - swarm.TaskStateShutdown: 11, - swarm.TaskStateFailed: 12, - swarm.TaskStateRejected: 13, -} - -func terminalState(state swarm.TaskState) bool { - return numberedStates[state] > numberedStates[swarm.TaskStateRunning] -} - -func waitOnTasks(ctx context.Context, apiClient client.APIClient, namespace string) error { - terminalStatesReached := 0 - for { - tasks, err := getStackTasks(ctx, apiClient, namespace) - if err != nil { - return fmt.Errorf("failed to get tasks: %w", err) - } - - for _, task := range tasks { - if terminalState(task.Status.State) { - terminalStatesReached++ - break - } - } - - if terminalStatesReached == len(tasks) { - break - } - } - return nil -} diff --git a/cli/command/stack/swarm/services.go b/cli/command/stack/swarm/services.go deleted file mode 100644 index e75d3ccf8c80..000000000000 --- a/cli/command/stack/swarm/services.go +++ /dev/null @@ -1,52 +0,0 @@ -package swarm - -import ( - "context" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/service" - "github.com/docker/cli/cli/command/stack/options" - "github.com/docker/docker/api/types/swarm" -) - -// GetServices is the swarm implementation of listing stack services -func GetServices(ctx context.Context, dockerCli command.Cli, opts options.Services) ([]swarm.Service, error) { - var ( - err error - client = dockerCli.Client() - ) - - listOpts := swarm.ServiceListOptions{ - Filters: getStackFilterFromOpt(opts.Namespace, opts.Filter), - // When not running "quiet", also get service status (number of running - // and desired tasks). Note that this is only supported on API v1.41 and - // up; older API versions ignore this option, and we will have to collect - // the information manually below. - Status: !opts.Quiet, - } - - services, err := client.ServiceList(ctx, listOpts) - if err != nil { - return nil, err - } - - if listOpts.Status { - // Now that a request was made, we know what API version was used (either - // through configuration, or after client and daemon negotiated a version). - // If API version v1.41 or up was used; the daemon should already have done - // the legwork for us, and we don't have to calculate the number of desired - // and running tasks. On older API versions, we need to do some extra requests - // to get that information. - // - // So theoretically, this step can be skipped based on API version, however, - // some of our unit tests don't set the API version, and there may be other - // situations where the client uses the "default" version. To account for - // these situations, we do a quick check for services that do not have - // a ServiceStatus set, and perform a lookup for those. - services, err = service.AppendServiceStatus(ctx, client, services) - if err != nil { - return nil, err - } - } - return services, nil -} diff --git a/cli/command/swarm/ca.go b/cli/command/swarm/ca.go index a648d3e83308..749afd860de5 100644 --- a/cli/command/swarm/ca.go +++ b/cli/command/swarm/ca.go @@ -2,17 +2,17 @@ package swarm import ( "context" + "errors" "fmt" "io" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/swarm/progress" "github.com/docker/cli/internal/jsonstream" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -26,7 +26,7 @@ type caOptions struct { quiet bool } -func newCACommand(dockerCli command.Cli) *cobra.Command { +func newCACommand(dockerCLI command.Cli) *cobra.Command { opts := caOptions{} cmd := &cobra.Command{ @@ -34,13 +34,14 @@ func newCACommand(dockerCli command.Cli) *cobra.Command { Short: "Display and rotate the root CA", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runCA(cmd.Context(), dockerCli, cmd.Flags(), opts) + return runCA(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, Annotations: map[string]string{ "version": "1.30", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -54,10 +55,10 @@ func newCACommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error { - client := dockerCli.Client() +func runCA(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts caOptions) error { + apiClient := dockerCLI.Client() - swarmInspect, err := client.SwarmInspect(ctx) + res, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } @@ -68,7 +69,7 @@ func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opt return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f) } } - return displayTrustRoot(dockerCli.Out(), swarmInspect) + return displayTrustRoot(dockerCLI.Out(), res) } if flags.Changed(flagExternalCA) && len(opts.externalCA.Value()) > 0 && !flags.Changed(flagCACert) { @@ -82,15 +83,18 @@ func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opt flagCACert, flagCAKey, flagExternalCA) } - updateSwarmSpec(&swarmInspect.Spec, flags, opts) - if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil { + updateSwarmSpec(&res.Swarm.Spec, flags, opts) + if _, err := apiClient.SwarmUpdate(ctx, client.SwarmUpdateOptions{ + Version: res.Swarm.Version, + Spec: res.Swarm.Spec, + }); err != nil { return err } if opts.detach { return nil } - return attach(ctx, dockerCli, opts) + return attach(ctx, dockerCLI, opts) } func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) { @@ -106,13 +110,13 @@ func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) { } } -func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { - client := dockerCli.Client() +func attach(ctx context.Context, dockerCLI command.Cli, opts caOptions) error { + apiClient := dockerCLI.Client() errChan := make(chan error, 1) pipeReader, pipeWriter := io.Pipe() go func() { - errChan <- progress.RootRotationProgress(ctx, client, pipeWriter) + errChan <- progress.RootRotationProgress(ctx, apiClient, pipeWriter) }() if opts.quiet { @@ -120,7 +124,7 @@ func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { return <-errChan } - err := jsonstream.Display(ctx, pipeReader, dockerCli.Out()) + err := jsonstream.Display(ctx, pipeReader, dockerCLI.Out()) if err == nil { err = <-errChan } @@ -128,17 +132,17 @@ func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { return err } - swarmInspect, err := client.SwarmInspect(ctx) + res, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } - return displayTrustRoot(dockerCli.Out(), swarmInspect) + return displayTrustRoot(dockerCLI.Out(), res) } -func displayTrustRoot(out io.Writer, info swarm.Swarm) error { - if info.ClusterInfo.TLSInfo.TrustRoot == "" { - return errors.New("No CA information available") +func displayTrustRoot(out io.Writer, info client.SwarmInspectResult) error { + if info.Swarm.ClusterInfo.TLSInfo.TrustRoot == "" { + return errors.New("no CA information available") } - fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot)) + _, _ = fmt.Fprintln(out, strings.TrimSpace(info.Swarm.ClusterInfo.TLSInfo.TrustRoot)) return nil } diff --git a/cli/command/swarm/ca_test.go b/cli/command/swarm/ca_test.go index 9b7107d4fe9a..870f9b49f24b 100644 --- a/cli/command/swarm/ca_test.go +++ b/cli/command/swarm/ca_test.go @@ -8,7 +8,8 @@ import ( "time" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -55,8 +56,8 @@ func swarmSpecWithFullCAConfig() *swarm.Spec { func TestDisplayTrustRootNoRoot(t *testing.T) { buffer := new(bytes.Buffer) - err := displayTrustRoot(buffer, swarm.Swarm{}) - assert.Error(t, err, "No CA information available") + err := displayTrustRoot(buffer, client.SwarmInspectResult{}) + assert.Error(t, err, "no CA information available") } type invalidCATestCases struct { @@ -64,37 +65,37 @@ type invalidCATestCases struct { errorMsg string } -func writeFile(data string) (string, error) { - tmpfile, err := os.CreateTemp("", "testfile") +func writeFile(dir, data string) (string, error) { + tmpFile, err := os.CreateTemp(dir, "testfile") if err != nil { return "", err } - _, err = tmpfile.WriteString(data) + _, err = tmpFile.WriteString(data) if err != nil { return "", err } - return tmpfile.Name(), tmpfile.Close() + return tmpFile.Name(), tmpFile.Close() } func TestDisplayTrustRootInvalidFlags(t *testing.T) { // we need an actual PEMfile to test - tmpfile, err := writeFile(cert) + tmpDir := t.TempDir() + tmpFile, err := writeFile(tmpDir, cert) assert.NilError(t, err) - t.Cleanup(func() { _ = os.Remove(tmpfile) }) errorTestCases := []invalidCATestCases{ { - args: []string{"--ca-cert=" + tmpfile}, + args: []string{"--ca-cert=" + tmpFile}, errorMsg: "flag requires the `--rotate` flag to update the CA", }, { - args: []string{"--ca-key=" + tmpfile}, + args: []string{"--ca-key=" + tmpFile}, errorMsg: "flag requires the `--rotate` flag to update the CA", }, { // to make sure we're not erroring because we didn't provide a CA key along with the CA cert args: []string{ - "--ca-cert=" + tmpfile, - "--ca-key=" + tmpfile, + "--ca-cert=" + tmpFile, + "--ca-key=" + tmpFile, }, errorMsg: "flag requires the `--rotate` flag to update the CA", }, @@ -108,7 +109,7 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) { }, { // to make sure we're not erroring because we didn't provide a CA cert and external CA args: []string{ - "--ca-cert=" + tmpfile, + "--ca-cert=" + tmpFile, "--external-ca=protocol=cfssl,url=https://some.example.com/https/url", }, errorMsg: "flag requires the `--rotate` flag to update the CA", @@ -124,7 +125,7 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) { { args: []string{ "--rotate", - "--ca-cert=" + tmpfile, + "--ca-cert=" + tmpFile, }, errorMsg: "the --ca-cert flag requires that a --ca-key flag and/or --external-ca flag be provided as well", }, @@ -133,11 +134,13 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) { for _, testCase := range errorTestCases { cmd := newCACommand( test.NewFakeCli(&fakeClient{ - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{ - ClusterInfo: swarm.ClusterInfo{ - TLSInfo: swarm.TLSInfo{ - TrustRoot: "root", + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + TLSInfo: swarm.TLSInfo{ + TrustRoot: "root", + }, }, }, }, nil @@ -154,9 +157,11 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) { func TestDisplayTrustRoot(t *testing.T) { buffer := new(bytes.Buffer) trustRoot := "trustme" - err := displayTrustRoot(buffer, swarm.Swarm{ - ClusterInfo: swarm.ClusterInfo{ - TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot}, + err := displayTrustRoot(buffer, client.SwarmInspectResult{ + Swarm: swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + TLSInfo: swarm.TLSInfo{TrustRoot: trustRoot}, + }, }, }) assert.NilError(t, err) @@ -167,15 +172,17 @@ type swarmUpdateRecorder struct { spec swarm.Spec } -func (s *swarmUpdateRecorder) swarmUpdate(sp swarm.Spec, _ swarm.UpdateFlags) error { - s.spec = sp - return nil +func (s *swarmUpdateRecorder) swarmUpdate(opts client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + s.spec = opts.Spec + return client.SwarmUpdateResult{}, nil } -func swarmInspectFuncWithFullCAConfig() (swarm.Swarm, error) { - return swarm.Swarm{ - ClusterInfo: swarm.ClusterInfo{ - Spec: *swarmSpecWithFullCAConfig(), +func swarmInspectFuncWithFullCAConfig() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + Spec: *swarmSpecWithFullCAConfig(), + }, }, }, nil } @@ -199,13 +206,12 @@ func TestUpdateSwarmSpecDefaultRotate(t *testing.T) { } func TestUpdateSwarmSpecCertAndKey(t *testing.T) { - certfile, err := writeFile(cert) + tmpDir := t.TempDir() + certFile, err := writeFile(tmpDir, cert) assert.NilError(t, err) - defer os.Remove(certfile) - keyfile, err := writeFile(key) + keyFile, err := writeFile(tmpDir, key) assert.NilError(t, err) - defer os.Remove(keyfile) s := &swarmUpdateRecorder{} cli := test.NewFakeCli(&fakeClient{ @@ -216,8 +222,8 @@ func TestUpdateSwarmSpecCertAndKey(t *testing.T) { cmd.SetArgs([]string{ "--rotate", "--detach", - "--ca-cert=" + certfile, - "--ca-key=" + keyfile, + "--ca-cert=" + certFile, + "--ca-key=" + keyFile, "--cert-expiry=3m", }) cmd.SetOut(cli.OutBuffer()) @@ -231,9 +237,9 @@ func TestUpdateSwarmSpecCertAndKey(t *testing.T) { } func TestUpdateSwarmSpecCertAndExternalCA(t *testing.T) { - certfile, err := writeFile(cert) + tmpDir := t.TempDir() + certFile, err := writeFile(tmpDir, cert) assert.NilError(t, err) - defer os.Remove(certfile) s := &swarmUpdateRecorder{} cli := test.NewFakeCli(&fakeClient{ @@ -244,7 +250,7 @@ func TestUpdateSwarmSpecCertAndExternalCA(t *testing.T) { cmd.SetArgs([]string{ "--rotate", "--detach", - "--ca-cert=" + certfile, + "--ca-cert=" + certFile, "--external-ca=protocol=cfssl,url=https://some.external.ca.example.com", }) cmd.SetOut(cli.OutBuffer()) @@ -265,13 +271,12 @@ func TestUpdateSwarmSpecCertAndExternalCA(t *testing.T) { } func TestUpdateSwarmSpecCertAndKeyAndExternalCA(t *testing.T) { - certfile, err := writeFile(cert) + tmpDir := t.TempDir() + certFile, err := writeFile(tmpDir, cert) assert.NilError(t, err) - defer os.Remove(certfile) - keyfile, err := writeFile(key) + keyFile, err := writeFile(tmpDir, key) assert.NilError(t, err) - defer os.Remove(keyfile) s := &swarmUpdateRecorder{} cli := test.NewFakeCli(&fakeClient{ @@ -282,8 +287,8 @@ func TestUpdateSwarmSpecCertAndKeyAndExternalCA(t *testing.T) { cmd.SetArgs([]string{ "--rotate", "--detach", - "--ca-cert=" + certfile, - "--ca-key=" + keyfile, + "--ca-cert=" + certFile, + "--ca-key=" + keyFile, "--external-ca=protocol=cfssl,url=https://some.external.ca.example.com", }) cmd.SetOut(cli.OutBuffer()) diff --git a/cli/command/swarm/client_test.go b/cli/command/swarm/client_test.go index 99f2dcfe7fd6..ea2d24d13194 100644 --- a/cli/command/swarm/client_test.go +++ b/cli/command/swarm/client_test.go @@ -3,83 +3,85 @@ package swarm import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client infoFunc func() (system.Info, error) - swarmInitFunc func(req swarm.InitRequest) (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) - swarmJoinFunc func() error - swarmLeaveFunc func() error - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmUnlockFunc func(req swarm.UnlockRequest) error + swarmInitFunc func(client.SwarmInitOptions) (client.SwarmInitResult, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) + swarmJoinFunc func() (client.SwarmJoinResult, error) + swarmLeaveFunc func() (client.SwarmLeaveResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + swarmUnlockFunc func(client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) } -func (cli *fakeClient) Info(context.Context) (system.Info, error) { +func (cli *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { if cli.infoFunc != nil { - return cli.infoFunc() + inf, err := cli.infoFunc() + return client.SystemInfoResult{ + Info: inf, + }, err } - return system.Info{}, nil + return client.SystemInfoResult{}, nil } -func (cli *fakeClient) NodeInspectWithRaw(context.Context, string) (swarm.Node, []byte, error) { +func (cli *fakeClient) NodeInspect(context.Context, string, client.NodeInspectOptions) (client.NodeInspectResult, error) { if cli.nodeInspectFunc != nil { return cli.nodeInspectFunc() } - return swarm.Node{}, []byte{}, nil + return client.NodeInspectResult{}, nil } -func (cli *fakeClient) SwarmInit(_ context.Context, req swarm.InitRequest) (string, error) { +func (cli *fakeClient) SwarmInit(_ context.Context, options client.SwarmInitOptions) (client.SwarmInitResult, error) { if cli.swarmInitFunc != nil { - return cli.swarmInitFunc(req) + return cli.swarmInitFunc(options) } - return "", nil + return client.SwarmInitResult{}, nil } -func (cli *fakeClient) SwarmInspect(context.Context) (swarm.Swarm, error) { +func (cli *fakeClient) SwarmInspect(context.Context, client.SwarmInspectOptions) (client.SwarmInspectResult, error) { if cli.swarmInspectFunc != nil { return cli.swarmInspectFunc() } - return swarm.Swarm{}, nil + return client.SwarmInspectResult{}, nil } -func (cli *fakeClient) SwarmGetUnlockKey(context.Context) (swarm.UnlockKeyResponse, error) { +func (cli *fakeClient) SwarmGetUnlockKey(ctx context.Context) (client.SwarmGetUnlockKeyResult, error) { if cli.swarmGetUnlockKeyFunc != nil { return cli.swarmGetUnlockKeyFunc() } - return swarm.UnlockKeyResponse{}, nil + return client.SwarmGetUnlockKeyResult{}, nil } -func (cli *fakeClient) SwarmJoin(context.Context, swarm.JoinRequest) error { +func (cli *fakeClient) SwarmJoin(context.Context, client.SwarmJoinOptions) (client.SwarmJoinResult, error) { if cli.swarmJoinFunc != nil { return cli.swarmJoinFunc() } - return nil + return client.SwarmJoinResult{}, nil } -func (cli *fakeClient) SwarmLeave(context.Context, bool) error { +func (cli *fakeClient) SwarmLeave(context.Context, client.SwarmLeaveOptions) (client.SwarmLeaveResult, error) { if cli.swarmLeaveFunc != nil { return cli.swarmLeaveFunc() } - return nil + return client.SwarmLeaveResult{}, nil } -func (cli *fakeClient) SwarmUpdate(_ context.Context, _ swarm.Version, swarmSpec swarm.Spec, flags swarm.UpdateFlags) error { +func (cli *fakeClient) SwarmUpdate(_ context.Context, options client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { if cli.swarmUpdateFunc != nil { - return cli.swarmUpdateFunc(swarmSpec, flags) + return cli.swarmUpdateFunc(options) } - return nil + return client.SwarmUpdateResult{}, nil } -func (cli *fakeClient) SwarmUnlock(_ context.Context, req swarm.UnlockRequest) error { +func (cli *fakeClient) SwarmUnlock(_ context.Context, options client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) { if cli.swarmUnlockFunc != nil { - return cli.swarmUnlockFunc(req) + return cli.swarmUnlockFunc(options) } - return nil + return client.SwarmUnlockResult{}, nil } diff --git a/cli/command/swarm/cmd.go b/cli/command/swarm/cmd.go index e78e33d003ec..72d873aadfb9 100644 --- a/cli/command/swarm/cmd.go +++ b/cli/command/swarm/cmd.go @@ -5,29 +5,35 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" ) -// NewSwarmCommand returns a cobra command for `swarm` subcommands -func NewSwarmCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newSwarmCommand) +} + +// newSwarmCommand returns a cobra command for `swarm` subcommands +func newSwarmCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "swarm", Short: "Manage Swarm", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "", // swarm command itself does not require swarm to be enabled (so swarm init and join is always available on API 1.24 and up) }, + DisableFlagsInUseLine: true, } cmd.AddCommand( - newInitCommand(dockerCli), - newJoinCommand(dockerCli), - newJoinTokenCommand(dockerCli), - newUnlockKeyCommand(dockerCli), - newUpdateCommand(dockerCli), - newLeaveCommand(dockerCli), - newUnlockCommand(dockerCli), - newCACommand(dockerCli), + newInitCommand(dockerCLI), + newJoinCommand(dockerCLI), + newJoinTokenCommand(dockerCLI), + newUnlockKeyCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newLeaveCommand(dockerCLI), + newUnlockCommand(dockerCLI), + newCACommand(dockerCLI), ) return cmd } diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go index d61aab755bd0..e977e4e71479 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "net" + "net/netip" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -28,7 +28,7 @@ type initOptions struct { DefaultAddrPoolMaskLength uint32 } -func newInitCommand(dockerCli command.Cli) *cobra.Command { +func newInitCommand(dockerCLI command.Cli) *cobra.Command { opts := initOptions{ listenAddr: NewListenAddrOption(), } @@ -38,29 +38,30 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { Short: "Initialize a swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runInit(cmd.Context(), dockerCli, cmd.Flags(), opts) + return runInit(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, Annotations: map[string]string{ "version": "1.24", "swarm": "", // swarm init does not require swarm to be active, and is always available on API 1.24 and up }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, `Listen address (format: "[:port]")`) flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", `Advertised address (format: "[:port]")`) flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", `Address or interface to use for data path traffic (format: "")`) - flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"}) + _ = flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"}) flags.Uint32Var(&opts.dataPathPort, flagDataPathPort, 0, "Port number to use for data path traffic (1024 - 49151). If no value is set or is set to 0, the default port (4789) is used.") - flags.SetAnnotation(flagDataPathPort, "version", []string{"1.40"}) + _ = flags.SetAnnotation(flagDataPathPort, "version", []string{"1.40"}) flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active", "pause", "drain")`) flags.IPNetSliceVar(&opts.defaultAddrPools, flagDefaultAddrPool, []net.IPNet{}, "default address pool in CIDR format") - flags.SetAnnotation(flagDefaultAddrPool, "version", []string{"1.39"}) + _ = flags.SetAnnotation(flagDefaultAddrPool, "version", []string{"1.39"}) flags.Uint32Var(&opts.DefaultAddrPoolMaskLength, flagDefaultAddrPoolMaskLength, 24, "default address pool subnet mask length") - flags.SetAnnotation(flagDefaultAddrPoolMaskLength, "version", []string{"1.39"}) + _ = flags.SetAnnotation(flagDefaultAddrPoolMaskLength, "version", []string{"1.39"}) addSwarmFlags(flags, &opts.swarmOptions) return cmd } @@ -68,11 +69,34 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { func runInit(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts initOptions) error { apiClient := dockerCLI.Client() - defaultAddrPool := make([]string, 0, len(opts.defaultAddrPools)) + // TODO(thaJeztah): change opts.defaultAddrPools a []netip.Prefix; see https://github.com/docker/cli/pull/6545#discussion_r2420361609 + defaultAddrPool := make([]netip.Prefix, 0, len(opts.defaultAddrPools)) for _, p := range opts.defaultAddrPools { - defaultAddrPool = append(defaultAddrPool, p.String()) + if len(p.IP) == 0 { + continue + } + ip := p.IP.To4() + if ip == nil { + ip = p.IP.To16() + } + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return fmt.Errorf("invalid IP address: %s", p.IP) + } + ones, _ := p.Mask.Size() + defaultAddrPool = append(defaultAddrPool, netip.PrefixFrom(addr, ones)) + } + var availability swarm.NodeAvailability + if flags.Changed(flagAvailability) { + switch a := swarm.NodeAvailability(strings.ToLower(opts.availability)); a { + case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: + availability = a + default: + return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + } } - req := swarm.InitRequest{ + + res, err := apiClient.SwarmInit(ctx, client.SwarmInitOptions{ ListenAddr: opts.listenAddr.String(), AdvertiseAddr: opts.advertiseAddr, DataPathAddr: opts.dataPathAddr, @@ -81,40 +105,30 @@ func runInit(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, o ForceNewCluster: opts.forceNewCluster, Spec: opts.swarmOptions.ToSpec(flags), AutoLockManagers: opts.swarmOptions.autolock, + Availability: availability, SubnetSize: opts.DefaultAddrPoolMaskLength, - } - if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { - case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability - default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - } - - nodeID, err := apiClient.SwarmInit(ctx, req) + }) if err != nil { if strings.Contains(err.Error(), "could not choose an IP address to advertise") || strings.Contains(err.Error(), "could not find the system's IP address") { - return errors.New(err.Error() + " - specify one with --advertise-addr") + return fmt.Errorf("%w - specify one with --advertise-addr", err) } return err } - _, _ = fmt.Fprintf(dockerCLI.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", res.NodeID) - if err := printJoinCommand(ctx, dockerCLI, nodeID, true, false); err != nil { + if err := printJoinCommand(ctx, dockerCLI, res.NodeID, true, false); err != nil { return err } _, _ = fmt.Fprintln(dockerCLI.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.") - if req.AutoLockManagers { - unlockKeyResp, err := apiClient.SwarmGetUnlockKey(ctx) + if opts.swarmOptions.autolock { + resp, err := apiClient.SwarmGetUnlockKey(ctx) if err != nil { - return errors.Wrap(err, "could not fetch unlock key") + return fmt.Errorf("could not fetch unlock key: %w", err) } - printUnlockCommand(dockerCLI.Out(), unlockKeyResp.UnlockKey) + printUnlockCommand(dockerCLI.Out(), resp.Key) } return nil diff --git a/cli/command/swarm/init_test.go b/cli/command/swarm/init_test.go index 562751bfb616..a524d9932efc 100644 --- a/cli/command/swarm/init_test.go +++ b/cli/command/swarm/init_test.go @@ -9,7 +9,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -19,37 +20,37 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { testCases := []struct { name string flags map[string]string - swarmInitFunc func(swarm.InitRequest) (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) + swarmInitFunc func(client.SwarmInitOptions) (client.SwarmInitResult, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) expectedError string }{ { name: "init-failed", - swarmInitFunc: func(swarm.InitRequest) (string, error) { - return "", errors.New("error initializing the swarm") + swarmInitFunc: func(client.SwarmInitOptions) (client.SwarmInitResult, error) { + return client.SwarmInitResult{}, errors.New("error initializing the swarm") }, expectedError: "error initializing the swarm", }, { name: "init-failed-with-ip-choice", - swarmInitFunc: func(swarm.InitRequest) (string, error) { - return "", errors.New("could not choose an IP address to advertise") + swarmInitFunc: func(client.SwarmInitOptions) (client.SwarmInitResult, error) { + return client.SwarmInitResult{}, errors.New("could not choose an IP address to advertise") }, expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", }, { name: "swarm-inspect-after-init-failed", - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.New("error inspecting the swarm") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, { name: "node-inspect-after-init-failed", - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting the node") + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{}, errors.New("error inspecting the node") }, expectedError: "error inspecting the node", }, @@ -58,8 +59,8 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { flags: map[string]string{ flagAutolock: "true", }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{}, errors.New("error getting swarm unlock key") + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{}, errors.New("error getting swarm unlock key") }, expectedError: "could not fetch unlock key: error getting swarm unlock key", }, @@ -88,29 +89,25 @@ func TestSwarmInit(t *testing.T) { testCases := []struct { name string flags map[string]string - swarmInitFunc func(req swarm.InitRequest) (string, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) - nodeInspectFunc func() (swarm.Node, []byte, error) + swarmInitFunc func(client.SwarmInitOptions) (client.SwarmInitResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) }{ { name: "init", - swarmInitFunc: func(swarm.InitRequest) (string, error) { - return "nodeID", nil + swarmInitFunc: func(client.SwarmInitOptions) (client.SwarmInitResult, error) { + return client.SwarmInitResult{NodeID: "nodeID"}, nil }, }, { - name: "init-autolock", + name: "init-auto-lock", flags: map[string]string{ flagAutolock: "true", }, - swarmInitFunc: func(swarm.InitRequest) (string, error) { - return "nodeID", nil + swarmInitFunc: func(client.SwarmInitOptions) (client.SwarmInitResult, error) { + return client.SwarmInitResult{NodeID: "nodeID"}, nil }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", - }, nil + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{Key: "unlock-key"}, nil }, }, } @@ -118,9 +115,7 @@ func TestSwarmInit(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ swarmInitFunc: tc.swarmInitFunc, - swarmInspectFunc: tc.swarmInspectFunc, swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, - nodeInspectFunc: tc.nodeInspectFunc, }) cmd := newInitCommand(cli) cmd.SetArgs([]string{}) @@ -137,13 +132,13 @@ func TestSwarmInit(t *testing.T) { func TestSwarmInitWithExternalCA(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - swarmInitFunc: func(req swarm.InitRequest) (string, error) { - if assert.Check(t, is.Len(req.Spec.CAConfig.ExternalCAs, 1)) { - assert.Equal(t, req.Spec.CAConfig.ExternalCAs[0].CACert, cert) - assert.Equal(t, req.Spec.CAConfig.ExternalCAs[0].Protocol, swarm.ExternalCAProtocolCFSSL) - assert.Equal(t, req.Spec.CAConfig.ExternalCAs[0].URL, "https://example.com") + swarmInitFunc: func(options client.SwarmInitOptions) (client.SwarmInitResult, error) { + if assert.Check(t, is.Len(options.Spec.CAConfig.ExternalCAs, 1)) { + assert.Equal(t, options.Spec.CAConfig.ExternalCAs[0].CACert, cert) + assert.Equal(t, options.Spec.CAConfig.ExternalCAs[0].Protocol, swarm.ExternalCAProtocolCFSSL) + assert.Equal(t, options.Spec.CAConfig.ExternalCAs[0].URL, "https://example.com") } - return "nodeID", nil + return client.SwarmInitResult{NodeID: "nodeID"}, nil }, }) diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go index 3e497e7f1f60..3fd68fe9daf5 100644 --- a/cli/command/swarm/join.go +++ b/cli/command/swarm/join.go @@ -7,8 +7,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -23,7 +23,7 @@ type joinOptions struct { availability string } -func newJoinCommand(dockerCli command.Cli) *cobra.Command { +func newJoinCommand(dockerCLI command.Cli) *cobra.Command { opts := joinOptions{ listenAddr: NewListenAddrOption(), } @@ -34,12 +34,13 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runJoin(cmd.Context(), dockerCli, cmd.Flags(), opts) + return runJoin(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, Annotations: map[string]string{ "version": "1.24", "swarm": "", // swarm join does not require swarm to be active, and is always available on API 1.24 and up }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -52,40 +53,40 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runJoin(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) error { - client := dockerCli.Client() +func runJoin(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts joinOptions) error { + apiClient := dockerCLI.Client() - req := swarm.JoinRequest{ - JoinToken: opts.token, - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - DataPathAddr: opts.dataPathAddr, - RemoteAddrs: []string{opts.remote}, - } + var availability swarm.NodeAvailability if flags.Changed(flagAvailability) { - availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) - switch availability { + switch a := swarm.NodeAvailability(strings.ToLower(opts.availability)); a { case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: - req.Availability = availability + availability = a default: - return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) } } - err := client.SwarmJoin(ctx, req) + _, err := apiClient.SwarmJoin(ctx, client.SwarmJoinOptions{ + JoinToken: opts.token, + ListenAddr: opts.listenAddr.String(), + AdvertiseAddr: opts.advertiseAddr, + DataPathAddr: opts.dataPathAddr, + RemoteAddrs: []string{opts.remote}, + Availability: availability, + }) if err != nil { return err } - info, err := client.Info(ctx) + res, err := apiClient.Info(ctx, client.InfoOptions{}) if err != nil { return err } - if info.Swarm.ControlAvailable { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") + if res.Info.Swarm.ControlAvailable { + _, _ = fmt.Fprintln(dockerCLI.Out(), "This node joined a swarm as a manager.") } else { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "This node joined a swarm as a worker.") } return nil } diff --git a/cli/command/swarm/join_test.go b/cli/command/swarm/join_test.go index b6d092e8ce7a..707b2cb28bec 100644 --- a/cli/command/swarm/join_test.go +++ b/cli/command/swarm/join_test.go @@ -7,8 +7,9 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -17,7 +18,7 @@ func TestSwarmJoinErrors(t *testing.T) { testCases := []struct { name string args []string - swarmJoinFunc func() error + swarmJoinFunc func() (client.SwarmJoinResult, error) infoFunc func() (system.Info, error) expectedError string }{ @@ -34,8 +35,8 @@ func TestSwarmJoinErrors(t *testing.T) { { name: "join-failed", args: []string{"remote"}, - swarmJoinFunc: func() error { - return errors.New("error joining the swarm") + swarmJoinFunc: func() (client.SwarmJoinResult, error) { + return client.SwarmJoinResult{}, errors.New("error joining the swarm") }, expectedError: "error joining the swarm", }, diff --git a/cli/command/swarm/join_token.go b/cli/command/swarm/join_token.go index 08a5d8766c2b..a1c01ec3c988 100644 --- a/cli/command/swarm/join_token.go +++ b/cli/command/swarm/join_token.go @@ -2,12 +2,12 @@ package swarm import ( "context" + "errors" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,7 +17,7 @@ type joinTokenOptions struct { quiet bool } -func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { +func newJoinTokenCommand(dockerCLI command.Cli) *cobra.Command { opts := joinTokenOptions{} cmd := &cobra.Command{ @@ -26,12 +26,13 @@ func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.role = args[0] - return runJoinToken(cmd.Context(), dockerCli, opts) + return runJoinToken(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -52,12 +53,14 @@ func runJoinToken(ctx context.Context, dockerCLI command.Cli, opts joinTokenOpti apiClient := dockerCLI.Client() if opts.rotate { - sw, err := apiClient.SwarmInspect(ctx) + res, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } - err = apiClient.SwarmUpdate(ctx, sw.Version, sw.Spec, swarm.UpdateFlags{ + _, err = apiClient.SwarmUpdate(ctx, client.SwarmUpdateOptions{ + Version: res.Swarm.Version, + Spec: res.Swarm.Spec, RotateWorkerToken: worker, RotateManagerToken: manager, }) @@ -72,48 +75,54 @@ func runJoinToken(ctx context.Context, dockerCLI command.Cli, opts joinTokenOpti // second SwarmInspect in this function, // this is necessary since SwarmUpdate after first changes the join tokens - sw, err := apiClient.SwarmInspect(ctx) + res, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } if opts.quiet && worker { - _, _ = fmt.Fprintln(dockerCLI.Out(), sw.JoinTokens.Worker) + _, _ = fmt.Fprintln(dockerCLI.Out(), res.Swarm.JoinTokens.Worker) return nil } if opts.quiet && manager { - _, _ = fmt.Fprintln(dockerCLI.Out(), sw.JoinTokens.Manager) + _, _ = fmt.Fprintln(dockerCLI.Out(), res.Swarm.JoinTokens.Manager) return nil } - info, err := apiClient.Info(ctx) + infoResp, err := apiClient.Info(ctx, client.InfoOptions{}) if err != nil { return err } - return printJoinCommand(ctx, dockerCLI, info.Swarm.NodeID, worker, manager) + return printJoinCommand(ctx, dockerCLI, infoResp.Info.Swarm.NodeID, worker, manager) } func printJoinCommand(ctx context.Context, dockerCLI command.Cli, nodeID string, worker bool, manager bool) error { apiClient := dockerCLI.Client() - node, _, err := apiClient.NodeInspectWithRaw(ctx, nodeID) + res, err := apiClient.NodeInspect(ctx, nodeID, client.NodeInspectOptions{}) if err != nil { return err } - sw, err := apiClient.SwarmInspect(ctx) + sw, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } - if node.ManagerStatus != nil { + if res.Node.ManagerStatus != nil { if worker { - _, _ = fmt.Fprintf(dockerCLI.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) + _, _ = fmt.Fprintf(dockerCLI.Out(), + "To add a worker to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", + sw.Swarm.JoinTokens.Worker, res.Node.ManagerStatus.Addr, + ) } if manager { - _, _ = fmt.Fprintf(dockerCLI.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) + _, _ = fmt.Fprintf(dockerCLI.Out(), + "To add a manager to this swarm, run the following command:\n\n docker swarm join --token %s %s\n\n", + sw.Swarm.JoinTokens.Manager, res.Node.ManagerStatus.Addr, + ) } } diff --git a/cli/command/swarm/join_token_test.go b/cli/command/swarm/join_token_test.go index 7c819b6fea71..b2ac55c675a9 100644 --- a/cli/command/swarm/join_token_test.go +++ b/cli/command/swarm/join_token_test.go @@ -8,8 +8,9 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -20,9 +21,9 @@ func TestSwarmJoinTokenErrors(t *testing.T) { args []string flags map[string]string infoFunc func() (system.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - nodeInspectFunc func() (swarm.Node, []byte, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) expectedError string }{ { @@ -43,8 +44,8 @@ func TestSwarmJoinTokenErrors(t *testing.T) { { name: "swarm-inspect-failed", args: []string{"worker"}, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.New("error inspecting the swarm") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -54,8 +55,8 @@ func TestSwarmJoinTokenErrors(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.New("error inspecting the swarm") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -65,16 +66,16 @@ func TestSwarmJoinTokenErrors(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.New("error updating the swarm") + swarmUpdateFunc: func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + return client.SwarmUpdateResult{}, errors.New("error updating the swarm") }, expectedError: "error updating the swarm", }, { name: "node-inspect-failed", args: []string{"worker"}, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, errors.New("error inspecting node") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting node") }, expectedError: "error inspecting node", }, @@ -113,8 +114,8 @@ func TestSwarmJoinToken(t *testing.T) { args []string flags map[string]string infoFunc func() (system.Info, error) - swarmInspectFunc func() (swarm.Swarm, error) - nodeInspectFunc func() (swarm.Node, []byte, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + nodeInspectFunc func() (client.NodeInspectResult, error) }{ { name: "worker", @@ -126,11 +127,15 @@ func TestSwarmJoinToken(t *testing.T) { }, }, nil }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, }, { @@ -143,11 +148,15 @@ func TestSwarmJoinToken(t *testing.T) { }, }, nil }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, }, { @@ -163,11 +172,15 @@ func TestSwarmJoinToken(t *testing.T) { }, }, nil }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, }, { @@ -176,11 +189,15 @@ func TestSwarmJoinToken(t *testing.T) { flags: map[string]string{ flagQuiet: "true", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, }, { @@ -189,11 +206,15 @@ func TestSwarmJoinToken(t *testing.T) { flags: map[string]string{ flagQuiet: "true", }, - nodeInspectFunc: func() (swarm.Node, []byte, error) { - return *builders.Node(builders.Manager()), []byte{}, nil + nodeInspectFunc: func() (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.Manager()), + }, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, }, } diff --git a/cli/command/swarm/leave.go b/cli/command/swarm/leave.go index 9d7385215825..36c529d696d1 100644 --- a/cli/command/swarm/leave.go +++ b/cli/command/swarm/leave.go @@ -1,48 +1,38 @@ package swarm import ( - "context" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) -type leaveOptions struct { - force bool -} - -func newLeaveCommand(dockerCli command.Cli) *cobra.Command { - opts := leaveOptions{} +func newLeaveCommand(dockerCLI command.Cli) *cobra.Command { + var opts client.SwarmLeaveOptions cmd := &cobra.Command{ Use: "leave [OPTIONS]", Short: "Leave the swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runLeave(cmd.Context(), dockerCli, opts) + if _, err := dockerCLI.Client().SwarmLeave(cmd.Context(), opts); err != nil { + return err + } + + _, _ = fmt.Fprintln(dockerCLI.Out(), "Node left the swarm.") + return nil }, Annotations: map[string]string{ "version": "1.24", "swarm": "active", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() - flags.BoolVarP(&opts.force, "force", "f", false, "Force this node to leave the swarm, ignoring warnings") + flags.BoolVarP(&opts.Force, "force", "f", false, "Force this node to leave the swarm, ignoring warnings") return cmd } - -func runLeave(ctx context.Context, dockerCli command.Cli, opts leaveOptions) error { - client := dockerCli.Client() - - if err := client.SwarmLeave(ctx, opts.force); err != nil { - return err - } - - fmt.Fprintln(dockerCli.Out(), "Node left the swarm.") - return nil -} diff --git a/cli/command/swarm/leave_test.go b/cli/command/swarm/leave_test.go index a389d90fc7f7..72b828beae66 100644 --- a/cli/command/swarm/leave_test.go +++ b/cli/command/swarm/leave_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/docker/cli/internal/test" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -15,7 +16,7 @@ func TestSwarmLeaveErrors(t *testing.T) { testCases := []struct { name string args []string - swarmLeaveFunc func() error + swarmLeaveFunc func() (client.SwarmLeaveResult, error) expectedError string }{ { @@ -26,8 +27,8 @@ func TestSwarmLeaveErrors(t *testing.T) { { name: "leave-failed", args: []string{}, - swarmLeaveFunc: func() error { - return errors.New("error leaving the swarm") + swarmLeaveFunc: func() (client.SwarmLeaveResult, error) { + return client.SwarmLeaveResult{}, errors.New("error leaving the swarm") }, expectedError: "error leaving the swarm", }, diff --git a/cli/command/swarm/opts.go b/cli/command/swarm/opts.go index 41a0244ed498..57429ffb0904 100644 --- a/cli/command/swarm/opts.go +++ b/cli/command/swarm/opts.go @@ -3,14 +3,14 @@ package swarm import ( "encoding/csv" "encoding/pem" + "errors" "fmt" "os" "strings" "time" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" "github.com/spf13/pflag" ) @@ -177,7 +177,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { for _, field := range fields { key, value, ok := strings.Cut(field, "=") if !ok { - return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) + return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field) } // TODO(thaJeztah): these options should not be case-insensitive. @@ -187,7 +187,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { externalCA.Protocol = swarm.ExternalCAProtocolCFSSL } else { - return nil, errors.Errorf("unrecognized external CA protocol %s", value) + return nil, fmt.Errorf("unrecognized external CA protocol %s", value) } case "url": hasURL = true @@ -195,7 +195,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { case "cacert": cacontents, err := os.ReadFile(value) if err != nil { - return nil, errors.Wrap(err, "unable to read CA cert for external CA") + return nil, fmt.Errorf("unable to read CA cert for external CA: %w", err) } if pemBlock, _ := pem.Decode(cacontents); pemBlock == nil { return nil, errors.New("CA cert for external CA must be in PEM format") diff --git a/cli/command/swarm/progress/root_rotation.go b/cli/command/swarm/progress/root_rotation.go index 007418d34676..54e53e55d960 100644 --- a/cli/command/swarm/progress/root_rotation.go +++ b/cli/command/swarm/progress/root_rotation.go @@ -8,10 +8,10 @@ import ( "os/signal" "time" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/progress" - "github.com/docker/docker/pkg/streamformatter" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/progress" + "github.com/moby/moby/client/pkg/streamformatter" "github.com/opencontainers/go-digest" ) @@ -25,8 +25,10 @@ const ( ) // RootRotationProgress outputs progress information for convergence of a root rotation. -func RootRotationProgress(ctx context.Context, dclient client.APIClient, progressWriter io.WriteCloser) error { - defer progressWriter.Close() +func RootRotationProgress(ctx context.Context, apiClient client.APIClient, progressWriter io.WriteCloser) error { + defer func() { + _ = progressWriter.Close() + }() progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false) @@ -42,7 +44,7 @@ func RootRotationProgress(ctx context.Context, dclient client.APIClient, progres var done bool for { - info, err := dclient.SwarmInspect(ctx) + info, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } @@ -51,12 +53,12 @@ func RootRotationProgress(ctx context.Context, dclient client.APIClient, progres return nil } - nodes, err := dclient.NodeList(ctx, swarm.NodeListOptions{}) + res, err := apiClient.NodeList(ctx, client.NodeListOptions{}) if err != nil { return err } - done = updateProgress(progressOut, info.ClusterInfo.TLSInfo, nodes, info.ClusterInfo.RootRotationInProgress) + done = updateProgress(progressOut, info.Swarm.ClusterInfo.TLSInfo, res.Items, info.Swarm.ClusterInfo.RootRotationInProgress) select { case <-time.After(200 * time.Millisecond): @@ -72,7 +74,7 @@ func RootRotationProgress(ctx context.Context, dclient client.APIClient, progres func updateProgress(progressOut progress.Output, desiredTLSInfo swarm.TLSInfo, nodes []swarm.Node, rootRotationInProgress bool) bool { // write the current desired root cert's digest, because the desired root certs might be too long - progressOut.WriteProgress(progress.Progress{ + _ = progressOut.WriteProgress(progress.Progress{ ID: "desired root digest", Action: digest.FromBytes([]byte(desiredTLSInfo.TrustRoot)).String(), }) @@ -91,7 +93,7 @@ func updateProgress(progressOut progress.Output, desiredTLSInfo swarm.TLSInfo, n } total := int64(len(nodes)) - progressOut.WriteProgress(progress.Progress{ + _ = progressOut.WriteProgress(progress.Progress{ ID: certsRotatedStr, Action: certsAction, Current: certsRight, @@ -108,12 +110,12 @@ func updateProgress(progressOut progress.Output, desiredTLSInfo swarm.TLSInfo, n } if certsRight == total && !rootRotationInProgress { - progressOut.WriteProgress(rootsProgress) + _ = progressOut.WriteProgress(rootsProgress) return certsRight == total && trustRootsRight == total } // we still have certs that need renewing, so display that there are zero roots rotated yet rootsProgress.Current = 0 - progressOut.WriteProgress(rootsProgress) + _ = progressOut.WriteProgress(rootsProgress) return false } diff --git a/cli/command/swarm/testdata/init-init-autolock.golden b/cli/command/swarm/testdata/init-init-auto-lock.golden similarity index 100% rename from cli/command/swarm/testdata/init-init-autolock.golden rename to cli/command/swarm/testdata/init-init-auto-lock.golden diff --git a/cli/command/swarm/testdata/update-autolock-unlock-key.golden b/cli/command/swarm/testdata/update-auto-lock-unlock-key.golden similarity index 100% rename from cli/command/swarm/testdata/update-autolock-unlock-key.golden rename to cli/command/swarm/testdata/update-auto-lock-unlock-key.golden diff --git a/cli/command/swarm/testdata/update-noargs.golden b/cli/command/swarm/testdata/update-noargs.golden index a2ce75897c77..c6b8bed8d444 100644 --- a/cli/command/swarm/testdata/update-noargs.golden +++ b/cli/command/swarm/testdata/update-noargs.golden @@ -1,7 +1,7 @@ Update the swarm Usage: - update [OPTIONS] [flags] + update [OPTIONS] Flags: --autolock Change manager autolocking setting (true|false) diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go index 420482ee1eda..bf15ed2868c7 100644 --- a/cli/command/swarm/unlock.go +++ b/cli/command/swarm/unlock.go @@ -3,65 +3,67 @@ package swarm import ( "bufio" "context" + "errors" "fmt" "io" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/streams" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" "golang.org/x/term" ) -func newUnlockCommand(dockerCli command.Cli) *cobra.Command { +func newUnlockCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "unlock", Short: "Unlock swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runUnlock(cmd.Context(), dockerCli) + return runUnlock(cmd.Context(), dockerCLI) }, Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } return cmd } -func runUnlock(ctx context.Context, dockerCli command.Cli) error { - client := dockerCli.Client() +func runUnlock(ctx context.Context, dockerCLI command.Cli) error { + apiClient := dockerCLI.Client() // First see if the node is actually part of a swarm, and if it is actually locked first. // If it's in any other state than locked, don't ask for the key. - info, err := client.Info(ctx) + res, err := apiClient.Info(ctx, client.InfoOptions{}) if err != nil { return err } - switch info.Swarm.LocalNodeState { + switch res.Info.Swarm.LocalNodeState { case swarm.LocalNodeStateInactive: - return errors.New("Error: This node is not part of a swarm") + return errors.New("error: this node is not part of a swarm") case swarm.LocalNodeStateLocked: break case swarm.LocalNodeStatePending, swarm.LocalNodeStateActive, swarm.LocalNodeStateError: - return errors.New("Error: swarm is not locked") + return errors.New("error: swarm is not locked") } - key, err := readKey(dockerCli.In(), "Enter unlock key: ") + key, err := readKey(dockerCLI.In(), "Enter unlock key: ") if err != nil { return err } - return client.SwarmUnlock(ctx, swarm.UnlockRequest{ - UnlockKey: key, + _, err = apiClient.SwarmUnlock(ctx, client.SwarmUnlockOptions{ + Key: key, }) + return err } func readKey(in *streams.In, prompt string) (string, error) { diff --git a/cli/command/swarm/unlock_key.go b/cli/command/swarm/unlock_key.go index af62e0b09a95..a8df5089e5c9 100644 --- a/cli/command/swarm/unlock_key.go +++ b/cli/command/swarm/unlock_key.go @@ -2,14 +2,13 @@ package swarm import ( "context" + "errors" "fmt" "io" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,7 +17,7 @@ type unlockKeyOptions struct { quiet bool } -func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { +func newUnlockKeyCommand(dockerCLI command.Cli) *cobra.Command { opts := unlockKeyOptions{} cmd := &cobra.Command{ @@ -26,13 +25,14 @@ func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { Short: "Manage the unlock key", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runUnlockKey(cmd.Context(), dockerCli, opts) + return runUnlockKey(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -46,18 +46,22 @@ func runUnlockKey(ctx context.Context, dockerCLI command.Cli, opts unlockKeyOpti apiClient := dockerCLI.Client() if opts.rotate { - flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} - - sw, err := apiClient.SwarmInspect(ctx) + res, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } - if !sw.Spec.EncryptionConfig.AutoLockManagers { + if !res.Swarm.Spec.EncryptionConfig.AutoLockManagers { return errors.New("cannot rotate because autolock is not turned on") } - if err := apiClient.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { + _, err = apiClient.SwarmUpdate(ctx, client.SwarmUpdateOptions{ + Version: res.Swarm.Version, + Spec: res.Swarm.Spec, + + RotateManagerUnlockKey: true, + }) + if err != nil { return err } @@ -66,21 +70,21 @@ func runUnlockKey(ctx context.Context, dockerCLI command.Cli, opts unlockKeyOpti } } - unlockKeyResp, err := apiClient.SwarmGetUnlockKey(ctx) + resp, err := apiClient.SwarmGetUnlockKey(ctx) if err != nil { - return errors.Wrap(err, "could not fetch unlock key") + return fmt.Errorf("could not fetch unlock key: %w", err) } - if unlockKeyResp.UnlockKey == "" { + if resp.Key == "" { return errors.New("no unlock key is set") } if opts.quiet { - _, _ = fmt.Fprintln(dockerCLI.Out(), unlockKeyResp.UnlockKey) + _, _ = fmt.Fprintln(dockerCLI.Out(), resp.Key) return nil } - printUnlockCommand(dockerCLI.Out(), unlockKeyResp.UnlockKey) + printUnlockCommand(dockerCLI.Out(), resp.Key) return nil } diff --git a/cli/command/swarm/unlock_key_test.go b/cli/command/swarm/unlock_key_test.go index ec80727dd648..4f6348116096 100644 --- a/cli/command/swarm/unlock_key_test.go +++ b/cli/command/swarm/unlock_key_test.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -18,9 +18,9 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { name string args []string flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) expectedError string }{ { @@ -33,8 +33,8 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.New("error inspecting the swarm") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -43,8 +43,10 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, expectedError: "cannot rotate because autolock is not turned on", }, @@ -53,25 +55,27 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(builders.Autolock()), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(builders.Autolock()), + }, nil }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.New("error updating the swarm") + swarmUpdateFunc: func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + return client.SwarmUpdateResult{}, errors.New("error updating the swarm") }, expectedError: "error updating the swarm", }, { name: "swarm-get-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{}, errors.New("error getting unlock key") + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{}, errors.New("error getting unlock key") }, expectedError: "error getting unlock key", }, { name: "swarm-no-unlock-key-failed", - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{}, nil + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{}, nil }, expectedError: "no unlock key is set", }, @@ -103,15 +107,15 @@ func TestSwarmUnlockKey(t *testing.T) { testCases := []struct { name string flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) }{ { name: "unlock-key", - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{ + Key: "unlock-key", }, nil }, }, @@ -120,9 +124,9 @@ func TestSwarmUnlockKey(t *testing.T) { flags: map[string]string{ flagQuiet: "true", }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{ + Key: "unlock-key", }, nil }, }, @@ -131,12 +135,14 @@ func TestSwarmUnlockKey(t *testing.T) { flags: map[string]string{ flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(builders.Autolock()), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(builders.Autolock()), + }, nil }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{ + Key: "unlock-key", }, nil }, }, @@ -146,12 +152,14 @@ func TestSwarmUnlockKey(t *testing.T) { flagQuiet: "true", flagRotate: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(builders.Autolock()), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(builders.Autolock()), + }, nil }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{ + Key: "unlock-key", }, nil }, }, diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go index b437cac07fd7..f71bd1ba8c0b 100644 --- a/cli/command/swarm/unlock_test.go +++ b/cli/command/swarm/unlock_test.go @@ -8,8 +8,9 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -17,7 +18,7 @@ func TestSwarmUnlockErrors(t *testing.T) { testCases := []struct { name string args []string - swarmUnlockFunc func(req swarm.UnlockRequest) error + swarmUnlockFunc func(client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) infoFunc func() (system.Info, error) expectedError string }{ @@ -35,7 +36,7 @@ func TestSwarmUnlockErrors(t *testing.T) { }, }, nil }, - expectedError: "This node is not part of a swarm", + expectedError: "this node is not part of a swarm", }, { name: "is-not-locked", @@ -46,7 +47,7 @@ func TestSwarmUnlockErrors(t *testing.T) { }, }, nil }, - expectedError: "Error: swarm is not locked", + expectedError: "error: swarm is not locked", }, { name: "unlockrequest-failed", @@ -57,8 +58,8 @@ func TestSwarmUnlockErrors(t *testing.T) { }, }, nil }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - return errors.New("error unlocking the swarm") + swarmUnlockFunc: func(client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) { + return client.SwarmUnlockResult{}, errors.New("error unlocking the swarm") }, expectedError: "error unlocking the swarm", }, @@ -92,11 +93,11 @@ func TestSwarmUnlock(t *testing.T) { }, }, nil }, - swarmUnlockFunc: func(req swarm.UnlockRequest) error { - if req.UnlockKey != input { - return errors.New("invalid unlock key") + swarmUnlockFunc: func(req client.SwarmUnlockOptions) (client.SwarmUnlockResult, error) { + if req.Key != input { + return client.SwarmUnlockResult{}, errors.New("invalid unlock key") } - return nil + return client.SwarmUnlockResult{}, nil }, }) dockerCli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) diff --git a/cli/command/swarm/update.go b/cli/command/swarm/update.go index 2e853a9312a0..9dc373211a34 100644 --- a/cli/command/swarm/update.go +++ b/cli/command/swarm/update.go @@ -6,14 +6,12 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/swarm" - "github.com/pkg/errors" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { opts := swarmOptions{} cmd := &cobra.Command{ @@ -21,7 +19,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { Short: "Update the swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), opts) + return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, PreRunE: func(cmd *cobra.Command, args []string) error { if cmd.Flags().NFlag() == 0 { @@ -33,7 +31,8 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") @@ -41,35 +40,36 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { - client := dockerCli.Client() +func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { + apiClient := dockerCLI.Client() - var updateFlags swarm.UpdateFlags - - swarmInspect, err := client.SwarmInspect(ctx) + sw, err := apiClient.SwarmInspect(ctx, client.SwarmInspectOptions{}) if err != nil { return err } - prevAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers + prevAutoLock := sw.Swarm.Spec.EncryptionConfig.AutoLockManagers - opts.mergeSwarmSpec(&swarmInspect.Spec, flags, &swarmInspect.ClusterInfo.TLSInfo.TrustRoot) + opts.mergeSwarmSpec(&sw.Swarm.Spec, flags, &sw.Swarm.ClusterInfo.TLSInfo.TrustRoot) - curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers + curAutoLock := sw.Swarm.Spec.EncryptionConfig.AutoLockManagers - err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) + _, err = apiClient.SwarmUpdate(ctx, client.SwarmUpdateOptions{ + Version: sw.Swarm.Version, + Spec: sw.Swarm.Spec, + }) if err != nil { return err } - fmt.Fprintln(dockerCli.Out(), "Swarm updated.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "Swarm updated.") if curAutoLock && !prevAutoLock { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + resp, err := apiClient.SwarmGetUnlockKey(ctx) if err != nil { - return errors.Wrap(err, "could not fetch unlock key") + return fmt.Errorf("could not fetch unlock key: %w", err) } - printUnlockCommand(dockerCli.Out(), unlockKeyResp.UnlockKey) + printUnlockCommand(dockerCLI.Out(), resp.Key) } return nil diff --git a/cli/command/swarm/update_test.go b/cli/command/swarm/update_test.go index 373c2712bc56..c611b269f929 100644 --- a/cli/command/swarm/update_test.go +++ b/cli/command/swarm/update_test.go @@ -9,7 +9,7 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -19,9 +19,9 @@ func TestSwarmUpdateErrors(t *testing.T) { name string args []string flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) expectedError string }{ { @@ -34,8 +34,8 @@ func TestSwarmUpdateErrors(t *testing.T) { flags: map[string]string{ flagTaskHistoryLimit: "10", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, errors.New("error inspecting the swarm") + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{}, errors.New("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -44,21 +44,23 @@ func TestSwarmUpdateErrors(t *testing.T) { flags: map[string]string{ flagTaskHistoryLimit: "10", }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return errors.New("error updating the swarm") + swarmUpdateFunc: func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + return client.SwarmUpdateResult{}, errors.New("error updating the swarm") }, expectedError: "error updating the swarm", }, { - name: "swarm-unlockkey-error", + name: "swarm-unlock-key-error", flags: map[string]string{ flagAutolock: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{}, errors.New("error getting unlock key") + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{}, errors.New("error getting unlock key") }, expectedError: "error getting unlock key", }, @@ -88,15 +90,15 @@ func TestSwarmUpdateErrors(t *testing.T) { func TestSwarmUpdate(t *testing.T) { swarmInfo := builders.Swarm() - swarmInfo.ClusterInfo.TLSInfo.TrustRoot = "trustroot" + swarmInfo.ClusterInfo.TLSInfo.TrustRoot = "trust-root" testCases := []struct { name string args []string flags map[string]string - swarmInspectFunc func() (swarm.Swarm, error) - swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error - swarmGetUnlockKeyFunc func() (swarm.UnlockKeyResponse, error) + swarmInspectFunc func() (client.SwarmInspectResult, error) + swarmUpdateFunc func(client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) + swarmGetUnlockKeyFunc func() (client.SwarmGetUnlockKeyResult, error) }{ { name: "noargs", @@ -112,60 +114,64 @@ func TestSwarmUpdate(t *testing.T) { flagSnapshotInterval: "100", flagAutolock: "true", }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *swarmInfo, nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *swarmInfo, + }, nil }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.New("historyLimit not correctly set") + swarmUpdateFunc: func(options client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + if *options.Spec.Orchestration.TaskHistoryRetentionLimit != 10 { + return client.SwarmUpdateResult{}, errors.New("historyLimit not correctly set") } heartbeatDuration, err := time.ParseDuration("10s") if err != nil { - return err + return client.SwarmUpdateResult{}, err } - if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { - return errors.New("heartbeatPeriodLimit not correctly set") + if options.Spec.Dispatcher.HeartbeatPeriod != heartbeatDuration { + return client.SwarmUpdateResult{}, errors.New("heartbeatPeriodLimit not correctly set") } certExpiryDuration, err := time.ParseDuration("20s") if err != nil { - return err + return client.SwarmUpdateResult{}, err } - if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { - return errors.New("certExpiry not correctly set") + if options.Spec.CAConfig.NodeCertExpiry != certExpiryDuration { + return client.SwarmUpdateResult{}, errors.New("certExpiry not correctly set") } - if len(swarm.CAConfig.ExternalCAs) != 1 || swarm.CAConfig.ExternalCAs[0].CACert != "trustroot" { - return errors.New("externalCA not correctly set") + if len(options.Spec.CAConfig.ExternalCAs) != 1 || options.Spec.CAConfig.ExternalCAs[0].CACert != "trust-root" { + return client.SwarmUpdateResult{}, errors.New("externalCA not correctly set") } - if *swarm.Raft.KeepOldSnapshots != 10 { - return errors.New("keepOldSnapshots not correctly set") + if *options.Spec.Raft.KeepOldSnapshots != 10 { + return client.SwarmUpdateResult{}, errors.New("keepOldSnapshots not correctly set") } - if swarm.Raft.SnapshotInterval != 100 { - return errors.New("snapshotInterval not correctly set") + if options.Spec.Raft.SnapshotInterval != 100 { + return client.SwarmUpdateResult{}, errors.New("snapshotInterval not correctly set") } - if !swarm.EncryptionConfig.AutoLockManagers { - return errors.New("autolock not correctly set") + if !options.Spec.EncryptionConfig.AutoLockManagers { + return client.SwarmUpdateResult{}, errors.New("auto-lock not correctly set") } - return nil + return client.SwarmUpdateResult{}, nil }, }, { - name: "autolock-unlock-key", + name: "auto-lock-unlock-key", flags: map[string]string{ flagTaskHistoryLimit: "10", flagAutolock: "true", }, - swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return errors.New("historyLimit not correctly set") + swarmUpdateFunc: func(options client.SwarmUpdateOptions) (client.SwarmUpdateResult, error) { + if *options.Spec.Orchestration.TaskHistoryRetentionLimit != 10 { + return client.SwarmUpdateResult{}, errors.New("historyLimit not correctly set") } - return nil + return client.SwarmUpdateResult{}, nil }, - swarmInspectFunc: func() (swarm.Swarm, error) { - return *builders.Swarm(), nil + swarmInspectFunc: func() (client.SwarmInspectResult, error) { + return client.SwarmInspectResult{ + Swarm: *builders.Swarm(), + }, nil }, - swarmGetUnlockKeyFunc: func() (swarm.UnlockKeyResponse, error) { - return swarm.UnlockKeyResponse{ - UnlockKey: "unlock-key", + swarmGetUnlockKeyFunc: func() (client.SwarmGetUnlockKeyResult, error) { + return client.SwarmGetUnlockKeyResult{ + Key: "unlock-key", }, nil }, }, diff --git a/cli/command/system/client_test.go b/cli/command/system/client_test.go index d188ea9f9319..898edf916fe6 100644 --- a/cli/command/system/client_test.go +++ b/cli/command/system/client_test.go @@ -3,98 +3,98 @@ package system import ( "context" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client version string - containerListFunc func(context.Context, container.ListOptions) ([]container.Summary, error) - containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) - eventsFn func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error) - imageListFunc func(ctx context.Context, options image.ListOptions) ([]image.Summary, error) - infoFunc func(ctx context.Context) (system.Info, error) - networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) - networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) - nodeListFunc func(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) - serverVersion func(ctx context.Context) (types.Version, error) - volumeListFunc func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) + containerListFunc func(context.Context, client.ContainerListOptions) ([]container.Summary, error) + containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) + eventsFn func(context.Context, client.EventsListOptions) (<-chan events.Message, <-chan error) + imageListFunc func(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) + infoFunc func(ctx context.Context, options client.InfoOptions) (client.SystemInfoResult, error) + networkListFunc func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) + networkPruneFunc func(ctx context.Context, options client.NetworkPruneOptions) (client.NetworkPruneResult, error) + nodeListFunc func(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) + serverVersion func(ctx context.Context, options client.ServerVersionOptions) (client.ServerVersionResult, error) + volumeListFunc func(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) } func (cli *fakeClient) ClientVersion() string { return cli.version } -func (cli *fakeClient) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) { +func (cli *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) { if cli.containerListFunc != nil { - return cli.containerListFunc(ctx, options) + res, err := cli.containerListFunc(ctx, options) + return client.ContainerListResult{ + Items: res, + }, err } - return []container.Summary{}, nil + return client.ContainerListResult{}, nil } -func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { +func (cli *fakeClient) ContainerPrune(ctx context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) { if cli.containerPruneFunc != nil { - return cli.containerPruneFunc(ctx, pruneFilters) + return cli.containerPruneFunc(ctx, opts) } - return container.PruneReport{}, nil + return client.ContainerPruneResult{}, nil } -func (cli *fakeClient) Events(ctx context.Context, opts events.ListOptions) (<-chan events.Message, <-chan error) { - return cli.eventsFn(ctx, opts) +func (cli *fakeClient) Events(ctx context.Context, opts client.EventsListOptions) client.EventsResult { + eventC, errC := cli.eventsFn(ctx, opts) + return client.EventsResult{ + Messages: eventC, + Err: errC, + } } -func (cli *fakeClient) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) { +func (cli *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) { if cli.imageListFunc != nil { return cli.imageListFunc(ctx, options) } - return []image.Summary{}, nil + return client.ImageListResult{}, nil } -func (cli *fakeClient) Info(ctx context.Context) (system.Info, error) { +func (cli *fakeClient) Info(ctx context.Context, options client.InfoOptions) (client.SystemInfoResult, error) { if cli.infoFunc != nil { - return cli.infoFunc(ctx) + return cli.infoFunc(ctx, options) } - return system.Info{}, nil + return client.SystemInfoResult{}, nil } -func (cli *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { +func (cli *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { if cli.networkListFunc != nil { return cli.networkListFunc(ctx, options) } - return []network.Summary{}, nil + return client.NetworkListResult{}, nil } -func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) { +func (cli *fakeClient) NetworksPrune(ctx context.Context, opts client.NetworkPruneOptions) (client.NetworkPruneResult, error) { if cli.networkPruneFunc != nil { - return cli.networkPruneFunc(ctx, pruneFilter) + return cli.networkPruneFunc(ctx, opts) } - return network.PruneReport{}, nil + return client.NetworkPruneResult{}, nil } -func (cli *fakeClient) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { +func (cli *fakeClient) NodeList(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) { if cli.nodeListFunc != nil { return cli.nodeListFunc(ctx, options) } - return []swarm.Node{}, nil + return client.NodeListResult{}, nil } -func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { - return cli.serverVersion(ctx) +func (cli *fakeClient) ServerVersion(ctx context.Context, options client.ServerVersionOptions) (client.ServerVersionResult, error) { + return cli.serverVersion(ctx, options) } -func (cli *fakeClient) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) { +func (cli *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) { if cli.volumeListFunc != nil { return cli.volumeListFunc(ctx, options) } - return volume.ListResponse{}, nil + return client.VolumeListResult{}, nil } diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go index 6accb98f0c4e..793b997eb57e 100644 --- a/cli/command/system/cmd.go +++ b/cli/command/system/cmd.go @@ -3,23 +3,34 @@ package system import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewSystemCommand returns a cobra command for `system` subcommands -func NewSystemCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newVersionCommand) + commands.Register(newInfoCommand) + commands.Register(newSystemCommand) + commands.RegisterLegacy(newEventsCommand) + commands.RegisterLegacy(newInspectCommand) +} + +// newSystemCommand returns a cobra command for `system` subcommands +func newSystemCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "system", Short: "Manage Docker", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, } cmd.AddCommand( - NewEventsCommand(dockerCli), - NewInfoCommand(dockerCli), - newDiskUsageCommand(dockerCli), - newPruneCommand(dockerCli), - newDialStdioCommand(dockerCli), + newEventsCommand(dockerCLI), + newInfoCommand(dockerCLI), + newDiskUsageCommand(dockerCLI), + newPruneCommand(dockerCLI), + newDialStdioCommand(dockerCLI), ) return cmd diff --git a/cli/command/system/completion.go b/cli/command/system/completion.go index 0b96e1157eaa..058dd4b29a1f 100644 --- a/cli/command/system/completion.go +++ b/cli/command/system/completion.go @@ -1,14 +1,14 @@ package system import ( + "fmt" "strings" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/volume" + "github.com/docker/cli/cli/command/idresolver" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -160,6 +160,20 @@ func validEventNames() []string { return names } +// configNames contacts the API to get a list of config names. +// In case of an error, an empty list is returned. +func configNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { + res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{}) + if err != nil { + return []string{} + } + names := make([]string, 0, len(res.Items)) + for _, v := range res.Items { + names = append(names, v.Spec.Name) + } + return names +} + // containerNames contacts the API to get names and optionally IDs of containers. // In case of an error, an empty list is returned. func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string { @@ -173,22 +187,22 @@ func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, // daemonNames contacts the API to get name and ID of the current docker daemon. // In case of an error, an empty list is returned. func daemonNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { - info, err := dockerCLI.Client().Info(cmd.Context()) + res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{}) if err != nil { return []string{} } - return []string{info.Name, info.ID} + return []string{res.Info.Name, res.Info.ID} } // imageNames contacts the API to get a list of image names. // In case of an error, an empty list is returned. func imageNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { - list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{}) + res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{}) if err != nil { return []string{} } - names := make([]string, 0, len(list)) - for _, img := range list { + names := make([]string, 0, len(res.Items)) + for _, img := range res.Items { names = append(names, img.RepoTags...) } return names @@ -197,12 +211,12 @@ func imageNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []st // networkNames contacts the API to get a list of network names. // In case of an error, an empty list is returned. func networkNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { - list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{}) + res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{}) if err != nil { return []string{} } - names := make([]string, 0, len(list)) - for _, nw := range list { + names := make([]string, 0, len(res.Items)) + for _, nw := range res.Items { names = append(names, nw.Name) } return names @@ -211,27 +225,129 @@ func networkNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) [] // nodeNames contacts the API to get a list of node names. // In case of an error, an empty list is returned. func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { - list, err := dockerCLI.Client().NodeList(cmd.Context(), swarm.NodeListOptions{}) + res, err := dockerCLI.Client().NodeList(cmd.Context(), client.NodeListOptions{}) if err != nil { return []string{} } - names := make([]string, 0, len(list)) - for _, node := range list { + names := make([]string, 0, len(res.Items)) + for _, node := range res.Items { names = append(names, node.Description.Hostname) } return names } +// pluginNames contacts the API to get a list of plugin names. +// In case of an error, an empty list is returned. +func pluginNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { + res, err := dockerCLI.Client().PluginList(cmd.Context(), client.PluginListOptions{}) + if err != nil { + return []string{} + } + names := make([]string, 0, len(res.Items)) + for _, v := range res.Items { + names = append(names, v.Name) + } + return names +} + +// secretNames contacts the API to get a list of secret names. +// In case of an error, an empty list is returned. +func secretNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { + res, err := dockerCLI.Client().SecretList(cmd.Context(), client.SecretListOptions{}) + if err != nil { + return []string{} + } + names := make([]string, 0, len(res.Items)) + for _, v := range res.Items { + names = append(names, v.Spec.Name) + } + return names +} + +// serviceNames contacts the API to get a list of service names. +// In case of an error, an empty list is returned. +func serviceNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { + res, err := dockerCLI.Client().ServiceList(cmd.Context(), client.ServiceListOptions{}) + if err != nil { + return []string{} + } + names := make([]string, 0, len(res.Items)) + for _, v := range res.Items { + names = append(names, v.Spec.Name) + } + return names +} + +// taskNames contacts the API to get a list of service names. +// In case of an error, an empty list is returned. +func taskNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { + res, err := dockerCLI.Client().TaskList(cmd.Context(), client.TaskListOptions{}) + if err != nil || len(res.Items) == 0 { + return []string{} + } + + resolver := idresolver.New(dockerCLI.Client(), false) + names := make([]string, 0, len(res.Items)) + for _, task := range res.Items { + serviceName, err := resolver.Resolve(cmd.Context(), swarm.Service{}, task.ServiceID) + if err != nil { + continue + } + if task.Slot != 0 { + names = append(names, fmt.Sprintf("%v.%v", serviceName, task.Slot)) + } else { + names = append(names, fmt.Sprintf("%v.%v", serviceName, task.NodeID)) + } + } + return names +} + // volumeNames contacts the API to get a list of volume names. // In case of an error, an empty list is returned. func volumeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string { - list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{}) + res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{}) if err != nil { return []string{} } - names := make([]string, 0, len(list.Volumes)) - for _, v := range list.Volumes { + names := make([]string, 0, len(res.Items)) + for _, v := range res.Items { names = append(names, v.Name) } return names } + +// completeObjectNames completes names of objects based on the "--type" flag +// +// TODO(thaJeztah): completion functions in this package don't remove names that have already been completed +// this causes completion to continue even if a given name was already completed. +func completeObjectNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if f := cmd.Flags().Lookup("type"); f != nil && f.Changed { + switch f.Value.String() { + case typeConfig: + return configNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeContainer: + return containerNames(dockerCLI, cmd, args, toComplete), cobra.ShellCompDirectiveNoFileComp + case typeImage: + return imageNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeNetwork: + return networkNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeNode: + return nodeNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typePlugin: + return pluginNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeSecret: + return secretNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeService: + return serviceNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeTask: + return taskNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + case typeVolume: + return volumeNames(dockerCLI, cmd), cobra.ShellCompDirectiveNoFileComp + default: + return nil, cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/cli/command/system/completion_test.go b/cli/command/system/completion_test.go index 3e703a6a49b2..0095ce598e62 100644 --- a/cli/command/system/completion_test.go +++ b/cli/command/system/completion_test.go @@ -8,12 +8,13 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/api/types/volume" + "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/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "github.com/spf13/cobra" "gotest.tools/v3/assert" ) @@ -26,7 +27,7 @@ func TestCompleteEventFilter(t *testing.T) { }{ { client: &fakeClient{ - containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) { + containerListFunc: func(_ context.Context, _ client.ContainerListOptions) ([]container.Summary, error) { return []container.Summary{ *builders.Container("c1"), *builders.Container("c2"), @@ -38,7 +39,7 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) { + containerListFunc: func(_ context.Context, _ client.ContainerListOptions) ([]container.Summary, error) { return nil, errors.New("API error") }, }, @@ -47,10 +48,12 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - infoFunc: func(ctx context.Context) (system.Info, error) { - return system.Info{ - ID: "daemon-id", - Name: "daemon-name", + infoFunc: func(_ context.Context, _ client.InfoOptions) (client.SystemInfoResult, error) { + return client.SystemInfoResult{ + Info: system.Info{ + ID: "daemon-id", + Name: "daemon-name", + }, }, nil }, }, @@ -59,8 +62,8 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - infoFunc: func(ctx context.Context) (system.Info, error) { - return system.Info{}, errors.New("API error") + infoFunc: func(_ context.Context, _ client.InfoOptions) (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, errors.New("API error") }, }, toComplete: "daemon=", @@ -68,10 +71,12 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - imageListFunc: func(_ context.Context, _ image.ListOptions) ([]image.Summary, error) { - return []image.Summary{ - {RepoTags: []string{"img:1"}}, - {RepoTags: []string{"img:2"}}, + imageListFunc: func(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) { + return client.ImageListResult{ + Items: []image.Summary{ + {RepoTags: []string{"img:1"}}, + {RepoTags: []string{"img:2"}}, + }, }, nil }, }, @@ -80,8 +85,8 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - imageListFunc: func(_ context.Context, _ image.ListOptions) ([]image.Summary, error) { - return []image.Summary{}, errors.New("API error") + imageListFunc: func(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) { + return client.ImageListResult{}, errors.New("API error") }, }, toComplete: "image=", @@ -89,10 +94,12 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) { - return []network.Summary{ - *builders.NetworkResource(builders.NetworkResourceName("nw1")), - *builders.NetworkResource(builders.NetworkResourceName("nw2")), + networkListFunc: func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { + return client.NetworkListResult{ + Items: []network.Summary{ + *builders.NetworkResource(builders.NetworkResourceName("nw1")), + *builders.NetworkResource(builders.NetworkResourceName("nw2")), + }, }, nil }, }, @@ -101,8 +108,8 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) { - return nil, errors.New("API error") + networkListFunc: func(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) { + return client.NetworkListResult{}, errors.New("API error") }, }, toComplete: "network=", @@ -110,9 +117,11 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - nodeListFunc: func(_ context.Context, _ swarm.NodeListOptions) ([]swarm.Node, error) { - return []swarm.Node{ - *builders.Node(builders.Hostname("n1")), + nodeListFunc: func(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) { + return client.NodeListResult{ + Items: []swarm.Node{ + *builders.Node(builders.Hostname("n1")), + }, }, nil }, }, @@ -121,8 +130,8 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - nodeListFunc: func(_ context.Context, _ swarm.NodeListOptions) ([]swarm.Node, error) { - return []swarm.Node{}, errors.New("API error") + nodeListFunc: func(ctx context.Context, options client.NodeListOptions) (client.NodeListResult, error) { + return client.NodeListResult{}, errors.New("API error") }, }, toComplete: "node=", @@ -130,9 +139,9 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - volumeListFunc: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ builders.Volume(builders.VolumeName("v1")), builders.Volume(builders.VolumeName("v2")), }, @@ -144,8 +153,8 @@ func TestCompleteEventFilter(t *testing.T) { }, { client: &fakeClient{ - volumeListFunc: func(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) { - return volume.ListResponse{}, errors.New("API error") + volumeListFunc: func(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{}, errors.New("API error") }, }, toComplete: "volume=", @@ -156,7 +165,7 @@ func TestCompleteEventFilter(t *testing.T) { for _, tc := range tests { cli := test.NewFakeCli(tc.client) - completions, directive := completeEventFilters(cli)(NewEventsCommand(cli), nil, tc.toComplete) + completions, directive := completeEventFilters(cli)(newEventsCommand(cli), nil, tc.toComplete) assert.DeepEqual(t, completions, tc.expected) assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp, fmt.Sprintf("wrong directive in completion for '%s'", tc.toComplete)) diff --git a/cli/command/system/df.go b/cli/command/system/df.go index f6a98a036776..2c689da0206d 100644 --- a/cli/command/system/df.go +++ b/cli/command/system/df.go @@ -5,10 +5,9 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -18,7 +17,7 @@ type diskUsageOptions struct { } // newDiskUsageCommand creates a new cobra.Command for `docker df` -func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command { +func newDiskUsageCommand(dockerCLI command.Cli) *cobra.Command { var opts diskUsageOptions cmd := &cobra.Command{ @@ -26,10 +25,11 @@ func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command { Short: "Show docker disk usage", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runDiskUsage(cmd.Context(), dockerCli, opts) + return runDiskUsage(cmd.Context(), dockerCLI, opts) }, - Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.25"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -42,7 +42,9 @@ func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command { func runDiskUsage(ctx context.Context, dockerCli command.Cli, opts diskUsageOptions) error { // TODO expose types.DiskUsageOptions.Types as flag on the command-line and/or as separate commands (docker container df / docker container usage) - du, err := dockerCli.Client().DiskUsage(ctx, types.DiskUsageOptions{}) + du, err := dockerCli.Client().DiskUsage(ctx, client.DiskUsageOptions{ + Verbose: opts.verbose, + }) if err != nil { return err } @@ -52,25 +54,16 @@ func runDiskUsage(ctx context.Context, dockerCli command.Cli, opts diskUsageOpti format = formatter.TableFormatKey } - var bsz int64 - for _, bc := range du.BuildCache { - if !bc.Shared { - bsz += bc.Size - } - } - duCtx := formatter.DiskUsageContext{ Context: formatter.Context{ Output: dockerCli.Out(), Format: formatter.NewDiskUsageFormat(format, opts.verbose), }, - LayersSize: du.LayersSize, - BuilderSize: bsz, - BuildCache: du.BuildCache, - Images: du.Images, - Containers: du.Containers, - Volumes: du.Volumes, - Verbose: opts.verbose, + Verbose: opts.verbose, + ImageDiskUsage: du.Images, + BuildCacheDiskUsage: du.BuildCache, + ContainerDiskUsage: du.Containers, + VolumeDiskUsage: du.Volumes, } return duCtx.Write() diff --git a/cli/command/system/dial_stdio.go b/cli/command/system/dial_stdio.go index c8ccd181c794..62206af0edd8 100644 --- a/cli/command/system/dial_stdio.go +++ b/cli/command/system/dial_stdio.go @@ -2,28 +2,29 @@ package system import ( "context" + "errors" + "fmt" "io" "os" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // newDialStdioCommand creates a new cobra.Command for `docker system dial-stdio` -func newDialStdioCommand(dockerCli command.Cli) *cobra.Command { +func newDialStdioCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "dial-stdio", Short: "Proxy the stdio stream to the daemon connection. Should not be invoked manually.", Args: cli.NoArgs, Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { - return runDialStdio(cmd.Context(), dockerCli) + return runDialStdio(cmd.Context(), dockerCLI) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } return cmd } @@ -35,7 +36,7 @@ func runDialStdio(ctx context.Context, dockerCli command.Cli) error { dialer := dockerCli.Client().Dialer() conn, err := dialer(ctx) if err != nil { - return errors.Wrap(err, "failed to open the raw stream connection") + return fmt.Errorf("failed to open the raw stream connection: %w", err) } defer conn.Close() @@ -81,7 +82,7 @@ func copier(to halfWriteCloser, from halfReadCloser, debugDescription string) er } }() if _, err := io.Copy(to, from); err != nil { - return errors.Wrapf(err, "error while Copy (%s)", debugDescription) + return fmt.Errorf("error while Copy (%s): %w", debugDescription, err) } return nil } diff --git a/cli/command/system/events.go b/cli/command/system/events.go index d83d36351e33..03f465fabd2f 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -11,12 +11,12 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/docker/cli/templates" - "github.com/docker/docker/api/types/events" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -27,8 +27,8 @@ type eventsOptions struct { format string } -// NewEventsCommand creates a new cobra.Command for `docker events` -func NewEventsCommand(dockerCli command.Cli) *cobra.Command { +// newEventsCommand creates a new cobra.Command for `docker events` +func newEventsCommand(dockerCLI command.Cli) *cobra.Command { options := eventsOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -36,12 +36,13 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { Short: "Get real time events from the server", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(cmd.Context(), dockerCli, &options) + return runEvents(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker system events, docker events", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -50,12 +51,12 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now. - _ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCli)) + _ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCLI)) return cmd } -func runEvents(ctx context.Context, dockerCli command.Cli, options *eventsOptions) error { +func runEvents(ctx context.Context, dockerCLI command.Cli, options *eventsOptions) error { tmpl, err := makeTemplate(options.format) if err != nil { return cli.StatusError{ @@ -64,22 +65,22 @@ func runEvents(ctx context.Context, dockerCli command.Cli, options *eventsOption } } ctx, cancel := context.WithCancel(ctx) - evts, errs := dockerCli.Client().Events(ctx, events.ListOptions{ + eventRes := dockerCLI.Client().Events(ctx, client.EventsListOptions{ Since: options.since, Until: options.until, Filters: options.filter.Value(), }) defer cancel() - out := dockerCli.Out() + out := dockerCLI.Out() for { select { - case event := <-evts: + case event := <-eventRes.Messages: if err := handleEvent(out, event, tmpl); err != nil { return err } - case err := <-errs: + case err := <-eventRes.Err: if err == io.EOF { return nil } @@ -92,8 +93,11 @@ func handleEvent(out io.Writer, event events.Message, tmpl *template.Template) e if tmpl == nil { return prettyPrintEvent(out, event) } - - return formatEvent(out, event, tmpl) + if err := tmpl.Execute(out, event); err != nil { + return err + } + _, _ = out.Write([]byte{'\n'}) + return nil } func makeTemplate(format string) (*template.Template, error) { @@ -144,8 +148,3 @@ func prettyPrintEvent(out io.Writer, event events.Message) error { _, _ = fmt.Fprint(out, "\n") return nil } - -func formatEvent(out io.Writer, event events.Message, tmpl *template.Template) error { - defer out.Write([]byte{'\n'}) - return tmpl.Execute(out, event) -} diff --git a/cli/command/system/events_test.go b/cli/command/system/events_test.go index 847605a44860..cda6e54ecc60 100644 --- a/cli/command/system/events_test.go +++ b/cli/command/system/events_test.go @@ -9,7 +9,8 @@ import ( "time" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/events" + "github.com/moby/moby/api/types/events" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -18,9 +19,6 @@ func TestEventsFormat(t *testing.T) { var evts []events.Message //nolint:prealloc for i, action := range []events.Action{events.ActionCreate, events.ActionStart, events.ActionAttach, events.ActionDie} { evts = append(evts, events.Message{ - Status: string(action), - ID: "abc123", - From: "ubuntu:latest", Type: events.ContainerEventType, Action: action, Actor: events.Actor{ @@ -59,7 +57,7 @@ func TestEventsFormat(t *testing.T) { // Set to UTC timezone as timestamps in output are // printed in the current timezone t.Setenv("TZ", "UTC") - cli := test.NewFakeCli(&fakeClient{eventsFn: func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error) { + fakeCLI := test.NewFakeCli(&fakeClient{eventsFn: func(context.Context, client.EventsListOptions) (<-chan events.Message, <-chan error) { messages := make(chan events.Message) errs := make(chan error, 1) go func() { @@ -70,14 +68,14 @@ func TestEventsFormat(t *testing.T) { }() return messages, errs }}) - cmd := NewEventsCommand(cli) + cmd := newEventsCommand(fakeCLI) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) assert.Check(t, cmd.Execute()) - out := cli.OutBuffer().String() + out := fakeCLI.OutBuffer().String() assert.Check(t, golden.String(out, fmt.Sprintf("docker-events-%s.golden", strings.ReplaceAll(tc.name, " ", "-")))) - cli.OutBuffer().Reset() + fakeCLI.OutBuffer().Reset() }) } } diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 7f82e4e5d85f..2dba5351131f 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package system @@ -14,17 +14,17 @@ import ( "github.com/docker/cli/cli" pluginmanager "github.com/docker/cli/cli-plugins/manager" "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/debug" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/internal/lazyregexp" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/templates" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" - "github.com/docker/docker/registry" "github.com/docker/go-units" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" + "github.com/moby/moby/client/pkg/security" "github.com/spf13/cobra" ) @@ -59,8 +59,8 @@ func (i *dockerInfo) clientPlatform() string { return "" } -// NewInfoCommand creates a new cobra.Command for `docker info` -func NewInfoCommand(dockerCli command.Cli) *cobra.Command { +// newInfoCommand creates a new cobra.Command for `docker info` +func newInfoCommand(dockerCLI command.Cli) *cobra.Command { var opts infoOptions cmd := &cobra.Command{ @@ -68,13 +68,14 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command { Short: "Display system-wide information", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runInfo(cmd.Context(), cmd, dockerCli, &opts) + return runInfo(cmd.Context(), cmd, dockerCLI, &opts) }, Annotations: map[string]string{ "category-top": "12", "aliases": "docker system info, docker info", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -116,7 +117,7 @@ func runInfo(ctx context.Context, cmd *cobra.Command, dockerCli command.Cli, opt // if a connection error occurs, it will be returned as an error. // other errors are appended to the info.ServerErrors field. func addServerInfo(ctx context.Context, dockerCli command.Cli, format string, info *dockerInfo) error { - dinfo, err := dockerCli.Client().Info(ctx) + res, err := dockerCli.Client().Info(ctx, client.InfoOptions{}) if err != nil { // if no format is provided and we have an error, don't print the server info if format == "" { @@ -136,7 +137,7 @@ func addServerInfo(ctx context.Context, dockerCli command.Cli, format string, in } // only assign the server info if we have no error - info.Info = &dinfo + info.Info = &res.Info return nil } @@ -162,7 +163,7 @@ func needsServerInfo(template string, info dockerInfo) bool { } // A template is provided and has at least one field set. - tmpl, err := templates.NewParse("", template) + tmpl, err := templates.Parse(template) if err != nil { // ignore parsing errors here, and let regular code handle them return true @@ -201,9 +202,7 @@ func prettyPrintInfo(streams command.Streams, info dockerInfo) error { fprintln(streams.Out()) fprintln(streams.Out(), "Server:") if info.Info != nil { - for _, err := range prettyPrintServerInfo(streams, &info) { - info.ServerErrors = append(info.ServerErrors, err.Error()) - } + prettyPrintServerInfo(streams, &info) } for _, err := range info.ServerErrors { fprintln(streams.Err(), "ERROR:", err) @@ -239,8 +238,7 @@ func prettyPrintClientInfo(streams command.Streams, info clientInfo) { } //nolint:gocyclo -func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { - var errs []error +func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) { output := streams.Out() fprintln(output, " Containers:", info.Containers) @@ -305,17 +303,14 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { fprintln(output, " containerd version:", info.ContainerdCommit.ID) fprintln(output, " runc version:", info.RuncCommit.ID) fprintln(output, " init version:", info.InitCommit.ID) - if len(info.SecurityOptions) != 0 { - if kvs, err := system.DecodeSecurityOptions(info.SecurityOptions); err != nil { - errs = append(errs, err) - } else { - fprintln(output, " Security Options:") - for _, so := range kvs { - fprintln(output, " "+so.Name) - for _, o := range so.Options { - if o.Key == "profile" { - fprintln(output, " Profile:", o.Value) - } + secopts := security.DecodeOptions(info.SecurityOptions) + if len(secopts) != 0 { + fprintln(output, " Security Options:") + for _, so := range secopts { + fprintln(output, " "+so.Name) + for _, o := range so.Options { + if o.Key == "profile" { + fprintln(output, " Profile:", o.Value) } } } @@ -371,9 +366,8 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { } } - for _, registryConfig := range info.RegistryConfig.InsecureRegistryCIDRs { - mask, _ := registryConfig.Mask.Size() - fprintf(output, " %s/%d\n", registryConfig.IP.String(), mask) + for _, cidr := range info.RegistryConfig.InsecureRegistryCIDRs { + fprintf(output, " %s\n", cidr) } } @@ -396,12 +390,17 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error { } } + if info.FirewallBackend != nil { + fprintln(output, " Firewall Backend:", info.FirewallBackend.Driver) + for _, v := range info.FirewallBackend.Info { + fprintf(output, " %s: %s\n", v[0], v[1]) + } + } + fprintln(output) for _, w := range info.Warnings { fprintln(streams.Err(), w) } - - return errs } //nolint:gocyclo @@ -421,7 +420,7 @@ func printSwarmInfo(output io.Writer, info system.Info) { var strAddrPool strings.Builder if info.Swarm.Cluster.DefaultAddrPool != nil { for _, p := range info.Swarm.Cluster.DefaultAddrPool { - strAddrPool.WriteString(p + " ") + strAddrPool.WriteString(p.String() + " ") } fprintln(output, " Default Address Pool:", strAddrPool.String()) fprintln(output, " SubnetSize:", info.Swarm.Cluster.SubnetSize) diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index d996ea23f49a..09278ee206e8 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -2,16 +2,17 @@ package system import ( "encoding/base64" - "net" + "errors" + "net/netip" "testing" "time" pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/internal/test" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/system" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -48,7 +49,6 @@ var sampleInfoNoSwarm = system.Info{ }, MemoryLimit: true, SwapLimit: true, - KernelMemory: true, CPUCfsPeriod: true, CPUCfsQuota: true, CPUShares: true, @@ -69,16 +69,12 @@ var sampleInfoNoSwarm = system.Info{ Architecture: "x86_64", IndexServerAddress: "https://index.docker.io/v1/", RegistryConfig: ®istrytypes.ServiceConfig{ - InsecureRegistryCIDRs: []*registrytypes.NetIPNet{ - { - IP: net.ParseIP("127.0.0.0"), - Mask: net.IPv4Mask(255, 0, 0, 0), - }, + InsecureRegistryCIDRs: []netip.Prefix{ + netip.MustParsePrefix("127.0.0.0/8"), }, IndexConfigs: map[string]*registrytypes.IndexInfo{ "docker.io": { Name: "docker.io", - Mirrors: nil, Secure: true, Official: true, }, @@ -109,24 +105,27 @@ var sampleInfoNoSwarm = system.Info{ Isolation: "", InitBinary: "docker-init", ContainerdCommit: system.Commit{ - ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", - Expected: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", + ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", }, RuncCommit: system.Commit{ - ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", - Expected: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", + ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", }, InitCommit: system.Commit{ - ID: "949e6fa", - Expected: "949e6fa", + ID: "949e6fa", }, SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"}, DefaultAddressPools: []system.NetworkAddressPool{ { - Base: "10.123.0.0/16", + Base: netip.MustParsePrefix("10.123.0.0/16"), Size: 24, }, }, + FirewallBackend: &system.FirewallInfo{ + Driver: "nftables+firewalld", + Info: [][2]string{ + {"ReloadedAt", "2025-07-16T16:59:14Z"}, + }, + }, CDISpecDirs: []string{"/etc/cdi", "/var/run/cdi"}, } @@ -221,7 +220,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{ { Name: "badplugin", Path: "/path/to/docker-badplugin", - Err: pluginmanager.NewPluginError("something wrong"), + Err: errors.New("something wrong"), }, } @@ -232,7 +231,6 @@ func TestPrettyPrintInfo(t *testing.T) { infoWithWarningsLinux := sampleInfoNoSwarm infoWithWarningsLinux.MemoryLimit = false infoWithWarningsLinux.SwapLimit = false - infoWithWarningsLinux.KernelMemory = false infoWithWarningsLinux.OomKillDisable = false infoWithWarningsLinux.CPUCfsQuota = false infoWithWarningsLinux.CPUCfsPeriod = false diff --git a/cli/command/system/inspect.go b/cli/command/system/inspect.go index 0afe65e41108..8bd191e2813d 100644 --- a/cli/command/system/inspect.go +++ b/cli/command/system/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package system @@ -9,19 +9,15 @@ import ( "fmt" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) type objectType = string @@ -59,8 +55,8 @@ type inspectOptions struct { ids []string } -// NewInspectCommand creates a new cobra.Command for `docker inspect` -func NewInspectCommand(dockerCli command.Cli) *cobra.Command { +// newInspectCommand creates a new cobra.Command for `docker inspect` +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -72,10 +68,11 @@ func NewInspectCommand(dockerCli command.Cli) *cobra.Command { if cmd.Flags().Changed("type") && opts.objectType == "" { return fmt.Errorf(`type is empty: must be one of "%s"`, strings.Join(allTypes, `", "`)) } - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, // TODO(thaJeztah): should we consider adding completion for common object-types? (images, containers?) - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: completeObjectNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -84,12 +81,7 @@ func NewInspectCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container") _ = cmd.RegisterFlagCompletionFunc("type", completion.FromList(allTypes...)) - flags.VisitAll(func(flag *pflag.Flag) { - // Set a default completion function if none was set. We don't look - // up if it does already have one set, because Cobra does this for - // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) - }) + return cmd } @@ -100,14 +92,18 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) typePlugin, typeSecret, typeService, typeTask, typeVolume: elementSearcher = inspectAll(ctx, dockerCli, opts.size, opts.objectType) default: - return errors.Errorf(`unknown type: %q: must be one of "%s"`, opts.objectType, strings.Join(allTypes, `", "`)) + return fmt.Errorf(`unknown type: %q: must be one of "%s"`, opts.objectType, strings.Join(allTypes, `", "`)) } return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher) } func inspectContainers(ctx context.Context, dockerCli command.Cli, getSize bool) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().ContainerInspectWithRaw(ctx, ref, getSize) + res, err := dockerCli.Client().ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: getSize}) + if err != nil { + return nil, nil, err + } + return res.Container, res.Raw, err } } @@ -124,50 +120,58 @@ func inspectImages(ctx context.Context, dockerCli command.Cli) inspect.GetRefFun func inspectNetwork(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, network.InspectOptions{}) + res, err := dockerCli.Client().NetworkInspect(ctx, ref, client.NetworkInspectOptions{}) + return res.Network, res.Raw, err } } func inspectNode(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().NodeInspectWithRaw(ctx, ref) + res, err := dockerCli.Client().NodeInspect(ctx, ref, client.NodeInspectOptions{}) + return res.Node, res.Raw, err } } func inspectService(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { // Service inspect shows defaults values in empty fields. - return dockerCli.Client().ServiceInspectWithRaw(ctx, ref, swarm.ServiceInspectOptions{InsertDefaults: true}) + res, err := dockerCli.Client().ServiceInspect(ctx, ref, client.ServiceInspectOptions{InsertDefaults: true}) + return res.Service, res.Raw, err } } func inspectTasks(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().TaskInspectWithRaw(ctx, ref) + res, err := dockerCli.Client().TaskInspect(ctx, ref, client.TaskInspectOptions{}) + return res.Task, res.Raw, err } } func inspectVolume(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().VolumeInspectWithRaw(ctx, ref) + res, err := dockerCli.Client().VolumeInspect(ctx, ref, client.VolumeInspectOptions{}) + return res.Volume, res.Raw, err } } func inspectPlugin(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().PluginInspectWithRaw(ctx, ref) + res, err := dockerCli.Client().PluginInspect(ctx, ref, client.PluginInspectOptions{}) + return res.Plugin, res.Raw, err } } func inspectSecret(ctx context.Context, dockerCli command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCli.Client().SecretInspectWithRaw(ctx, ref) + res, err := dockerCli.Client().SecretInspect(ctx, ref, client.SecretInspectOptions{}) + return res.Secret, res.Raw, err } } func inspectConfig(ctx context.Context, dockerCLI command.Cli) inspect.GetRefFunc { return func(ref string) (any, []byte, error) { - return dockerCLI.Client().ConfigInspectWithRaw(ctx, ref) + res, err := dockerCLI.Client().ConfigInspect(ctx, ref, client.ConfigInspectOptions{}) + return res.Config, res.Raw, err } } @@ -229,12 +233,12 @@ func inspectAll(ctx context.Context, dockerCLI command.Cli, getSize bool, typeCo // isSwarmManager does an Info API call to verify that the daemon is // a swarm manager. isSwarmManager := func() bool { - info, err := dockerCLI.Client().Info(ctx) + res, err := dockerCLI.Client().Info(ctx, client.InfoOptions{}) if err != nil { _, _ = fmt.Fprintln(dockerCLI.Err(), err) return false } - return info.Swarm.ControlAvailable + return res.Info.Swarm.ControlAvailable } return func(ref string) (any, []byte, error) { @@ -274,12 +278,12 @@ func inspectAll(ctx context.Context, dockerCLI command.Cli, getSize bool, typeCo } return v, raw, err } - return nil, nil, errors.Errorf("Error: No such object: %s", ref) + return nil, nil, fmt.Errorf("error: no such object: %s", ref) } } func isErrSkippable(err error) bool { - return cerrdefs.IsNotFound(err) || + return errdefs.IsNotFound(err) || strings.Contains(err.Error(), "not supported") || strings.Contains(err.Error(), "invalid reference format") } diff --git a/cli/command/system/inspect_test.go b/cli/command/system/inspect_test.go index 56ad5fd5b588..3fbadc7f6d0f 100644 --- a/cli/command/system/inspect_test.go +++ b/cli/command/system/inspect_test.go @@ -32,7 +32,7 @@ func TestInspectValidateFlagsAndArgs(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewInspectCommand(test.NewFakeCli(&fakeClient{})) + cmd := newInspectCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index a1bcb5f2241b..5f0896289b10 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -1,39 +1,36 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + package system import ( "bytes" "context" + "errors" "fmt" "sort" "text/template" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/builder" - "github.com/docker/cli/cli/command/completion" - "github.com/docker/cli/cli/command/container" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/command/network" - "github.com/docker/cli/cli/command/volume" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/versions" "github.com/docker/go-units" "github.com/fvbommel/sortorder" - "github.com/pkg/errors" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool - pruneVolumes bool - pruneBuildCache bool - filter opts.FilterOpt + force bool + all bool + pruneVolumes bool + filter opts.FilterOpt } // newPruneCommand creates a new cobra.Command for `docker prune` -func newPruneCommand(dockerCli command.Cli) *cobra.Command { +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -41,11 +38,11 @@ func newPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused data", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - options.pruneBuildCache = versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") - return runPrune(cmd.Context(), dockerCli, options) + return runPrune(cmd.Context(), dockerCLI, options) }, - Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.25"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -72,35 +69,44 @@ const confirmationTemplate = `WARNING! This will remove: Are you sure you want to continue?` func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) error { - // TODO version this once "until" filter is supported for volumes - if options.pruneVolumes && options.filter.Value().Contains("until") { - return errors.New(`ERROR: The "until" filter is not supported with "--volumes"`) + // prune requires either force, or a user to confirm after prompting. + confirmed := options.force + + // Validate the given options for each pruner and construct a confirmation-message. + confirmationMessage, err := dryRun(ctx, dockerCli, options) + if err != nil { + return err } - if !options.force { - r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)) + if !confirmed { + var err error + confirmed, err = prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage) if err != nil { return err } - if !r { + if !confirmed { return cancelledErr{errors.New("system prune has been cancelled")} } } - pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){ - container.RunPrune, - network.RunPrune, - } - if options.pruneVolumes { - pruneFuncs = append(pruneFuncs, volume.RunPrune) - } - pruneFuncs = append(pruneFuncs, image.RunPrune) - if options.pruneBuildCache { - pruneFuncs = append(pruneFuncs, builder.CachePrune) - } var spaceReclaimed uint64 - for _, pruneFn := range pruneFuncs { - spc, output, err := pruneFn(ctx, dockerCli, options.all, options.filter) - if err != nil { + for contentType, pruneFn := range pruner.List() { + switch contentType { + case pruner.TypeVolume: + if !options.pruneVolumes { + continue + } + case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache: + // no special handling; keeping the "exhaustive" linter happy. + default: + // other pruners; no special handling; keeping the "exhaustive" linter happy. + } + + spc, output, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{ + Confirmed: confirmed, + All: options.all, + Filter: options.filter, + }) + if err != nil && !errdefs.IsNotImplemented(err) { return err } spaceReclaimed += spc @@ -118,37 +124,51 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// confirmationMessage constructs a confirmation message that depends on the cli options. -func confirmationMessage(dockerCli command.Cli, options pruneOptions) string { - t := template.Must(template.New("confirmation message").Parse(confirmationTemplate)) - - warnings := []string{ - "all stopped containers", - "all networks not used by at least one container", - } - if options.pruneVolumes { - warnings = append(warnings, "all anonymous volumes not used by at least one container") - } - if options.all { - warnings = append(warnings, "all images without at least one container associated to them") - } else { - warnings = append(warnings, "all dangling images") - } - if options.pruneBuildCache { - if options.all { - warnings = append(warnings, "all build cache") - } else { - warnings = append(warnings, "unused build cache") +// dryRun validates the given options for each prune-function and constructs +// a confirmation message that depends on the cli options. +func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (string, error) { + var ( + errs []error + warnings []string + ) + for contentType, pruneFn := range pruner.List() { + switch contentType { + case pruner.TypeVolume: + if !options.pruneVolumes { + continue + } + case pruner.TypeContainer, pruner.TypeNetwork, pruner.TypeImage, pruner.TypeBuildCache: + // no special handling; keeping the "exhaustive" linter happy. + default: + // other pruners; no special handling; keeping the "exhaustive" linter happy. + } + // Always run with "[pruner.PruneOptions.Confirmed] = false" + // to perform validation of the given options and produce + // a confirmation message for the pruner. + _, confirmMsg, err := pruneFn(ctx, dockerCli, pruner.PruneOptions{ + All: options.all, + Filter: options.filter, + }) + // A "canceled" error is expected in dry-run mode; any other error + // must be returned as a "fatal" error. + if err != nil && !errdefs.IsCanceled(err) && !errdefs.IsNotImplemented(err) { + errs = append(errs, err) + } + if confirmMsg != "" { + warnings = append(warnings, confirmMsg) } } + if len(errs) > 0 { + return "", errors.Join(errs...) + } var filters []string pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) - if pruneFilters.Len() > 0 { + if len(pruneFilters) > 0 { // TODO remove fixed list of filters, and print all filters instead, // because the list of filters that is supported by the engine may evolve over time. for _, name := range []string{"label", "label!", "until"} { - for _, v := range pruneFilters.Get(name) { + for v := range pruneFilters[name] { filters = append(filters, name+"="+v) } } @@ -158,6 +178,7 @@ func confirmationMessage(dockerCli command.Cli, options pruneOptions) string { } var buffer bytes.Buffer - t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters}) - return buffer.String() + t := template.Must(template.New("confirmation message").Parse(confirmationTemplate)) + _ = t.Execute(&buffer, map[string][]string{"warnings": warnings, "filters": filters}) + return buffer.String(), nil } diff --git a/cli/command/system/prune_test.go b/cli/command/system/prune_test.go index 5544c69ac4c2..9bf66c0337b1 100644 --- a/cli/command/system/prune_test.go +++ b/cli/command/system/prune_test.go @@ -8,31 +8,20 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" -) -func TestPrunePromptPre131DoesNotIncludeBuildCache(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{version: "1.30"}) - cmd := newPruneCommand(cli) - cmd.SetArgs([]string{}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), "system prune has been cancelled") - expected := `WARNING! This will remove: - - all stopped containers - - all networks not used by at least one container - - all dangling images - -Are you sure you want to continue? [y/N] ` - assert.Check(t, is.Equal(expected, cli.OutBuffer().String())) -} + // Make sure pruners are registered for tests (they're included automatically when building). + _ "github.com/docker/cli/cli/command/builder" + _ "github.com/docker/cli/cli/command/container" + _ "github.com/docker/cli/cli/command/image" + _ "github.com/docker/cli/cli/command/network" + _ "github.com/docker/cli/cli/command/volume" +) func TestPrunePromptFilters(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{version: "1.31"}) + cli := test.NewFakeCli(&fakeClient{version: "1.51"}) cli.SetConfigFile(&configfile.ConfigFile{ PruneFilters: []string{"label!=never=remove-me", "label=remove=me"}, }) @@ -65,11 +54,11 @@ func TestSystemPrunePromptTermination(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { - return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called") + containerPruneFunc: func(context.Context, client.ContainerPruneOptions) (client.ContainerPruneResult, error) { + return client.ContainerPruneResult{}, errors.New("fakeClient containerPruneFunc should not be called") }, - networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) { - return network.PruneReport{}, errors.New("fakeClient networkPruneFunc should not be called") + networkPruneFunc: func(context.Context, client.NetworkPruneOptions) (client.NetworkPruneResult, error) { + return client.NetworkPruneResult{}, errors.New("fakeClient networkPruneFunc should not be called") }, }) diff --git a/cli/command/system/pruner/pruner.go b/cli/command/system/pruner/pruner.go new file mode 100644 index 000000000000..90011eb85679 --- /dev/null +++ b/cli/command/system/pruner/pruner.go @@ -0,0 +1,140 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +// Package pruner registers "prune" functions to be included as part of +// "docker system prune". +package pruner + +import ( + "context" + "errors" + "fmt" + "iter" + "maps" + "slices" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/opts" +) + +// ContentType is an identifier for content that can be pruned. +type ContentType string + +// Pre-defined content-types to prune. Additional types can be registered, +// and will be pruned after the list of pre-defined types. +const ( + TypeContainer ContentType = "container" + TypeNetwork ContentType = "network" + TypeImage ContentType = "image" + TypeVolume ContentType = "volume" + TypeBuildCache ContentType = "buildcache" +) + +// pruneOrder is the order in which ContentType must be pruned. The order +// in which pruning happens is important to make sure that resources are +// released before pruning (e.g., a "container" can use a "network" and +// "volume", so containers must be pruned before networks and volumes). +var pruneOrder = []ContentType{ + TypeContainer, + TypeNetwork, + TypeVolume, + TypeImage, + TypeBuildCache, +} + +// PruneFunc is the signature for prune-functions. The action performed +// depends on the [PruneOptions.Confirmed] field. +// +// - If [PruneOptions.Confirmed] is "false", the PruneFunc must be run +// in "dry-run" mode and return a short description of what content +// will be pruned (for example, "all stopped containers") instead of +// executing the prune. This summary is presented to the user as a +// confirmation message. It may return a [ErrCancelled] to indicate +// the operation was canceled or a [ErrNotImplemented] if the prune +// function is not implemented for the daemon's API version. Any +// other error is considered a validation error for the given options +// (such as a filter that is not supported). +// - If [PruneOptions.Confirmed] is "true", the PruneFunc must execute +// the prune with the given options. +// +// After a successful prune the PruneFunc must return details about the +// content pruned; +// +// - spaceReclaimed is the amount of data removed (in bytes), if any. +// - details is arbitrary information about the content pruned to be +// presented to the user. +// +// [ErrCancelled]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrCancelled +// [ErrNotImplemented]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/errdefs#ErrNotImplemented +type PruneFunc func(ctx context.Context, dockerCLI command.Cli, pruneOpts PruneOptions) (spaceReclaimed uint64, details string, _ error) + +type PruneOptions struct { + // Confirmed indicates whether pruning was confirmed (or "forced") + // by the user. If not set, the PruneFunc must be run in "dry-run" + // mode and return a short description of what content will be pruned + // (for example, "all stopped containers") instead of executing the + // prune. This summary is presented to the user as a confirmation message. + Confirmed bool + All bool // Remove all unused content not just dangling (exact meaning differs per content-type). + Filter opts.FilterOpt +} + +// registered holds a map of PruneFunc functions registered through [Register]. +// It is considered immutable after startup. +var registered map[ContentType]PruneFunc + +// Register registers a [PruneFunc] under the given name to be included in +// "docker system prune". It is designed to be called in an init function +// and is not safe for concurrent use. +// +// For example: +// +// func init() { +// // Register the prune command to run as part of "docker system prune". +// if err := prune.Register(prune.TypeImage, prunerFn); err != nil { +// panic(err) +// } +// } +func Register(name ContentType, pruneFunc PruneFunc) error { + if name == "" { + return errors.New("error registering pruner: invalid prune type: cannot be empty") + } + if pruneFunc == nil { + return errors.New("error registering pruner: prune function is nil for " + string(name)) + } + if registered == nil { + registered = make(map[ContentType]PruneFunc) + } + if _, exists := registered[name]; exists { + return fmt.Errorf("error registering pruner: content-type %s is already registered", name) + } + registered[name] = pruneFunc + return nil +} + +// List iterates over all registered pruners, starting with known pruners +// in their predefined order, followed by any others (sorted alphabetically). +func List() iter.Seq2[ContentType, PruneFunc] { + all := maps.Clone(registered) + ordered := make([]ContentType, 0, len(all)) + for _, ct := range pruneOrder { + if _, ok := all[ct]; ok { + ordered = append(ordered, ct) + delete(all, ct) + } + } + // append any remaining content-types (if any) that may be registered. + if len(all) > 0 { + ordered = append(ordered, slices.Sorted(maps.Keys(all))...) + } + + return func(yield func(ContentType, PruneFunc) bool) { + for _, ct := range ordered { + if fn := registered[ct]; fn != nil { + if !yield(ct, fn) { + return + } + } + } + } +} diff --git a/cli/command/system/testdata/docker-client-version.golden b/cli/command/system/testdata/TestVersionFormat/default.golden similarity index 100% rename from cli/command/system/testdata/docker-client-version.golden rename to cli/command/system/testdata/TestVersionFormat/default.golden diff --git a/cli/command/system/testdata/TestVersionFormat/json.golden b/cli/command/system/testdata/TestVersionFormat/json.golden new file mode 100644 index 000000000000..61d885928716 --- /dev/null +++ b/cli/command/system/testdata/TestVersionFormat/json.golden @@ -0,0 +1 @@ +{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"2018-07-09T22:38:38.000000000+00:00"}} diff --git a/cli/command/system/testdata/TestVersionFormat/json_template.golden b/cli/command/system/testdata/TestVersionFormat/json_template.golden new file mode 100644 index 000000000000..61d885928716 --- /dev/null +++ b/cli/command/system/testdata/TestVersionFormat/json_template.golden @@ -0,0 +1 @@ +{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"2018-07-09T22:38:38.000000000+00:00"}} diff --git a/cli/command/system/testdata/docker-client-version.json.golden b/cli/command/system/testdata/docker-client-version.json.golden deleted file mode 100644 index 088d7dc466c9..000000000000 --- a/cli/command/system/testdata/docker-client-version.json.golden +++ /dev/null @@ -1 +0,0 @@ -{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"Version":"","ApiVersion":"","GitCommit":"","GoVersion":"","Os":"","Arch":""}} diff --git a/cli/command/system/testdata/docker-events-json-template.golden b/cli/command/system/testdata/docker-events-json-template.golden index ec5343fe682e..1a0427163cf0 100644 --- a/cli/command/system/testdata/docker-events-json-template.golden +++ b/cli/command/system/testdata/docker-events-json-template.golden @@ -1,4 +1,4 @@ -{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} -{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} -{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} -{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} +{"Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/cli/command/system/testdata/docker-events-json.golden b/cli/command/system/testdata/docker-events-json.golden index ec5343fe682e..1a0427163cf0 100644 --- a/cli/command/system/testdata/docker-events-json.golden +++ b/cli/command/system/testdata/docker-events-json.golden @@ -1,4 +1,4 @@ -{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} -{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} -{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} -{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} +{"Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/cli/command/system/testdata/docker-info-badsec-stderr.golden b/cli/command/system/testdata/docker-info-badsec-stderr.golden index 72a09117c3c2..7cd534e3ceb1 100644 --- a/cli/command/system/testdata/docker-info-badsec-stderr.golden +++ b/cli/command/system/testdata/docker-info-badsec-stderr.golden @@ -1,2 +1 @@ ERROR: a server error occurred -ERROR: invalid empty security option diff --git a/cli/command/system/testdata/docker-info-badsec.golden b/cli/command/system/testdata/docker-info-badsec.golden index ba5b99e8c99f..7828fc912e81 100644 --- a/cli/command/system/testdata/docker-info-badsec.golden +++ b/cli/command/system/testdata/docker-info-badsec.golden @@ -52,4 +52,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-badsec.json.golden b/cli/command/system/testdata/docker-info-badsec.json.golden index c20a427f208d..4e0f89bc86c1 100644 --- a/cli/command/system/testdata/docker-info-badsec.json.golden +++ b/cli/command/system/testdata/docker-info-badsec.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden index cdc36eb5dfa8..b1691f3e4b47 100644 --- a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden +++ b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-no-swarm.golden b/cli/command/system/testdata/docker-info-no-swarm.golden index ef38b4350d09..e87a5b76af06 100644 --- a/cli/command/system/testdata/docker-info-no-swarm.golden +++ b/cli/command/system/testdata/docker-info-no-swarm.golden @@ -57,4 +57,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-no-swarm.json.golden b/cli/command/system/testdata/docker-info-no-swarm.json.golden index 7fc58311e8c1..1776a608c940 100644 --- a/cli/command/system/testdata/docker-info-no-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-no-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-plugins.golden b/cli/command/system/testdata/docker-info-plugins.golden index 71fe0f858425..a8e47d71eefa 100644 --- a/cli/command/system/testdata/docker-info-plugins.golden +++ b/cli/command/system/testdata/docker-info-plugins.golden @@ -62,4 +62,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-plugins.json.golden b/cli/command/system/testdata/docker-info-plugins.json.golden index d7e39ee90eba..1a01ad8d9a23 100644 --- a/cli/command/system/testdata/docker-info-plugins.json.golden +++ b/cli/command/system/testdata/docker-info-plugins.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-with-devices.golden b/cli/command/system/testdata/docker-info-with-devices.golden index ab9b9b9011da..f943021fda43 100644 --- a/cli/command/system/testdata/docker-info-with-devices.golden +++ b/cli/command/system/testdata/docker-info-with-devices.golden @@ -57,4 +57,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-with-devices.json.golden b/cli/command/system/testdata/docker-info-with-devices.json.golden index c2fa190b03b9..9e42a8b2d6e1 100644 --- a/cli/command/system/testdata/docker-info-with-devices.json.golden +++ b/cli/command/system/testdata/docker-info-with-devices.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"Warnings":null} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"Warnings":null} diff --git a/cli/command/system/testdata/docker-info-with-labels-empty.golden b/cli/command/system/testdata/docker-info-with-labels-empty.golden index 866244855c43..61a56a92b601 100644 --- a/cli/command/system/testdata/docker-info-with-labels-empty.golden +++ b/cli/command/system/testdata/docker-info-with-labels-empty.golden @@ -54,4 +54,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-with-labels-nil.golden b/cli/command/system/testdata/docker-info-with-labels-nil.golden index 866244855c43..61a56a92b601 100644 --- a/cli/command/system/testdata/docker-info-with-labels-nil.golden +++ b/cli/command/system/testdata/docker-info-with-labels-nil.golden @@ -54,4 +54,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-with-swarm.golden b/cli/command/system/testdata/docker-info-with-swarm.golden index 97d9590c140e..9f655148481e 100644 --- a/cli/command/system/testdata/docker-info-with-swarm.golden +++ b/cli/command/system/testdata/docker-info-with-swarm.golden @@ -78,4 +78,6 @@ Server: Live Restore Enabled: false Default Address Pools: Base: 10.123.0.0/16, Size: 24 + Firewall Backend: nftables+firewalld + ReloadedAt: 2025-07-16T16:59:14Z diff --git a/cli/command/system/testdata/docker-info-with-swarm.json.golden b/cli/command/system/testdata/docker-info-with-swarm.json.golden index 3351600a6f16..385b682167dd 100644 --- a/cli/command/system/testdata/docker-info-with-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-with-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 3a0ad75c351e..698fb70917f1 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -2,6 +2,8 @@ package system import ( "context" + "fmt" + "io" "runtime" "sort" "strconv" @@ -10,15 +12,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter/tabwriter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/version" "github.com/docker/cli/templates" - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/tonistiigi/go-rosetta" ) @@ -64,7 +64,7 @@ type versionOptions struct { // versionInfo contains version information of both the Client, and Server type versionInfo struct { Client clientVersion - Server *types.Version + Server *serverVersion } type platformInfo struct { @@ -84,6 +84,26 @@ type clientVersion struct { Context string `json:"Context"` } +// serverVersion contains information about the Docker server host. +// it's the client-side presentation of [client.ServerVersionResult]. +type serverVersion struct { + Platform client.PlatformInfo `json:",omitempty"` // Platform is the platform (product name) the server is running on. + Version string `json:"Version"` // Version is the version of the daemon. + APIVersion string `json:"ApiVersion"` // APIVersion is the highest API version supported by the server. + MinAPIVersion string `json:"MinAPIVersion,omitempty"` // MinAPIVersion is the minimum API version the server supports. + Os string `json:"Os"` // Os is the operating system the server runs on. + Arch string `json:"Arch"` // Arch is the hardware architecture the server runs on. + Components []system.ComponentVersion `json:"Components,omitempty"` // Components contains version information for the components making up the server. + + // The following fields are deprecated, they relate to the Engine component and are kept for backwards compatibility + + GitCommit string `json:"GitCommit,omitempty"` + GoVersion string `json:"GoVersion,omitempty"` + KernelVersion string `json:"KernelVersion,omitempty"` + Experimental bool `json:"Experimental,omitempty"` + BuildTime string `json:"BuildTime,omitempty"` +} + // newClientVersion constructs a new clientVersion. If a dockerCLI is // passed as argument, additional information is included (API version), // which may invoke an API connection. Pass nil to omit the additional @@ -91,7 +111,7 @@ type clientVersion struct { func newClientVersion(contextName string, dockerCli command.Cli) clientVersion { v := clientVersion{ Version: version.Version, - DefaultAPIVersion: api.DefaultVersion, + DefaultAPIVersion: client.MaxAPIVersion, GoVersion: runtime.Version(), GitCommit: version.GitCommit, BuildTime: reformatDate(version.BuildTime), @@ -108,8 +128,51 @@ func newClientVersion(contextName string, dockerCli command.Cli) clientVersion { return v } -// NewVersionCommand creates a new cobra.Command for `docker version` -func NewVersionCommand(dockerCli command.Cli) *cobra.Command { +func newServerVersion(sv client.ServerVersionResult) *serverVersion { + out := &serverVersion{ + Platform: sv.Platform, + Version: sv.Version, + APIVersion: sv.APIVersion, + MinAPIVersion: sv.MinAPIVersion, + Os: sv.Os, + Arch: sv.Arch, + Experimental: sv.Experimental, //nolint:staticcheck // ignore deprecated field. + Components: make([]system.ComponentVersion, 0, len(sv.Components)), + } + foundEngine := false + for _, component := range sv.Components { + if component.Name == "Engine" { + foundEngine = true + buildTime, ok := component.Details["BuildTime"] + if ok { + component.Details["BuildTime"] = reformatDate(buildTime) + } + out.GitCommit = component.Details["GitCommit"] + out.GoVersion = component.Details["GoVersion"] + out.KernelVersion = component.Details["KernelVersion"] + out.Experimental = func() bool { b, _ := strconv.ParseBool(component.Details["Experimental"]); return b }() + out.BuildTime = buildTime + } + out.Components = append(out.Components, component) + } + + if !foundEngine { + out.Components = append(out.Components, system.ComponentVersion{ + Name: "Engine", + Version: sv.Version, + Details: map[string]string{ + "ApiVersion": sv.APIVersion, + "MinAPIVersion": sv.MinAPIVersion, + "Os": sv.Os, + "Arch": sv.Arch, + }, + }) + } + return out +} + +// newVersionCommand creates a new cobra.Command for `docker version` +func newVersionCommand(dockerCLI command.Cli) *cobra.Command { var opts versionOptions cmd := &cobra.Command{ @@ -117,12 +180,13 @@ func NewVersionCommand(dockerCli command.Cli) *cobra.Command { Short: "Show the Docker version information", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runVersion(cmd.Context(), dockerCli, &opts) + return runVersion(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "category-top": "10", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -138,64 +202,35 @@ func reformatDate(buildTime string) string { } func arch() string { - arch := runtime.GOARCH + out := runtime.GOARCH if rosetta.Enabled() { - arch += " (rosetta)" + out += " (rosetta)" } - return arch + return out } -func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions) error { +func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions) error { var err error tmpl, err := newVersionTemplate(opts.format) if err != nil { return cli.StatusError{StatusCode: 64, Status: err.Error()} } - // TODO print error if kubernetes is used? - vd := versionInfo{ - Client: newClientVersion(dockerCli.CurrentContext(), dockerCli), + Client: newClientVersion(dockerCLI.CurrentContext(), dockerCLI), } - sv, err := dockerCli.Client().ServerVersion(ctx) + sv, err := dockerCLI.Client().ServerVersion(ctx, client.ServerVersionOptions{}) if err == nil { - vd.Server = &sv - foundEngine := false - for _, component := range sv.Components { - if component.Name == "Engine" { - foundEngine = true - buildTime, ok := component.Details["BuildTime"] - if ok { - component.Details["BuildTime"] = reformatDate(buildTime) - } - } - } - - if !foundEngine { - vd.Server.Components = append(vd.Server.Components, types.ComponentVersion{ - Name: "Engine", - Version: sv.Version, - Details: map[string]string{ - "ApiVersion": sv.APIVersion, - "MinAPIVersion": sv.MinAPIVersion, - "GitCommit": sv.GitCommit, - "GoVersion": sv.GoVersion, - "Os": sv.Os, - "Arch": sv.Arch, - "BuildTime": reformatDate(vd.Server.BuildTime), - "Experimental": strconv.FormatBool(sv.Experimental), - }, - }) - } + vd.Server = newServerVersion(sv) } - if err2 := prettyPrintVersion(dockerCli, vd, tmpl); err2 != nil && err == nil { + if err2 := prettyPrintVersion(dockerCLI.Out(), vd, tmpl); err2 != nil && err == nil { err = err2 } return err } -func prettyPrintVersion(dockerCli command.Cli, vd versionInfo, tmpl *template.Template) error { - t := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 1, ' ', 0) +func prettyPrintVersion(out io.Writer, vd versionInfo, tmpl *template.Template) error { + t := tabwriter.NewWriter(out, 20, 1, 1, ' ', 0) err := tmpl.Execute(t, vd) _, _ = t.Write([]byte("\n")) _ = t.Flush() @@ -209,15 +244,14 @@ func newVersionTemplate(templateFormat string) (*template.Template, error) { case formatter.JSONFormatKey: templateFormat = formatter.JSONFormat } - tmpl := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}) - tmpl, err := tmpl.Parse(templateFormat) + tmpl, err := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}).Parse(templateFormat) if err != nil { - return nil, errors.Wrap(err, "template parsing error") + return nil, fmt.Errorf("template parsing error: %w", err) } return tmpl, nil } -func getDetailsOrder(v types.ComponentVersion) []string { +func getDetailsOrder(v system.ComponentVersion) []string { out := make([]string, 0, len(v.Details)) for k := range v.Details { out = append(out, k) diff --git a/cli/command/system/version_test.go b/cli/command/system/version_test.go index 6f8b243de4b4..b82693185b88 100644 --- a/cli/command/system/version_test.go +++ b/cli/command/system/version_test.go @@ -1,6 +1,7 @@ package system import ( + "bytes" "context" "errors" "io" @@ -8,7 +9,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/system" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -16,11 +18,11 @@ import ( func TestVersionWithoutServer(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - serverVersion: func(ctx context.Context) (types.Version, error) { - return types.Version{}, errors.New("no server") + serverVersion: func(ctx context.Context, options client.ServerVersionOptions) (client.ServerVersionResult, error) { + return client.ServerVersionResult{}, errors.New("no server") }, }) - cmd := NewVersionCommand(cli) + cmd := newVersionCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(cli.Err()) cmd.SetErr(io.Discard) @@ -45,89 +47,99 @@ func TestVersionFormat(t *testing.T) { BuildTime: "Wed May 30 22:21:05 2018", Context: "my-context", }, - Server: &types.Version{}, + Server: &serverVersion{ + Platform: client.PlatformInfo{Name: "Docker Enterprise Edition (EE) 2.0"}, + Version: "18.99.5-ce", + APIVersion: "1.30", + MinAPIVersion: "1.12", + Os: "linux", + Arch: "amd64", + GitCommit: "64ddfa6", + GoVersion: "go1.8.7", + KernelVersion: "v1.0.0", + Experimental: false, + BuildTime: "2018-07-09T22:38:38.000000000+00:00", + Components: []system.ComponentVersion{ + { + Name: "Engine", + Version: "17.06.2-ee-15", + Details: map[string]string{ + "ApiVersion": "1.30", + "MinAPIVersion": "1.12", + "GitCommit": "64ddfa6", + "GoVersion": "go1.8.7", + "Os": "linux", + "Arch": "amd64", + "BuildTime": "Mon Jul 9 23:38:38 2018", + "Experimental": "false", + }, + }, + { + Name: "Universal Control Plane", + Version: "17.06.2-ee-15", + Details: map[string]string{ + "Version": "3.0.3-tp2", + "ApiVersion": "1.30", + "Arch": "amd64", + "BuildTime": "Mon Jul 2 21:24:07 UTC 2018", + "GitCommit": "4513922", + "GoVersion": "go1.9.4", + "MinApiVersion": "1.20", + "Os": "linux", + }, + }, + { + Name: "Kubernetes", + Version: "1.8+", + Details: map[string]string{ + "buildDate": "2018-04-26T16:51:21Z", + "compiler": "gc", + "gitCommit": "8d637aedf46b9c21dde723e29c645b9f27106fa5", + "gitTreeState": "clean", + "gitVersion": "v1.8.11-docker-8d637ae", + "goVersion": "go1.8.3", + "major": "1", + "minor": "8+", + "platform": "linux/amd64", + }, + }, + { + Name: "Calico", + Version: "v3.0.8", + Details: map[string]string{ + "cni": "v2.0.6", + "kube-controllers": "v2.0.5", + "node": "v3.0.8", + }, + }, + }, + }, } - vi.Server.Platform.Name = "Docker Enterprise Edition (EE) 2.0" - - vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{ - Name: "Engine", - Version: "17.06.2-ee-15", - Details: map[string]string{ - "ApiVersion": "1.30", - "MinAPIVersion": "1.12", - "GitCommit": "64ddfa6", - "GoVersion": "go1.8.7", - "Os": "linux", - "Arch": "amd64", - "BuildTime": "Mon Jul 9 23:38:38 2018", - "Experimental": "false", + tests := []struct { + name string + format string + }{ + { + name: "default", }, - }) - - vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{ - Name: "Universal Control Plane", - Version: "17.06.2-ee-15", - Details: map[string]string{ - "Version": "3.0.3-tp2", - "ApiVersion": "1.30", - "Arch": "amd64", - "BuildTime": "Mon Jul 2 21:24:07 UTC 2018", - "GitCommit": "4513922", - "GoVersion": "go1.9.4", - "MinApiVersion": "1.20", - "Os": "linux", + { + name: "json", + format: "json", }, - }) - - vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{ - Name: "Kubernetes", - Version: "1.8+", - Details: map[string]string{ - "buildDate": "2018-04-26T16:51:21Z", - "compiler": "gc", - "gitCommit": "8d637aedf46b9c21dde723e29c645b9f27106fa5", - "gitTreeState": "clean", - "gitVersion": "v1.8.11-docker-8d637ae", - "goVersion": "go1.8.3", - "major": "1", - "minor": "8+", - "platform": "linux/amd64", + { + name: "json template", + format: "json", }, - }) - - vi.Server.Components = append(vi.Server.Components, types.ComponentVersion{ - Name: "Calico", - Version: "v3.0.8", - Details: map[string]string{ - "cni": "v2.0.6", - "kube-controllers": "v2.0.5", - "node": "v3.0.8", - }, - }) + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + tmpl, err := newVersionTemplate(tc.format) + assert.NilError(t, err) - t.Run("default", func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - tmpl, err := newVersionTemplate("") - assert.NilError(t, err) - assert.NilError(t, prettyPrintVersion(cli, vi, tmpl)) - assert.Check(t, golden.String(cli.OutBuffer().String(), "docker-client-version.golden")) - assert.Check(t, is.Equal("", cli.ErrBuffer().String())) - }) - t.Run("json", func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - tmpl, err := newVersionTemplate("json") - assert.NilError(t, err) - assert.NilError(t, prettyPrintVersion(cli, vi, tmpl)) - assert.Check(t, golden.String(cli.OutBuffer().String(), "docker-client-version.json.golden")) - assert.Check(t, is.Equal("", cli.ErrBuffer().String())) - }) - t.Run("json template", func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - tmpl, err := newVersionTemplate("{{json .}}") - assert.NilError(t, err) - assert.NilError(t, prettyPrintVersion(cli, vi, tmpl)) - assert.Check(t, golden.String(cli.OutBuffer().String(), "docker-client-version.json.golden")) - assert.Check(t, is.Equal("", cli.ErrBuffer().String())) - }) + var out bytes.Buffer + assert.NilError(t, prettyPrintVersion(&out, vi, tmpl)) + assert.Check(t, golden.String(out.String(), t.Name()+".golden")) + }) + } } diff --git a/cli/command/task/client_test.go b/cli/command/task/client_test.go index fa178898e2dc..3d1590cb5616 100644 --- a/cli/command/task/client_test.go +++ b/cli/command/task/client_test.go @@ -3,26 +3,25 @@ package task import ( "context" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.APIClient - nodeInspectWithRaw func(ref string) (swarm.Node, []byte, error) - serviceInspectWithRaw func(ref string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) + nodeInspectFunc func(ref string) (client.NodeInspectResult, error) + serviceInspectFunc func(ref string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) } -func (cli *fakeClient) NodeInspectWithRaw(_ context.Context, ref string) (swarm.Node, []byte, error) { - if cli.nodeInspectWithRaw != nil { - return cli.nodeInspectWithRaw(ref) +func (cli *fakeClient) NodeInspect(_ context.Context, ref string, _ client.NodeInspectOptions) (client.NodeInspectResult, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc(ref) } - return swarm.Node{}, nil, nil + return client.NodeInspectResult{}, nil } -func (cli *fakeClient) ServiceInspectWithRaw(_ context.Context, ref string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - if cli.serviceInspectWithRaw != nil { - return cli.serviceInspectWithRaw(ref, options) +func (cli *fakeClient) ServiceInspect(_ context.Context, ref string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + if cli.serviceInspectFunc != nil { + return cli.serviceInspectFunc(ref, options) } - return swarm.Service{}, nil, nil + return client.ServiceInspectResult{}, nil } diff --git a/cli/command/task/formatter.go b/cli/command/task/formatter.go index b87ab3a4b715..cbe8c00d8dc4 100644 --- a/cli/command/task/formatter.go +++ b/cli/command/task/formatter.go @@ -7,8 +7,9 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-units" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -22,8 +23,8 @@ const ( maxErrLength = 30 ) -// NewTaskFormat returns a Format for rendering using a task Context -func NewTaskFormat(source string, quiet bool) formatter.Format { +// newTaskFormat returns a Format for rendering using a taskContext. +func newTaskFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -39,29 +40,35 @@ func NewTaskFormat(source string, quiet bool) formatter.Format { return formatter.Format(source) } -// FormatWrite writes the context -func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, task := range tasks { - taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} - if err := format(taskCtx); err != nil { +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, tasks client.TaskListResult, names map[string]string, nodes map[string]string) error { + taskCtx := &taskContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "ID": taskIDHeader, + "Name": formatter.NameHeader, + "Image": formatter.ImageHeader, + "Node": nodeHeader, + "DesiredState": desiredStateHeader, + "CurrentState": currentStateHeader, + "Error": formatter.ErrorHeader, + "Ports": formatter.PortsHeader, + }, + }, + } + return fmtCtx.Write(taskCtx, func(format func(subContext formatter.SubContext) error) error { + for _, task := range tasks.Items { + if err := format(&taskContext{ + trunc: fmtCtx.Trunc, + task: task, + name: names[task.ID], + node: nodes[task.ID], + }); err != nil { return err } } return nil - } - taskCtx := taskContext{} - taskCtx.Header = formatter.SubHeaderContext{ - "ID": taskIDHeader, - "Name": formatter.NameHeader, - "Image": formatter.ImageHeader, - "Node": nodeHeader, - "DesiredState": desiredStateHeader, - "CurrentState": currentStateHeader, - "Error": formatter.ErrorHeader, - "Ports": formatter.PortsHeader, - } - return ctx.Write(&taskCtx, render) + }) } type taskContext struct { @@ -134,7 +141,7 @@ func (c *taskContext) Ports() string { if len(c.task.Status.PortStatus.Ports) == 0 { return "" } - ports := []string{} + ports := make([]string, 0, len(c.task.Status.PortStatus.Ports)) for _, pConfig := range c.task.Status.PortStatus.Ports { ports = append(ports, fmt.Sprintf("*:%d->%d/%s", pConfig.PublishedPort, diff --git a/cli/command/task/formatter_test.go b/cli/command/task/formatter_test.go index c857c64bb4eb..278e072efdb6 100644 --- a/cli/command/task/formatter_test.go +++ b/cli/command/task/formatter_test.go @@ -7,7 +7,8 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -27,39 +28,41 @@ func TestTaskContextWrite(t *testing.T) { `template parsing error: template: :1:2: executing "" at : nil is not a command`, }, { - formatter.Context{Format: NewTaskFormat("table", true)}, + formatter.Context{Format: newTaskFormat("table", true)}, `taskID1 taskID2 `, }, { - formatter.Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, + formatter.Context{Format: newTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, string(golden.Get(t, "task-context-write-table-custom.golden")), }, { - formatter.Context{Format: NewTaskFormat("table {{.Name}}", true)}, + formatter.Context{Format: newTaskFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewTaskFormat("raw", true)}, + formatter.Context{Format: newTaskFormat("raw", true)}, `id: taskID1 id: taskID2 `, }, { - formatter.Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, + formatter.Context{Format: newTaskFormat("{{.Name}} {{.Node}}", false)}, `foobar_baz foo1 foobar_bar foo2 `, }, } - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, + tasks := client.TaskListResult{ + Items: []swarm.Task{ + {ID: "taskID1"}, + {ID: "taskID2"}, + }, } names := map[string]string{ "taskID1": "foobar_baz", @@ -75,7 +78,7 @@ foobar_bar foo2 var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, tasks, names, nodes); err != nil { + if err := formatWrite(tc.context, tasks, names, nodes); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) @@ -85,16 +88,18 @@ foobar_bar foo2 } func TestTaskContextWriteJSONField(t *testing.T) { - tasks := []swarm.Task{ - {ID: "taskID1"}, - {ID: "taskID2"}, + tasks := client.TaskListResult{ + Items: []swarm.Task{ + {ID: "taskID1"}, + {ID: "taskID2"}, + }, } names := map[string]string{ "taskID1": "foobar_baz", "taskID2": "foobar_bar", } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) if err != nil { t.Fatal(err) } @@ -103,6 +108,6 @@ func TestTaskContextWriteJSONField(t *testing.T) { if err := json.Unmarshal([]byte(line), &s); err != nil { t.Fatal(err) } - assert.Check(t, is.Equal(tasks[i].ID, s)) + assert.Check(t, is.Equal(tasks.Items[i].ID, s)) } } diff --git a/cli/command/task/print.go b/cli/command/task/print.go index 4c9b362a2fb3..ae27d42d0133 100644 --- a/cli/command/task/print.go +++ b/cli/command/task/print.go @@ -9,8 +9,9 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/cli/config/configfile" - "github.com/docker/docker/api/types/swarm" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) type tasksSortable []swarm.Task @@ -34,7 +35,7 @@ func (t tasksSortable) Less(i, j int) bool { // Print task information in a format. // Besides this, command `docker node ps ` // and `docker stack ps` will call this, too. -func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error { +func Print(ctx context.Context, dockerCli command.Cli, tasks client.TaskListResult, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error { tasks, err := generateTaskNames(ctx, tasks, resolver) if err != nil { return err @@ -43,14 +44,14 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol // First sort tasks, so that all tasks (including previous ones) of the same // service and slot are together. This must be done first, to print "previous" // tasks indented - sort.Stable(tasksSortable(tasks)) + sort.Stable(tasksSortable(tasks.Items)) names := map[string]string{} nodes := map[string]string{} tasksCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewTaskFormat(format, quiet), + Format: newTaskFormat(format, quiet), Trunc: trunc, } @@ -59,7 +60,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol indent = ` \_ ` } prevName := "" - for _, task := range tasks { + for _, task := range tasks.Items { if task.Name == prevName { // Indent previous tasks of the same slot names[task.ID] = indent + task.Name @@ -75,7 +76,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol nodes[task.ID] = nodeValue } - return FormatWrite(tasksCtx, tasks, names, nodes) + return formatWrite(tasksCtx, tasks, names, nodes) } // generateTaskNames generates names for the given tasks, and returns a copy of @@ -87,15 +88,15 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol // - ServiceName.NodeName or ServiceID.NodeID for tasks that are part of a global service // // Task-names are not unique in cases where "tasks" contains previous/rotated tasks. -func generateTaskNames(ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver) ([]swarm.Task, error) { +func generateTaskNames(ctx context.Context, tasks client.TaskListResult, resolver *idresolver.IDResolver) (client.TaskListResult, error) { // Use a copy of the tasks list, to not modify the original slice // see https://github.com/go101/go101/wiki/How-to-efficiently-clone-a-slice%3F - t := append(tasks[:0:0], tasks...) //nolint:gocritic // ignore appendAssign: append result not assigned to the same slice + t := append(tasks.Items[:0:0], tasks.Items...) //nolint:gocritic // ignore appendAssign: append result not assigned to the same slice for i, task := range t { serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) if err != nil { - return nil, err + return client.TaskListResult{}, err } if task.Slot != 0 { t[i].Name = fmt.Sprintf("%v.%v", serviceName, task.Slot) @@ -103,7 +104,7 @@ func generateTaskNames(ctx context.Context, tasks []swarm.Task, resolver *idreso t[i].Name = fmt.Sprintf("%v.%v", serviceName, task.NodeID) } } - return t, nil + return client.TaskListResult{Items: t}, nil } // DefaultFormat returns the default format from the config file, or table diff --git a/cli/command/task/print_test.go b/cli/command/task/print_test.go index 64160ac900c2..7c3281e803fe 100644 --- a/cli/command/task/print_test.go +++ b/cli/command/task/print_test.go @@ -9,42 +9,49 @@ import ( "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) func TestTaskPrintSorted(t *testing.T) { apiClient := &fakeClient{ - serviceInspectWithRaw: func(ref string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { + serviceInspectFunc: func(ref string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { if ref == "service-id-one" { - return *builders.Service(builders.ServiceName("service-name-1")), nil, nil + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceName("service-name-1")), + }, nil } - return *builders.Service(builders.ServiceName("service-name-10")), nil, nil + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceName("service-name-10")), + }, nil }, } cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task( - builders.TaskID("id-foo"), - builders.TaskServiceID("service-id-ten"), - builders.TaskNodeID("id-node"), - builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), - builders.TaskDesiredState(swarm.TaskStateReady), - builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), - ), - *builders.Task( - builders.TaskID("id-bar"), - builders.TaskServiceID("service-id-one"), - builders.TaskNodeID("id-node"), - builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), - builders.TaskDesiredState(swarm.TaskStateReady), - builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), - ), + res := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task( + builders.TaskID("id-foo"), + builders.TaskServiceID("service-id-ten"), + builders.TaskNodeID("id-node"), + builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), + builders.TaskDesiredState(swarm.TaskStateReady), + builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), + ), + *builders.Task( + builders.TaskID("id-bar"), + builders.TaskServiceID("service-id-one"), + builders.TaskNodeID("id-node"), + builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), + builders.TaskDesiredState(swarm.TaskStateReady), + builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), + ), + }, } - err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, false), false, false, formatter.TableFormatKey) + err := Print(context.Background(), cli, res, idresolver.New(apiClient, false), false, false, formatter.TableFormatKey) assert.NilError(t, err) golden.Assert(t, cli.OutBuffer().String(), "task-print-sorted.golden") } @@ -55,7 +62,11 @@ func TestTaskPrintWithQuietOption(t *testing.T) { const noResolve = true apiClient := &fakeClient{} cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{*builders.Task(builders.TaskID("id-foo"))} + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskID("id-foo")), + }, + } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey) assert.NilError(t, err) golden.Assert(t, cli.OutBuffer().String(), "task-print-with-quiet-option.golden") @@ -67,8 +78,10 @@ func TestTaskPrintWithNoTruncOption(t *testing.T) { const noResolve = true apiClient := &fakeClient{} cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task(builders.TaskID("id-foo-yov6omdek8fg3k5stosyp2m50")), + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskID("id-foo-yov6omdek8fg3k5stosyp2m50")), + }, } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .ID }}") assert.NilError(t, err) @@ -81,8 +94,10 @@ func TestTaskPrintWithGlobalService(t *testing.T) { const noResolve = true apiClient := &fakeClient{} cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskNodeID("node-id-bar"), builders.TaskSlot(0)), + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskNodeID("node-id-bar"), builders.TaskSlot(0)), + }, } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}") assert.NilError(t, err) @@ -95,8 +110,10 @@ func TestTaskPrintWithReplicatedService(t *testing.T) { const noResolve = true apiClient := &fakeClient{} cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskSlot(1)), + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskSlot(1)), + }, } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }}") assert.NilError(t, err) @@ -108,31 +125,37 @@ func TestTaskPrintWithIndentation(t *testing.T) { const trunc = false const noResolve = false apiClient := &fakeClient{ - serviceInspectWithRaw: func(ref string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return *builders.Service(builders.ServiceName("service-name-foo")), nil, nil + serviceInspectFunc: func(ref string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceName("service-name-foo")), + }, nil }, - nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName("node-name-bar")), nil, nil + nodeInspectFunc: func(ref string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-name-bar")), + }, nil }, } cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task( - builders.TaskID("id-foo"), - builders.TaskServiceID("service-id-foo"), - builders.TaskNodeID("id-node"), - builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), - builders.TaskDesiredState(swarm.TaskStateReady), - builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), - ), - *builders.Task( - builders.TaskID("id-bar"), - builders.TaskServiceID("service-id-foo"), - builders.TaskNodeID("id-node"), - builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), - builders.TaskDesiredState(swarm.TaskStateReady), - builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), - ), + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task( + builders.TaskID("id-foo"), + builders.TaskServiceID("service-id-foo"), + builders.TaskNodeID("id-node"), + builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), + builders.TaskDesiredState(swarm.TaskStateReady), + builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), + ), + *builders.Task( + builders.TaskID("id-bar"), + builders.TaskServiceID("service-id-foo"), + builders.TaskNodeID("id-node"), + builders.WithTaskSpec(builders.TaskImage("myimage:mytag")), + builders.TaskDesiredState(swarm.TaskStateReady), + builders.WithStatus(builders.TaskState(swarm.TaskStateFailed), builders.Timestamp(time.Now().Add(-2*time.Hour))), + ), + }, } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, formatter.TableFormatKey) assert.NilError(t, err) @@ -144,16 +167,22 @@ func TestTaskPrintWithResolution(t *testing.T) { const trunc = false const noResolve = false apiClient := &fakeClient{ - serviceInspectWithRaw: func(ref string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - return *builders.Service(builders.ServiceName("service-name-foo")), nil, nil + serviceInspectFunc: func(ref string, options client.ServiceInspectOptions) (client.ServiceInspectResult, error) { + return client.ServiceInspectResult{ + Service: *builders.Service(builders.ServiceName("service-name-foo")), + }, nil }, - nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) { - return *builders.Node(builders.NodeName("node-name-bar")), nil, nil + nodeInspectFunc: func(ref string) (client.NodeInspectResult, error) { + return client.NodeInspectResult{ + Node: *builders.Node(builders.NodeName("node-name-bar")), + }, nil }, } cli := test.NewFakeCli(apiClient) - tasks := []swarm.Task{ - *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskSlot(1)), + tasks := client.TaskListResult{ + Items: []swarm.Task{ + *builders.Task(builders.TaskServiceID("service-id-foo"), builders.TaskSlot(1)), + }, } err := Print(context.Background(), cli, tasks, idresolver.New(apiClient, noResolve), trunc, quiet, "{{ .Name }} {{ .Node }}") assert.NilError(t, err) diff --git a/cli/command/telemetry.go b/cli/command/telemetry.go index e8e6296b5a92..93f0ed27a230 100644 --- a/cli/command/telemetry.go +++ b/cli/command/telemetry.go @@ -11,11 +11,12 @@ import ( "github.com/google/uuid" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" + otelsdk "go.opentelemetry.io/otel/sdk" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/metric/metricdata" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + semconv "go.opentelemetry.io/otel/semconv/v1.37.0" "go.opentelemetry.io/otel/trace" ) @@ -146,7 +147,7 @@ func defaultResourceOptions() []resource.Option { semconv.ServiceInstanceID(uuid.NewString()), ), resource.WithFromEnv(), - resource.WithTelemetrySDK(), + resource.WithDetectors(telemetrySDK{}), } } @@ -157,7 +158,10 @@ func (r *telemetryResource) AppendOptions(opts ...resource.Option) { r.opts = append(r.opts, opts...) } -type serviceNameDetector struct{} +type ( + serviceNameDetector struct{} + telemetrySDK struct{} +) func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) { return resource.StringDetector( @@ -169,6 +173,16 @@ func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, erro ).Detect(ctx) } +// Detect returns a *Resource that describes the OpenTelemetry SDK used. +func (telemetrySDK) Detect(context.Context) (*resource.Resource, error) { + return resource.NewWithAttributes( + semconv.SchemaURL, + semconv.TelemetrySDKName("opentelemetry"), + semconv.TelemetrySDKLanguageGo, + semconv.TelemetrySDKVersion(otelsdk.Version()), + ), nil +} + // cliReader is an implementation of Reader that will automatically // report to a designated Exporter when Shutdown is called. type cliReader struct { diff --git a/cli/command/telemetry_docker.go b/cli/command/telemetry_docker.go index 6598997d6817..f0a43a4259b2 100644 --- a/cli/command/telemetry_docker.go +++ b/cli/command/telemetry_docker.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command @@ -14,7 +14,6 @@ import ( "strings" "unicode" - "github.com/pkg/errors" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" @@ -48,7 +47,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) { if otelCfg != nil { otelMap, ok := otelCfg.(map[string]any) if !ok { - otel.Handle(errors.Errorf( + otel.Handle(fmt.Errorf( "unexpected type for field %q: %T (expected: %T)", otelContextFieldName, otelCfg, @@ -76,7 +75,7 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) { // We pretend we're the same as the environment reader. u, err := url.Parse(endpoint) if err != nil { - otel.Handle(errors.Errorf("docker otel endpoint is invalid: %s", err)) + otel.Handle(fmt.Errorf("docker otel endpoint is invalid: %s", err)) return "", false } diff --git a/cli/command/telemetry_utils.go b/cli/command/telemetry_utils.go index 680415b63770..dadbd13fd72c 100644 --- a/cli/command/telemetry_utils.go +++ b/cli/command/telemetry_utils.go @@ -2,12 +2,12 @@ package command import ( "context" + "errors" "fmt" "strings" "time" "github.com/docker/cli/cli/version" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" diff --git a/cli/command/trust.go b/cli/command/trust.go deleted file mode 100644 index 65f2408585d8..000000000000 --- a/cli/command/trust.go +++ /dev/null @@ -1,15 +0,0 @@ -package command - -import ( - "github.com/spf13/pflag" -) - -// AddTrustVerificationFlags adds content trust flags to the provided flagset -func AddTrustVerificationFlags(fs *pflag.FlagSet, v *bool, trusted bool) { - fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image verification") -} - -// AddTrustSigningFlags adds "signing" flags to the provided flagset -func AddTrustSigningFlags(fs *pflag.FlagSet, v *bool, trusted bool) { - fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image signing") -} diff --git a/cli/command/trust/cmd.go b/cli/command/trust/cmd.go deleted file mode 100644 index bb6ceace042b..000000000000 --- a/cli/command/trust/cmd.go +++ /dev/null @@ -1,25 +0,0 @@ -package trust - -import ( - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" -) - -// NewTrustCommand returns a cobra command for `trust` subcommands -func NewTrustCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "trust", - Short: "Manage trust on Docker images", - Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), - } - cmd.AddCommand( - newRevokeCommand(dockerCli), - newSignCommand(dockerCli), - newTrustKeyCommand(dockerCli), - newTrustSignerCommand(dockerCli), - newInspectCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/trust/common.go b/cli/command/trust/common.go deleted file mode 100644 index 7d1d50e29688..000000000000 --- a/cli/command/trust/common.go +++ /dev/null @@ -1,169 +0,0 @@ -package trust - -import ( - "context" - "encoding/hex" - "fmt" - "sort" - "strings" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" - "github.com/fvbommel/sortorder" - "github.com/sirupsen/logrus" - "github.com/theupdateframework/notary" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/tuf/data" -) - -// trustTagKey represents a unique signed tag and hex-encoded hash pair -type trustTagKey struct { - SignedTag string - Digest string -} - -// trustTagRow encodes all human-consumable information for a signed tag, including signers -type trustTagRow struct { - trustTagKey - Signers []string -} - -// trustRepo represents consumable information about a trusted repository -type trustRepo struct { - Name string - SignedTags []trustTagRow - Signers []trustSigner - AdministrativeKeys []trustSigner -} - -// trustSigner represents a trusted signer in a trusted repository -// a signer is defined by a name and list of trustKeys -type trustSigner struct { - Name string `json:",omitempty"` - Keys []trustKey `json:",omitempty"` -} - -// trustKey contains information about trusted keys -type trustKey struct { - ID string `json:",omitempty"` -} - -// notaryClientProvider is used in tests to provide a dummy notary client. -type notaryClientProvider interface { - NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) -} - -// newNotaryClient provides a Notary Repository to interact with signed metadata for an image. -func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) { - if ncp, ok := cli.(notaryClientProvider); ok { - // notaryClientProvider is used in tests to provide a dummy notary client. - return ncp.NotaryClient(imgRefAndAuth, actions) - } - return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) -} - -// lookupTrustInfo returns processed signature and role information about a notary repository. -// This information is to be pretty printed or serialized into a machine-readable format. -func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) - if err != nil { - return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err - } - tag := imgRefAndAuth.Tag() - notaryRepo, err := newNotaryClient(cli, imgRefAndAuth, trust.ActionsPullOnly) - if err != nil { - return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err) - } - - if err = clearChangeList(notaryRepo); err != nil { - return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err - } - defer clearChangeList(notaryRepo) - - // Retrieve all released signatures, match them, and pretty print them - allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) - if err != nil { - logrus.Debug(trust.NotaryError(remote, err)) - // print an empty table if we don't have signed targets, but have an initialized notary repo - if _, ok := err.(client.ErrNoSuchTarget); !ok { - return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("no signatures or cannot access %s", remote) - } - } - signatureRows := matchReleasedSignatures(allSignedTargets) - - // get the administrative roles - adminRolesWithSigs, err := notaryRepo.ListRoles() - if err != nil { - return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("no signers for %s", remote) - } - - // get delegation roles with the canonical key IDs - delegationRoles, err := notaryRepo.GetDelegationRoles() - if err != nil { - logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) - } - - return signatureRows, adminRolesWithSigs, delegationRoles, nil -} - -func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { - adminKeyList := roleWithSigs.KeyIDs - sort.Strings(adminKeyList) - - var role string - switch roleWithSigs.Name { - case data.CanonicalTargetsRole: - role = "Repository Key" - case data.CanonicalRootRole: - role = "Root Key" - default: - return "" - } - return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", ")) -} - -func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string { - signerRoleToKeyIDs := make(map[string][]string) - for _, delRole := range rawDelegationRoles { - switch delRole.Name { - case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole: - continue - default: - signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs - } - } - return signerRoleToKeyIDs -} - -// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been -// signed into the "targets" or "targets/releases" role. Output is sorted by tag name -func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagRow { - signatureRows := []trustTagRow{} - // do a first pass to get filter on tags signed into "targets" or "targets/releases" - releasedTargetRows := map[trustTagKey][]string{} - for _, tgt := range allTargets { - if isReleasedTarget(tgt.Role.Name) { - releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} - releasedTargetRows[releasedKey] = []string{} - } - } - - // now fill out all signers on released keys - for _, tgt := range allTargets { - targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} - // only considered released targets - if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) { - releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name)) - } - } - - // compile the final output as a sorted slice - for targetKey, signers := range releasedTargetRows { - signatureRows = append(signatureRows, trustTagRow{targetKey, signers}) - } - sort.Slice(signatureRows, func(i, j int) bool { - return sortorder.NaturalLess(signatureRows[i].SignedTag, signatureRows[j].SignedTag) - }) - return signatureRows -} diff --git a/cli/command/trust/formatter.go b/cli/command/trust/formatter.go deleted file mode 100644 index 9597cfbd7697..000000000000 --- a/cli/command/trust/formatter.go +++ /dev/null @@ -1,131 +0,0 @@ -package trust - -import ( - "sort" - "strings" - - "github.com/docker/cli/cli/command/formatter" -) - -const ( - defaultTrustTagTableFormat = "table {{.SignedTag}}\t{{.Digest}}\t{{.Signers}}" - signedTagNameHeader = "SIGNED TAG" - trustedDigestHeader = "DIGEST" - signersHeader = "SIGNERS" - defaultSignerInfoTableFormat = "table {{.Signer}}\t{{.Keys}}" - signerNameHeader = "SIGNER" - keysHeader = "KEYS" -) - -// SignedTagInfo represents all formatted information needed to describe a signed tag: -// Name: name of the signed tag -// Digest: hex encoded digest of the contents -// Signers: list of entities who signed the tag -type SignedTagInfo struct { - Name string - Digest string - Signers []string -} - -// SignerInfo represents all formatted information needed to describe a signer: -// Name: name of the signer role -// Keys: the keys associated with the signer -type SignerInfo struct { - Name string - Keys []string -} - -// NewTrustTagFormat returns a Format for rendering using a trusted tag Context -func NewTrustTagFormat() formatter.Format { - return defaultTrustTagTableFormat -} - -// NewSignerInfoFormat returns a Format for rendering a signer role info Context -func NewSignerInfoFormat() formatter.Format { - return defaultSignerInfoTableFormat -} - -// TagWrite writes the context -func TagWrite(ctx formatter.Context, signedTagInfoList []SignedTagInfo) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, signedTag := range signedTagInfoList { - if err := format(&trustTagContext{s: signedTag}); err != nil { - return err - } - } - return nil - } - trustTagCtx := trustTagContext{} - trustTagCtx.Header = formatter.SubHeaderContext{ - "SignedTag": signedTagNameHeader, - "Digest": trustedDigestHeader, - "Signers": signersHeader, - } - return ctx.Write(&trustTagCtx, render) -} - -type trustTagContext struct { - formatter.HeaderContext - s SignedTagInfo -} - -// SignedTag returns the name of the signed tag -func (c *trustTagContext) SignedTag() string { - return c.s.Name -} - -// Digest returns the hex encoded digest associated with this signed tag -func (c *trustTagContext) Digest() string { - return c.s.Digest -} - -// Signers returns the sorted list of entities who signed this tag -func (c *trustTagContext) Signers() string { - sort.Strings(c.s.Signers) - return strings.Join(c.s.Signers, ", ") -} - -// SignerInfoWrite writes the context -func SignerInfoWrite(ctx formatter.Context, signerInfoList []SignerInfo) error { - render := func(format func(subContext formatter.SubContext) error) error { - for _, signerInfo := range signerInfoList { - if err := format(&signerInfoContext{ - trunc: ctx.Trunc, - s: signerInfo, - }); err != nil { - return err - } - } - return nil - } - signerInfoCtx := signerInfoContext{} - signerInfoCtx.Header = formatter.SubHeaderContext{ - "Signer": signerNameHeader, - "Keys": keysHeader, - } - return ctx.Write(&signerInfoCtx, render) -} - -type signerInfoContext struct { - formatter.HeaderContext - trunc bool - s SignerInfo -} - -// Keys returns the sorted list of keys associated with the signer -func (c *signerInfoContext) Keys() string { - sort.Strings(c.s.Keys) - truncatedKeys := []string{} - if c.trunc { - for _, keyID := range c.s.Keys { - truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID)) - } - return strings.Join(truncatedKeys, ", ") - } - return strings.Join(c.s.Keys, ", ") -} - -// Signer returns the name of the signer -func (c *signerInfoContext) Signer() string { - return c.s.Name -} diff --git a/cli/command/trust/formatter_test.go b/cli/command/trust/formatter_test.go deleted file mode 100644 index 4c0c194f4c2a..000000000000 --- a/cli/command/trust/formatter_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package trust - -import ( - "bytes" - "testing" - - "github.com/docker/cli/cli/command/formatter" - "github.com/docker/cli/internal/test" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestTrustTag(t *testing.T) { - digest := test.RandomID() - trustedTag := "tag" - - var ctx trustTagContext - - cases := []struct { - trustTagCtx trustTagContext - expValue string - call func() string - }{ - { - trustTagContext{ - s: SignedTagInfo{ - Name: trustedTag, - Digest: digest, - Signers: nil, - }, - }, - digest, - ctx.Digest, - }, - { - trustTagContext{ - s: SignedTagInfo{ - Name: trustedTag, - Digest: digest, - Signers: nil, - }, - }, - trustedTag, - ctx.SignedTag, - }, - // Empty signers makes a row with empty string - { - trustTagContext{ - s: SignedTagInfo{ - Name: trustedTag, - Digest: digest, - Signers: nil, - }, - }, - "", - ctx.Signers, - }, - { - trustTagContext{ - s: SignedTagInfo{ - Name: trustedTag, - Digest: digest, - Signers: []string{"alice", "bob", "claire"}, - }, - }, - "alice, bob, claire", - ctx.Signers, - }, - // alphabetic signing on Signers - { - trustTagContext{ - s: SignedTagInfo{ - Name: trustedTag, - Digest: digest, - Signers: []string{"claire", "bob", "alice"}, - }, - }, - "alice, bob, claire", - ctx.Signers, - }, - } - - for _, c := range cases { - ctx = c.trustTagCtx - v := c.call() - if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) - } - } -} - -func TestTrustTagContextWrite(t *testing.T) { - cases := []struct { - context formatter.Context - expected string - }{ - // Errors - { - formatter.Context{ - Format: "{{InvalidFunction}}", - }, - `template parsing error: template: :1: function "InvalidFunction" not defined`, - }, - { - formatter.Context{ - Format: "{{nil}}", - }, - `template parsing error: template: :1:2: executing "" at : nil is not a command`, - }, - // Table Format - { - formatter.Context{ - Format: NewTrustTagFormat(), - }, - `SIGNED TAG DIGEST SIGNERS -tag1 deadbeef alice -tag2 aaaaaaaa alice, bob -tag3 bbbbbbbb -`, - }, - } - - signedTags := []SignedTagInfo{ - {Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}}, - {Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}}, - {Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}}, - } - - for _, tc := range cases { - t.Run(string(tc.context.Format), func(t *testing.T) { - var out bytes.Buffer - tc.context.Output = &out - - if err := TagWrite(tc.context, signedTags); err != nil { - assert.Error(t, err, tc.expected) - } else { - assert.Equal(t, out.String(), tc.expected) - } - }) - } -} - -// With no trust data, the TagWrite will print an empty table: -// it's up to the caller to decide whether or not to print this versus an error -func TestTrustTagContextEmptyWrite(t *testing.T) { - emptyCase := struct { - context formatter.Context - expected string - }{ - formatter.Context{ - Format: NewTrustTagFormat(), - }, - `SIGNED TAG DIGEST SIGNERS -`, - } - - emptySignedTags := []SignedTagInfo{} - out := bytes.NewBufferString("") - emptyCase.context.Output = out - err := TagWrite(emptyCase.context, emptySignedTags) - assert.NilError(t, err) - assert.Check(t, is.Equal(emptyCase.expected, out.String())) -} - -func TestSignerInfoContextEmptyWrite(t *testing.T) { - emptyCase := struct { - context formatter.Context - expected string - }{ - formatter.Context{ - Format: NewSignerInfoFormat(), - }, - `SIGNER KEYS -`, - } - emptySignerInfo := []SignerInfo{} - out := bytes.NewBufferString("") - emptyCase.context.Output = out - err := SignerInfoWrite(emptyCase.context, emptySignerInfo) - assert.NilError(t, err) - assert.Check(t, is.Equal(emptyCase.expected, out.String())) -} - -func TestSignerInfoContextWrite(t *testing.T) { - cases := []struct { - context formatter.Context - expected string - }{ - // Errors - { - formatter.Context{ - Format: "{{InvalidFunction}}", - }, - `template parsing error: template: :1: function "InvalidFunction" not defined`, - }, - { - formatter.Context{ - Format: "{{nil}}", - }, - `template parsing error: template: :1:2: executing "" at : nil is not a command`, - }, - // Table Format - { - formatter.Context{ - Format: NewSignerInfoFormat(), - Trunc: true, - }, - `SIGNER KEYS -alice key11, key12 -bob key21 -eve foobarbazqux, key31, key32 -`, - }, - // No truncation - { - formatter.Context{ - Format: NewSignerInfoFormat(), - }, - `SIGNER KEYS -alice key11, key12 -bob key21 -eve foobarbazquxquux, key31, key32 -`, - }, - } - - signerInfo := []SignerInfo{ - {Name: "alice", Keys: []string{"key11", "key12"}}, - {Name: "bob", Keys: []string{"key21"}}, - {Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}}, - } - for _, tc := range cases { - t.Run(string(tc.context.Format), func(t *testing.T) { - var out bytes.Buffer - tc.context.Output = &out - - if err := SignerInfoWrite(tc.context, signerInfo); err != nil { - assert.Error(t, err, tc.expected) - } else { - assert.Equal(t, out.String(), tc.expected) - } - }) - } -} diff --git a/cli/command/trust/helpers.go b/cli/command/trust/helpers.go deleted file mode 100644 index 9f4e58b8b791..000000000000 --- a/cli/command/trust/helpers.go +++ /dev/null @@ -1,55 +0,0 @@ -package trust - -import ( - "strings" - - "github.com/docker/cli/cli/trust" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/tuf/data" -) - -const ( - releasedRoleName = "Repo Admin" - releasesRoleTUFName = "targets/releases" -) - -// isReleasedTarget checks if a role name is "released": -// either targets/releases or targets TUF roles -func isReleasedTarget(role data.RoleName) bool { - return role == data.CanonicalTargetsRole || role == trust.ReleasesRole -} - -// notaryRoleToSigner converts TUF role name to a human-understandable signer name -func notaryRoleToSigner(tufRole data.RoleName) string { - // don't show a signer for "targets" or "targets/releases" - if isReleasedTarget(data.RoleName(tufRole.String())) { - return releasedRoleName - } - return strings.TrimPrefix(tufRole.String(), "targets/") -} - -// clearChangeList clears the notary staging changelist. -func clearChangeList(notaryRepo client.Repository) error { - cl, err := notaryRepo.GetChangelist() - if err != nil { - return err - } - return cl.Clear("") -} - -// getOrGenerateRootKeyAndInitRepo initializes the notary repository -// with a remotely managed snapshot key. The initialization will use -// an existing root key if one is found, else a new one will be generated. -func getOrGenerateRootKeyAndInitRepo(notaryRepo client.Repository) error { - rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) - if err != nil { - return err - } - return notaryRepo.Initialize([]string{rootKey.ID()}, data.CanonicalSnapshotRole) -} - -const testPass = "password" - -func testPassRetriever(string, string, bool, int) (string, bool, error) { - return testPass, false, nil -} diff --git a/cli/command/trust/inspect.go b/cli/command/trust/inspect.go deleted file mode 100644 index 18705f519c12..000000000000 --- a/cli/command/trust/inspect.go +++ /dev/null @@ -1,119 +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.23 - -package trust - -import ( - "context" - "encoding/json" - "fmt" - "sort" - - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/inspect" - "github.com/spf13/cobra" - "github.com/theupdateframework/notary/tuf/data" -) - -type inspectOptions struct { - remotes []string - // FIXME(n4ss): this is consistent with `docker service inspect` but we should provide - // a `--format` flag too. (format and pretty-print should be exclusive) - prettyPrint bool -} - -func newInspectCommand(dockerCli command.Cli) *cobra.Command { - options := inspectOptions{} - cmd := &cobra.Command{ - Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]", - Short: "Return low-level information about keys and signatures", - Args: cli.RequiresMinArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.remotes = args - - return runInspect(cmd.Context(), dockerCli, options) - }, - } - - flags := cmd.Flags() - flags.BoolVar(&options.prettyPrint, "pretty", false, "Print the information in a human friendly format") - - return cmd -} - -func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { - if opts.prettyPrint { - var err error - - for index, remote := range opts.remotes { - if err = prettyPrintTrustInfo(ctx, dockerCLI, remote); err != nil { - return err - } - - // Additional separator between the inspection output of each image - if index < len(opts.remotes)-1 { - _, _ = fmt.Fprint(dockerCLI.Out(), "\n\n") - } - } - - return err - } - - getRefFunc := func(ref string) (any, []byte, error) { - i, err := getRepoTrustInfo(ctx, dockerCLI, ref) - return nil, i, err - } - return inspect.Inspect(dockerCLI.Out(), opts.remotes, "", getRefFunc) -} - -func getRepoTrustInfo(ctx context.Context, dockerCLI command.Cli, remote string) ([]byte, error) { - signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(ctx, dockerCLI, remote) - if err != nil { - return []byte{}, err - } - // process the signatures to include repo admin if signed by the base targets role - for idx, sig := range signatureRows { - if len(sig.Signers) == 0 { - signatureRows[idx].Signers = []string{releasedRoleName} - } - } - - signerList, adminList := []trustSigner{}, []trustSigner{} - - signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) - - for signerName, signerKeys := range signerRoleToKeyIDs { - signerKeyList := []trustKey{} - for _, keyID := range signerKeys { - signerKeyList = append(signerKeyList, trustKey{ID: keyID}) - } - signerList = append(signerList, trustSigner{signerName, signerKeyList}) - } - sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name }) - - for _, adminRole := range adminRolesWithSigs { - switch adminRole.Name { - case data.CanonicalRootRole: - rootKeys := []trustKey{} - for _, keyID := range adminRole.KeyIDs { - rootKeys = append(rootKeys, trustKey{ID: keyID}) - } - adminList = append(adminList, trustSigner{"Root", rootKeys}) - case data.CanonicalTargetsRole: - targetKeys := []trustKey{} - for _, keyID := range adminRole.KeyIDs { - targetKeys = append(targetKeys, trustKey{ID: keyID}) - } - adminList = append(adminList, trustSigner{"Repository", targetKeys}) - } - } - sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) - - return json.Marshal(trustRepo{ - Name: remote, - SignedTags: signatureRows, - Signers: signerList, - AdministrativeKeys: adminList, - }) -} diff --git a/cli/command/trust/key.go b/cli/command/trust/key.go deleted file mode 100644 index f57b44c771bb..000000000000 --- a/cli/command/trust/key.go +++ /dev/null @@ -1,22 +0,0 @@ -package trust - -import ( - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" -) - -// newTrustKeyCommand returns a cobra command for `trust key` subcommands -func newTrustKeyCommand(dockerCli command.Streams) *cobra.Command { - cmd := &cobra.Command{ - Use: "key", - Short: "Manage keys for signing Docker images", - Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), - } - cmd.AddCommand( - newKeyGenerateCommand(dockerCli), - newKeyLoadCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/trust/revoke_test.go b/cli/command/trust/revoke_test.go deleted file mode 100644 index da1e48ecf876..000000000000 --- a/cli/command/trust/revoke_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package trust - -import ( - "context" - "io" - "testing" - - "github.com/docker/cli/cli/trust" - "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" - "github.com/theupdateframework/notary/client" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/golden" -) - -func TestTrustRevokeCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires 1 argument", - }, - { - name: "too-many-args", - args: []string{"remote1", "remote2"}, - expectedError: "requires 1 argument", - }, - { - name: "sha-reference", - args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, - expectedError: "invalid repository name", - }, - { - name: "invalid-img-reference", - args: []string{"ALPINE"}, - expectedError: "invalid reference format", - }, - { - name: "digest-reference", - args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, - expectedError: "cannot use a digest reference for IMAGE:TAG", - }, - } - for _, tc := range testCases { - cmd := newRevokeCommand( - test.NewFakeCli(&fakeClient{})) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestTrustRevokeCommand(t *testing.T) { - revokeCancelledError := "trust revoke has been cancelled" - - testCases := []struct { - doc string - notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error) - args []string - expectedErr string - expectedMessage string - }{ - { - doc: "OfflineErrors_Confirm", - notaryRepository: notary.GetOfflineNotaryRepository, - args: []string{"reg-name.io/image"}, - expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", - expectedErr: revokeCancelledError, - }, - { - doc: "OfflineErrors_Offline", - notaryRepository: notary.GetOfflineNotaryRepository, - args: []string{"reg-name.io/image", "-y"}, - expectedErr: "could not remove signature for reg-name.io/image: client is offline", - }, - { - doc: "OfflineErrors_WithTag_Offline", - notaryRepository: notary.GetOfflineNotaryRepository, - args: []string{"reg-name.io/image:tag"}, - expectedErr: "could not remove signature for reg-name.io/image:tag: client is offline", - }, - { - doc: "UninitializedErrors_Confirm", - notaryRepository: notary.GetUninitializedNotaryRepository, - args: []string{"reg-name.io/image"}, - expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", - expectedErr: revokeCancelledError, - }, - { - doc: "UninitializedErrors_NoTrustData", - notaryRepository: notary.GetUninitializedNotaryRepository, - args: []string{"reg-name.io/image", "-y"}, - expectedErr: "could not remove signature for reg-name.io/image: does not have trust data for", - }, - { - doc: "UninitializedErrors_WithTag_NoTrustData", - notaryRepository: notary.GetUninitializedNotaryRepository, - args: []string{"reg-name.io/image:tag"}, - expectedErr: "could not remove signature for reg-name.io/image:tag: does not have trust data for", - }, - { - doc: "EmptyNotaryRepo_Confirm", - notaryRepository: notary.GetEmptyTargetsNotaryRepository, - args: []string{"reg-name.io/image"}, - expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", - expectedErr: revokeCancelledError, - }, - { - doc: "EmptyNotaryRepo_NoSignedTags", - notaryRepository: notary.GetEmptyTargetsNotaryRepository, - args: []string{"reg-name.io/image", "-y"}, - expectedErr: "could not remove signature for reg-name.io/image: no signed tags to remove", - }, - { - doc: "EmptyNotaryRepo_NoValidTrustData", - notaryRepository: notary.GetEmptyTargetsNotaryRepository, - args: []string{"reg-name.io/image:tag"}, - expectedErr: "could not remove signature for reg-name.io/image:tag: No valid trust data for tag", - }, - { - doc: "AllSigConfirmation", - notaryRepository: notary.GetEmptyTargetsNotaryRepository, - args: []string{"alpine"}, - expectedMessage: "Confirm you would like to delete all signature data for alpine? [y/N] ", - expectedErr: revokeCancelledError, - }, - } - - for _, tc := range testCases { - t.Run(tc.doc, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - cli.SetNotaryClient(tc.notaryRepository) - cmd := newRevokeCommand(cli) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - if tc.expectedErr != "" { - assert.ErrorContains(t, cmd.Execute(), tc.expectedErr) - } else { - assert.NilError(t, cmd.Execute()) - } - assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage)) - }) - } -} - -func TestRevokeTrustPromptTermination(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - cli := test.NewFakeCli(&fakeClient{}) - cmd := newRevokeCommand(cli) - cmd.SetArgs([]string{"example/trust-demo"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - test.TerminatePrompt(ctx, t, cmd, cli) - golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden") -} diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go deleted file mode 100644 index 077620155aee..000000000000 --- a/cli/command/trust/sign.go +++ /dev/null @@ -1,267 +0,0 @@ -package trust - -import ( - "context" - "fmt" - "io" - "path" - "sort" - "strings" - - "github.com/distribution/reference" - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" - imagetypes "github.com/docker/docker/api/types/image" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/client" - "github.com/pkg/errors" - "github.com/spf13/cobra" - notaryclient "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/tuf/data" -) - -type signOptions struct { - local bool - imageName string -} - -func newSignCommand(dockerCLI command.Cli) *cobra.Command { - options := signOptions{} - cmd := &cobra.Command{ - Use: "sign IMAGE:TAG", - Short: "Sign an image", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - options.imageName = args[0] - return runSignImage(cmd.Context(), dockerCLI, options) - }, - } - flags := cmd.Flags() - flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image") - return cmd -} - -func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOptions) error { - imageName := options.imageName - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), imageName) - if err != nil { - return err - } - if err := validateTag(imgRefAndAuth); err != nil { - return err - } - - notaryRepo, err := newNotaryClient(dockerCLI, imgRefAndAuth, trust.ActionsPushAndPull) - if err != nil { - return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) - } - if err = clearChangeList(notaryRepo); err != nil { - return err - } - defer clearChangeList(notaryRepo) - - // get the latest repository metadata so we can figure out which roles to sign - if _, err = notaryRepo.ListTargets(); err != nil { - switch err.(type) { - case notaryclient.ErrRepoNotInitialized, notaryclient.ErrRepositoryNotExist: - // before initializing a new repo, check that the image exists locally: - if err := checkLocalImageExistence(ctx, dockerCLI.Client(), imageName); err != nil { - return err - } - - userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username)) - if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil { - return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) - } - - _, _ = fmt.Fprintln(dockerCLI.Out(), "Created signer:", imgRefAndAuth.AuthConfig().Username) - _, _ = fmt.Fprintln(dockerCLI.Out(), "Finished initializing signed repository for", imageName) - default: - return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err) - } - } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCLI.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCLI, imgRefAndAuth.RepoInfo().Index, "push") - } - target, err := createTarget(notaryRepo, imgRefAndAuth.Tag()) - if err != nil || options.local { - switch err := err.(type) { - // If the error is nil then the local flag is set - case notaryclient.ErrNoSuchTarget, notaryclient.ErrRepositoryNotExist, nil: - // Fail fast if the image doesn't exist locally - if err := checkLocalImageExistence(ctx, dockerCLI.Client(), imageName); err != nil { - return err - } - _, _ = fmt.Fprintf(dockerCLI.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName) - - authConfig := command.ResolveAuthConfig(dockerCLI.ConfigFile(), imgRefAndAuth.RepoInfo().Index) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) - if err != nil { - return err - } - responseBody, err := dockerCLI.Client().ImagePush(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), imagetypes.PushOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - }) - if err != nil { - return err - } - defer responseBody.Close() - return trust.PushTrustedReference(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), authConfig, responseBody, command.UserAgent()) - default: - return err - } - } - return signAndPublishToTarget(dockerCLI.Out(), imgRefAndAuth, notaryRepo, target) -} - -func signAndPublishToTarget(out io.Writer, imgRefAndAuth trust.ImageRefAndAuth, notaryRepo notaryclient.Repository, target notaryclient.Target) error { - tag := imgRefAndAuth.Tag() - _, _ = fmt.Fprintln(out, "Signing and pushing trust metadata for", imgRefAndAuth.Name()) - existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag) - if err != nil { - return err - } - err = trust.AddToAllSignableRoles(notaryRepo, &target) - if err == nil { - prettyPrintExistingSignatureInfo(out, existingSigInfo) - err = notaryRepo.Publish() - } - if err != nil { - return errors.Wrapf(err, "failed to sign %s:%s", imgRefAndAuth.RepoInfo().Name.Name(), tag) - } - _, _ = fmt.Fprintf(out, "Successfully signed %s:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag) - return nil -} - -func validateTag(imgRefAndAuth trust.ImageRefAndAuth) error { - tag := imgRefAndAuth.Tag() - if tag == "" { - if imgRefAndAuth.Digest() != "" { - return errors.New("cannot use a digest reference for IMAGE:TAG") - } - return fmt.Errorf("no tag specified for %s", imgRefAndAuth.Name()) - } - return nil -} - -func checkLocalImageExistence(ctx context.Context, apiClient client.APIClient, imageName string) error { - _, err := apiClient.ImageInspect(ctx, imageName) - return err -} - -func createTarget(notaryRepo notaryclient.Repository, tag string) (notaryclient.Target, error) { - target := ¬aryclient.Target{} - var err error - if tag == "" { - return *target, errors.New("no tag specified") - } - target.Name = tag - target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag) - return *target, err -} - -func getSignedManifestHashAndSize(notaryRepo notaryclient.Repository, tag string) (data.Hashes, int64, error) { - targets, err := notaryRepo.GetAllTargetMetadataByName(tag) - if err != nil { - return nil, 0, err - } - return getReleasedTargetHashAndSize(targets, tag) -} - -func getReleasedTargetHashAndSize(targets []notaryclient.TargetSignedStruct, tag string) (data.Hashes, int64, error) { - for _, tgt := range targets { - if isReleasedTarget(tgt.Role.Name) { - return tgt.Target.Hashes, tgt.Target.Length, nil - } - } - return nil, 0, notaryclient.ErrNoSuchTarget(tag) -} - -func getExistingSignatureInfoForReleasedTag(notaryRepo notaryclient.Repository, tag string) (trustTagRow, error) { - targets, err := notaryRepo.GetAllTargetMetadataByName(tag) - if err != nil { - return trustTagRow{}, err - } - releasedTargetInfoList := matchReleasedSignatures(targets) - if len(releasedTargetInfoList) == 0 { - return trustTagRow{}, nil - } - return releasedTargetInfoList[0], nil -} - -func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) { - sort.Strings(existingSigInfo.Signers) - joinedSigners := strings.Join(existingSigInfo.Signers, ", ") - _, _ = fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners) -} - -func initNotaryRepoWithSigners(notaryRepo notaryclient.Repository, newSigner data.RoleName) error { - rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) - if err != nil { - return err - } - rootKeyID := rootKey.ID() - - // Initialize the notary repository with a remotely managed snapshot key - if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return err - } - - signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner) - if err != nil { - return err - } - if err := addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey}); err != nil { - return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSigner.String(), "targets/")) - } - - return notaryRepo.Publish() -} - -// generates an ECDSA key without a GUN for the specified role -func getOrGenerateNotaryKey(notaryRepo notaryclient.Repository, role data.RoleName) (data.PublicKey, error) { - // use the signer name in the PEM headers if this is a delegation key - if data.IsDelegation(role) { - role = data.RoleName(notaryRoleToSigner(role)) - } - keys := notaryRepo.GetCryptoService().ListKeys(role) - var err error - var key data.PublicKey - // always select the first key by ID - if len(keys) > 0 { - sort.Strings(keys) - keyID := keys[0] - privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID) - if err != nil { - return nil, err - } - key = data.PublicKeyFromPrivate(privKey) - } else { - key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey) - if err != nil { - return nil, err - } - } - return key, nil -} - -// stages changes to add a signer with the specified name and key(s). Adds to targets/ and targets/releases -func addStagedSigner(notaryRepo notaryclient.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) error { - // create targets/ - if err := notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys); err != nil { - return err - } - if err := notaryRepo.AddDelegationPaths(newSigner, []string{""}); err != nil { - return err - } - - // create targets/releases - if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil { - return err - } - return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}) -} diff --git a/cli/command/trust/sign_test.go b/cli/command/trust/sign_test.go deleted file mode 100644 index e24ffbba73cb..000000000000 --- a/cli/command/trust/sign_test.go +++ /dev/null @@ -1,282 +0,0 @@ -package trust - -import ( - "bytes" - "encoding/json" - "io" - "runtime" - "testing" - - "github.com/docker/cli/cli/config" - "github.com/docker/cli/cli/trust" - "github.com/docker/cli/internal/test" - notaryfake "github.com/docker/cli/internal/test/notary" - "github.com/theupdateframework/notary" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/client/changelist" - "github.com/theupdateframework/notary/trustpinning" - "github.com/theupdateframework/notary/tuf/data" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/skip" -) - -func TestTrustSignCommandErrors(t *testing.T) { - testCases := []struct { - name string - args []string - expectedError string - }{ - { - name: "not-enough-args", - expectedError: "requires 1 argument", - }, - { - name: "too-many-args", - args: []string{"image", "tag"}, - expectedError: "requires 1 argument", - }, - { - name: "sha-reference", - args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, - expectedError: "invalid repository name", - }, - { - name: "invalid-img-reference", - args: []string{"ALPINE:latest"}, - expectedError: "invalid reference format", - }, - { - name: "no-tag", - args: []string{"reg/img"}, - expectedError: "no tag specified for reg/img", - }, - { - name: "digest-reference", - args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, - expectedError: "cannot use a digest reference for IMAGE:TAG", - }, - } - // change to a tmpdir - config.SetDir(t.TempDir()) - for _, tc := range testCases { - cmd := newSignCommand( - test.NewFakeCli(&fakeClient{})) - cmd.SetArgs(tc.args) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), tc.expectedError) - } -} - -func TestTrustSignCommandOfflineErrors(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) - cmd := newSignCommand(cli) - cmd.SetArgs([]string{"reg-name.io/image:tag"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), "client is offline") -} - -func TestGetOrGenerateNotaryKey(t *testing.T) { - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - - // repo is empty, try making a root key - rootKeyA, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) - assert.NilError(t, err) - assert.Check(t, rootKeyA != nil) - - // we should only have one newly generated key - allKeys := notaryRepo.GetCryptoService().ListAllKeys() - assert.Check(t, is.Len(allKeys, 1)) - assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyA.ID()) != nil) - - // this time we should get back the same key if we ask for another root key - rootKeyB, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) - assert.NilError(t, err) - assert.Check(t, rootKeyB != nil) - - // we should only have one newly generated key - allKeys = notaryRepo.GetCryptoService().ListAllKeys() - assert.Check(t, is.Len(allKeys, 1)) - assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyB.ID()) != nil) - - // The key we retrieved should be identical to the one we generated - assert.Check(t, is.DeepEqual(rootKeyA.Public(), rootKeyB.Public())) - - // Now also try with a delegation key - releasesKey, err := getOrGenerateNotaryKey(notaryRepo, trust.ReleasesRole) - assert.NilError(t, err) - assert.Check(t, releasesKey != nil) - - // we should now have two keys - allKeys = notaryRepo.GetCryptoService().ListAllKeys() - assert.Check(t, is.Len(allKeys, 2)) - assert.Check(t, notaryRepo.GetCryptoService().GetKey(releasesKey.ID()) != nil) - // The key we retrieved should be identical to the one we generated - assert.Check(t, releasesKey != rootKeyA) - assert.Check(t, releasesKey != rootKeyB) -} - -func TestAddStageSigners(t *testing.T) { - skip.If(t, runtime.GOOS == "windows", "FIXME: not supported currently") - - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - - // stage targets/user - userRole := data.RoleName("targets/user") - userKey := data.NewPublicKey("algoA", []byte("a")) - err = addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey}) - assert.NilError(t, err) - // check the changelist for four total changes: two on targets/releases and two on targets/user - cl, err := notaryRepo.GetChangelist() - assert.NilError(t, err) - changeList := cl.List() - assert.Check(t, is.Len(changeList, 4)) - // ordering is deterministic: - - // first change is for targets/user key creation - newSignerKeyChange := changeList[0] - expectedJSON, err := json.Marshal(&changelist.TUFDelegation{ - NewThreshold: notary.MinThreshold, - AddKeys: data.KeyList([]data.PublicKey{userKey}), - }) - assert.NilError(t, err) - expectedChange := changelist.NewTUFChange( - changelist.ActionCreate, - userRole, - changelist.TypeTargetsDelegation, - "", // no path for delegations - expectedJSON, - ) - assert.Check(t, is.DeepEqual(expectedChange, newSignerKeyChange)) - - // second change is for targets/user getting all paths - newSignerPathsChange := changeList[1] - expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ - AddPaths: []string{""}, - }) - assert.NilError(t, err) - expectedChange = changelist.NewTUFChange( - changelist.ActionCreate, - userRole, - changelist.TypeTargetsDelegation, - "", // no path for delegations - expectedJSON, - ) - assert.Check(t, is.DeepEqual(expectedChange, newSignerPathsChange)) - - releasesRole := data.RoleName("targets/releases") - - // third change is for targets/releases key creation - releasesKeyChange := changeList[2] - expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ - NewThreshold: notary.MinThreshold, - AddKeys: data.KeyList([]data.PublicKey{userKey}), - }) - assert.NilError(t, err) - expectedChange = changelist.NewTUFChange( - changelist.ActionCreate, - releasesRole, - changelist.TypeTargetsDelegation, - "", // no path for delegations - expectedJSON, - ) - assert.Check(t, is.DeepEqual(expectedChange, releasesKeyChange)) - - // fourth change is for targets/releases getting all paths - releasesPathsChange := changeList[3] - expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ - AddPaths: []string{""}, - }) - assert.NilError(t, err) - expectedChange = changelist.NewTUFChange( - changelist.ActionCreate, - releasesRole, - changelist.TypeTargetsDelegation, - "", // no path for delegations - expectedJSON, - ) - assert.Check(t, is.DeepEqual(expectedChange, releasesPathsChange)) -} - -func TestGetSignedManifestHashAndSize(t *testing.T) { - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - _, _, err = getSignedManifestHashAndSize(notaryRepo, "test") - assert.Error(t, err, "client is offline") -} - -func TestGetReleasedTargetHashAndSize(t *testing.T) { - oneReleasedTgt := []client.TargetSignedStruct{} - // make and append 3 non-released signatures on the "unreleased" target - unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}} - for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} { - oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt}) - } - _, _, err := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased") - assert.Error(t, err, "No valid trust data for unreleased") - releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}} - oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt}) - hash, _, _ := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased") - assert.Check(t, is.DeepEqual(data.Hashes{notary.SHA256: []byte("released-hash")}, hash)) -} - -func TestCreateTarget(t *testing.T) { - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - _, err = createTarget(notaryRepo, "") - assert.Error(t, err, "no tag specified") - _, err = createTarget(notaryRepo, "1") - assert.Error(t, err, "client is offline") -} - -func TestGetExistingSignatureInfoForReleasedTag(t *testing.T) { - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - _, err = getExistingSignatureInfoForReleasedTag(notaryRepo, "test") - assert.Error(t, err, "client is offline") -} - -func TestPrettyPrintExistingSignatureInfo(t *testing.T) { - buf := bytes.NewBuffer(nil) - signers := []string{"Bob", "Alice", "Carol"} - existingSig := trustTagRow{trustTagKey{"tagName", "abc123"}, signers} - prettyPrintExistingSignatureInfo(buf, existingSig) - - assert.Check(t, is.Contains(buf.String(), "Existing signatures for tag tagName digest abc123 from:\nAlice, Bob, Carol")) -} - -func TestSignCommandChangeListIsCleanedOnError(t *testing.T) { - tmpDir := t.TempDir() - - config.SetDir(tmpDir) - cli := test.NewFakeCli(&fakeClient{}) - cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) - cmd := newSignCommand(cli) - cmd.SetArgs([]string{"ubuntu:latest"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - err := cmd.Execute() - assert.Assert(t, err != nil) - - notaryRepo, err := client.NewFileCachedRepository(tmpDir, "docker.io/library/ubuntu", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - cl, err := notaryRepo.GetChangelist() - assert.NilError(t, err) - assert.Check(t, is.Equal(len(cl.List()), 0)) -} - -func TestSignCommandLocalFlag(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) - cmd := newSignCommand(cli) - cmd.SetArgs([]string{"--local", "reg-name.io/image:red"}) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - assert.ErrorContains(t, cmd.Execute(), "error contacting notary server: dial tcp: lookup reg-name.io") -} diff --git a/cli/command/trust/signer.go b/cli/command/trust/signer.go deleted file mode 100644 index 807ad6c955dd..000000000000 --- a/cli/command/trust/signer.go +++ /dev/null @@ -1,22 +0,0 @@ -package trust - -import ( - "github.com/docker/cli/cli" - "github.com/docker/cli/cli/command" - "github.com/spf13/cobra" -) - -// newTrustSignerCommand returns a cobra command for `trust signer` subcommands -func newTrustSignerCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ - Use: "signer", - Short: "Manage entities who can sign Docker images", - Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), - } - cmd.AddCommand( - newSignerAddCommand(dockerCli), - newSignerRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/cli/command/utils.go b/cli/command/utils.go index ab64ef8fc120..dcbeed2f7191 100644 --- a/cli/command/utils.go +++ b/cli/command/utils.go @@ -1,80 +1,30 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package command import ( - "context" - "io" + "errors" + "fmt" "os" "path/filepath" "strings" "github.com/docker/cli/cli/config" - "github.com/docker/cli/cli/streams" - "github.com/docker/cli/internal/prompt" - "github.com/docker/docker/api/types/filters" - "github.com/moby/sys/atomicwriter" - "github.com/pkg/errors" - "github.com/spf13/pflag" + "github.com/moby/moby/client" ) -// CopyToFile writes the content of the reader to the specified file -// -// Deprecated: use [atomicwriter.New]. -func CopyToFile(outfile string, r io.Reader) error { - writer, err := atomicwriter.New(outfile, 0o600) - if err != nil { - return err - } - defer writer.Close() - _, err = io.Copy(writer, r) - return err -} - -const ErrPromptTerminated = prompt.ErrTerminated - -// DisableInputEcho disables input echo on the provided streams.In. -// This is useful when the user provides sensitive information like passwords. -// The function returns a restore function that should be called to restore the -// terminal state. -func DisableInputEcho(ins *streams.In) (restore func() error, err error) { - return prompt.DisableInputEcho(ins) -} - -// PromptForInput requests input from the user. -// -// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is -// active, the prompt will return an empty string ("") with an ErrPromptTerminated error. -// When the prompt returns an error, the caller should propagate the error up -// the stack and close the io.Reader used for the prompt which will prevent the -// background goroutine from blocking indefinitely. -func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) { - return prompt.ReadInput(ctx, in, out, message) -} - -// PromptForConfirmation requests and checks confirmation from the user. -// This will display the provided message followed by ' [y/N] '. If the user -// input 'y' or 'Y' it returns true otherwise false. If no message is provided, -// "Are you sure you want to proceed? [y/N] " will be used instead. -// -// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is -// active, the prompt will return false with an ErrPromptTerminated error. -// When the prompt returns an error, the caller should propagate the error up -// the stack and close the io.Reader used for the prompt which will prevent the -// background goroutine from blocking indefinitely. -func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) { - return prompt.Confirm(ctx, ins, outs, message) -} - // PruneFilters merges prune filters specified in config.json with those specified -// as command-line flags. +// as command-line flags. It returns a deep copy of filters to prevent mutating +// the original. // // CLI label filters have precedence over those specified in config.json. If a // label filter specified as flag conflicts with a label defined in config.json // (i.e., "label=some-value" conflicts with "label!=some-value", and vice versa), // then the filter defined in config.json is omitted. -func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args { +func PruneFilters(dockerCLI config.Provider, filters client.Filters) client.Filters { + pruneFilters := filters.Clone() + cfg := dockerCLI.ConfigFile() if cfg == nil { return pruneFilters @@ -90,13 +40,13 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters. switch k { case "label": // "label != some-value" conflicts with "label = some-value" - if pruneFilters.ExactMatch("label!", v) { + if pruneFilters["label!"][v] { continue } pruneFilters.Add(k, v) case "label!": // "label != some-value" conflicts with "label = some-value" - if pruneFilters.ExactMatch("label", v) { + if pruneFilters["label"][v] { continue } pruneFilters.Add(k, v) @@ -108,18 +58,12 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters. return pruneFilters } -// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later. -func AddPlatformFlag(flags *pflag.FlagSet, target *string) { - flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") - _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) -} - // ValidateOutputPath validates the output paths of the "docker cp" command. func ValidateOutputPath(path string) error { dir := filepath.Dir(filepath.Clean(path)) if dir != "" && dir != "." { if _, err := os.Stat(dir); os.IsNotExist(err) { - return errors.Errorf("invalid output path: directory %q does not exist", dir) + return fmt.Errorf("invalid output path: directory %q does not exist", dir) } } // check whether `path` points to a regular file @@ -134,7 +78,7 @@ func ValidateOutputPath(path string) error { } if err := ValidateOutputPathFileMode(fileInfo.Mode()); err != nil { - return errors.Wrapf(err, "invalid output path: %q must be a directory or a regular file", path) + return fmt.Errorf("invalid output path: %q must be a directory or a regular file: %w", path, err) } } return nil diff --git a/cli/command/volume/client_test.go b/cli/command/volume/client_test.go index 81df85807660..f9b44d3e6c1f 100644 --- a/cli/command/volume/client_test.go +++ b/cli/command/volume/client_test.go @@ -3,51 +3,49 @@ package volume import ( "context" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) type fakeClient struct { client.Client - volumeCreateFunc func(volume.CreateOptions) (volume.Volume, error) - volumeInspectFunc func(volumeID string) (volume.Volume, error) - volumeListFunc func(filter filters.Args) (volume.ListResponse, error) + volumeCreateFunc func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) + volumeInspectFunc func(volumeID string) (client.VolumeInspectResult, error) + volumeListFunc func(client.VolumeListOptions) (client.VolumeListResult, error) volumeRemoveFunc func(volumeID string, force bool) error - volumePruneFunc func(filter filters.Args) (volume.PruneReport, error) + volumePruneFunc func(opts client.VolumePruneOptions) (client.VolumePruneResult, error) } -func (c *fakeClient) VolumeCreate(_ context.Context, options volume.CreateOptions) (volume.Volume, error) { +func (c *fakeClient) VolumeCreate(_ context.Context, options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { if c.volumeCreateFunc != nil { return c.volumeCreateFunc(options) } - return volume.Volume{}, nil + return client.VolumeCreateResult{}, nil } -func (c *fakeClient) VolumeInspect(_ context.Context, volumeID string) (volume.Volume, error) { +func (c *fakeClient) VolumeInspect(_ context.Context, volumeID string, options client.VolumeInspectOptions) (client.VolumeInspectResult, error) { if c.volumeInspectFunc != nil { return c.volumeInspectFunc(volumeID) } - return volume.Volume{}, nil + return client.VolumeInspectResult{}, nil } -func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) { +func (c *fakeClient) VolumeList(_ context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) { if c.volumeListFunc != nil { - return c.volumeListFunc(options.Filters) + return c.volumeListFunc(options) } - return volume.ListResponse{}, nil + return client.VolumeListResult{}, nil } -func (c *fakeClient) VolumesPrune(_ context.Context, filter filters.Args) (volume.PruneReport, error) { +func (c *fakeClient) VolumePrune(_ context.Context, opts client.VolumePruneOptions) (client.VolumePruneResult, error) { if c.volumePruneFunc != nil { - return c.volumePruneFunc(filter) + return c.volumePruneFunc(opts) } - return volume.PruneReport{}, nil + return client.VolumePruneResult{}, nil } -func (c *fakeClient) VolumeRemove(_ context.Context, volumeID string, force bool) error { +func (c *fakeClient) VolumeRemove(_ context.Context, volumeID string, options client.VolumeRemoveOptions) (client.VolumeRemoveResult, error) { if c.volumeRemoveFunc != nil { - return c.volumeRemoveFunc(volumeID, force) + return client.VolumeRemoveResult{}, c.volumeRemoveFunc(volumeID, options.Force) } - return nil + return client.VolumeRemoveResult{}, nil } diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go index 386352791e2f..61477d168740 100644 --- a/cli/command/volume/cmd.go +++ b/cli/command/volume/cmd.go @@ -3,25 +3,32 @@ package volume import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" "github.com/spf13/cobra" ) -// NewVolumeCommand returns a cobra command for `volume` subcommands -func NewVolumeCommand(dockerCli command.Cli) *cobra.Command { +func init() { + commands.Register(newVolumeCommand) +} + +// newVolumeCommand returns a cobra command for `volume` subcommands +func newVolumeCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "volume COMMAND", Short: "Manage volumes", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.21"}, + + DisableFlagsInUseLine: true, } cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - newUpdateCommand(dockerCli), + newCreateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPruneCommand(dockerCLI), + newUpdateCommand(dockerCLI), ) return cmd } diff --git a/cli/command/volume/create.go b/cli/command/volume/create.go index 8c13f65a74eb..aee5e29a1f56 100644 --- a/cli/command/volume/create.go +++ b/cli/command/volume/create.go @@ -9,9 +9,9 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -36,7 +36,7 @@ type createOptions struct { preferredTopology opts.ListOpts } -func newCreateCommand(dockerCli command.Cli) *cobra.Command { +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { options := createOptions{ driverOpts: *opts.NewMapOpts(nil, nil), labels: opts.NewListOpts(opts.ValidateLabel), @@ -57,9 +57,10 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { options.name = args[0] } options.cluster = hasClusterVolumeOptionSet(cmd.Flags()) - return runCreate(cmd.Context(), dockerCli, options) + return runCreate(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVarP(&options.driver, "driver", "d", "local", "Specify volume driver name") @@ -113,7 +114,7 @@ func hasClusterVolumeOptionSet(flags *pflag.FlagSet) bool { } func runCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error { - volOpts := volume.CreateOptions{ + volOpts := client.VolumeCreateOptions{ Driver: options.driver, DriverOpts: options.driverOpts.GetAll(), Name: options.name, @@ -195,11 +196,11 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options createOptions volOpts.ClusterVolumeSpec.AccessibilityRequirements = topology } - vol, err := dockerCli.Client().VolumeCreate(ctx, volOpts) + res, err := dockerCli.Client().VolumeCreate(ctx, volOpts) if err != nil { return err } - _, _ = fmt.Fprintln(dockerCli.Out(), vol.Name) + _, _ = fmt.Fprintln(dockerCli.Out(), res.Volume.Name) return nil } diff --git a/cli/command/volume/create_test.go b/cli/command/volume/create_test.go index 9984f835cb6a..cad6ee3f1cdf 100644 --- a/cli/command/volume/create_test.go +++ b/cli/command/volume/create_test.go @@ -10,7 +10,8 @@ import ( "testing" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -19,7 +20,7 @@ func TestVolumeCreateErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - volumeCreateFunc func(volume.CreateOptions) (volume.Volume, error) + volumeCreateFunc func(client.VolumeCreateOptions) (client.VolumeCreateResult, error) expectedError string }{ { @@ -34,8 +35,8 @@ func TestVolumeCreateErrors(t *testing.T) { expectedError: "requires at most 1 argument", }, { - volumeCreateFunc: func(createBody volume.CreateOptions) (volume.Volume, error) { - return volume.Volume{}, errors.New("error creating volume") + volumeCreateFunc: func(client.VolumeCreateOptions) (client.VolumeCreateResult, error) { + return client.VolumeCreateResult{}, errors.New("error creating volume") }, expectedError: "error creating volume", }, @@ -59,12 +60,12 @@ func TestVolumeCreateErrors(t *testing.T) { func TestVolumeCreateWithName(t *testing.T) { const name = "my-volume-name" cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volume.CreateOptions) (volume.Volume, error) { - if body.Name != name { - return volume.Volume{}, fmt.Errorf("expected name %q, got %q", name, body.Name) + volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { + if options.Name != name { + return client.VolumeCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Name) } - return volume.Volume{ - Name: body.Name, + return client.VolumeCreateResult{ + Volume: volume.Volume{Name: options.Name}, }, nil }, }) @@ -116,21 +117,23 @@ func TestVolumeCreateWithFlags(t *testing.T) { } cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volume.CreateOptions) (volume.Volume, error) { - if body.Name != "" { - return volume.Volume{}, fmt.Errorf("expected empty name, got %q", body.Name) + volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { + if options.Name != "" { + return client.VolumeCreateResult{}, fmt.Errorf("expected empty name, got %q", options.Name) } - if body.Driver != expectedDriver { - return volume.Volume{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) + if options.Driver != expectedDriver { + return client.VolumeCreateResult{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, options.Driver) } - if !reflect.DeepEqual(body.DriverOpts, expectedOpts) { - return volume.Volume{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) + if !reflect.DeepEqual(options.DriverOpts, expectedOpts) { + return client.VolumeCreateResult{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, options.DriverOpts) } - if !reflect.DeepEqual(body.Labels, expectedLabels) { - return volume.Volume{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) + if !reflect.DeepEqual(options.Labels, expectedLabels) { + return client.VolumeCreateResult{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, options.Labels) } - return volume.Volume{ - Name: name, + return client.VolumeCreateResult{ + Volume: volume.Volume{ + Name: name, + }, }, nil }, }) @@ -150,14 +153,14 @@ func TestVolumeCreateWithFlags(t *testing.T) { func TestVolumeCreateCluster(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volume.CreateOptions) (volume.Volume, error) { - if body.Driver == "csi" && body.ClusterVolumeSpec == nil { - return volume.Volume{}, errors.New("expected ClusterVolumeSpec, but none present") + volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { + if options.Driver == "csi" && options.ClusterVolumeSpec == nil { + return client.VolumeCreateResult{}, errors.New("expected ClusterVolumeSpec, but none present") } - if body.Driver == "notcsi" && body.ClusterVolumeSpec != nil { - return volume.Volume{}, errors.New("expected no ClusterVolumeSpec, but present") + if options.Driver == "notcsi" && options.ClusterVolumeSpec != nil { + return client.VolumeCreateResult{}, errors.New("expected no ClusterVolumeSpec, but present") } - return volume.Volume{}, nil + return client.VolumeCreateResult{}, nil }, }) @@ -185,7 +188,7 @@ func TestVolumeCreateCluster(t *testing.T) { } func TestVolumeCreateClusterOpts(t *testing.T) { - expectedBody := volume.CreateOptions{ + expectedOptions := client.VolumeCreateOptions{ Name: "name", Driver: "csi", DriverOpts: map[string]string{}, @@ -223,12 +226,12 @@ func TestVolumeCreateClusterOpts(t *testing.T) { } cli := test.NewFakeCli(&fakeClient{ - volumeCreateFunc: func(body volume.CreateOptions) (volume.Volume, error) { - sort.SliceStable(body.ClusterVolumeSpec.Secrets, func(i, j int) bool { - return body.ClusterVolumeSpec.Secrets[i].Key < body.ClusterVolumeSpec.Secrets[j].Key + volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { + sort.SliceStable(options.ClusterVolumeSpec.Secrets, func(i, j int) bool { + return options.ClusterVolumeSpec.Secrets[i].Key < options.ClusterVolumeSpec.Secrets[j].Key }) - assert.DeepEqual(t, body, expectedBody) - return volume.Volume{}, nil + assert.DeepEqual(t, options, expectedOptions) + return client.VolumeCreateResult{}, nil }, }) diff --git a/cli/command/volume/inspect.go b/cli/command/volume/inspect.go index 1105a052068b..9b06a3491dfd 100644 --- a/cli/command/volume/inspect.go +++ b/cli/command/volume/inspect.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package volume @@ -11,6 +11,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/inspect" flagsHelper "github.com/docker/cli/cli/flags" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -19,7 +20,7 @@ type inspectOptions struct { names []string } -func newInspectCommand(dockerCli command.Cli) *cobra.Command { +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -28,9 +29,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.names = args - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.VolumeNames(dockerCli), + ValidArgsFunction: completion.VolumeNames(dockerCLI), + DisableFlagsInUseLine: true, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -38,13 +40,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - - getVolFunc := func(name string) (any, []byte, error) { - i, err := client.VolumeInspect(ctx, name) - return i, nil, err - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() + return inspect.Inspect(dockerCLI.Out(), opts.names, opts.format, func(name string) (any, []byte, error) { + res, err := apiClient.VolumeInspect(ctx, name, client.VolumeInspectOptions{}) + return res.Volume, res.Raw, err + }) } diff --git a/cli/command/volume/inspect_test.go b/cli/command/volume/inspect_test.go index 91cfbd3306f6..e56d0a4fab53 100644 --- a/cli/command/volume/inspect_test.go +++ b/cli/command/volume/inspect_test.go @@ -8,8 +8,9 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -18,7 +19,7 @@ func TestVolumeInspectErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - volumeInspectFunc func(volumeID string) (volume.Volume, error) + volumeInspectFunc func(volumeID string) (client.VolumeInspectResult, error) expectedError string }{ { @@ -26,8 +27,8 @@ func TestVolumeInspectErrors(t *testing.T) { }, { args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (volume.Volume, error) { - return volume.Volume{}, errors.New("error while inspecting the volume") + volumeInspectFunc: func(volumeID string) (client.VolumeInspectResult, error) { + return client.VolumeInspectResult{}, errors.New("error while inspecting the volume") }, expectedError: "error while inspecting the volume", }, @@ -40,13 +41,15 @@ func TestVolumeInspectErrors(t *testing.T) { }, { args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (volume.Volume, error) { + volumeInspectFunc: func(volumeID string) (client.VolumeInspectResult, error) { if volumeID == "foo" { - return volume.Volume{ - Name: "foo", + return client.VolumeInspectResult{ + Volume: volume.Volume{ + Name: "foo", + }, }, nil } - return volume.Volume{}, errors.New("error while inspecting the volume") + return client.VolumeInspectResult{}, errors.New("error while inspecting the volume") }, expectedError: "error while inspecting the volume", }, @@ -71,25 +74,29 @@ func TestVolumeInspectWithoutFormat(t *testing.T) { testCases := []struct { name string args []string - volumeInspectFunc func(volumeID string) (volume.Volume, error) + volumeInspectFunc func(volumeID string) (client.VolumeInspectResult, error) }{ { name: "single-volume", args: []string{"foo"}, - volumeInspectFunc: func(volumeID string) (volume.Volume, error) { + volumeInspectFunc: func(volumeID string) (client.VolumeInspectResult, error) { if volumeID != "foo" { - return volume.Volume{}, fmt.Errorf("invalid volumeID, expected %s, got %s", "foo", volumeID) + return client.VolumeInspectResult{}, fmt.Errorf("invalid volumeID, expected %s, got %s", "foo", volumeID) } - return *builders.Volume(), nil + return client.VolumeInspectResult{ + Volume: builders.Volume(), + }, nil }, }, { name: "multiple-volume-with-labels", args: []string{"foo", "bar"}, - volumeInspectFunc: func(volumeID string) (volume.Volume, error) { - return *builders.Volume(builders.VolumeName(volumeID), builders.VolumeLabels(map[string]string{ - "foo": "bar", - })), nil + volumeInspectFunc: func(volumeID string) (client.VolumeInspectResult, error) { + return client.VolumeInspectResult{ + Volume: builders.Volume(builders.VolumeName(volumeID), builders.VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, nil }, }, } @@ -105,16 +112,18 @@ func TestVolumeInspectWithoutFormat(t *testing.T) { } func TestVolumeInspectWithFormat(t *testing.T) { - volumeInspectFunc := func(volumeID string) (volume.Volume, error) { - return *builders.Volume(builders.VolumeLabels(map[string]string{ - "foo": "bar", - })), nil + volumeInspectFunc := func(volumeID string) (client.VolumeInspectResult, error) { + return client.VolumeInspectResult{ + Volume: builders.Volume(builders.VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, nil } testCases := []struct { name string format string args []string - volumeInspectFunc func(volumeID string) (volume.Volume, error) + volumeInspectFunc func(volumeID string) (client.VolumeInspectResult, error) }{ { name: "simple-template", @@ -142,40 +151,72 @@ func TestVolumeInspectWithFormat(t *testing.T) { } func TestVolumeInspectCluster(t *testing.T) { - volumeInspectFunc := func(volumeID string) (volume.Volume, error) { - return volume.Volume{ - Name: "clustervolume", - Driver: "clusterdriver1", - Scope: "global", - ClusterVolume: &volume.ClusterVolume{ - ID: "fooid", - Meta: swarm.Meta{ - Version: swarm.Version{ - Index: uint64(123), - }, - }, - Spec: volume.ClusterVolumeSpec{ - Group: "group0", - AccessMode: &volume.AccessMode{ - Scope: volume.ScopeMultiNode, - Sharing: volume.SharingAll, - BlockVolume: &volume.TypeBlock{}, + volumeInspectFunc := func(volumeID string) (client.VolumeInspectResult, error) { + return client.VolumeInspectResult{ + Volume: volume.Volume{ + Name: "clustervolume", + Driver: "clusterdriver1", + Scope: "global", + ClusterVolume: &volume.ClusterVolume{ + ID: "fooid", + Meta: swarm.Meta{ + Version: swarm.Version{ + Index: uint64(123), + }, }, - AccessibilityRequirements: &volume.TopologyRequirement{ - Requisite: []volume.Topology{ - { - Segments: map[string]string{ - "region": "R1", - "zone": "Z1", + Spec: volume.ClusterVolumeSpec{ + Group: "group0", + AccessMode: &volume.AccessMode{ + Scope: volume.ScopeMultiNode, + Sharing: volume.SharingAll, + BlockVolume: &volume.TypeBlock{}, + }, + AccessibilityRequirements: &volume.TopologyRequirement{ + Requisite: []volume.Topology{ + { + Segments: map[string]string{ + "region": "R1", + "zone": "Z1", + }, + }, { + Segments: map[string]string{ + "region": "R1", + "zone": "Z2", + }, }, - }, { - Segments: map[string]string{ - "region": "R1", - "zone": "Z2", + }, + Preferred: []volume.Topology{ + { + Segments: map[string]string{ + "region": "R1", + "zone": "Z1", + }, }, }, }, - Preferred: []volume.Topology{ + CapacityRange: &volume.CapacityRange{ + RequiredBytes: 1000, + LimitBytes: 1000000, + }, + Secrets: []volume.Secret{ + { + Key: "secretkey1", + Secret: "mysecret1", + }, { + Key: "secretkey2", + Secret: "mysecret2", + }, + }, + Availability: volume.AvailabilityActive, + }, + Info: &volume.Info{ + CapacityBytes: 10000, + VolumeContext: map[string]string{ + "the": "context", + "has": "entries", + }, + VolumeID: "clusterdriver1volume1id", + AccessibleTopology: []volume.Topology{ { Segments: map[string]string{ "region": "R1", @@ -184,54 +225,24 @@ func TestVolumeInspectCluster(t *testing.T) { }, }, }, - CapacityRange: &volume.CapacityRange{ - RequiredBytes: 1000, - LimitBytes: 1000000, - }, - Secrets: []volume.Secret{ + PublishStatus: []*volume.PublishStatus{ { - Key: "secretkey1", - Secret: "mysecret1", + NodeID: "node1", + State: volume.StatePublished, + PublishContext: map[string]string{ + "some": "data", + "yup": "data", + }, }, { - Key: "secretkey2", - Secret: "mysecret2", - }, - }, - Availability: volume.AvailabilityActive, - }, - Info: &volume.Info{ - CapacityBytes: 10000, - VolumeContext: map[string]string{ - "the": "context", - "has": "entries", - }, - VolumeID: "clusterdriver1volume1id", - AccessibleTopology: []volume.Topology{ - { - Segments: map[string]string{ - "region": "R1", - "zone": "Z1", + NodeID: "node2", + State: volume.StatePendingNodeUnpublish, + PublishContext: map[string]string{ + "some": "more", + "publish": "context", }, }, }, }, - PublishStatus: []*volume.PublishStatus{ - { - NodeID: "node1", - State: volume.StatePublished, - PublishContext: map[string]string{ - "some": "data", - "yup": "data", - }, - }, { - NodeID: "node2", - State: volume.StatePendingNodeUnpublish, - PublishContext: map[string]string{ - "some": "more", - "publish": "context", - }, - }, - }, }, }, nil } diff --git a/cli/command/volume/list.go b/cli/command/volume/list.go index e51bfe5e1c6e..1e0df1f84b59 100644 --- a/cli/command/volume/list.go +++ b/cli/command/volume/list.go @@ -6,12 +6,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/volume" "github.com/fvbommel/sortorder" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -26,7 +25,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli command.Cli) *cobra.Command { +func newListCommand(dockerCLI command.Cli) *cobra.Command { options := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -35,9 +34,10 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List volumes", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runList(cmd.Context(), dockerCli, options) + return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -45,23 +45,23 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", `Provide filter values (e.g. "dangling=true")`) flags.BoolVar(&options.cluster, "cluster", false, "Display only cluster volumes, and use cluster volume list formatting") - flags.SetAnnotation("cluster", "version", []string{"1.42"}) - flags.SetAnnotation("cluster", "swarm", []string{"manager"}) + _ = flags.SetAnnotation("cluster", "version", []string{"1.42"}) + _ = flags.SetAnnotation("cluster", "swarm", []string{"manager"}) return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() - volumes, err := client.VolumeList(ctx, volume.ListOptions{Filters: options.filter.Value()}) +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() + res, err := apiClient.VolumeList(ctx, client.VolumeListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 && !options.cluster { - if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().VolumesFormat + if len(dockerCLI.ConfigFile().VolumesFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().VolumesFormat } else { format = formatter.TableFormatKey } @@ -71,13 +71,13 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er // trick for filtering in place n := 0 - for _, vol := range volumes.Volumes { + for _, vol := range res.Items { if vol.ClusterVolume != nil { - volumes.Volumes[n] = vol + res.Items[n] = vol n++ } } - volumes.Volumes = volumes.Volumes[:n] + res.Items = res.Items[:n] if !options.quiet { format = clusterTableFormat } else { @@ -85,13 +85,13 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er } } - sort.Slice(volumes.Volumes, func(i, j int) bool { - return sortorder.NaturalLess(volumes.Volumes[i].Name, volumes.Volumes[j].Name) + sort.Slice(res.Items, func(i, j int) bool { + return sortorder.NaturalLess(res.Items[i].Name, res.Items[j].Name) }) volumeCtx := formatter.Context{ - Output: dockerCli.Out(), + Output: dockerCLI.Out(), Format: formatter.NewVolumeFormat(format, options.quiet), } - return formatter.VolumeWrite(volumeCtx, volumes.Volumes) + return formatter.VolumeWrite(volumeCtx, res.Items) } diff --git a/cli/command/volume/list_test.go b/cli/command/volume/list_test.go index 8b2a8f3161cc..5535aafead5b 100644 --- a/cli/command/volume/list_test.go +++ b/cli/command/volume/list_test.go @@ -8,8 +8,8 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/builders" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) @@ -18,7 +18,7 @@ func TestVolumeListErrors(t *testing.T) { testCases := []struct { args []string flags map[string]string - volumeListFunc func(filter filters.Args) (volume.ListResponse, error) + volumeListFunc func(client.VolumeListOptions) (client.VolumeListResult, error) expectedError string }{ { @@ -26,8 +26,8 @@ func TestVolumeListErrors(t *testing.T) { expectedError: "accepts no argument", }, { - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{}, errors.New("error listing volumes") + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{}, errors.New("error listing volumes") }, expectedError: "error listing volumes", }, @@ -50,9 +50,9 @@ func TestVolumeListErrors(t *testing.T) { func TestVolumeListWithoutFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ builders.Volume(), builders.Volume(builders.VolumeName("foo"), builders.VolumeDriver("bar")), builders.Volume(builders.VolumeName("baz"), builders.VolumeLabels(map[string]string{ @@ -69,9 +69,9 @@ func TestVolumeListWithoutFormat(t *testing.T) { func TestVolumeListWithConfigFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ builders.Volume(), builders.Volume(builders.VolumeName("foo"), builders.VolumeDriver("bar")), builders.Volume(builders.VolumeName("baz"), builders.VolumeLabels(map[string]string{ @@ -91,9 +91,9 @@ func TestVolumeListWithConfigFormat(t *testing.T) { func TestVolumeListWithFormat(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ builders.Volume(), builders.Volume(builders.VolumeName("foo"), builders.VolumeDriver("bar")), builders.Volume(builders.VolumeName("baz"), builders.VolumeLabels(map[string]string{ @@ -111,9 +111,9 @@ func TestVolumeListWithFormat(t *testing.T) { func TestVolumeListSortOrder(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ builders.Volume(builders.VolumeName("volume-2-foo")), builders.Volume(builders.VolumeName("volume-10-foo")), builders.Volume(builders.VolumeName("volume-1-foo")), @@ -129,9 +129,9 @@ func TestVolumeListSortOrder(t *testing.T) { func TestClusterVolumeList(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ - volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) { - return volume.ListResponse{ - Volumes: []*volume.Volume{ + volumeListFunc: func(client.VolumeListOptions) (client.VolumeListResult, error) { + return client.VolumeListResult{ + Items: []volume.Volume{ { Name: "volume1", Scope: "global", diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index 0765e0228948..53c218203aaf 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -4,25 +4,33 @@ import ( "context" "errors" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/versions" "github.com/docker/go-units" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) +func init() { + // Register the prune command to run as part of "docker system prune" + if err := pruner.Register(pruner.TypeVolume, pruneFn); err != nil { + panic(err) + } +} + type pruneOptions struct { all bool force bool filter opts.FilterOpt } -// NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// newPruneCommand returns a new cobra prune command for volumes +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -30,18 +38,19 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused local volumes", Args: cli.NoArgs, 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 { return err } 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 }, - Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + Annotations: map[string]string{"version": "1.25"}, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, } flags := cmd.Flags() @@ -60,20 +69,15 @@ Are you sure you want to continue?` Are you sure you want to continue?` ) -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, _ error) { pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) warning := unusedVolumesWarning - if versions.GreaterThanOrEqualTo(dockerCli.CurrentVersion(), "1.42") { - if options.all { - if pruneFilters.Contains("all") { - return 0, "", invalidParamErr{errors.New("conflicting options: cannot specify both --all and --filter all=1")} - } - pruneFilters.Add("all", "true") - warning = allVolumesWarning + if options.all { + if _, ok := pruneFilters["all"]; ok { + return 0, "", invalidParamErr{errors.New("conflicting options: cannot specify both --all and --filter all=1")} } - } else { - // API < v1.42 removes all volumes (anonymous and named) by default. + pruneFilters.Add("all", "true") warning = allVolumesWarning } if !options.force { @@ -86,20 +90,23 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } } - report, err := dockerCli.Client().VolumesPrune(ctx, pruneFilters) + res, err := dockerCli.Client().VolumePrune(ctx, client.VolumePruneOptions{ + Filters: pruneFilters, + }) if err != nil { return 0, "", err } - if len(report.VolumesDeleted) > 0 { - output = "Deleted Volumes:\n" - for _, id := range report.VolumesDeleted { - output += id + "\n" + var out strings.Builder + if len(res.Report.VolumesDeleted) > 0 { + out.WriteString("Deleted Volumes:\n") + for _, id := range res.Report.VolumesDeleted { + out.WriteString(id + "\n") } - spaceReclaimed = report.SpaceReclaimed + spaceReclaimed = res.Report.SpaceReclaimed } - return spaceReclaimed, output, nil + return spaceReclaimed, out.String(), nil } type invalidParamErr struct{ error } @@ -110,8 +117,22 @@ type cancelledErr struct{ error } func (cancelledErr) Cancelled() {} -// RunPrune calls the Volume Prune API -// This returns the amount of space reclaimed and a detailed output string -func RunPrune(ctx context.Context, dockerCli command.Cli, _ bool, filter opts.FilterOpt) (uint64, string, error) { - return runPrune(ctx, dockerCli, pruneOptions{force: true, filter: filter}) +// pruneFn calls the Volume Prune API 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) { + // TODO version this once "until" filter is supported for volumes + // Ideally, this check wasn't done on the CLI because the list of + // filters that is supported by the daemon may evolve over time. + if _, ok := options.Filter.Value()["until"]; ok { + return 0, "", errors.New(`ERROR: The "until" filter is not supported with "--volumes"`) + } + if !options.Confirmed { + // Dry-run: perform validation and produce confirmation before pruning. + confirmMsg := "all anonymous volumes not used by at least one container" + return 0, confirmMsg, cancelledErr{errors.New("volume prune has been cancelled")} + } + return runPrune(ctx, dockerCli, pruneOptions{ + force: true, + filter: options.Filter, + }) } diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 11ee8adef879..cbce72d22199 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -11,8 +11,8 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -21,11 +21,11 @@ import ( func TestVolumePruneErrors(t *testing.T) { testCases := []struct { - name string - args []string - flags map[string]string - volumePruneFunc func(args filters.Args) (volume.PruneReport, error) - expectedError string + name string + args []string + flags map[string]string + pruneFunc func(client.VolumePruneOptions) (client.VolumePruneResult, error) + expectedError string }{ { name: "accepts no arguments", @@ -37,8 +37,8 @@ func TestVolumePruneErrors(t *testing.T) { flags: map[string]string{ "force": "true", }, - volumePruneFunc: func(args filters.Args) (volume.PruneReport, error) { - return volume.PruneReport{}, errors.New("error pruning volumes") + pruneFunc: func(opts client.VolumePruneOptions) (client.VolumePruneResult, error) { + return client.VolumePruneResult{}, errors.New("error pruning volumes") }, expectedError: "error pruning volumes", }, @@ -53,9 +53,9 @@ func TestVolumePruneErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewPruneCommand( + cmd := newPruneCommand( test.NewFakeCli(&fakeClient{ - volumePruneFunc: tc.volumePruneFunc, + volumePruneFunc: tc.pruneFunc, }), ) cmd.SetArgs(tc.args) @@ -71,41 +71,41 @@ func TestVolumePruneErrors(t *testing.T) { func TestVolumePruneSuccess(t *testing.T) { testCases := []struct { - name string - args []string - input string - volumePruneFunc func(args filters.Args) (volume.PruneReport, error) + name string + args []string + input string + pruneFunc func(client.VolumePruneOptions) (client.VolumePruneResult, error) }{ { name: "all", args: []string{"--all"}, input: "y", - volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) { - assert.Check(t, is.DeepEqual([]string{"true"}, pruneFilter.Get("all"))) - return volume.PruneReport{}, nil + pruneFunc: func(opts client.VolumePruneOptions) (client.VolumePruneResult, error) { + assert.Check(t, is.DeepEqual(opts.Filters["all"], map[string]bool{"true": true})) + return client.VolumePruneResult{}, nil }, }, { name: "all-forced", args: []string{"--all", "--force"}, - volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) { - return volume.PruneReport{}, nil + pruneFunc: func(opts client.VolumePruneOptions) (client.VolumePruneResult, error) { + return client.VolumePruneResult{}, nil }, }, { name: "label-filter", args: []string{"--filter", "label=foobar"}, input: "y", - volumePruneFunc: func(pruneFilter filters.Args) (volume.PruneReport, error) { - assert.Check(t, is.DeepEqual([]string{"foobar"}, pruneFilter.Get("label"))) - return volume.PruneReport{}, nil + pruneFunc: func(opts client.VolumePruneOptions) (client.VolumePruneResult, error) { + assert.Check(t, is.DeepEqual(opts.Filters["label"], map[string]bool{"foobar": true})) + return client.VolumePruneResult{}, nil }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.volumePruneFunc}) - cmd := NewPruneCommand(cli) + cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.pruneFunc}) + cmd := newPruneCommand(cli) if tc.input != "" { cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(tc.input)))) } @@ -121,7 +121,7 @@ func TestVolumePruneSuccess(t *testing.T) { func TestVolumePruneForce(t *testing.T) { testCases := []struct { name string - volumePruneFunc func(args filters.Args) (volume.PruneReport, error) + volumePruneFunc func(options client.VolumePruneOptions) (client.VolumePruneResult, error) }{ { name: "empty", @@ -135,7 +135,7 @@ func TestVolumePruneForce(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ volumePruneFunc: tc.volumePruneFunc, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.Flags().Set("force", "true") assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("volume-prune.%s.golden", tc.name)) @@ -152,7 +152,7 @@ func TestVolumePrunePromptYes(t *testing.T) { }) cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden") @@ -170,7 +170,7 @@ func TestVolumePrunePromptNo(t *testing.T) { }) cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -180,12 +180,14 @@ func TestVolumePrunePromptNo(t *testing.T) { } } -func simplePruneFunc(filters.Args) (volume.PruneReport, error) { - return volume.PruneReport{ - VolumesDeleted: []string{ - "foo", "bar", "baz", +func simplePruneFunc(options client.VolumePruneOptions) (client.VolumePruneResult, error) { + return client.VolumePruneResult{ + Report: volume.PruneReport{ + VolumesDeleted: []string{ + "foo", "bar", "baz", + }, + SpaceReclaimed: 2000, }, - SpaceReclaimed: 2000, }, nil } @@ -194,12 +196,12 @@ func TestVolumePrunePromptTerminate(t *testing.T) { t.Cleanup(cancel) cli := test.NewFakeCli(&fakeClient{ - volumePruneFunc: func(filter filters.Args) (volume.PruneReport, error) { - return volume.PruneReport{}, errors.New("fakeClient volumePruneFunc should not be called") + volumePruneFunc: func(options client.VolumePruneOptions) (client.VolumePruneResult, error) { + return client.VolumePruneResult{}, errors.New("fakeClient volumePruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/volume/remove.go b/cli/command/volume/remove.go index eab894b02b4b..4cfc04e7bb4d 100644 --- a/cli/command/volume/remove.go +++ b/cli/command/volume/remove.go @@ -8,6 +8,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,7 +18,7 @@ type removeOptions struct { volumes []string } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -28,14 +29,15 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.volumes = args - return runRemove(cmd.Context(), dockerCli, &opts) + return runRemove(cmd.Context(), dockerCLI, &opts) }, - ValidArgsFunction: completion.VolumeNames(dockerCli), + ValidArgsFunction: completion.VolumeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes") - flags.SetAnnotation("force", "version", []string{"1.25"}) + _ = flags.SetAnnotation("force", "version", []string{"1.25"}) return cmd } @@ -44,7 +46,10 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, opts *removeOptions) var errs []error for _, name := range opts.volumes { - if err := apiClient.VolumeRemove(ctx, name, opts.force); err != nil { + _, err := apiClient.VolumeRemove(ctx, name, client.VolumeRemoveOptions{ + Force: opts.force, + }) + if err != nil { errs = append(errs, err) continue } diff --git a/cli/command/volume/update.go b/cli/command/volume/update.go index 5512c3f3674b..0d986051b8a0 100644 --- a/cli/command/volume/update.go +++ b/cli/command/volume/update.go @@ -7,12 +7,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" - "github.com/docker/docker/api/types/volume" + "github.com/moby/moby/api/types/volume" + "github.com/moby/moby/client" "github.com/spf13/cobra" "github.com/spf13/pflag" ) -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { var availability string cmd := &cobra.Command{ @@ -20,19 +21,20 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { Short: "Update a volume (cluster volumes only)", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(cmd.Context(), dockerCli, args[0], availability, cmd.Flags()) + return runUpdate(cmd.Context(), dockerCLI, args[0], availability, cmd.Flags()) }, Annotations: map[string]string{ "version": "1.42", "swarm": "manager", }, - ValidArgsFunction: completion.VolumeNames(dockerCli), + ValidArgsFunction: completion.VolumeNames(dockerCLI), + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVar(&availability, "availability", "active", `Cluster Volume availability ("active", "pause", "drain")`) - flags.SetAnnotation("availability", "version", []string{"1.42"}) - flags.SetAnnotation("availability", "swarm", []string{"manager"}) + _ = flags.SetAnnotation("availability", "version", []string{"1.42"}) + _ = flags.SetAnnotation("availability", "swarm", []string{"manager"}) return cmd } @@ -44,23 +46,21 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, volumeID, availabilit apiClient := dockerCli.Client() - vol, _, err := apiClient.VolumeInspectWithRaw(ctx, volumeID) + res, err := apiClient.VolumeInspect(ctx, volumeID, client.VolumeInspectOptions{}) if err != nil { return err } - if vol.ClusterVolume == nil { + if res.Volume.ClusterVolume == nil { return errors.New("can only update cluster volumes") } if flags.Changed("availability") { - vol.ClusterVolume.Spec.Availability = volume.Availability(availability) + res.Volume.ClusterVolume.Spec.Availability = volume.Availability(availability) } - - return apiClient.VolumeUpdate( - ctx, vol.ClusterVolume.ID, vol.ClusterVolume.Version, - volume.UpdateOptions{ - Spec: &vol.ClusterVolume.Spec, - }, - ) + _, err = apiClient.VolumeUpdate(ctx, res.Volume.ClusterVolume.ID, client.VolumeUpdateOptions{ + Version: res.Volume.ClusterVolume.Version, + Spec: &res.Volume.ClusterVolume.Spec, + }) + return err } diff --git a/cli/compose/convert/compose.go b/cli/compose/convert/compose.go index a04f22e80c36..4062bbb7c096 100644 --- a/cli/compose/convert/compose.go +++ b/cli/compose/convert/compose.go @@ -1,12 +1,14 @@ package convert import ( + "net/netip" "os" "strings" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -41,6 +43,11 @@ func NewNamespace(name string) Namespace { // AddStackLabel returns labels with the namespace label added func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string { + return addStackLabel(namespace, labels) +} + +// addStackLabel returns labels with the namespace label added +func addStackLabel(namespace Namespace, labels map[string]string) map[string]string { if labels == nil { labels = make(map[string]string) } @@ -51,13 +58,13 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str type networkMap map[string]composetypes.NetworkConfig // Networks from the compose-file type to the engine API type -func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]network.CreateOptions, []string) { +func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]client.NetworkCreateOptions, []string) { if networks == nil { networks = make(map[string]composetypes.NetworkConfig) } externalNetworks := []string{} - result := make(map[string]network.CreateOptions) + result := make(map[string]client.NetworkCreateOptions) for internalName := range servicesNetworks { nw := networks[internalName] if nw.External.External { @@ -65,8 +72,8 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str continue } - createOpts := network.CreateOptions{ - Labels: AddStackLabel(namespace, nw.Labels), + createOpts := client.NetworkCreateOptions{ + Labels: addStackLabel(namespace, nw.Labels), Driver: nw.Driver, Options: nw.DriverOpts, Internal: nw.Internal, @@ -74,22 +81,20 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str } if nw.Ipam.Driver != "" || len(nw.Ipam.Config) > 0 { - createOpts.IPAM = &network.IPAM{} - } - - if nw.Ipam.Driver != "" { - createOpts.IPAM.Driver = nw.Ipam.Driver - } - for _, ipamConfig := range nw.Ipam.Config { - config := network.IPAMConfig{ - Subnet: ipamConfig.Subnet, + createOpts.IPAM = &network.IPAM{ + Driver: nw.Ipam.Driver, + } + for _, ipamConfig := range nw.Ipam.Config { + sn, _ := netip.ParsePrefix(ipamConfig.Subnet) + createOpts.IPAM.Config = append(createOpts.IPAM.Config, network.IPAMConfig{ + Subnet: sn, + }) } - createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) } - networkName := namespace.Scope(internalName) - if nw.Name != "" { - networkName = nw.Name + networkName := nw.Name + if nw.Name == "" { + networkName = namespace.Scope(internalName) } result[networkName] = createOpts } @@ -170,7 +175,7 @@ func driverObjectConfig(namespace Namespace, name string, obj composetypes.FileO return swarmFileObject{ Annotations: swarm.Annotations{ Name: name, - Labels: AddStackLabel(namespace, obj.Labels), + Labels: addStackLabel(namespace, obj.Labels), }, Data: []byte{}, } @@ -191,7 +196,7 @@ func fileObjectConfig(namespace Namespace, name string, obj composetypes.FileObj return swarmFileObject{ Annotations: swarm.Annotations{ Name: name, - Labels: AddStackLabel(namespace, obj.Labels), + Labels: addStackLabel(namespace, obj.Labels), }, Data: data, }, nil diff --git a/cli/compose/convert/compose_test.go b/cli/compose/convert/compose_test.go index c29596e4237f..afd7420576ef 100644 --- a/cli/compose/convert/compose_test.go +++ b/cli/compose/convert/compose_test.go @@ -1,10 +1,13 @@ package convert import ( + "net/netip" "testing" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/network" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -29,7 +32,7 @@ func TestAddStackLabel(t *testing.T) { labels := map[string]string{ "something": "labeled", } - actual := AddStackLabel(Namespace{name: "foo"}, labels) + actual := addStackLabel(Namespace{name: "foo"}, labels) expected := map[string]string{ "something": "labeled", LabelNamespace: "foo", @@ -56,7 +59,7 @@ func TestNetworks(t *testing.T) { Driver: "driver", Config: []*composetypes.IPAMPool{ { - Subnet: "10.0.0.0", + Subnet: "10.0.0.0/32", }, }, }, @@ -76,7 +79,7 @@ func TestNetworks(t *testing.T) { Name: "othername", }, } - expected := map[string]network.CreateOptions{ + expected := map[string]client.NetworkCreateOptions{ "foo_default": { Labels: map[string]string{ LabelNamespace: "foo", @@ -88,7 +91,7 @@ func TestNetworks(t *testing.T) { Driver: "driver", Config: []network.IPAMConfig{ { - Subnet: "10.0.0.0", + Subnet: netip.MustParsePrefix("10.0.0.0/32"), }, }, }, @@ -113,7 +116,7 @@ func TestNetworks(t *testing.T) { } networks, externals := Networks(namespace, source, serviceNetworks) - assert.DeepEqual(t, expected, networks) + assert.DeepEqual(t, expected, networks, cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{})) assert.DeepEqual(t, []string{"special"}, externals) } diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index d5237d72912b..5fbc289d830e 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -2,6 +2,9 @@ package convert import ( "context" + "errors" + "fmt" + "net/netip" "os" "sort" "strings" @@ -10,11 +13,10 @@ import ( servicecli "github.com/docker/cli/cli/command/service" composetypes "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/opts" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/client" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" ) const ( @@ -34,16 +36,16 @@ func Services( for _, service := range config.Services { secrets, err := convertServiceSecrets(ctx, apiClient, namespace, service.Secrets, config.Secrets) if err != nil { - return nil, errors.Wrapf(err, "service %s", service.Name) + return nil, fmt.Errorf("service %s: %w", service.Name, err) } configs, err := convertServiceConfigObjs(ctx, apiClient, namespace, service, config.Configs) if err != nil { - return nil, errors.Wrapf(err, "service %s", service.Name) + return nil, fmt.Errorf("service %s: %w", service.Name, err) } - serviceSpec, err := Service(apiClient.ClientVersion(), namespace, service, config.Networks, config.Volumes, secrets, configs) + serviceSpec, err := Service(namespace, service, config.Networks, config.Volumes, secrets, configs) if err != nil { - return nil, errors.Wrapf(err, "service %s", service.Name) + return nil, fmt.Errorf("service %s: %w", service.Name, err) } result[service.Name] = serviceSpec } @@ -53,7 +55,6 @@ func Services( // Service converts a ServiceConfig into a swarm ServiceSpec func Service( - apiVersion string, namespace Namespace, service composetypes.ServiceConfig, networkConfigs map[string]composetypes.NetworkConfig, @@ -95,7 +96,7 @@ func Service( return swarm.ServiceSpec{}, err } - dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) + dnsConfig := convertDNSConfig(service.DNS, service.DNSSearch) // TODO(thaJeztah): change service.DNS to a []netip.Addr var privileges swarm.Privileges privileges.CredentialSpec, err = convertCredentialSpec( @@ -118,7 +119,7 @@ func Service( serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: AddStackLabel(namespace, service.Deploy.Labels), + Labels: addStackLabel(namespace, service.Deploy.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: &swarm.ContainerSpec{ @@ -130,7 +131,7 @@ func Service( DNSConfig: dnsConfig, Healthcheck: healthcheck, Env: convertEnvironment(service.Environment), - Labels: AddStackLabel(namespace, service.Labels), + Labels: addStackLabel(namespace, service.Labels), Dir: service.WorkingDir, User: service.User, Mounts: mounts, @@ -158,6 +159,7 @@ func Service( Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), MaxReplicas: service.Deploy.Placement.MaxReplicas, }, + Networks: networks, }, EndpointSpec: endpoint, Mode: mode, @@ -168,18 +170,6 @@ func Service( // add an image label to serviceSpec serviceSpec.Labels[LabelImage] = service.Image - // ServiceSpec.Networks is deprecated and should not have been used by - // this package. It is possible to update TaskTemplate.Networks, but it - // is not possible to update ServiceSpec.Networks. Unfortunately, we - // can't unconditionally start using TaskTemplate.Networks, because that - // will break with older daemons that don't support migrating from - // ServiceSpec.Networks to TaskTemplate.Networks. So which field to use - // is conditional on daemon version. - if versions.LessThan(apiVersion, "1.29") { - serviceSpec.Networks = networks //nolint:staticcheck // ignore SA1019: field is deprecated. - } else { - serviceSpec.TaskTemplate.Networks = networks - } return serviceSpec, nil } @@ -209,16 +199,16 @@ func convertServiceNetworks( } nets := []swarm.NetworkAttachmentConfig{} - for networkName, network := range networks { + for networkName, nw := range networks { networkConfig, ok := networkConfigs[networkName] if !ok && networkName != defaultNetwork { - return nil, errors.Errorf("undefined network %q", networkName) + return nil, fmt.Errorf("undefined network %q", networkName) } var aliases []string var driverOpts map[string]string - if network != nil { - aliases = network.Aliases - driverOpts = network.DriverOpts + if nw != nil { + aliases = nw.Aliases + driverOpts = nw.DriverOpts } target := namespace.Scope(networkName) if networkConfig.Name != "" { @@ -256,7 +246,7 @@ func convertServiceSecrets( lookup := func(key string) (composetypes.FileObjectConfig, error) { secretSpec, exists := secretSpecs[key] if !exists { - return composetypes.FileObjectConfig{}, errors.Errorf("undefined secret %q", key) + return composetypes.FileObjectConfig{}, fmt.Errorf("undefined secret %q", key) } return composetypes.FileObjectConfig(secretSpec), nil } @@ -301,7 +291,7 @@ func convertServiceConfigObjs( lookup := func(key string) (composetypes.FileObjectConfig, error) { configSpec, exists := configSpecs[key] if !exists { - return composetypes.FileObjectConfig{}, errors.Errorf("undefined config %q", key) + return composetypes.FileObjectConfig{}, fmt.Errorf("undefined config %q", key) } return composetypes.FileObjectConfig(configSpec), nil } @@ -318,7 +308,7 @@ func convertServiceConfigObjs( }) } - // finally, after converting all of the file objects, create any + // finally, after converting all file objects, create any // Runtime-type configs that are needed. these are configs that are not // mounted into the container, but are used in some other way by the // container runtime. Currently, this only means CredentialSpecs, but in @@ -442,7 +432,7 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container ) if healthcheck.Disable { if len(healthcheck.Test) != 0 { - return nil, errors.Errorf("test and disable can't be set at the same time") + return nil, errors.New("test and disable can't be set at the same time") } return &container.HealthConfig{ Test: []string{"NONE"}, @@ -494,7 +484,7 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* MaxAttempts: &attempts, }, nil default: - return nil, errors.Errorf("unknown restart policy: %s", restart) + return nil, fmt.Errorf("unknown restart policy: %s", restart) } } @@ -517,10 +507,10 @@ func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig return &swarm.UpdateConfig{ Parallelism: parallel, Delay: time.Duration(source.Delay), - FailureAction: source.FailureAction, + FailureAction: swarm.FailureAction(source.FailureAction), Monitor: time.Duration(source.Monitor), MaxFailureRatio: source.MaxFailureRatio, - Order: source.Order, + Order: swarm.UpdateOrder(source.Order), } } @@ -577,7 +567,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC portConfigs := []swarm.PortConfig{} for _, port := range source { portConfig := swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(port.Protocol), + Protocol: network.IPProtocol(port.Protocol), TargetPort: port.Target, PublishedPort: port.Published, PublishMode: swarm.PortConfigPublishMode(port.Mode), @@ -618,12 +608,12 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) switch mode { case "global-job": if replicas != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") + return serviceMode, errors.New("replicas can only be used with replicated or replicated-job mode") } serviceMode.GlobalJob = &swarm.GlobalJob{} case "global": if replicas != nil { - return serviceMode, errors.Errorf("replicas can only be used with replicated or replicated-job mode") + return serviceMode, errors.New("replicas can only be used with replicated or replicated-job mode") } serviceMode.Global = &swarm.GlobalService{} case "replicated-job": @@ -634,25 +624,39 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) case "replicated", "": serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: - return serviceMode, errors.Errorf("Unknown mode: %s", mode) + return serviceMode, fmt.Errorf("unknown mode: %s", mode) } return serviceMode, nil } func convertDNSConfig(dns []string, dnsSearch []string) *swarm.DNSConfig { - if dns != nil || dnsSearch != nil { + if len(dns) > 0 || len(dnsSearch) > 0 { return &swarm.DNSConfig{ - Nameservers: dns, + Nameservers: toNetipAddrSlice(dns), Search: dnsSearch, } } return nil } +func toNetipAddrSlice(ips []string) []netip.Addr { + if len(ips) == 0 { + return nil + } + netIPs := make([]netip.Addr, 0, len(ips)) + for _, ip := range ips { + addr, err := netip.ParseAddr(ip) + if err != nil { + continue + } + netIPs = append(netIPs, addr) + } + return netIPs +} + func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpecConfig, refs []*swarm.ConfigReference) (*swarm.CredentialSpec, error) { var o []string - // Config was added in API v1.40 if spec.Config != "" { o = append(o, `"Config"`) } @@ -667,9 +671,9 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec case l == 0: return nil, nil case l == 2: - return nil, errors.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1]) + return nil, fmt.Errorf("invalid credential spec: cannot specify both %s and %s", o[0], o[1]) case l > 2: - return nil, errors.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]) + return nil, fmt.Errorf("invalid credential spec: cannot specify both %s, and %s", strings.Join(o[:l-1], ", "), o[l-1]) } swarmCredSpec := swarm.CredentialSpec(spec) // if we're using a swarm Config for the credential spec, over-write it @@ -688,7 +692,7 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec return &swarmCredSpec, nil } } - return nil, errors.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config) + return nil, fmt.Errorf("invalid credential spec: spec specifies config %v, but no such config can be found", swarmCredSpec.Config) } return &swarmCredSpec, nil } diff --git a/cli/compose/convert/service_test.go b/cli/compose/convert/service_test.go index a5c1239ea4fb..b5999fe6ea8e 100644 --- a/cli/compose/convert/service_test.go +++ b/cli/compose/convert/service_test.go @@ -3,15 +3,17 @@ package convert import ( "context" "errors" + "net/netip" "os" "strings" "testing" "time" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/swarm" + "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -307,17 +309,17 @@ var ( func TestConvertDNSConfigAll(t *testing.T) { dnsConfig := convertDNSConfig(nameservers, search) assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ - Nameservers: nameservers, + Nameservers: toNetipAddrSlice(nameservers), Search: search, - }, dnsConfig)) + }, dnsConfig, cmpopts.EquateComparable(netip.Addr{}))) } func TestConvertDNSConfigNameservers(t *testing.T) { dnsConfig := convertDNSConfig(nameservers, nil) assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ - Nameservers: nameservers, + Nameservers: toNetipAddrSlice(nameservers), Search: nil, - }, dnsConfig)) + }, dnsConfig, cmpopts.EquateComparable(netip.Addr{}))) } func TestConvertDNSConfigSearch(t *testing.T) { @@ -325,7 +327,7 @@ func TestConvertDNSConfigSearch(t *testing.T) { assert.Check(t, is.DeepEqual(&swarm.DNSConfig{ Nameservers: nil, Search: search, - }, dnsConfig)) + }, dnsConfig, cmpopts.EquateComparable(netip.Addr{}))) } func TestConvertCredentialSpec(t *testing.T) { @@ -425,19 +427,19 @@ func TestConvertCredentialSpec(t *testing.T) { func TestConvertUpdateConfigOrder(t *testing.T) { // test default behavior updateConfig := convertUpdateConfig(&composetypes.UpdateConfig{}) - assert.Check(t, is.Equal("", updateConfig.Order)) + assert.Check(t, is.Equal("", string(updateConfig.Order))) // test start-first updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ Order: "start-first", }) - assert.Check(t, is.Equal(updateConfig.Order, "start-first")) + assert.Check(t, is.Equal(string(updateConfig.Order), "start-first")) // test stop-first updateConfig = convertUpdateConfig(&composetypes.UpdateConfig{ Order: "stop-first", }) - assert.Check(t, is.Equal(updateConfig.Order, "stop-first")) + assert.Check(t, is.Equal(string(updateConfig.Order), "stop-first")) } func TestConvertFileObject(t *testing.T) { @@ -493,7 +495,7 @@ func TestServiceConvertsIsolation(t *testing.T) { src := composetypes.ServiceConfig{ Isolation: "hyperv", } - result, err := Service("1.35", Namespace{name: "foo"}, src, nil, nil, nil, nil) + result, err := Service(Namespace{name: "foo"}, src, nil, nil, nil, nil) assert.NilError(t, err) assert.Check(t, is.Equal(container.IsolationHyperV, result.TaskTemplate.ContainerSpec.Isolation)) } @@ -513,12 +515,14 @@ func TestConvertServiceSecrets(t *testing.T) { }, } apiClient := &fakeClient{ - secretListFunc: func(opts swarm.SecretListOptions) ([]swarm.Secret, error) { - assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_secret")) - assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_secret")) - return []swarm.Secret{ - {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}}, - {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}}, + secretListFunc: func(opts client.SecretListOptions) (client.SecretListResult, error) { + assert.Check(t, opts.Filters["name"]["foo_secret"]) + assert.Check(t, opts.Filters["name"]["bar_secret"]) + return client.SecretListResult{ + Items: []swarm.Secret{ + {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo_secret"}}}, + {Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "bar_secret"}}}, + }, }, nil }, } @@ -571,14 +575,16 @@ func TestConvertServiceConfigs(t *testing.T) { }, } apiClient := &fakeClient{ - configListFunc: func(opts swarm.ConfigListOptions) ([]swarm.Config, error) { - assert.Check(t, is.Contains(opts.Filters.Get("name"), "foo_config")) - assert.Check(t, is.Contains(opts.Filters.Get("name"), "bar_config")) - assert.Check(t, is.Contains(opts.Filters.Get("name"), "baz_config")) - return []swarm.Config{ - {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}}, - {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}}, - {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}}, + configListFunc: func(opts client.ConfigListOptions) (client.ConfigListResult, error) { + assert.Check(t, opts.Filters["name"]["foo_config"]) + assert.Check(t, opts.Filters["name"]["bar_config"]) + assert.Check(t, opts.Filters["name"]["baz_config"]) + return client.ConfigListResult{ + Items: []swarm.Config{ + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "foo_config"}}}, + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "bar_config"}}}, + {Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "baz_config"}}}, + }, }, nil }, } @@ -614,22 +620,22 @@ func TestConvertServiceConfigs(t *testing.T) { type fakeClient struct { client.Client - secretListFunc func(swarm.SecretListOptions) ([]swarm.Secret, error) - configListFunc func(swarm.ConfigListOptions) ([]swarm.Config, error) + secretListFunc func(client.SecretListOptions) (client.SecretListResult, error) + configListFunc func(client.ConfigListOptions) (client.ConfigListResult, error) } -func (c *fakeClient) SecretList(_ context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { +func (c *fakeClient) SecretList(_ context.Context, options client.SecretListOptions) (client.SecretListResult, error) { if c.secretListFunc != nil { return c.secretListFunc(options) } - return []swarm.Secret{}, nil + return client.SecretListResult{}, nil } -func (c *fakeClient) ConfigList(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { +func (c *fakeClient) ConfigList(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { if c.configListFunc != nil { return c.configListFunc(options) } - return []swarm.Config{}, nil + return client.ConfigListResult{}, nil } func TestConvertUpdateConfigParallelism(t *testing.T) { @@ -690,7 +696,7 @@ func TestConvertServiceCapAddAndCapDrop(t *testing.T) { } for _, tc := range tests { t.Run(tc.title, func(t *testing.T) { - result, err := Service("1.41", Namespace{name: "foo"}, tc.in, nil, nil, nil, nil) + result, err := Service(Namespace{name: "foo"}, tc.in, nil, nil, nil, nil) assert.NilError(t, err) assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityAdd, tc.out.CapAdd)) assert.Check(t, is.DeepEqual(result.TaskTemplate.ContainerSpec.CapabilityDrop, tc.out.CapDrop)) diff --git a/cli/compose/convert/volume.go b/cli/compose/convert/volume.go index 8787ea317c7c..8eaa934cc097 100644 --- a/cli/compose/convert/volume.go +++ b/cli/compose/convert/volume.go @@ -1,11 +1,12 @@ package convert import ( + "errors" + "fmt" "strings" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/mount" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/mount" ) type volumes map[string]composetypes.VolumeConfig @@ -59,7 +60,7 @@ func handleVolumeToMount( stackVolume, exists := stackVolumes[volume.Source] if !exists { - return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source) + return mount.Mount{}, fmt.Errorf("undefined volume %q", volume.Source) } result.Source = namespace.Scope(volume.Source) @@ -79,7 +80,7 @@ func handleVolumeToMount( return result, nil } - result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels) + result.VolumeOptions.Labels = addStackLabel(namespace, stackVolume.Labels) if stackVolume.Driver != "" || stackVolume.DriverOpts != nil { result.VolumeOptions.DriverConfig = &mount.Driver{ Name: stackVolume.Driver, @@ -219,7 +220,7 @@ func handleClusterToMount( // external volumes with a given group exist. stackVolume, exists := stackVolumes[volume.Source] if !exists { - return mount.Mount{}, errors.Errorf("undefined volume %q", volume.Source) + return mount.Mount{}, fmt.Errorf("undefined volume %q", volume.Source) } // if the volume is not specified with a group source, we may namespace diff --git a/cli/compose/convert/volume_test.go b/cli/compose/convert/volume_test.go index cc2fdc851e7e..44865a0d7525 100644 --- a/cli/compose/convert/volume_test.go +++ b/cli/compose/convert/volume_test.go @@ -4,7 +4,7 @@ import ( "testing" composetypes "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/mount" + "github.com/moby/moby/api/types/mount" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) diff --git a/cli/compose/interpolation/interpolation.go b/cli/compose/interpolation/interpolation.go index c7bee693a531..25fafdd44cc6 100644 --- a/cli/compose/interpolation/interpolation.go +++ b/cli/compose/interpolation/interpolation.go @@ -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: -//go:build go1.23 +//go:build go1.24 package interpolation import ( + "fmt" "os" "strings" "github.com/docker/cli/cli/compose/template" - "github.com/pkg/errors" ) // Options supported by Interpolate @@ -68,7 +68,7 @@ func recursiveInterpolate(value any, path Path, opts Options) (any, error) { } casted, err := caster(newValue) if err != nil { - return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type")) + return casted, newPathError(path, fmt.Errorf("failed to cast to expected type: %w", err)) } return casted, nil @@ -104,11 +104,11 @@ func newPathError(path Path, err error) error { case nil: return nil case *template.InvalidTemplateError: - return errors.Errorf( + return fmt.Errorf( "invalid interpolation format for %s: %#v; you may need to escape any $ with another $", path, err.Template) default: - return errors.Wrapf(err, "error while interpolating %s", path) + return fmt.Errorf("error while interpolating %s: %w", path, err) } } diff --git a/cli/compose/interpolation/interpolation_test.go b/cli/compose/interpolation/interpolation_test.go index d1409f7e2adf..9f4ae5d1d20b 100644 --- a/cli/compose/interpolation/interpolation_test.go +++ b/cli/compose/interpolation/interpolation_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package interpolation diff --git a/cli/compose/loader/envfile.go b/cli/compose/loader/envfile.go new file mode 100644 index 000000000000..6430f4b5f5f8 --- /dev/null +++ b/cli/compose/loader/envfile.go @@ -0,0 +1,26 @@ +package loader + +import ( + "os" + + "github.com/docker/cli/pkg/kvfile" +) + +// parseEnvFile reads a file with environment variables enumerated by lines +// +// “Environment variable names used by the utilities in the Shell and +// Utilities volume of [IEEE Std 1003.1-2001] consist solely of uppercase +// letters, digits, and the '_' (underscore) from the characters defined in +// Portable Character Set and do not begin with a digit. *But*, other +// characters may be permitted by an implementation; applications shall +// tolerate the presence of such names.” +// +// As of [moby-16585], it's up to application inside docker to validate or not +// environment variables, that's why we just strip leading whitespace and +// nothing more. +// +// [IEEE Std 1003.1-2001]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html +// [moby-16585]: https://github.com/moby/moby/issues/16585 +func parseEnvFile(filename string) ([]string, error) { + return kvfile.Parse(filename, os.LookupEnv) +} diff --git a/opts/envfile_test.go b/cli/compose/loader/envfile_test.go similarity index 79% rename from opts/envfile_test.go rename to cli/compose/loader/envfile_test.go index 2c01de4fd520..723b1e2269e7 100644 --- a/opts/envfile_test.go +++ b/cli/compose/loader/envfile_test.go @@ -1,4 +1,4 @@ -package opts +package loader import ( "os" @@ -17,13 +17,13 @@ func tmpFileWithContent(t *testing.T, content string) string { return fileName } -// Test ParseEnvFile for a non existent file +// Test parseEnvFile for a non existent file func TestParseEnvFileNonExistentFile(t *testing.T) { - _, err := ParseEnvFile("no_such_file") + _, err := parseEnvFile("no_such_file") assert.Check(t, is.ErrorType(err, os.IsNotExist)) } -// ParseEnvFile with environment variable import definitions +// parseEnvFile with environment variable import definitions func TestParseEnvVariableDefinitionsFile(t *testing.T) { content := `# comment= UNDEFINED_VAR @@ -32,7 +32,7 @@ DEFINED_VAR tmpFile := tmpFileWithContent(t, content) t.Setenv("DEFINED_VAR", "defined-value") - variables, err := ParseEnvFile(tmpFile) + variables, err := parseEnvFile(tmpFile) assert.NilError(t, err) expectedLines := []string{"DEFINED_VAR=defined-value"} diff --git a/cli/compose/loader/full-struct_test.go b/cli/compose/loader/full-struct_test.go index 5a11c9aed8d6..5d420c872bcf 100644 --- a/cli/compose/loader/full-struct_test.go +++ b/cli/compose/loader/full-struct_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package loader diff --git a/cli/compose/loader/interpolate.go b/cli/compose/loader/interpolate.go index 93ac9d83dd4f..edacd4f831ed 100644 --- a/cli/compose/loader/interpolate.go +++ b/cli/compose/loader/interpolate.go @@ -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: -//go:build go1.23 +//go:build go1.24 package loader import ( + "fmt" "strconv" "strings" interp "github.com/docker/cli/cli/compose/interpolation" - "github.com/pkg/errors" ) var interpolateTypeCastMapping = map[interp.Path]interp.Cast{ @@ -67,7 +67,7 @@ func toBoolean(value string) (any, error) { case "n", "no", "false", "off": return false, nil default: - return nil, errors.Errorf("invalid boolean: %s", value) + return nil, fmt.Errorf("invalid boolean: %s", value) } } diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index b2673394b5bb..dda62f862eaa 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -1,9 +1,10 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package loader import ( + "errors" "fmt" "path" "path/filepath" @@ -17,16 +18,17 @@ import ( "github.com/docker/cli/cli/compose/schema" "github.com/docker/cli/cli/compose/template" "github.com/docker/cli/cli/compose/types" + "github.com/docker/cli/internal/volumespec" "github.com/docker/cli/opts" "github.com/docker/cli/opts/swarmopts" - "github.com/docker/docker/api/types/versions" "github.com/docker/go-connections/nat" "github.com/docker/go-units" "github.com/go-viper/mapstructure/v2" "github.com/google/shlex" - "github.com/pkg/errors" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/client/pkg/versions" "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) // Options supported by Load @@ -41,6 +43,13 @@ type Options struct { discardEnvFiles bool } +// ParseVolume parses a volume spec without any knowledge of the target platform. +// +// This function is unused, but kept for backward-compatibility for external users. +func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { + return volumespec.Parse(spec) +} + // WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to // the `environment` section func WithDiscardEnvFiles(options *Options) { @@ -56,7 +65,7 @@ func ParseYAML(source []byte) (map[string]any, error) { } _, ok := cfg.(map[string]any) if !ok { - return nil, errors.Errorf("top-level object must be a mapping") + return nil, errors.New("top-level object must be a mapping") } converted, err := convertToStringKeysRecursive(cfg, "") if err != nil { @@ -68,7 +77,7 @@ func ParseYAML(source []byte) (map[string]any, error) { // Load reads a ConfigDetails and returns a fully loaded configuration func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Config, error) { if len(configDetails.ConfigFiles) < 1 { - return nil, errors.Errorf("No files specified") + return nil, errors.New("no files specified") } options := &Options{ @@ -93,7 +102,7 @@ func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Conf configDetails.Version = version } if configDetails.Version != version { - return nil, errors.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version) + return nil, fmt.Errorf("version mismatched between two composefiles : %v and %v", configDetails.Version, version) } if err := validateForbidden(configDict); err != nil { @@ -460,7 +469,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l for _, file := range serviceConfig.EnvFile { filePath := absPath(workingDir, file) - fileVars, err := opts.ParseEnvFile(filePath) + fileVars, err := parseEnvFile(filePath) if err != nil { return err } @@ -524,7 +533,7 @@ func transformUlimits(data any) (any, error) { ulimit.Hard = value["hard"].(int) return ulimit, nil default: - return data, errors.Errorf("invalid type %T for ulimits", value) + return data, fmt.Errorf("invalid type %T for ulimits", value) } } @@ -536,33 +545,31 @@ func LoadNetworks(source map[string]any, version string) (map[string]types.Netwo if err != nil { return networks, err } - for name, network := range networks { - if !network.External.External { + for name, nw := range networks { + if !nw.External.External { continue } switch { - case network.External.Name != "": - if network.Name != "" { - return nil, errors.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) + case nw.External.Name != "": + if nw.Name != "" { + return nil, fmt.Errorf("network %s: network.external.name and network.name conflict; only use network.name", name) } if versions.GreaterThanOrEqualTo(version, "3.5") { logrus.Warnf("network %s: network.external.name is deprecated in favor of network.name", name) } - network.Name = network.External.Name - network.External.Name = "" - case network.Name == "": - network.Name = name + nw.Name = nw.External.Name + nw.External.Name = "" + case nw.Name == "": + nw.Name = name } - network.Extras = loadExtras(name, source) - networks[name] = network + nw.Extras = loadExtras(name, source) + networks[name] = nw } return networks, nil } func externalVolumeError(volume, key string) error { - return errors.Errorf( - "conflicting parameters \"external\" and %q specified for volume %q", - key, volume) + return fmt.Errorf(`conflicting parameters "external" and %q specified for volume %q`, key, volume) } // LoadVolumes produces a VolumeConfig map from a compose file Dict @@ -586,7 +593,7 @@ func LoadVolumes(source map[string]any, version string) (map[string]types.Volume return nil, externalVolumeError(name, "labels") case volume.External.Name != "": if volume.Name != "" { - return nil, errors.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) + return nil, fmt.Errorf("volume %s: volume.external.name and volume.name conflict; only use volume.name", name) } if versions.GreaterThanOrEqualTo(version, "3.4") { logrus.Warnf("volume %s: volume.external.name is deprecated in favor of volume.name", name) @@ -647,7 +654,7 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi // handle deprecated external.name if obj.External.Name != "" { if obj.Name != "" { - return obj, errors.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) + return obj, fmt.Errorf("%[1]s %[2]s: %[1]s.external.name and %[1]s.name conflict; only use %[1]s.name", objType, name) } if versions.GreaterThanOrEqualTo(details.Version, "3.5") { logrus.Warnf("%[1]s %[2]s: %[1]s.external.name is deprecated in favor of %[1]s.name", objType, name) @@ -660,7 +667,7 @@ func loadFileObjectConfig(name string, objType string, obj types.FileObjectConfi // if not "external: true" case obj.Driver != "": if obj.File != "" { - return obj, errors.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) + return obj, fmt.Errorf("%[1]s %[2]s: %[1]s.driver and %[1]s.file conflict; only use %[1]s.driver", objType, name) } default: obj.File = absPath(details.WorkingDir, obj.File) @@ -683,7 +690,7 @@ var transformMapStringString TransformerFunc = func(data any) (any, error) { case map[string]string: return value, nil default: - return data, errors.Errorf("invalid type %T for map[string]string", value) + return data, fmt.Errorf("invalid type %T for map[string]string", value) } } @@ -694,7 +701,7 @@ var transformExternal TransformerFunc = func(data any) (any, error) { case map[string]any: return map[string]any{"external": true, "name": value["name"]}, nil default: - return data, errors.Errorf("invalid type %T for external", value) + return data, fmt.Errorf("invalid type %T for external", value) } } @@ -722,12 +729,12 @@ var transformServicePort TransformerFunc = func(data any) (any, error) { case map[string]any: ports = append(ports, value) default: - return data, errors.Errorf("invalid type %T for port", value) + return data, fmt.Errorf("invalid type %T for port", value) } } return ports, nil default: - return data, errors.Errorf("invalid type %T for port", entries) + return data, fmt.Errorf("invalid type %T for port", entries) } } @@ -738,7 +745,7 @@ var transformStringSourceMap TransformerFunc = func(data any) (any, error) { case map[string]any: return data, nil default: - return data, errors.Errorf("invalid type %T for secret", value) + return data, fmt.Errorf("invalid type %T for secret", value) } } @@ -749,18 +756,18 @@ var transformBuildConfig TransformerFunc = func(data any) (any, error) { case map[string]any: return data, nil default: - return data, errors.Errorf("invalid type %T for service build", value) + return data, fmt.Errorf("invalid type %T for service build", value) } } var transformServiceVolumeConfig TransformerFunc = func(data any) (any, error) { switch value := data.(type) { case string: - return ParseVolume(value) + return volumespec.Parse(value) case map[string]any: return data, nil default: - return data, errors.Errorf("invalid type %T for service volume", value) + return data, fmt.Errorf("invalid type %T for service volume", value) } } @@ -791,7 +798,7 @@ var transformStringList TransformerFunc = func(data any) (any, error) { case []any: return value, nil default: - return data, errors.Errorf("invalid type %T for string list", value) + return data, fmt.Errorf("invalid type %T for string list", value) } } @@ -838,7 +845,7 @@ func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowS } return result } - panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) + panic(fmt.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping)) } func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc { @@ -866,7 +873,7 @@ func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any { } return result } - panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) + panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) } var transformShellCommand TransformerFunc = func(value any) (any, error) { @@ -883,7 +890,7 @@ var transformHealthCheckTest TransformerFunc = func(data any) (any, error) { case []any: return value, nil default: - return value, errors.Errorf("invalid type %T for healthcheck.test", value) + return value, fmt.Errorf("invalid type %T for healthcheck.test", value) } } @@ -894,7 +901,7 @@ var transformSize TransformerFunc = func(value any) (any, error) { case string: return units.RAMInBytes(value) } - panic(errors.Errorf("invalid type for size %T", value)) + panic(fmt.Errorf("invalid type for size %T", value)) } var transformStringToDuration TransformerFunc = func(value any) (any, error) { @@ -906,27 +913,33 @@ var transformStringToDuration TransformerFunc = func(value any) (any, error) { } return types.Duration(d), nil default: - return value, errors.Errorf("invalid type %T for duration", value) + return value, fmt.Errorf("invalid type %T for duration", value) } } func toServicePortConfigs(value string) ([]any, error) { - var portConfigs []any - + // short syntax ([ip:]public:private[/proto]) + // + // TODO(thaJeztah): we need an equivalent that handles the "ip-address" part without depending on the nat package. ports, portBindings, err := nat.ParsePortSpecs([]string{value}) if err != nil { return nil, err } // We need to sort the key of the ports to make sure it is consistent - keys := []string{} + keys := make([]string, 0, len(ports)) for port := range ports { keys = append(keys, string(port)) } sort.Strings(keys) + var portConfigs []any for _, key := range keys { // Reuse ConvertPortToPortConfig so that it is consistent - portConfig, err := swarmopts.ConvertPortToPortConfig(nat.Port(key), portBindings) + port, err := network.ParsePort(key) + if err != nil { + return nil, err + } + portConfig, err := swarmopts.ConvertPortToPortConfig(port, portBindings) if err != nil { return nil, err } diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 1f28c996ffa3..cb2c9f20a191 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package loader @@ -333,7 +333,7 @@ func TestInvalidTopLevelObjectType(t *testing.T) { func TestNonStringKeys(t *testing.T) { // FIXME(thaJeztah): opkg.in/yaml.v3, which always unmarshals to a map[string]any, so we cannot produce a customized error for invalid types. - t.Skip("not supported by gopkg.in/yaml.v3, which always unmarshals to a map[string]any") + t.Skip("not supported by go.yaml.in/yaml/v3, which always unmarshals to a map[string]any") _, err := loadYAML(` version: "3" 123: diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 8c0f35db433d..a321fd18c489 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -1,15 +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.23 +//go:build go1.24 package loader import ( + "fmt" "reflect" "sort" "dario.cat/mergo" "github.com/docker/cli/cli/compose/types" - "github.com/pkg/errors" ) type specials struct { @@ -29,23 +29,23 @@ func merge(configs []*types.Config) (*types.Config, error) { var err error base.Services, err = mergeServices(base.Services, override.Services) if err != nil { - return base, errors.Wrapf(err, "cannot merge services from %s", override.Filename) + return base, fmt.Errorf("cannot merge services from %s: %w", override.Filename, err) } base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes) if err != nil { - return base, errors.Wrapf(err, "cannot merge volumes from %s", override.Filename) + return base, fmt.Errorf("cannot merge volumes from %s: %w", override.Filename, err) } base.Networks, err = mergeNetworks(base.Networks, override.Networks) if err != nil { - return base, errors.Wrapf(err, "cannot merge networks from %s", override.Filename) + return base, fmt.Errorf("cannot merge networks from %s: %w", override.Filename, err) } base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets) if err != nil { - return base, errors.Wrapf(err, "cannot merge secrets from %s", override.Filename) + return base, fmt.Errorf("cannot merge secrets from %s: %w", override.Filename, err) } base.Configs, err = mergeConfigs(base.Configs, override.Configs) if err != nil { - return base, errors.Wrapf(err, "cannot merge configs from %s", override.Filename) + return base, fmt.Errorf("cannot merge configs from %s: %w", override.Filename, err) } } return base, nil @@ -70,7 +70,7 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, for name, overrideService := range overrideServices { if baseService, ok := baseServices[name]; ok { if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(specials)); err != nil { - return base, errors.Wrapf(err, "cannot merge service %s", name) + return base, fmt.Errorf("cannot merge service %s: %w", name, err) } baseServices[name] = baseService continue @@ -88,7 +88,7 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, func toServiceSecretConfigsMap(s any) (map[any]any, error) { secrets, ok := s.([]types.ServiceSecretConfig) if !ok { - return nil, errors.Errorf("not a serviceSecretConfig: %v", s) + return nil, fmt.Errorf("not a serviceSecretConfig: %v", s) } m := map[any]any{} for _, secret := range secrets { @@ -100,7 +100,7 @@ func toServiceSecretConfigsMap(s any) (map[any]any, error) { func toServiceConfigObjConfigsMap(s any) (map[any]any, error) { secrets, ok := s.([]types.ServiceConfigObjConfig) if !ok { - return nil, errors.Errorf("not a serviceSecretConfig: %v", s) + return nil, fmt.Errorf("not a serviceSecretConfig: %v", s) } m := map[any]any{} for _, secret := range secrets { @@ -112,7 +112,7 @@ func toServiceConfigObjConfigsMap(s any) (map[any]any, error) { func toServicePortConfigsMap(s any) (map[any]any, error) { ports, ok := s.([]types.ServicePortConfig) if !ok { - return nil, errors.Errorf("not a servicePortConfig slice: %v", s) + return nil, fmt.Errorf("not a servicePortConfig slice: %v", s) } m := map[any]any{} for _, p := range ports { @@ -124,7 +124,7 @@ func toServicePortConfigsMap(s any) (map[any]any, error) { func toServiceVolumeConfigsMap(s any) (map[any]any, error) { volumes, ok := s.([]types.ServiceVolumeConfig) if !ok { - return nil, errors.Errorf("not a serviceVolumeConfig slice: %v", s) + return nil, fmt.Errorf("not a serviceVolumeConfig slice: %v", s) } m := map[any]any{} for _, v := range volumes { @@ -211,7 +211,7 @@ func mergeSlice(tomap tomapFn, writeValue writeValueFromMapFn) func(dst, src ref func sliceToMap(tomap tomapFn, v reflect.Value) (map[any]any, error) { // check if valid if !v.IsValid() { - return nil, errors.Errorf("invalid value : %+v", v) + return nil, fmt.Errorf("invalid value : %+v", v) } return tomap(v.Interface()) } diff --git a/cli/compose/loader/merge_test.go b/cli/compose/loader/merge_test.go index d2d45b4e0f0f..40445d6c4208 100644 --- a/cli/compose/loader/merge_test.go +++ b/cli/compose/loader/merge_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package loader diff --git a/cli/compose/loader/types_test.go b/cli/compose/loader/types_test.go index df32acb30b0a..bdb36b89166b 100644 --- a/cli/compose/loader/types_test.go +++ b/cli/compose/loader/types_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" "gotest.tools/v3/assert" "gotest.tools/v3/golden" ) diff --git a/cli/compose/loader/volume.go b/cli/compose/loader/volume.go deleted file mode 100644 index f043f4aa57ff..000000000000 --- a/cli/compose/loader/volume.go +++ /dev/null @@ -1,125 +0,0 @@ -package loader - -import ( - "strings" - "unicode" - "unicode/utf8" - - "github.com/docker/cli/cli/compose/types" - "github.com/docker/docker/api/types/mount" - "github.com/pkg/errors" -) - -const endOfSpec = rune(0) - -// ParseVolume parses a volume spec without any knowledge of the target platform -func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { - volume := types.ServiceVolumeConfig{} - - switch len(spec) { - case 0: - return volume, errors.New("invalid empty volume spec") - case 1, 2: - volume.Target = spec - volume.Type = string(mount.TypeVolume) - return volume, nil - } - - buffer := []rune{} - for _, char := range spec + string(endOfSpec) { - switch { - case isWindowsDrive(buffer, char): - buffer = append(buffer, char) - case char == ':' || char == endOfSpec: - if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { - populateType(&volume) - return volume, errors.Wrapf(err, "invalid spec: %s", spec) - } - buffer = []rune{} - default: - buffer = append(buffer, char) - } - } - - populateType(&volume) - return volume, nil -} - -func isWindowsDrive(buffer []rune, char rune) bool { - return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) -} - -func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error { - strBuffer := string(buffer) - switch { - case len(buffer) == 0: - return errors.New("empty section between colons") - // Anonymous volume - case volume.Source == "" && char == endOfSpec: - volume.Target = strBuffer - return nil - case volume.Source == "": - volume.Source = strBuffer - return nil - case volume.Target == "": - volume.Target = strBuffer - return nil - case char == ':': - return errors.New("too many colons") - } - for _, option := range strings.Split(strBuffer, ",") { - switch option { - case "ro": - volume.ReadOnly = true - case "rw": - volume.ReadOnly = false - case "nocopy": - volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} - default: - if isBindOption(option) { - volume.Bind = &types.ServiceVolumeBind{Propagation: option} - } - // ignore unknown options - } - } - return nil -} - -func isBindOption(option string) bool { - for _, propagation := range mount.Propagations { - if mount.Propagation(option) == propagation { - return true - } - } - return false -} - -func populateType(volume *types.ServiceVolumeConfig) { - switch { - // Anonymous volume - case volume.Source == "": - volume.Type = string(mount.TypeVolume) - case isFilePath(volume.Source): - volume.Type = string(mount.TypeBind) - default: - volume.Type = string(mount.TypeVolume) - } -} - -func isFilePath(source string) bool { - switch source[0] { - case '.', '/', '~': - return true - } - if len([]rune(source)) == 1 { - return false - } - - // windows named pipes - if strings.HasPrefix(source, `\\`) { - return true - } - - first, nextIndex := utf8.DecodeRuneInString(source) - return isWindowsDrive([]rune{first}, rune(source[nextIndex])) -} diff --git a/cli/compose/loader/volume_test.go b/cli/compose/loader/volume_test.go deleted file mode 100644 index 54dc200fe8f6..000000000000 --- a/cli/compose/loader/volume_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package loader - -import ( - "fmt" - "testing" - - "github.com/docker/cli/cli/compose/types" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestParseVolumeAnonymousVolume(t *testing.T) { - for _, path := range []string{"/path", "/path/foo"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { - for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeTooManyColons(t *testing.T) { - _, err := ParseVolume("/foo:/foo:ro:foo") - assert.Error(t, err, "invalid spec: /foo:/foo:ro:foo: too many colons") -} - -func TestParseVolumeShortVolumes(t *testing.T) { - for _, path := range []string{".", "/a"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeMissingSource(t *testing.T) { - for _, spec := range []string{":foo", "/foo::ro"} { - _, err := ParseVolume(spec) - assert.ErrorContains(t, err, "empty section between colons") - } -} - -func TestParseVolumeBindMount(t *testing.T) { - for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: path, - Target: "/target", - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeRelativeBindMountWindows(t *testing.T) { - for _, path := range []string{ - "./foo", - "~/thing", - "../other", - "D:\\path", "/home/user", - } { - volume, err := ParseVolume(path + ":d:\\target") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: path, - Target: "d:\\target", - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeWithBindOptions(t *testing.T) { - volume, err := ParseVolume("/source:/target:slave") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: "/source", - Target: "/target", - Bind: &types.ServiceVolumeBind{Propagation: "slave"}, - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) -} - -func TestParseVolumeWithBindOptionsWindows(t *testing.T) { - volume, err := ParseVolume("C:\\source\\foo:D:\\target:ro,rprivate") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: "C:\\source\\foo", - Target: "D:\\target", - ReadOnly: true, - Bind: &types.ServiceVolumeBind{Propagation: "rprivate"}, - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) -} - -func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { - _, err := ParseVolume("name:/target:bogus") - assert.NilError(t, err) -} - -func TestParseVolumeWithVolumeOptions(t *testing.T) { - volume, err := ParseVolume("name:/target:nocopy") - expected := types.ServiceVolumeConfig{ - Type: "volume", - Source: "name", - Target: "/target", - Volume: &types.ServiceVolumeVolume{NoCopy: true}, - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) -} - -func TestParseVolumeWithReadOnly(t *testing.T) { - for _, path := range []string{"./foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target:ro") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: path, - Target: "/target", - ReadOnly: true, - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeWithRW(t *testing.T) { - for _, path := range []string{"./foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target:rw") - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: path, - Target: "/target", - ReadOnly: false, - } - assert.NilError(t, err) - assert.Check(t, is.DeepEqual(expected, volume)) - } -} - -func TestParseVolumeWindowsNamedPipe(t *testing.T) { - volume, err := ParseVolume(`\\.\pipe\docker_engine:\\.\pipe\inside`) - assert.NilError(t, err) - expected := types.ServiceVolumeConfig{ - Type: "bind", - Source: `\\.\pipe\docker_engine`, - Target: `\\.\pipe\inside`, - } - assert.Check(t, is.DeepEqual(expected, volume)) -} - -func TestIsFilePath(t *testing.T) { - assert.Check(t, !isFilePath("a界")) - assert.Check(t, !isFilePath("1")) - assert.Check(t, !isFilePath("c")) -} - -// Preserve the test cases for VolumeSplitN -func TestParseVolumeSplitCases(t *testing.T) { - for casenumber, x := range []struct { - input string - n int - expected []string - }{ - {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, - {`:C:\foo:d:`, -1, nil}, - {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, - {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, - {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, - {`d:\`, -1, []string{`d:\`}}, - {`d:`, -1, []string{`d:`}}, - {`d:\path`, -1, []string{`d:\path`}}, - {`d:\path with space`, -1, []string{`d:\path with space`}}, - {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, - - {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, - {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, - {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, - {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, - {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, - {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, - {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, - {`name:D:`, -1, []string{`name`, `D:`}}, - {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, - {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, - - {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, - {`c:\Windows`, -1, []string{`c:\Windows`}}, - {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, - {``, -1, nil}, - {`.`, -1, []string{`.`}}, - {`..\`, -1, []string{`..\`}}, - {`c:\:..\`, -1, []string{`c:\`, `..\`}}, - {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, - // Cover directories with one-character name - {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, - } { - parsed, _ := ParseVolume(x.input) - - expected := len(x.expected) > 1 - msg := fmt.Sprintf("Case %d: %s", casenumber, x.input) - assert.Check(t, is.Equal(expected, parsed.Source != ""), msg) - } -} - -func TestParseVolumeInvalidEmptySpec(t *testing.T) { - _, err := ParseVolume("") - assert.ErrorContains(t, err, "invalid empty volume spec") -} - -func TestParseVolumeInvalidSections(t *testing.T) { - _, err := ParseVolume("/foo::rw") - assert.ErrorContains(t, err, "invalid spec") -} - -func TestParseVolumeWithEmptySource(t *testing.T) { - _, err := ParseVolume(":/vol") - assert.ErrorContains(t, err, "empty section between colons") -} diff --git a/cli/compose/schema/schema.go b/cli/compose/schema/schema.go index 1484410dad27..ea8543a85957 100644 --- a/cli/compose/schema/schema.go +++ b/cli/compose/schema/schema.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package schema @@ -11,7 +11,6 @@ import ( "time" "github.com/docker/go-connections/nat" - "github.com/pkg/errors" "github.com/xeipuuv/gojsonschema" ) @@ -80,7 +79,7 @@ func Validate(config map[string]any, version string) error { version = normalizeVersion(version) schemaData, err := schemas.ReadFile("data/config_schema_v" + version + ".json") if err != nil { - return errors.Errorf("unsupported Compose file version: %s", version) + return fmt.Errorf("unsupported Compose file version: %s", version) } schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index 041175f20e70..8d039a6a8ccb 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package schema diff --git a/cli/compose/template/template.go b/cli/compose/template/template.go index b823b4998d6d..b8a77b9bc08e 100644 --- a/cli/compose/template/template.go +++ b/cli/compose/template/template.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package template diff --git a/cli/compose/template/template_test.go b/cli/compose/template/template_test.go index db829cd1a289..2ae3baaa4816 100644 --- a/cli/compose/template/template_test.go +++ b/cli/compose/template/template_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package template diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 0804388a57ce..fdc0eb44feda 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package types @@ -8,6 +8,8 @@ import ( "fmt" "strconv" "time" + + "github.com/docker/cli/internal/volumespec" ) // UnsupportedProperties not yet supported by this implementation of the compose file @@ -390,43 +392,23 @@ type ServicePortConfig struct { } // ServiceVolumeConfig are references to a volume used by a service -type ServiceVolumeConfig struct { - Type string `yaml:",omitempty" json:"type,omitempty"` - Source string `yaml:",omitempty" json:"source,omitempty"` - Target string `yaml:",omitempty" json:"target,omitempty"` - ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` - Consistency string `yaml:",omitempty" json:"consistency,omitempty"` - Bind *ServiceVolumeBind `yaml:",omitempty" json:"bind,omitempty"` - Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"` - Image *ServiceVolumeImage `yaml:",omitempty" json:"image,omitempty"` - Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"` - Cluster *ServiceVolumeCluster `yaml:",omitempty" json:"cluster,omitempty"` -} +type ServiceVolumeConfig = volumespec.VolumeConfig // ServiceVolumeBind are options for a service volume of type bind -type ServiceVolumeBind struct { - Propagation string `yaml:",omitempty" json:"propagation,omitempty"` -} +type ServiceVolumeBind = volumespec.BindOpts // ServiceVolumeVolume are options for a service volume of type volume -type ServiceVolumeVolume struct { - NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` - Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` -} +type ServiceVolumeVolume = volumespec.VolumeOpts // ServiceVolumeImage are options for a service volume of type image -type ServiceVolumeImage struct { - Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` -} +type ServiceVolumeImage = volumespec.ImageOpts // ServiceVolumeTmpfs are options for a service volume of type tmpfs -type ServiceVolumeTmpfs struct { - Size int64 `yaml:",omitempty" json:"size,omitempty"` -} +type ServiceVolumeTmpfs = volumespec.TmpFsOpts // ServiceVolumeCluster are options for a service volume of type cluster. // Deliberately left blank for future options, but unused now. -type ServiceVolumeCluster struct{} +type ServiceVolumeCluster = volumespec.ClusterOpts // FileReferenceConfig for a reference to a swarm file object type FileReferenceConfig struct { diff --git a/cli/config/config.go b/cli/config/config.go index cbb34486a6c8..5a637805091c 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -13,7 +13,6 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/types" - "github.com/pkg/errors" ) const ( @@ -101,7 +100,7 @@ func SetDir(dir string) { func Path(p ...string) (string, error) { path := filepath.Join(append([]string{Dir()}, p...)...) if !strings.HasPrefix(path, Dir()+string(filepath.Separator)) { - return "", errors.Errorf("path %q is outside of root config directory %q", path, Dir()) + return "", fmt.Errorf("path %q is outside of root config directory %q", path, Dir()) } return path, nil } @@ -143,12 +142,12 @@ func load(configDir string) (*configfile.ConfigFile, error) { return configFile, nil } // Any other error happening when failing to read the file must be returned. - return configFile, errors.Wrap(err, "loading config file") + return configFile, fmt.Errorf("loading config file: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() err = configFile.LoadFromReader(file) if err != nil { - err = errors.Wrapf(err, "parsing config file (%s)", filename) + err = fmt.Errorf("parsing config file (%s): %w", filename, err) } return configFile, err } diff --git a/cli/config/configfile/file.go b/cli/config/configfile/file.go index 530c5228561f..fab3ed4cba13 100644 --- a/cli/config/configfile/file.go +++ b/cli/config/configfile/file.go @@ -3,6 +3,7 @@ package configfile import ( "encoding/base64" "encoding/json" + "errors" "fmt" "io" "os" @@ -12,7 +13,6 @@ import ( "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/memorystore" "github.com/docker/cli/cli/config/types" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -43,9 +43,6 @@ type ConfigFile struct { Plugins map[string]map[string]string `json:"plugins,omitempty"` Aliases map[string]string `json:"aliases,omitempty"` Features map[string]string `json:"features,omitempty"` - - // Deprecated: experimental CLI features are always enabled and this field is no longer used. Use [Features] instead for optional features. This field will be removed in a future release. - Experimental string `json:"experimental,omitempty"` } type configEnvAuth struct { @@ -167,7 +164,7 @@ func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { // Save encodes and writes out all the authorization information func (configFile *ConfigFile) Save() (retErr error) { if configFile.Filename == "" { - return errors.Errorf("Can't save config with empty filename") + return errors.New("can't save config with empty filename") } dir := filepath.Dir(configFile.Filename) @@ -194,7 +191,7 @@ func (configFile *ConfigFile) Save() (retErr error) { } if err := temp.Close(); err != nil { - return errors.Wrap(err, "error closing temp file") + return fmt.Errorf("error closing temp file: %w", err) } // Handle situation where the configfile is a symlink, and allow for dangling symlinks @@ -278,11 +275,11 @@ func decodeAuth(authStr string) (string, string, error) { return "", "", err } if n > decLen { - return "", "", errors.Errorf("Something went wrong decoding auth config") + return "", "", errors.New("something went wrong decoding auth config") } userName, password, ok := strings.Cut(string(decoded), ":") if !ok || userName == "" { - return "", "", errors.Errorf("Invalid auth configuration file") + return "", "", errors.New("invalid auth configuration file") } return userName, strings.Trim(password, "\x00"), nil } diff --git a/cli/config/credentials/file_store_test.go b/cli/config/credentials/file_store_test.go index e4a43e11fb14..d4c8375ea25e 100644 --- a/cli/config/credentials/file_store_test.go +++ b/cli/config/credentials/file_store_test.go @@ -43,13 +43,13 @@ func TestFileStoreIdempotent(t *testing.T) { }, }) authOne := types.AuthConfig{ + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", } authTwo := types.AuthConfig{ + Username: "bar@example.com", Auth: "also_super_secret_token", - Email: "bar@example.com", ServerAddress: "https://other.example.com", } @@ -106,8 +106,8 @@ func TestFileStoreAddCredentials(t *testing.T) { s := NewFileStore(f) auth := types.AuthConfig{ + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", } err := s.Store(auth) @@ -122,8 +122,8 @@ func TestFileStoreAddCredentials(t *testing.T) { func TestFileStoreGet(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, }} @@ -136,8 +136,8 @@ func TestFileStoreGet(t *testing.T) { if a.Auth != "super_secret_token" { t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) } - if a.Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + if a.Username != "foo@example.com" { + t.Fatalf("expected username `foo@example.com`, got %s", a.Username) } } @@ -146,13 +146,13 @@ func TestFileStoreGetAll(t *testing.T) { s2 := "https://example2.example.com" f := &fakeStore{configs: map[string]types.AuthConfig{ s1: { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, s2: { + Username: "foo@example2.com", Auth: "super_secret_token2", - Email: "foo@example2.com", ServerAddress: "https://example2.example.com", }, }} @@ -168,22 +168,22 @@ func TestFileStoreGetAll(t *testing.T) { if as[s1].Auth != "super_secret_token" { t.Fatalf("expected auth `super_secret_token`, got %s", as[s1].Auth) } - if as[s1].Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", as[s1].Email) + if as[s1].Username != "foo@example.com" { + t.Fatalf("expected username `foo@example.com`, got %s", as[s1].Username) } if as[s2].Auth != "super_secret_token2" { t.Fatalf("expected auth `super_secret_token2`, got %s", as[s2].Auth) } - if as[s2].Email != "foo@example2.com" { - t.Fatalf("expected email `foo@example2.com`, got %s", as[s2].Email) + if as[s2].Username != "foo@example2.com" { + t.Fatalf("expected username `foo@example2.com`, got %s", as[s2].Username) } } func TestFileStoreErase(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, }} @@ -203,9 +203,6 @@ func TestFileStoreErase(t *testing.T) { if a.Auth != "" { t.Fatalf("expected empty auth token, got %s", a.Auth) } - if a.Email != "" { - t.Fatalf("expected empty email, got %s", a.Email) - } } func TestConvertToHostname(t *testing.T) { diff --git a/cli/config/credentials/native_store_test.go b/cli/config/credentials/native_store_test.go index c2f843bfbb95..cb31071e602a 100644 --- a/cli/config/credentials/native_store_test.go +++ b/cli/config/credentials/native_store_test.go @@ -99,7 +99,6 @@ func TestNativeStoreAddCredentials(t *testing.T) { auth := types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: validServerAddress, } err := s.Store(auth) @@ -109,7 +108,6 @@ func TestNativeStoreAddCredentials(t *testing.T) { actual, ok := f.GetAuthConfigs()[validServerAddress] assert.Check(t, ok) expected := types.AuthConfig{ - Email: auth.Email, ServerAddress: auth.ServerAddress, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -124,7 +122,6 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { err := s.Store(types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: invalidServerAddress, }) assert.ErrorContains(t, err, "program failed") @@ -134,7 +131,7 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { func TestNativeStoreGet(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { - Email: "foo@example.com", + Username: "foo@example.com", }, }} s := &nativeStore{ @@ -147,7 +144,6 @@ func TestNativeStoreGet(t *testing.T) { expected := types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: validServerAddress, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -155,9 +151,7 @@ func TestNativeStoreGet(t *testing.T) { func TestNativeStoreGetIdentityToken(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress2: { - Email: "foo@example2.com", - }, + validServerAddress2: {}, }} s := &nativeStore{ @@ -169,7 +163,6 @@ func TestNativeStoreGetIdentityToken(t *testing.T) { expected := types.AuthConfig{ IdentityToken: "abcd1234", - Email: "foo@example2.com", ServerAddress: validServerAddress2, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -177,9 +170,7 @@ func TestNativeStoreGetIdentityToken(t *testing.T) { func TestNativeStoreGetAll(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -189,38 +180,20 @@ func TestNativeStoreGetAll(t *testing.T) { as, err := s.GetAll() assert.NilError(t, err) assert.Check(t, is.Len(as, 2)) - - if as[validServerAddress].Username != "foo" { - t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) - } - if as[validServerAddress].Password != "bar" { - t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) - } - if as[validServerAddress].IdentityToken != "" { - t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) - } - if as[validServerAddress].Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) - } - if as[validServerAddress2].Username != "" { - t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) - } - if as[validServerAddress2].Password != "" { - t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) - } - if as[validServerAddress2].IdentityToken != "abcd1234" { - t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) - } - if as[validServerAddress2].Email != "" { - t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email) + expected := types.AuthConfig{ + Username: "foo", + Password: "bar", + ServerAddress: "https://index.docker.io/v1", + IdentityToken: "", } + actual, ok := as[validServerAddress] + assert.Check(t, ok) + assert.Check(t, is.DeepEqual(expected, actual)) } func TestNativeStoreGetMissingCredentials(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -233,9 +206,7 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) { func TestNativeStoreGetInvalidAddress(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -248,9 +219,7 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) { func TestNativeStoreErase(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -264,9 +233,7 @@ func TestNativeStoreErase(t *testing.T) { func TestNativeStoreEraseInvalidAddress(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ diff --git a/cli/config/memorystore/store.go b/cli/config/memorystore/store.go index 199083464ed8..f8ec62b95a8e 100644 --- a/cli/config/memorystore/store.go +++ b/cli/config/memorystore/store.go @@ -1,9 +1,9 @@ -//go:build go1.23 +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 package memorystore import ( - "errors" "fmt" "maps" "os" @@ -13,12 +13,17 @@ import ( "github.com/docker/cli/cli/config/types" ) -var errValueNotFound = errors.New("value not found") +// notFoundErr is the error returned when a plugin could not be found. +type notFoundErr string -func IsErrValueNotFound(err error) bool { - return errors.Is(err, errValueNotFound) +func (notFoundErr) NotFound() {} + +func (e notFoundErr) Error() string { + return string(e) } +var errValueNotFound notFoundErr = "value not found" + type Config struct { lock sync.RWMutex memoryCredentials map[string]types.AuthConfig diff --git a/cli/config/types/authconfig.go b/cli/config/types/authconfig.go index 056af6b84259..9fe90003b138 100644 --- a/cli/config/types/authconfig.go +++ b/cli/config/types/authconfig.go @@ -6,11 +6,6 @@ type AuthConfig struct { Password string `json:"password,omitempty"` Auth string `json:"auth,omitempty"` - // Email is an optional value associated with the username. - // This field is deprecated and will be removed in a later - // version of docker. - Email string `json:"email,omitempty"` - ServerAddress string `json:"serveraddress,omitempty"` // IdentityToken is used to authenticate the user and get diff --git a/cli/connhelper/commandconn/commandconn.go b/cli/connhelper/commandconn/commandconn.go index 4b04f8b39fb3..8084a65328b1 100644 --- a/cli/connhelper/commandconn/commandconn.go +++ b/cli/connhelper/commandconn/commandconn.go @@ -233,11 +233,9 @@ func (c *commandConn) Close() error { defer c.closing.Store(false) if err := c.CloseRead(); err != nil { - logrus.Warnf("commandConn.Close: CloseRead: %v", err) return err } if err := c.CloseWrite(); err != nil { - logrus.Warnf("commandConn.Close: CloseWrite: %v", err) return err } diff --git a/cli/connhelper/commandconn/commandconn_unix_test.go b/cli/connhelper/commandconn/commandconn_unix_test.go index 6c1219f4386e..b68ebe7bf4bb 100644 --- a/cli/connhelper/commandconn/commandconn_unix_test.go +++ b/cli/connhelper/commandconn/commandconn_unix_test.go @@ -4,12 +4,17 @@ package commandconn import ( "context" + "errors" "io" "io/fs" + "os" + "path/filepath" + "runtime" + "strconv" + "syscall" "testing" "time" - "github.com/docker/docker/pkg/process" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -51,16 +56,16 @@ func TestCloseRunningCommand(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) n, err := c.Write([]byte("hello")) assert.Check(t, is.Equal(len("hello"), n)) assert.NilError(t, err) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) done <- struct{}{} }() @@ -79,7 +84,7 @@ func TestCloseTwice(t *testing.T) { c, err := New(ctx, "sh", "-c", "echo hello; sleep 1; exit 0") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) b := make([]byte, 32) n, err := c.Read(b) @@ -88,11 +93,11 @@ func TestCloseTwice(t *testing.T) { err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) done <- struct{}{} }() @@ -111,7 +116,7 @@ func TestEOFTimeout(t *testing.T) { c, err := New(ctx, "sh", "-c", "sleep 20") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) cmdConn.stdout = mockStdoutEOF{} @@ -148,7 +153,7 @@ func TestCloseWhileWriting(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) writeErrC := make(chan error) go func() { @@ -164,7 +169,7 @@ func TestCloseWhileWriting(t *testing.T) { err = c.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) writeErr := <-writeErrC assert.ErrorContains(t, writeErr, "file already closed") @@ -176,7 +181,7 @@ func TestCloseWhileReading(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) readErrC := make(chan error) go func() { @@ -193,8 +198,37 @@ func TestCloseWhileReading(t *testing.T) { err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) readErr := <-readErrC assert.Check(t, is.ErrorIs(readErr, fs.ErrClosed)) } + +// processAlive returns true if a process with a given pid is running. It only considers +// positive PIDs; 0 (all processes in the current process group), -1 (all processes +// with a PID larger than 1), and negative (-n, all processes in process group +// "n") values for pid are never considered to be alive. +// +// It was forked from https://github.com/moby/moby/blob/v28.3.3/pkg/process/process_unix.go#L17-L42 +func processAlive(pid int) bool { + if pid < 1 { + return false + } + switch runtime.GOOS { + case "darwin": + // OS X does not have a proc filesystem. Use kill -0 pid to judge if the + // process exists. From KILL(2): https://www.freebsd.org/cgi/man.cgi?query=kill&sektion=2&manpath=OpenDarwin+7.2.1 + // + // Sig may be one of the signals specified in sigaction(2) or it may + // be 0, in which case error checking is performed but no signal is + // actually sent. This can be used to check the validity of pid. + err := syscall.Kill(pid, 0) + + // Either the PID was found (no error), or we get an EPERM, which means + // the PID exists, but we don't have permissions to signal it. + return err == nil || errors.Is(err, syscall.EPERM) + default: + _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))) + return err == nil + } +} diff --git a/cli/connhelper/ssh/ssh.go b/cli/connhelper/ssh/ssh.go index 2fcb54a98f68..34ae267e20e4 100644 --- a/cli/connhelper/ssh/ssh.go +++ b/cli/connhelper/ssh/ssh.go @@ -175,7 +175,7 @@ func quoteCommand(commandAndArgs ...string) (string, error) { quotedCmd = a continue } - quotedCmd += " " + a + quotedCmd += " " + a //nolint:perfsprint // ignore "concat-loop"; no need to use a string-builder for this. } // each part is quoted appropriately, so now we'll have a full // shell command to pass off to "ssh" diff --git a/cli/context/docker/load.go b/cli/context/docker/load.go index 89d43e2e3265..a5c44f93430a 100644 --- a/cli/context/docker/load.go +++ b/cli/context/docker/load.go @@ -4,6 +4,8 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "errors" + "fmt" "net" "net/http" "strings" @@ -12,9 +14,8 @@ import ( "github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/context" "github.com/docker/cli/cli/context/store" - "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" - "github.com/pkg/errors" + "github.com/moby/moby/client" ) // EndpointMeta is a typed wrapper around a context-store generic endpoint describing @@ -68,7 +69,7 @@ func (ep *Endpoint) tlsConfig() (*tls.Config, error) { x509cert, err := tls.X509KeyPair(ep.TLSData.Cert, keyBytes) if err != nil { - return nil, errors.Wrap(err, "failed to retrieve context tls info") + return nil, fmt.Errorf("failed to retrieve context tls info: %w", err) } tlsOpts = append(tlsOpts, func(cfg *tls.Config) { cfg.Certificates = []tls.Certificate{x509cert} @@ -101,7 +102,22 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) { if err != nil { return nil, err } - result = append(result, withHTTPClient(tlsConfig)) + + // If there's no tlsConfig available, we use the default HTTPClient. + if tlsConfig != nil { + result = append(result, + client.WithHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + DialContext: (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext, + }, + CheckRedirect: client.CheckRedirect, + }), + ) + } } result = append(result, client.WithHost(ep.Host)) } else { @@ -118,7 +134,7 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) { } } - result = append(result, client.WithVersionFromEnv(), client.WithAPIVersionNegotiation()) + result = append(result, client.WithAPIVersionFromEnv()) return result, nil } @@ -133,25 +149,6 @@ func isSocket(addr string) bool { } } -func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error { - return func(c *client.Client) error { - if tlsConfig == nil { - // Use the default HTTPClient - return nil - } - return client.WithHTTPClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - DialContext: (&net.Dialer{ - KeepAlive: 30 * time.Second, - Timeout: 30 * time.Second, - }).DialContext, - }, - CheckRedirect: client.CheckRedirect, - })(c) - } -} - // EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) { ep, ok := metadata.Endpoints[DockerEndpoint] @@ -160,7 +157,7 @@ func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) { } typed, ok := ep.(EndpointMeta) if !ok { - return EndpointMeta{}, errors.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint) + return EndpointMeta{}, fmt.Errorf("endpoint %q is not of type EndpointMeta", DockerEndpoint) } return typed, nil } diff --git a/cli/context/store/errors.go b/cli/context/store/errors.go index e85ce325a9a7..cabc3f7a342b 100644 --- a/cli/context/store/errors.go +++ b/cli/context/store/errors.go @@ -1,9 +1,9 @@ package store -import cerrdefs "github.com/containerd/errdefs" +import "github.com/containerd/errdefs" func invalidParameter(err error) error { - if err == nil || cerrdefs.IsInvalidArgument(err) { + if err == nil || errdefs.IsInvalidArgument(err) { return err } return invalidParameterErr{err} @@ -14,7 +14,7 @@ type invalidParameterErr struct{ error } func (invalidParameterErr) InvalidParameter() {} func notFound(err error) error { - if err == nil || cerrdefs.IsNotFound(err) { + if err == nil || errdefs.IsNotFound(err) { return err } return notFoundErr{err} diff --git a/cli/context/store/metadata_test.go b/cli/context/store/metadata_test.go index beaf133d379f..ac60d9073bae 100644 --- a/cli/context/store/metadata_test.go +++ b/cli/context/store/metadata_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -26,7 +26,7 @@ func testMetadata(name string) Metadata { func TestMetadataGetNotExisting(t *testing.T) { testee := metadataStore{root: t.TempDir(), config: testCfg} _, err := testee.get("noexist") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestMetadataCreateGetRemove(t *testing.T) { @@ -60,7 +60,7 @@ func TestMetadataCreateGetRemove(t *testing.T) { assert.NilError(t, testee.remove("test-context")) assert.NilError(t, testee.remove("test-context")) // support duplicate remove _, err = testee.get("test-context") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestMetadataRespectJsonAnnotation(t *testing.T) { diff --git a/cli/context/store/metadatastore.go b/cli/context/store/metadatastore.go index deec5cc9d7b5..ecef656d89ac 100644 --- a/cli/context/store/metadatastore.go +++ b/cli/context/store/metadatastore.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 91d9c19c6be8..f73b710c50cc 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store @@ -18,14 +18,9 @@ import ( "path/filepath" "strings" - "github.com/docker/cli/internal/lazyregexp" "github.com/opencontainers/go-digest" ) -const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$" - -var restrictedNameRegEx = lazyregexp.New(restrictedNamePattern) - // Store provides a context store for easily remembering endpoints configuration type Store interface { Reader @@ -225,12 +220,43 @@ func ValidateContextName(name string) error { if name == "default" { return errors.New(`"default" is a reserved context name`) } - if !restrictedNameRegEx.MatchString(name) { - return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, restrictedNamePattern) + if !isValidName(name) { + return fmt.Errorf("context name %q is invalid, names are validated against regexp %q", name, validNameFormat) } return nil } +// validNameFormat is used as part of errors for invalid context-names. +// We should consider making this less technical ("must start with "a-z", +// and only consist of alphanumeric characters and separators"). +const validNameFormat = `^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$` + +// isValidName checks if the context-name is valid ("^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"). +// +// Names must start with an alphanumeric character (a-zA-Z0-9), followed by +// alphanumeric or separators ("_", ".", "+", "-"). +func isValidName(s string) bool { + if len(s) < 2 || !isAlphaNum(s[0]) { + return false + } + + for i := 1; i < len(s); i++ { + c := s[i] + if isAlphaNum(c) || c == '_' || c == '.' || c == '+' || c == '-' { + continue + } + return false + } + + return true +} + +func isAlphaNum(c byte) bool { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') +} + // Export exports an existing namespace into an opaque data stream // This stream is actually a tarball containing context metadata and TLS materials, but it does // not map 1:1 the layout of the context store (don't try to restore it manually without calling store.Import) diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index 2d3074783a01..4603bde6056f 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store @@ -17,7 +17,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -107,7 +107,7 @@ func TestRemove(t *testing.T) { })) assert.NilError(t, s.Remove("source")) _, err = s.GetMetadata("source") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) f, err := s.ListTLSFiles("source") assert.NilError(t, err) assert.Equal(t, 0, len(f)) @@ -122,7 +122,7 @@ func TestListEmptyStore(t *testing.T) { func TestErrHasCorrectContext(t *testing.T) { _, err := New(t.TempDir(), testCfg).GetMetadata("no-exists") assert.ErrorContains(t, err, "no-exists") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } func TestDetectImportContentType(t *testing.T) { diff --git a/cli/context/store/storeconfig.go b/cli/context/store/storeconfig.go index fccbf1d1f495..1b7f33106dae 100644 --- a/cli/context/store/storeconfig.go +++ b/cli/context/store/storeconfig.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store diff --git a/cli/context/store/storeconfig_test.go b/cli/context/store/storeconfig_test.go index 7d72b202ffe2..a805cef1a777 100644 --- a/cli/context/store/storeconfig_test.go +++ b/cli/context/store/storeconfig_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package store diff --git a/cli/context/store/tlsstore_test.go b/cli/context/store/tlsstore_test.go index a6aae68ae0c3..799362d54e14 100644 --- a/cli/context/store/tlsstore_test.go +++ b/cli/context/store/tlsstore_test.go @@ -3,7 +3,7 @@ package store import ( "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" ) @@ -13,7 +13,7 @@ func TestTlsCreateUpdateGetRemove(t *testing.T) { const contextName = "test-ctx" _, err := testee.getData(contextName, "test-ep", "test-data") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) err = testee.createOrUpdate(contextName, "test-ep", "test-data", []byte("data")) assert.NilError(t, err) @@ -29,7 +29,7 @@ func TestTlsCreateUpdateGetRemove(t *testing.T) { err = testee.removeEndpoint(contextName, "test-ep") assert.NilError(t, err) _, err = testee.getData(contextName, "test-ep", "test-data") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestTlsListAndBatchRemove(t *testing.T) { diff --git a/cli/context/tlsdata.go b/cli/context/tlsdata.go index c758612a1dc0..9a53d2fd0108 100644 --- a/cli/context/tlsdata.go +++ b/cli/context/tlsdata.go @@ -1,10 +1,10 @@ package context import ( + "fmt" "os" "github.com/docker/cli/cli/context/store" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -45,14 +45,14 @@ func (data *TLSData) ToStoreTLSData() *store.EndpointTLSData { func LoadTLSData(s store.Reader, contextName, endpointName string) (*TLSData, error) { tlsFiles, err := s.ListTLSFiles(contextName) if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve TLS files for context %q", contextName) + return nil, fmt.Errorf("failed to retrieve TLS files for context %q: %w", contextName, err) } if epTLSFiles, ok := tlsFiles[endpointName]; ok { var tlsData TLSData for _, f := range epTLSFiles { data, err := s.GetTLSData(contextName, endpointName, f) if err != nil { - return nil, errors.Wrapf(err, "failed to retrieve TLS data (%s) for context %q", f, contextName) + return nil, fmt.Errorf("failed to retrieve TLS data (%s) for context %q: %w", f, contextName, err) } switch f { case caKey: diff --git a/cli/flags/options.go b/cli/flags/options.go index fc168984b44b..8c31a0f17216 100644 --- a/cli/flags/options.go +++ b/cli/flags/options.go @@ -1,14 +1,14 @@ package flags import ( + "errors" "fmt" "os" "path/filepath" "github.com/docker/cli/cli/config" - "github.com/docker/cli/opts" - "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" + "github.com/moby/moby/client" "github.com/sirupsen/logrus" "github.com/spf13/pflag" ) @@ -54,6 +54,39 @@ var ( dockerTLS = os.Getenv(EnvEnableTLS) != "" ) +// hostVar is used for the '--host' / '-H' flag to set [ClientOptions.Hosts]. +// The [ClientOptions.Hosts] field is a slice because it was originally shared +// with the daemon config. However, the CLI only allows for a single host to +// be specified. +// +// hostVar presents itself as a "string", but stores the value in a string +// slice. It produces an error when trying to set multiple values, matching +// the check in [getServerHost]. +// +// [getServerHost]: https://github.com/docker/cli/blob/7eab668982645def1cd46fe1b60894cba6fd17a4/cli/command/cli.go#L542-L551 +type hostVar struct { + dst *[]string + set bool +} + +func (h *hostVar) String() string { + if h.dst == nil || len(*h.dst) == 0 { + return "" + } + return (*h.dst)[0] +} + +func (h *hostVar) Set(s string) error { + if h.set { + return errors.New("specify only one -H") + } + *h.dst = []string{s} + h.set = true + return nil +} + +func (*hostVar) Type() string { return "string" } + // ClientOptions are the options used to configure the client cli. type ClientOptions struct { Debug bool @@ -77,26 +110,24 @@ func (o *ClientOptions) InstallFlags(flags *pflag.FlagSet) { if dockerCertPath == "" { dockerCertPath = configDir } + o.TLSOptions = &tlsconfig.Options{ + CAFile: filepath.Join(dockerCertPath, DefaultCaFile), + CertFile: filepath.Join(dockerCertPath, DefaultCertFile), + KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), + } flags.StringVar(&o.ConfigDir, "config", configDir, "Location of client config files") flags.BoolVarP(&o.Debug, "debug", "D", false, "Enable debug mode") flags.StringVarP(&o.LogLevel, "log-level", "l", "info", `Set the logging level ("debug", "info", "warn", "error", "fatal")`) flags.BoolVar(&o.TLS, "tls", dockerTLS, "Use TLS; implied by --tlsverify") flags.BoolVar(&o.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") + flags.StringVar(&o.TLSOptions.CAFile, "tlscacert", o.TLSOptions.CAFile, "Trust certs signed only by this CA") + flags.StringVar(&o.TLSOptions.CertFile, "tlscert", o.TLSOptions.CertFile, "Path to TLS certificate file") + flags.StringVar(&o.TLSOptions.KeyFile, "tlskey", o.TLSOptions.KeyFile, "Path to TLS key file") - o.TLSOptions = &tlsconfig.Options{ - CAFile: filepath.Join(dockerCertPath, DefaultCaFile), - CertFile: filepath.Join(dockerCertPath, DefaultCertFile), - KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), - } - tlsOptions := o.TLSOptions - flags.Var(opts.NewQuotedString(&tlsOptions.CAFile), "tlscacert", "Trust certs signed only by this CA") - flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file") - flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file") - - // opts.ValidateHost is not used here, so as to allow connection helpers - hostOpt := opts.NewNamedListOptsRef("hosts", &o.Hosts, nil) - flags.VarP(hostOpt, "host", "H", "Daemon socket to connect to") + // TODO(thaJeztah): show the default host. + // TODO(thaJeztah): this should be a string, not an "array" as we only allow a single host. + flags.VarP(&hostVar{dst: &o.Hosts}, "host", "H", "Daemon socket to connect to") flags.StringVarP(&o.Context, "context", "c", "", `Name of the context to use to connect to the daemon (overrides `+client.EnvOverrideHost+` env var and default context set with "docker context use")`) } diff --git a/cli/flags/options_test.go b/cli/flags/options_test.go index 1fef741757be..ed12e875a43d 100644 --- a/cli/flags/options_test.go +++ b/cli/flags/options_test.go @@ -16,9 +16,9 @@ func TestClientOptionsInstallFlags(t *testing.T) { opts.InstallFlags(flags) err := flags.Parse([]string{ - "--tlscacert=\"/foo/cafile\"", - "--tlscert=\"/foo/cert\"", - "--tlskey=\"/foo/key\"", + "--tlscacert=/foo/cafile", + "--tlscert=/foo/cert", + "--tlskey=/foo/key", }) assert.NilError(t, err) assert.Check(t, is.Equal("/foo/cafile", opts.TLSOptions.CAFile)) diff --git a/cli/manifest/store/store.go b/cli/manifest/store/store.go index e97e8628f125..f891f3ec2a51 100644 --- a/cli/manifest/store/store.go +++ b/cli/manifest/store/store.go @@ -2,16 +2,17 @@ package store import ( "encoding/json" + "fmt" "os" "path/filepath" "strings" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" "github.com/docker/distribution/manifest/manifestlist" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" ) // Store manages local storage of image distribution manifests @@ -72,7 +73,7 @@ func (*fsStore) getFromFilename(ref reference.Reference, filename string) (types return types.ImageManifest{}, err } if dgst := digest.FromBytes(raw); dgst != manifestInfo.Digest { - return types.ImageManifest{}, errors.Errorf("invalid manifest file %v: image manifest digest mismatch (%v != %v)", filename, manifestInfo.Digest, dgst) + return types.ImageManifest{}, fmt.Errorf("invalid manifest file %v: image manifest digest mismatch (%v != %v)", filename, manifestInfo.Digest, dgst) } manifestInfo.ImageManifest.Descriptor = ocispec.Descriptor{ Digest: manifestInfo.Digest, @@ -152,27 +153,6 @@ func makeFilesafeName(ref string) string { return strings.ReplaceAll(fileName, "/", "_") } -type notFoundError struct { - object string -} - -func newNotFoundError(ref string) *notFoundError { - return ¬FoundError{object: ref} -} - -func (n *notFoundError) Error() string { - return "No such manifest: " + n.object -} - -// NotFound interface -func (*notFoundError) NotFound() {} - -// IsNotFound returns true if the error is a not found error -func IsNotFound(err error) bool { - _, ok := err.(notFound) - return ok -} - -type notFound interface { - NotFound() +func newNotFoundError(ref string) error { + return errdefs.ErrNotFound.WithMessage("No such manifest: " + ref) } diff --git a/cli/manifest/store/store_test.go b/cli/manifest/store/store_test.go index 2b3a57819f8c..d5c3ef67818e 100644 --- a/cli/manifest/store/store_test.go +++ b/cli/manifest/store/store_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" "github.com/google/go-cmp/cmp" @@ -86,7 +87,7 @@ func TestStoreSaveAndGet(t *testing.T) { actual, err := store.Get(tc.listRef, tc.manifestRef) if tc.expectedErr != "" { assert.Error(t, err, tc.expectedErr) - assert.Check(t, IsNotFound(err)) + assert.Check(t, errdefs.IsNotFound(err)) return } assert.NilError(t, err) @@ -117,5 +118,5 @@ func TestStoreGetListDoesNotExist(t *testing.T) { listRef := ref("list") _, err := store.GetList(listRef) assert.Error(t, err, "No such manifest: list") - assert.Check(t, IsNotFound(err)) + assert.Check(t, errdefs.IsNotFound(err)) } diff --git a/cli/manifest/types/types.go b/cli/manifest/types/types.go index e098928de9dc..b901c263fefa 100644 --- a/cli/manifest/types/types.go +++ b/cli/manifest/types/types.go @@ -2,6 +2,7 @@ package types import ( "encoding/json" + "fmt" "github.com/distribution/reference" "github.com/docker/distribution" @@ -10,7 +11,6 @@ import ( "github.com/docker/distribution/manifest/schema2" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" ) // ImageManifest contains info to output for a manifest object. @@ -82,7 +82,7 @@ func (i ImageManifest) Payload() (string, []byte, error) { case i.OCIManifest != nil: return i.OCIManifest.Payload() default: - return "", nil, errors.Errorf("%s has no payload", i.Ref) + return "", nil, fmt.Errorf("%s has no payload", i.Ref) } } @@ -141,7 +141,7 @@ type SerializableNamed struct { func (s *SerializableNamed) UnmarshalJSON(b []byte) error { var raw string if err := json.Unmarshal(b, &raw); err != nil { - return errors.Wrapf(err, "invalid named reference bytes: %s", b) + return fmt.Errorf("invalid named reference bytes: %s: %w", b, err) } var err error s.Named, err = reference.ParseNamed(raw) diff --git a/cli/registry/client/client.go b/cli/registry/client/client.go deleted file mode 100644 index f1fee951f305..000000000000 --- a/cli/registry/client/client.go +++ /dev/null @@ -1,197 +0,0 @@ -package client - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/distribution/reference" - manifesttypes "github.com/docker/cli/cli/manifest/types" - "github.com/docker/distribution" - distributionclient "github.com/docker/distribution/registry/client" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// RegistryClient is a client used to communicate with a Docker distribution -// registry -type RegistryClient interface { - GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) - GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) - MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error - PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) -} - -// NewRegistryClient returns a new RegistryClient with a resolver -func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient { - return &client{ - authConfigResolver: resolver, - insecureRegistry: insecure, - userAgent: userAgent, - } -} - -// AuthConfigResolver returns Auth Configuration for an index -type AuthConfigResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig - -type client struct { - authConfigResolver AuthConfigResolver - insecureRegistry bool - userAgent string -} - -// ErrBlobCreated returned when a blob mount request was created -type ErrBlobCreated struct { - From reference.Named - Target reference.Named -} - -func (err ErrBlobCreated) Error() string { - return fmt.Sprintf("blob mounted from: %v to: %v", - err.From, err.Target) -} - -// httpProtoError returned if attempting to use TLS with a non-TLS registry -type httpProtoError struct { - cause error -} - -func (e httpProtoError) Error() string { - return e.cause.Error() -} - -var _ RegistryClient = &client{} - -// MountBlob into the registry, so it can be referenced by a manifest -func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error { - repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry) - if err != nil { - return err - } - repoEndpoint.actions = []string{"pull", "push"} - repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint) - if err != nil { - return err - } - lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef)) - switch err.(type) { - case distribution.ErrBlobMounted: - logrus.Debugf("mount of blob %s succeeded", sourceRef) - return nil - case nil: - default: - return errors.Wrapf(err, "failed to mount blob %s to %s", sourceRef, targetRef) - } - lu.Cancel(ctx) - logrus.Debugf("mount of blob %s created", sourceRef) - return ErrBlobCreated{From: sourceRef, Target: targetRef} -} - -// PutManifest sends the manifest to a registry and returns the new digest -func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) { - repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry) - if err != nil { - return "", err - } - - repoEndpoint.actions = []string{"pull", "push"} - repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) - if err != nil { - return "", err - } - - manifestService, err := repo.Manifests(ctx) - if err != nil { - return "", err - } - - _, opts, err := getManifestOptionsFromReference(ref) - if err != nil { - return "", err - } - - dgst, err := manifestService.Put(ctx, manifest, opts...) - if err != nil { - return dgst, errors.Wrapf(err, "failed to put manifest %s", ref) - } - return dgst, nil -} - -func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) { - repoName, err := reference.WithName(repoEndpoint.Name()) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse repo name from %s", ref) - } - httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) - if err != nil { - if !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { - return nil, err - } - if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify { - return nil, httpProtoError{cause: err} - } - // --insecure was set; fall back to plain HTTP - if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" { - url.Scheme = "http" - httpTransport, err = c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) - if err != nil { - return nil, err - } - } - } - return distributionclient.NewRepository(repoName, repoEndpoint.BaseURL(), httpTransport) -} - -func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) { - httpTransport, err := getHTTPTransport( - c.authConfigResolver(ctx, repoEndpoint.indexInfo), - repoEndpoint.endpoint, - repoEndpoint.Name(), - c.userAgent, - repoEndpoint.actions, - ) - if err != nil { - return nil, errors.Wrap(err, "failed to configure transport") - } - return httpTransport, nil -} - -// GetManifest returns an ImageManifest for the reference -func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { - var result manifesttypes.ImageManifest - fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { - var err error - result, err = fetchManifest(ctx, repo, ref) - return result.Ref != nil, err - } - - err := c.iterateEndpoints(ctx, ref, fetch) - return result, err -} - -// GetManifestList returns a list of ImageManifest for the reference -func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { - result := []manifesttypes.ImageManifest{} - fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { - var err error - result, err = fetchList(ctx, repo, ref) - return len(result) > 0, err - } - - err := c.iterateEndpoints(ctx, ref, fetch) - return result, err -} - -func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) { - if tagged, isTagged := ref.(reference.NamedTagged); isTagged { - tag := tagged.Tag() - return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil - } - if digested, isDigested := ref.(reference.Canonical); isDigested { - return digested.Digest(), []distribution.ManifestServiceOption{}, nil - } - return "", nil, errors.Errorf("%s no tag or digest", ref) -} diff --git a/cli/registry/client/endpoint.go b/cli/registry/client/endpoint.go deleted file mode 100644 index df0b190d56fa..000000000000 --- a/cli/registry/client/endpoint.go +++ /dev/null @@ -1,126 +0,0 @@ -package client - -import ( - "net" - "net/http" - "time" - - "github.com/distribution/reference" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/transport" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" - "github.com/pkg/errors" -) - -type repositoryEndpoint struct { - repoName reference.Named - indexInfo *registrytypes.IndexInfo - endpoint registry.APIEndpoint - actions []string -} - -// Name returns the repository name -func (r repositoryEndpoint) Name() string { - return reference.Path(r.repoName) -} - -// BaseURL returns the endpoint url -func (r repositoryEndpoint) BaseURL() string { - return r.endpoint.URL.String() -} - -func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { - repoName := reference.TrimNamed(ref) - repoInfo, _ := registry.ParseRepositoryInfo(ref) - indexInfo := repoInfo.Index - - endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure) - if err != nil { - return repositoryEndpoint{}, err - } - if insecure { - endpoint.TLSConfig.InsecureSkipVerify = true - } - return repositoryEndpoint{ - repoName: repoName, - indexInfo: indexInfo, - endpoint: endpoint, - }, nil -} - -func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEndpoint, error) { - registryService, err := registry.NewService(registry.ServiceOptions{}) - if err != nil { - return registry.APIEndpoint{}, err - } - endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName)) - if err != nil { - return registry.APIEndpoint{}, err - } - // Default to the highest priority endpoint to return - endpoint := endpoints[0] - if insecure { - for _, ep := range endpoints { - if ep.URL.Scheme == "http" { - endpoint = ep - } - } - } - return endpoint, nil -} - -// getHTTPTransport builds a transport for use in communicating with a registry -func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.APIEndpoint, repoName, userAgent string, actions []string) (http.RoundTripper, error) { - // get the http transport, this will be used in a client to upload manifest - base := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: endpoint.TLSConfig, - DisableKeepAlives: true, - } - - modifiers := registry.Headers(userAgent, http.Header{}) - authTransport := transport.NewTransport(base, modifiers...) - challengeManager, err := registry.PingV2Registry(endpoint.URL, authTransport) - if err != nil { - return nil, errors.Wrap(err, "error pinging v2 registry") - } - if authConfig.RegistryToken != "" { - passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} - modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) - } else { - if len(actions) == 0 { - actions = []string{"pull"} - } - creds := registry.NewStaticCredentialStore(&authConfig) - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) - } - return transport.NewTransport(base, modifiers...), nil -} - -// RepoNameForReference returns the repository name from a reference. -// -// Deprecated: this function is no longer used and will be removed in the next release. -func RepoNameForReference(ref reference.Named) (string, error) { - return reference.Path(reference.TrimNamed(ref)), nil -} - -type existingTokenHandler struct { - token string -} - -func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string]string) error { - req.Header.Set("Authorization", "Bearer "+th.token) - return nil -} - -func (*existingTokenHandler) Scheme() string { - return "bearer" -} diff --git a/cli/required.go b/cli/required.go index 6455e8867e0e..b6c3b1f9ca70 100644 --- a/cli/required.go +++ b/cli/required.go @@ -1,7 +1,8 @@ package cli import ( - "github.com/pkg/errors" + "fmt" + "github.com/spf13/cobra" ) @@ -12,7 +13,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { } if cmd.HasSubCommands() { - return errors.Errorf( + return fmt.Errorf( "%[1]s: unknown command: %[2]s %[3]s\n\nUsage: %[4]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -21,7 +22,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { ) } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' accepts no arguments\n\nUsage: %[3]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -35,7 +36,7 @@ func RequiresMinArgs(minArgs int) cobra.PositionalArgs { if len(args) >= minArgs { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires at least %[3]d %[4]s\n\nUsage: %[5]s\n\nSee '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -52,8 +53,8 @@ func RequiresMaxArgs(maxArgs int) cobra.PositionalArgs { if len(args) <= maxArgs { return nil } - return errors.Errorf( - "%[1]s: '%[2]s' requires at most %[3]d %[4]s\n\nUsage: %[5]s\n\nSRun '%[2]s --help' for more information", + return fmt.Errorf( + "%[1]s: '%[2]s' requires at most %[3]d %[4]s\n\nUsage: %[5]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), maxArgs, @@ -69,7 +70,7 @@ func RequiresRangeArgs(minArgs int, maxArgs int) cobra.PositionalArgs { if len(args) >= minArgs && len(args) <= maxArgs { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires at least %[3]d and at most %[4]d %[5]s\n\nUsage: %[6]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), @@ -87,7 +88,7 @@ func ExactArgs(number int) cobra.PositionalArgs { if len(args) == number { return nil } - return errors.Errorf( + return fmt.Errorf( "%[1]s: '%[2]s' requires %[3]d %[4]s\n\nUsage: %[5]s\n\nRun '%[2]s --help' for more information", binName(cmd), cmd.CommandPath(), diff --git a/cli/trust/trust.go b/cli/trust/trust.go deleted file mode 100644 index 27453ae22ee4..000000000000 --- a/cli/trust/trust.go +++ /dev/null @@ -1,386 +0,0 @@ -package trust - -import ( - "context" - "encoding/json" - "io" - "net" - "net/http" - "net/url" - "os" - "path" - "path/filepath" - "time" - - "github.com/distribution/reference" - "github.com/docker/cli/cli/config" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/auth/challenge" - "github.com/docker/distribution/registry/client/transport" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" - "github.com/docker/go-connections/tlsconfig" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/theupdateframework/notary" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/passphrase" - "github.com/theupdateframework/notary/storage" - "github.com/theupdateframework/notary/trustmanager" - "github.com/theupdateframework/notary/trustpinning" - "github.com/theupdateframework/notary/tuf/data" - "github.com/theupdateframework/notary/tuf/signed" -) - -var ( - // ReleasesRole is the role named "releases" - ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases")) - // ActionsPullOnly defines the actions for read-only interactions with a Notary Repository - ActionsPullOnly = []string{"pull"} - // ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository - ActionsPushAndPull = []string{"pull", "push"} -) - -// NotaryServer is the endpoint serving the Notary trust server -const NotaryServer = "https://notary.docker.io" - -// GetTrustDirectory returns the base trust directory name -func GetTrustDirectory() string { - return filepath.Join(config.Dir(), "trust") -} - -// certificateDirectory returns the directory containing -// TLS certificates for the given server. An error is -// returned if there was an error parsing the server string. -func certificateDirectory(server string) (string, error) { - u, err := url.Parse(server) - if err != nil { - return "", err - } - - return filepath.Join(config.Dir(), "tls", u.Host), nil -} - -// Server returns the base URL for the trust server. -func Server(index *registrytypes.IndexInfo) (string, error) { - if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { - urlObj, err := url.Parse(s) - if err != nil || urlObj.Scheme != "https" { - return "", errors.Errorf("valid https URL required for trust server, got %s", s) - } - - return s, nil - } - if index.Official { - return NotaryServer, nil - } - return "https://" + index.Name, nil -} - -type simpleCredentialStore struct { - auth registrytypes.AuthConfig -} - -func (scs simpleCredentialStore) Basic(*url.URL) (string, string) { - return scs.auth.Username, scs.auth.Password -} - -func (scs simpleCredentialStore) RefreshToken(*url.URL, string) string { - return scs.auth.IdentityToken -} - -func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {} - -// GetNotaryRepository returns a NotaryRepository which stores all the -// information needed to operate on a notary repository. -// It creates an HTTP transport providing authentication support. -func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { - server, err := Server(repoInfo.Index) - if err != nil { - return nil, err - } - - cfg := tlsconfig.ClientDefault() - cfg.InsecureSkipVerify = !repoInfo.Index.Secure - - // Get certificate base directory - certDir, err := certificateDirectory(server) - if err != nil { - return nil, err - } - logrus.Debugf("reading certificate directory: %s", certDir) - - if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { - return nil, err - } - - base := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: cfg, - DisableKeepAlives: true, - } - - // Skip configuration headers since request is not going to Docker daemon - modifiers := registry.Headers(userAgent, http.Header{}) - authTransport := transport.NewTransport(base, modifiers...) - pingClient := &http.Client{ - Transport: authTransport, - Timeout: 5 * time.Second, - } - endpointStr := server + "/v2/" - req, err := http.NewRequest(http.MethodGet, endpointStr, nil) - if err != nil { - return nil, err - } - - challengeManager := challenge.NewSimpleManager() - - resp, err := pingClient.Do(req) - if err != nil { - // Ignore error on ping to operate in offline mode - logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) - } else { - defer resp.Body.Close() - - // Add response to the challenge manager to parse out - // authentication header and register authentication method - if err := challengeManager.AddResponse(resp); err != nil { - return nil, err - } - } - - scope := auth.RepositoryScope{ - Repository: repoInfo.Name.Name(), - Actions: actions, - } - creds := simpleCredentialStore{auth: *authConfig} - tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ - Transport: authTransport, - Credentials: creds, - Scopes: []auth.Scope{scope}, - ClientID: registry.AuthClientID, - }) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) - tr := transport.NewTransport(base, modifiers...) - - return client.NewFileCachedRepository( - GetTrustDirectory(), - data.GUN(repoInfo.Name.Name()), - server, - tr, - GetPassphraseRetriever(in, out), - trustpinning.TrustPinConfig{}) -} - -// GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars -func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever { - aliasMap := map[string]string{ - "root": "root", - "snapshot": "repository", - "targets": "repository", - "default": "repository", - } - baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap) - env := map[string]string{ - "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), - "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - } - - return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { - if v := env[alias]; v != "" { - return v, numAttempts > 1, nil - } - // For non-root roles, we can also try the "default" alias if it is specified - if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() { - return v, numAttempts > 1, nil - } - return baseRetriever(keyName, alias, createNew, numAttempts) - } -} - -// NotaryError formats an error message received from the notary service -func NotaryError(repoName string, err error) error { - switch err.(type) { - case *json.SyntaxError: - logrus.Debugf("Notary syntax error: %s", err) - return errors.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) - case signed.ErrExpired: - return errors.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) - case trustmanager.ErrKeyNotFound: - return errors.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) - case storage.NetworkError: - return errors.Errorf("Error: error contacting notary server: %v", err) - case storage.ErrMetaNotFound: - return errors.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) - case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: - return errors.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) - case signed.ErrNoKeys: - return errors.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) - case signed.ErrLowVersion: - return errors.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) - case signed.ErrRoleThreshold: - return errors.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) - case client.ErrRepositoryNotExist: - return errors.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) - case signed.ErrInsufficientSignatures: - return errors.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) - } - - return err -} - -// AddToAllSignableRoles attempts to add the image target to all the top level -// delegation roles we can (based on whether we have the signing key and whether -// the role's path allows us to). -// -// If there are no delegation roles, we add to the targets role. -func AddToAllSignableRoles(repo client.Repository, target *client.Target) error { - signableRoles, err := GetSignableRoles(repo, target) - if err != nil { - return err - } - - return repo.AddTarget(target, signableRoles...) -} - -// GetSignableRoles returns a list of roles for which we have valid signing -// keys, given a notary repository and a target -func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) { - var signableRoles []data.RoleName - - // translate the full key names, which includes the GUN, into just the key IDs - allCanonicalKeyIDs := make(map[string]struct{}) - for fullKeyID := range repo.GetCryptoService().ListAllKeys() { - allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} - } - - allDelegationRoles, err := repo.GetDelegationRoles() - if err != nil { - return signableRoles, err - } - - // if there are no delegation roles, then just try to sign it into the targets role - if len(allDelegationRoles) == 0 { - signableRoles = append(signableRoles, data.CanonicalTargetsRole) - return signableRoles, nil - } - - // there are delegation roles, find every delegation role we have a key for, - // and attempt to sign in to all those roles. - for _, delegationRole := range allDelegationRoles { - // We do not support signing any delegation role that isn't a direct child of the targets role. - // Also don't bother checking the keys if we can't add the target - // to this role due to path restrictions - if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) { - continue - } - - for _, canonicalKeyID := range delegationRole.KeyIDs { - if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { - signableRoles = append(signableRoles, delegationRole.Name) - break - } - } - } - - if len(signableRoles) == 0 { - return signableRoles, errors.Errorf("no valid signing keys for delegation roles") - } - - return signableRoles, nil -} - -// ImageRefAndAuth contains all reference information and the auth config for an image request -type ImageRefAndAuth struct { - original string - authConfig *registrytypes.AuthConfig - reference reference.Named - repoInfo *registry.RepositoryInfo - tag string - digest digest.Digest -} - -// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name -// as an ImageRefAndAuth struct -func GetImageReferencesAndAuth(ctx context.Context, - authResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig, - imgName string, -) (ImageRefAndAuth, error) { - ref, err := reference.ParseNormalizedNamed(imgName) - if err != nil { - return ImageRefAndAuth{}, err - } - - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := authResolver(ctx, repoInfo.Index) - return ImageRefAndAuth{ - original: imgName, - authConfig: &authConfig, - reference: ref, - repoInfo: repoInfo, - tag: getTag(ref), - digest: getDigest(ref), - }, nil -} - -func getTag(ref reference.Named) string { - switch x := ref.(type) { - case reference.Canonical, reference.Digested: - return "" - case reference.NamedTagged: - return x.Tag() - default: - return "" - } -} - -func getDigest(ref reference.Named) digest.Digest { - switch x := ref.(type) { - case reference.Canonical: - return x.Digest() - case reference.Digested: - return x.Digest() - default: - return digest.Digest("") - } -} - -// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) AuthConfig() *registrytypes.AuthConfig { - return imgRefAuth.authConfig -} - -// Reference returns the Image reference for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { - return imgRefAuth.reference -} - -// RepoInfo returns the repository information for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { - return imgRefAuth.repoInfo -} - -// Tag returns the Image tag for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Tag() string { - return imgRefAuth.tag -} - -// Digest returns the Image digest for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest { - return imgRefAuth.digest -} - -// Name returns the image name used to initialize the ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) Name() string { - return imgRefAuth.original -} diff --git a/cli/trust/trust_test.go b/cli/trust/trust_test.go deleted file mode 100644 index 18531bf4c941..000000000000 --- a/cli/trust/trust_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package trust - -import ( - "testing" - - "github.com/distribution/reference" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/opencontainers/go-digest" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/trustpinning" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestGetTag(t *testing.T) { - ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") - assert.NilError(t, err) - tag := getTag(ref) - assert.Check(t, is.Equal("", tag)) - - ref, err = reference.ParseNormalizedNamed("alpine:latest") - assert.NilError(t, err) - tag = getTag(ref) - assert.Check(t, is.Equal(tag, "latest")) - - ref, err = reference.ParseNormalizedNamed("alpine") - assert.NilError(t, err) - tag = getTag(ref) - assert.Check(t, is.Equal(tag, "")) -} - -func TestGetDigest(t *testing.T) { - ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") - assert.NilError(t, err) - d := getDigest(ref) - assert.Check(t, is.Equal(digest.Digest("sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"), d)) - - ref, err = reference.ParseNormalizedNamed("alpine:latest") - assert.NilError(t, err) - d = getDigest(ref) - assert.Check(t, is.Equal(digest.Digest(""), d)) - - ref, err = reference.ParseNormalizedNamed("alpine") - assert.NilError(t, err) - d = getDigest(ref) - assert.Check(t, is.Equal(digest.Digest(""), d)) -} - -func TestGetSignableRolesError(t *testing.T) { - notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, nil, trustpinning.TrustPinConfig{}) - assert.NilError(t, err) - _, err = GetSignableRoles(notaryRepo, &client.Target{}) - const expected = "client is offline" - assert.Error(t, err, expected) -} - -func TestENVTrustServer(t *testing.T) { - t.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.example.com:5000") - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - output, err := Server(indexInfo) - const expected = "https://notary-test.example.com:5000" - assert.NilError(t, err) - assert.Equal(t, output, expected) -} - -func TestHTTPENVTrustServer(t *testing.T) { - t.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.example.com:5000") - indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} - _, err := Server(indexInfo) - const expected = "valid https URL required for trust server" - assert.ErrorContains(t, err, expected, "Expected error with invalid scheme") -} - -func TestOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} - output, err := Server(indexInfo) - const expected = NotaryServer - assert.NilError(t, err) - assert.Equal(t, output, expected) -} - -func TestNonOfficialTrustServer(t *testing.T) { - indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} - output, err := Server(indexInfo) - const expected = "https://testserver" - assert.NilError(t, err) - assert.Equal(t, output, expected) -} diff --git a/docs/reference/commandline/trust.md b/cmd/docker-trust/docs/reference/trust.md similarity index 100% rename from docs/reference/commandline/trust.md rename to cmd/docker-trust/docs/reference/trust.md diff --git a/docs/reference/commandline/trust_inspect.md b/cmd/docker-trust/docs/reference/trust_inspect.md similarity index 100% rename from docs/reference/commandline/trust_inspect.md rename to cmd/docker-trust/docs/reference/trust_inspect.md diff --git a/docs/reference/commandline/trust_key.md b/cmd/docker-trust/docs/reference/trust_key.md similarity index 100% rename from docs/reference/commandline/trust_key.md rename to cmd/docker-trust/docs/reference/trust_key.md diff --git a/docs/reference/commandline/trust_key_generate.md b/cmd/docker-trust/docs/reference/trust_key_generate.md similarity index 100% rename from docs/reference/commandline/trust_key_generate.md rename to cmd/docker-trust/docs/reference/trust_key_generate.md diff --git a/docs/reference/commandline/trust_key_load.md b/cmd/docker-trust/docs/reference/trust_key_load.md similarity index 100% rename from docs/reference/commandline/trust_key_load.md rename to cmd/docker-trust/docs/reference/trust_key_load.md diff --git a/docs/reference/commandline/trust_revoke.md b/cmd/docker-trust/docs/reference/trust_revoke.md similarity index 100% rename from docs/reference/commandline/trust_revoke.md rename to cmd/docker-trust/docs/reference/trust_revoke.md diff --git a/docs/reference/commandline/trust_sign.md b/cmd/docker-trust/docs/reference/trust_sign.md similarity index 100% rename from docs/reference/commandline/trust_sign.md rename to cmd/docker-trust/docs/reference/trust_sign.md diff --git a/docs/reference/commandline/trust_signer.md b/cmd/docker-trust/docs/reference/trust_signer.md similarity index 100% rename from docs/reference/commandline/trust_signer.md rename to cmd/docker-trust/docs/reference/trust_signer.md diff --git a/docs/reference/commandline/trust_signer_add.md b/cmd/docker-trust/docs/reference/trust_signer_add.md similarity index 100% rename from docs/reference/commandline/trust_signer_add.md rename to cmd/docker-trust/docs/reference/trust_signer_add.md diff --git a/docs/reference/commandline/trust_signer_remove.md b/cmd/docker-trust/docs/reference/trust_signer_remove.md similarity index 100% rename from docs/reference/commandline/trust_signer_remove.md rename to cmd/docker-trust/docs/reference/trust_signer_remove.md diff --git a/cmd/docker-trust/go.mod b/cmd/docker-trust/go.mod new file mode 100644 index 000000000000..91136929e9ad --- /dev/null +++ b/cmd/docker-trust/go.mod @@ -0,0 +1,80 @@ +module github.com/docker/cli/cmd/docker-trust + +go 1.24.0 + +require ( + github.com/containerd/errdefs v1.0.0 + github.com/distribution/reference v0.6.0 + github.com/docker/cli v29.0.4+incompatible + github.com/docker/cli-docs-tool v0.10.0 + github.com/docker/distribution v2.8.3+incompatible + github.com/docker/go-connections v0.6.0 + github.com/fvbommel/sortorder v1.1.0 + github.com/moby/moby/api v1.52.0 + github.com/moby/moby/client v0.1.0 + github.com/opencontainers/go-digest v1.0.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.10.1 + github.com/spf13/pflag v1.0.10 + github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a + go.opentelemetry.io/otel v1.38.0 + gotest.tools/v3 v3.5.2 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudflare/cfssl v1.6.4 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/docker/docker-credential-helpers v0.9.4 // indirect + github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect + k8s.io/klog/v2 v2.90.1 // indirect +) diff --git a/cmd/docker-trust/go.sum b/cmd/docker-trust/go.sum new file mode 100644 index 000000000000..c286f4bd9b41 --- /dev/null +++ b/cmd/docker-trust/go.sum @@ -0,0 +1,350 @@ +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= +github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bugsnag/bugsnag-go v1.0.5 h1:NIoY2u+am1/GRgUZa+ata8UUrRBuCK4pLq0/lcvMF7M= +github.com/bugsnag/bugsnag-go v1.0.5/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= +github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= +github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.0.4+incompatible h1:mffN/hPqaI39vx/4QiSkdldHeM0rP1ZZBIXRUOPI5+I= +github.com/docker/cli v29.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= +github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= +github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= +github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= +github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= +github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= +github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= +github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y= +github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I= +github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= +github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.1.0 h1:nt+hn6O9cyJQqq5UWnFGqsZRTS/JirUqzPjEl0Bdc/8= +github.com/moby/moby/client v0.1.0/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk= +github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= +github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg= +github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0= +github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw= +github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss= +github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= +github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw= +github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= +github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0= +github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= +gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= +gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/cmd/docker-trust/internal/registry/auth.go b/cmd/docker-trust/internal/registry/auth.go new file mode 100644 index 000000000000..98e1b42eddf6 --- /dev/null +++ b/cmd/docker-trust/internal/registry/auth.go @@ -0,0 +1,4 @@ +package registry + +// AuthClientID is used the ClientID used for the token server +const AuthClientID = "docker" diff --git a/cmd/docker-trust/internal/registry/config.go b/cmd/docker-trust/internal/registry/config.go new file mode 100644 index 000000000000..07237020e6a5 --- /dev/null +++ b/cmd/docker-trust/internal/registry/config.go @@ -0,0 +1,79 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +package registry + +import ( + "net" + "strings" + + "github.com/distribution/reference" + "github.com/moby/moby/api/types/registry" +) + +// IndexName is the name of the index +const IndexName = "docker.io" + +func normalizeIndexName(val string) string { + if val == "index.docker.io" { + return "docker.io" + } + return val +} + +// NewIndexInfo creates a new [registry.IndexInfo] or the given +// repository-name, and detects whether the registry is considered +// "secure" (non-localhost). +func NewIndexInfo(reposName reference.Named) *registry.IndexInfo { + indexName := normalizeIndexName(reference.Domain(reposName)) + if indexName == IndexName { + return ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + } + + return ®istry.IndexInfo{ + Name: indexName, + Secure: !isInsecure(indexName), + } +} + +// isInsecure is used to detect whether a registry domain or IP-address is allowed +// to use an insecure (non-TLS, or self-signed cert) connection according to the +// defaults, which allows for insecure connections with registries running on a +// loopback address ("localhost", "::1/128", "127.0.0.0/8"). +// +// It is used in situations where we don't have access to the daemon's configuration, +// for example, when used from the client / CLI. +func isInsecure(hostNameOrIP string) bool { + // Attempt to strip port if present; this also strips brackets for + // IPv6 addresses with a port (e.g. "[::1]:5000"). + // + // This is best-effort; we'll continue using the address as-is if it fails. + if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { + hostNameOrIP = host + } + if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { + // Fast path; no need to resolve these, assuming nobody overrides + // "localhost" for anything else than a loopback address (sorry, not sorry). + return true + } + + var addresses []net.IP + if ip := net.ParseIP(hostNameOrIP); ip != nil { + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-addresses. + addrs, _ := net.LookupIP(hostNameOrIP) + addresses = append(addresses, addrs...) + } + + for _, addr := range addresses { + if addr.IsLoopback() { + return true + } + } + return false +} diff --git a/cmd/docker-trust/internal/registry/doc.go b/cmd/docker-trust/internal/registry/doc.go new file mode 100644 index 000000000000..0b6a24767c84 --- /dev/null +++ b/cmd/docker-trust/internal/registry/doc.go @@ -0,0 +1,12 @@ +// Package registry is a fork of [github.com/docker/docker/registry], taken +// at commit [moby@49306c6]. Git history was not preserved in this fork, +// but can be found using the URLs provided. +// +// This fork was created to remove the dependency on the "Moby" codebase, +// and because the CLI only needs a subset of its features. The original +// package was written specifically for use in the daemon code, and includes +// functionality that cannot be used in the CLI. +// +// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry +// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry +package registry diff --git a/cmd/docker-trust/internal/registry/errors.go b/cmd/docker-trust/internal/registry/errors.go new file mode 100644 index 000000000000..06533446a9c0 --- /dev/null +++ b/cmd/docker-trust/internal/registry/errors.go @@ -0,0 +1,24 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +package registry + +import ( + "fmt" +) + +func invalidParam(err error) error { + return invalidParameterErr{err} +} + +func invalidParamf(format string, args ...any) error { + return invalidParameterErr{fmt.Errorf(format, args...)} +} + +type invalidParameterErr struct{ error } + +func (invalidParameterErr) InvalidParameter() {} + +func (e invalidParameterErr) Unwrap() error { + return e.error +} diff --git a/cmd/docker-trust/internal/registry/registry.go b/cmd/docker-trust/internal/registry/registry.go new file mode 100644 index 000000000000..dce11dcc0691 --- /dev/null +++ b/cmd/docker-trust/internal/registry/registry.go @@ -0,0 +1,101 @@ +// Package registry contains client primitives to interact with a remote Docker registry. +package registry + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/go-connections/tlsconfig" + "github.com/sirupsen/logrus" +) + +func hasFile(files []os.DirEntry, name string) bool { + for _, f := range files { + if f.Name() == name { + return true + } + } + return false +} + +// ReadCertsDirectory reads the directory for TLS certificates +// including roots and certificate pairs and updates the +// provided TLS configuration. +func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { + return loadTLSConfig(context.TODO(), directory, tlsConfig) +} + +// loadTLSConfig reads the directory for TLS certificates including roots and +// certificate pairs, and updates the provided TLS configuration. +func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error { + fs, err := os.ReadDir(directory) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return invalidParam(err) + } + + for _, f := range fs { + if ctx.Err() != nil { + return ctx.Err() + } + switch filepath.Ext(f.Name()) { + case ".crt": + if tlsConfig.RootCAs == nil { + systemPool, err := tlsconfig.SystemCertPool() + if err != nil { + return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err)) + } + tlsConfig.RootCAs = systemPool + } + fileName := filepath.Join(directory, f.Name()) + logrus.Debugf("crt: %s", fileName) + data, err := os.ReadFile(fileName) + if err != nil { + return err + } + tlsConfig.RootCAs.AppendCertsFromPEM(data) + case ".cert": + certName := f.Name() + keyName := certName[:len(certName)-5] + ".key" + logrus.Debugf("cert: %s", filepath.Join(directory, certName)) + if !hasFile(fs, keyName) { + return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName) + } + cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) + if err != nil { + return err + } + tlsConfig.Certificates = append(tlsConfig.Certificates, cert) + case ".key": + keyName := f.Name() + certName := keyName[:len(keyName)-4] + ".cert" + logrus.Debugf("key: %s", filepath.Join(directory, keyName)) + if !hasFile(fs, certName) { + return invalidParamf("missing client certificate %s for key %s", certName, keyName) + } + } + } + + return nil +} + +// Headers returns request modifiers with a User-Agent and metaHeaders +func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier { + modifiers := []transport.RequestModifier{} + if userAgent != "" { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{ + "User-Agent": []string{userAgent}, + })) + } + if metaHeaders != nil { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) + } + return modifiers +} diff --git a/cmd/docker-trust/internal/registry/registry_test.go b/cmd/docker-trust/internal/registry/registry_test.go new file mode 100644 index 000000000000..676076fc93d4 --- /dev/null +++ b/cmd/docker-trust/internal/registry/registry_test.go @@ -0,0 +1,281 @@ +package registry + +import ( + "testing" + + "github.com/distribution/reference" + "github.com/moby/moby/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestNewIndexInfo(t *testing.T) { + type staticRepositoryInfo struct { + Index *registry.IndexInfo + RemoteName string + CanonicalName string + LocalName string + } + + tests := map[string]staticRepositoryInfo{ + "fooo/bar": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "docker.io/fooo/bar", + }, + "library/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "nonlibrary/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "docker.io/nonlibrary/ubuntu", + }, + "ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "other/library": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "docker.io/other/library", + }, + "127.0.0.1:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + }, + "127.0.0.1:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + }, + "[::1]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "[::1]:8000/private/moonbase", + CanonicalName: "[::1]:8000/private/moonbase", + }, + "[::1]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "[::1]:8000/privatebase", + CanonicalName: "[::1]:8000/privatebase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "[::2]:8000/private/moonbase", + CanonicalName: "[::2]:8000/private/moonbase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "[::2]:8000/privatebase", + CanonicalName: "[::2]:8000/privatebase", + }, + "localhost:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + }, + "localhost:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + }, + "example.com/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + }, + "example.com/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + }, + "example.com:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + }, + "example.com:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + }, + "localhost/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + }, + "localhost/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + }, + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "index." + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + "index." + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + } + + for reposName, expected := range tests { + t.Run(reposName, func(t *testing.T) { + named, err := reference.ParseNormalizedNamed(reposName) + assert.NilError(t, err) + + indexInfo := NewIndexInfo(named) + repoInfoName := reference.TrimNamed(named) + + assert.Check(t, is.DeepEqual(indexInfo, expected.Index)) + assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName)) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName)) + assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName)) + }) + } +} diff --git a/cmd/docker-trust/internal/test/cli.go b/cmd/docker-trust/internal/test/cli.go new file mode 100644 index 000000000000..20047cb0f39e --- /dev/null +++ b/cmd/docker-trust/internal/test/cli.go @@ -0,0 +1,128 @@ +package test + +import ( + "bytes" + "errors" + "io" + "strings" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/streams" + "github.com/moby/moby/client" + notaryclient "github.com/theupdateframework/notary/client" +) + +// NotaryClientFuncType defines a function that returns a fake notary client +type NotaryClientFuncType func() (notaryclient.Repository, error) + +// FakeCli emulates the default DockerCli +type FakeCli struct { + command.DockerCli + client client.APIClient + configfile *configfile.ConfigFile + out *streams.Out + outBuffer *bytes.Buffer + err *streams.Out + errBuffer *bytes.Buffer + in *streams.In + server command.ServerInfo + notaryClientFunc NotaryClientFuncType + currentContext string +} + +// NewFakeCli returns a fake for the command.Cli interface +func NewFakeCli(apiClient client.APIClient, opts ...func(*FakeCli)) *FakeCli { + outBuffer := new(bytes.Buffer) + errBuffer := new(bytes.Buffer) + c := &FakeCli{ + client: apiClient, + out: streams.NewOut(outBuffer), + outBuffer: outBuffer, + err: streams.NewOut(errBuffer), + errBuffer: errBuffer, + in: streams.NewIn(io.NopCloser(strings.NewReader(""))), + // Use an empty string for filename so that tests don't create configfiles + // Set cli.ConfigFile().Filename to a tempfile to support Save. + configfile: configfile.New(""), + currentContext: command.DefaultContextName, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// SetIn sets the input of the cli to the specified ReadCloser +func (c *FakeCli) SetIn(in *streams.In) { + c.in = in +} + +// SetErr sets the stderr stream for the cli to the specified io.Writer +func (c *FakeCli) SetErr(err *streams.Out) { + c.err = err +} + +// SetOut sets the stdout stream for the cli to the specified io.Writer +func (c *FakeCli) SetOut(out *streams.Out) { + c.out = out +} + +// Client returns a docker API client +func (c *FakeCli) Client() client.APIClient { + return c.client +} + +// CurrentVersion returns the API version used by FakeCli. +// func (*FakeCli) CurrentVersion() string { +// return client.MaxAPIVersion +// } + +// Out returns the output stream (stdout) the cli should write on +func (c *FakeCli) Out() *streams.Out { + return c.out +} + +// Err returns the output stream (stderr) the cli should write on +func (c *FakeCli) Err() *streams.Out { + return c.err +} + +// In returns the input stream the cli will use +func (c *FakeCli) In() *streams.In { + return c.in +} + +// ConfigFile returns the cli configfile object (to get client configuration) +func (c *FakeCli) ConfigFile() *configfile.ConfigFile { + return c.configfile +} + +// OutBuffer returns the stdout buffer +func (c *FakeCli) OutBuffer() *bytes.Buffer { + return c.outBuffer +} + +// ErrBuffer Buffer returns the stderr buffer +func (c *FakeCli) ErrBuffer() *bytes.Buffer { + return c.errBuffer +} + +// ResetOutputBuffers resets the .OutBuffer() and.ErrBuffer() back to empty +func (c *FakeCli) ResetOutputBuffers() { + c.outBuffer.Reset() + c.errBuffer.Reset() +} + +// SetNotaryClient sets the internal getter for retrieving a NotaryClient +func (c *FakeCli) SetNotaryClient(notaryClientFunc NotaryClientFuncType) { + c.notaryClientFunc = notaryClientFunc +} + +// NotaryClient returns an err for testing unless defined +func (c *FakeCli) NotaryClient() (notaryclient.Repository, error) { + if c.notaryClientFunc != nil { + return c.notaryClientFunc() + } + return nil, errors.New("no notary client available unless defined") +} diff --git a/cmd/docker-trust/internal/test/notary/client.go b/cmd/docker-trust/internal/test/notary/client.go new file mode 100644 index 000000000000..fde935afa6ae --- /dev/null +++ b/cmd/docker-trust/internal/test/notary/client.go @@ -0,0 +1,555 @@ +package notary + +import ( + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/client/changelist" + "github.com/theupdateframework/notary/cryptoservice" + "github.com/theupdateframework/notary/storage" + "github.com/theupdateframework/notary/trustmanager" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/signed" +) + +// GetOfflineNotaryRepository returns a OfflineNotaryRepository +func GetOfflineNotaryRepository() (client.Repository, error) { + return OfflineNotaryRepository{}, nil +} + +// OfflineNotaryRepository is a mock Notary repository that is offline +type OfflineNotaryRepository struct{} + +// Initialize creates a new repository by using rootKey as the root Key for the +// TUF repository. +func (OfflineNotaryRepository) Initialize([]string, ...data.RoleName) error { + return storage.ErrOffline{} +} + +// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates +func (OfflineNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { + return storage.ErrOffline{} +} + +// Publish pushes the local changes in signed material to the remote notary-server +// Conceptually it performs an operation similar to a `git rebase` +func (OfflineNotaryRepository) Publish() error { + return storage.ErrOffline{} +} + +// AddTarget creates new changelist entries to add a target to the given roles +// in the repository when the changelist gets applied at publish time. +func (OfflineNotaryRepository) AddTarget(*client.Target, ...data.RoleName) error { + return nil +} + +// RemoveTarget creates new changelist entries to remove a target from the given +// roles in the repository when the changelist gets applied at publish time. +func (OfflineNotaryRepository) RemoveTarget(string, ...data.RoleName) error { + return nil +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +func (OfflineNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { + return nil, storage.ErrOffline{} +} + +// GetTargetByName returns a target by the given name. +func (OfflineNotaryRepository) GetTargetByName(string, ...data.RoleName) (*client.TargetWithRole, error) { + return nil, storage.ErrOffline{} +} + +// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all +// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. +func (OfflineNotaryRepository) GetAllTargetMetadataByName(string) ([]client.TargetSignedStruct, error) { + return nil, storage.ErrOffline{} +} + +// GetChangelist returns the list of the repository's unpublished changes +func (OfflineNotaryRepository) GetChangelist() (changelist.Changelist, error) { + return changelist.NewMemChangelist(), nil +} + +// ListRoles returns a list of RoleWithSignatures objects for this repo +func (OfflineNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + return nil, storage.ErrOffline{} +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +func (OfflineNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return nil, storage.ErrOffline{} +} + +// AddDelegation creates changelist entries to add provided delegation public keys and paths. +func (OfflineNotaryRepository) AddDelegation(data.RoleName, []data.PublicKey, []string) error { + return nil +} + +// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. +func (OfflineNotaryRepository) AddDelegationRoleAndKeys(data.RoleName, []data.PublicKey) error { + return nil +} + +// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. +func (OfflineNotaryRepository) AddDelegationPaths(data.RoleName, []string) error { + return nil +} + +// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths. +func (OfflineNotaryRepository) RemoveDelegationKeysAndPaths(data.RoleName, []string, []string) error { + return nil +} + +// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety. +func (OfflineNotaryRepository) RemoveDelegationRole(data.RoleName) error { + return nil +} + +// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. +func (OfflineNotaryRepository) RemoveDelegationPaths(data.RoleName, []string) error { + return nil +} + +// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. +func (OfflineNotaryRepository) RemoveDelegationKeys(data.RoleName, []string) error { + return nil +} + +// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. +func (OfflineNotaryRepository) ClearDelegationPaths(data.RoleName) error { + return nil +} + +// Witness creates change objects to witness (i.e. re-sign) the given +// roles on the next publish. One change is created per role +func (OfflineNotaryRepository) Witness(...data.RoleName) ([]data.RoleName, error) { + return nil, nil +} + +// RotateKey rotates a private key and returns the public component from the remote server +func (OfflineNotaryRepository) RotateKey(data.RoleName, bool, []string) error { + return storage.ErrOffline{} +} + +// GetCryptoService is the getter for the repository's CryptoService +func (OfflineNotaryRepository) GetCryptoService() signed.CryptoService { + return nil +} + +// SetLegacyVersions allows the number of legacy versions of the root +// to be inspected for old signing keys to be configured. +func (OfflineNotaryRepository) SetLegacyVersions(int) {} + +// GetGUN is a getter for the GUN object from a Repository +func (OfflineNotaryRepository) GetGUN() data.GUN { + return data.GUN("gun") +} + +// GetUninitializedNotaryRepository returns an UninitializedNotaryRepository +func GetUninitializedNotaryRepository() (client.Repository, error) { + return UninitializedNotaryRepository{}, nil +} + +// UninitializedNotaryRepository is a mock Notary repository that is uninintialized +// it builds on top of the OfflineNotaryRepository, instead returning ErrRepositoryNotExist +// for any online operation +type UninitializedNotaryRepository struct { + OfflineNotaryRepository +} + +// Initialize creates a new repository by using rootKey as the root Key for the +// TUF repository. +func (UninitializedNotaryRepository) Initialize([]string, ...data.RoleName) error { + return client.ErrRepositoryNotExist{} +} + +// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates +func (UninitializedNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { + return client.ErrRepositoryNotExist{} +} + +// Publish pushes the local changes in signed material to the remote notary-server +// Conceptually it performs an operation similar to a `git rebase` +func (UninitializedNotaryRepository) Publish() error { + return client.ErrRepositoryNotExist{} +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +func (UninitializedNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { + return nil, client.ErrRepositoryNotExist{} +} + +// GetTargetByName returns a target by the given name. +func (UninitializedNotaryRepository) GetTargetByName(string, ...data.RoleName) (*client.TargetWithRole, error) { + return nil, client.ErrRepositoryNotExist{} +} + +// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all +// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. +func (UninitializedNotaryRepository) GetAllTargetMetadataByName(string) ([]client.TargetSignedStruct, error) { + return nil, client.ErrRepositoryNotExist{} +} + +// ListRoles returns a list of RoleWithSignatures objects for this repo +func (UninitializedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + return nil, client.ErrRepositoryNotExist{} +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +func (UninitializedNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return nil, client.ErrRepositoryNotExist{} +} + +// RotateKey rotates a private key and returns the public component from the remote server +func (UninitializedNotaryRepository) RotateKey(data.RoleName, bool, []string) error { + return client.ErrRepositoryNotExist{} +} + +// GetEmptyTargetsNotaryRepository returns an EmptyTargetsNotaryRepository +func GetEmptyTargetsNotaryRepository() (client.Repository, error) { + return EmptyTargetsNotaryRepository{}, nil +} + +// EmptyTargetsNotaryRepository is a mock Notary repository that is initialized +// but does not have any signed targets +type EmptyTargetsNotaryRepository struct { + OfflineNotaryRepository +} + +// Initialize creates a new repository by using rootKey as the root Key for the +// TUF repository. +func (EmptyTargetsNotaryRepository) Initialize([]string, ...data.RoleName) error { + return nil +} + +// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates +func (EmptyTargetsNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { + return nil +} + +// Publish pushes the local changes in signed material to the remote notary-server +// Conceptually it performs an operation similar to a `git rebase` +func (EmptyTargetsNotaryRepository) Publish() error { + return nil +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +func (EmptyTargetsNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { + return []*client.TargetWithRole{}, nil +} + +// GetTargetByName returns a target by the given name. +func (EmptyTargetsNotaryRepository) GetTargetByName(name string, _ ...data.RoleName) (*client.TargetWithRole, error) { + return nil, client.ErrNoSuchTarget(name) +} + +// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all +// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. +func (EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + return nil, client.ErrNoSuchTarget(name) +} + +// ListRoles returns a list of RoleWithSignatures objects for this repo +func (EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + rootRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"rootID"}, + Threshold: 1, + }, + Name: data.CanonicalRootRole, + } + + targetsRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"targetsID"}, + Threshold: 1, + }, + Name: data.CanonicalTargetsRole, + } + return []client.RoleWithSignatures{ + {Role: rootRole}, + {Role: targetsRole}, + }, nil +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +func (EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return []data.Role{}, nil +} + +// RotateKey rotates a private key and returns the public component from the remote server +func (EmptyTargetsNotaryRepository) RotateKey(data.RoleName, bool, []string) error { + return nil +} + +// GetLoadedNotaryRepository returns a LoadedNotaryRepository +func GetLoadedNotaryRepository() (client.Repository, error) { + return LoadedNotaryRepository{}, nil +} + +// LoadedNotaryRepository is a mock Notary repository that is loaded with targets, delegations, and keys +type LoadedNotaryRepository struct { + EmptyTargetsNotaryRepository + statefulCryptoService signed.CryptoService +} + +// LoadedNotaryRepository has three delegations: +// - targets/releases: includes keys A and B +// - targets/alice: includes key A +// - targets/bob: includes key B +var loadedReleasesRole = data.DelegationRole{ + BaseRole: data.BaseRole{ + Name: "targets/releases", + Keys: map[string]data.PublicKey{"A": nil, "B": nil}, + Threshold: 1, + }, +} + +var loadedAliceRole = data.DelegationRole{ + BaseRole: data.BaseRole{ + Name: "targets/alice", + Keys: map[string]data.PublicKey{"A": nil}, + Threshold: 1, + }, +} + +var loadedBobRole = data.DelegationRole{ + BaseRole: data.BaseRole{ + Name: "targets/bob", + Keys: map[string]data.PublicKey{"B": nil}, + Threshold: 1, + }, +} + +var loadedDelegationRoles = []data.Role{ + { + Name: loadedReleasesRole.Name, + RootRole: data.RootRole{ + KeyIDs: []string{"A", "B"}, + Threshold: 1, + }, + }, + { + Name: loadedAliceRole.Name, + RootRole: data.RootRole{ + KeyIDs: []string{"A"}, + Threshold: 1, + }, + }, + { + Name: loadedBobRole.Name, + RootRole: data.RootRole{ + KeyIDs: []string{"B"}, + Threshold: 1, + }, + }, +} + +var loadedTargetsRole = data.DelegationRole{ + BaseRole: data.BaseRole{ + Name: data.CanonicalTargetsRole, + Keys: map[string]data.PublicKey{"C": nil}, + Threshold: 1, + }, +} + +// LoadedNotaryRepository has three targets: +// - red: signed by targets/releases, targets/alice, targets/bob +// - blue: signed by targets/releases, targets/alice +// - green: signed by targets/releases +var loadedRedTarget = client.Target{ + Name: "red", + Hashes: data.Hashes{"sha256": []byte("red-digest")}, +} + +var loadedBlueTarget = client.Target{ + Name: "blue", + Hashes: data.Hashes{"sha256": []byte("blue-digest")}, +} + +var loadedGreenTarget = client.Target{ + Name: "green", + Hashes: data.Hashes{"sha256": []byte("green-digest")}, +} + +var loadedTargets = []client.TargetSignedStruct{ + // red is signed by all three delegations + {Target: loadedRedTarget, Role: loadedReleasesRole}, + {Target: loadedRedTarget, Role: loadedAliceRole}, + {Target: loadedRedTarget, Role: loadedBobRole}, + + // blue is signed by targets/releases, targets/alice + {Target: loadedBlueTarget, Role: loadedReleasesRole}, + {Target: loadedBlueTarget, Role: loadedAliceRole}, + + // green is signed by targets/releases + {Target: loadedGreenTarget, Role: loadedReleasesRole}, +} + +// ListRoles returns a list of RoleWithSignatures objects for this repo +func (LoadedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { + rootRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"rootID"}, + Threshold: 1, + }, + Name: data.CanonicalRootRole, + } + + targetsRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"targetsID"}, + Threshold: 1, + }, + Name: data.CanonicalTargetsRole, + } + + aliceRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"A"}, + Threshold: 1, + }, + Name: data.RoleName("targets/alice"), + } + + bobRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"B"}, + Threshold: 1, + }, + Name: data.RoleName("targets/bob"), + } + + releasesRole := data.Role{ + RootRole: data.RootRole{ + KeyIDs: []string{"A", "B"}, + Threshold: 1, + }, + Name: data.RoleName("targets/releases"), + } + // have releases only signed off by Alice last + releasesSig := []data.Signature{{KeyID: "A"}} + + return []client.RoleWithSignatures{ + {Role: rootRole}, + {Role: targetsRole}, + {Role: aliceRole}, + {Role: bobRole}, + {Role: releasesRole, Signatures: releasesSig}, + }, nil +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +func (LoadedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { + filteredTargets := []*client.TargetWithRole{} + for _, tgt := range loadedTargets { + if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { + filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}) + } + } + return filteredTargets, nil +} + +// GetTargetByName returns a target by the given name. +func (LoadedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) { + for _, tgt := range loadedTargets { + if name == tgt.Target.Name { + if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { + return &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}, nil + } + } + } + return nil, client.ErrNoSuchTarget(name) +} + +// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all +// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. +func (LoadedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + if name == "" { + return loadedTargets, nil + } + filteredTargets := []client.TargetSignedStruct{} + for _, tgt := range loadedTargets { + if name == tgt.Target.Name { + filteredTargets = append(filteredTargets, tgt) + } + } + if len(filteredTargets) == 0 { + return nil, client.ErrNoSuchTarget(name) + } + return filteredTargets, nil +} + +// GetGUN is a getter for the GUN object from a Repository +func (LoadedNotaryRepository) GetGUN() data.GUN { + return "signed-repo" +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +func (LoadedNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return loadedDelegationRoles, nil +} + +const testPass = "password" + +func testPassRetriever(string, string, bool, int) (string, bool, error) { + return testPass, false, nil +} + +// GetCryptoService is the getter for the repository's CryptoService +func (l LoadedNotaryRepository) GetCryptoService() signed.CryptoService { + if l.statefulCryptoService == nil { + // give it an in-memory cryptoservice with a root key and targets key + l.statefulCryptoService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(testPassRetriever)) + l.statefulCryptoService.AddKey(data.CanonicalRootRole, l.GetGUN(), nil) + l.statefulCryptoService.AddKey(data.CanonicalTargetsRole, l.GetGUN(), nil) + } + return l.statefulCryptoService +} + +// GetLoadedWithNoSignersNotaryRepository returns a LoadedWithNoSignersNotaryRepository +func GetLoadedWithNoSignersNotaryRepository() (client.Repository, error) { + return LoadedWithNoSignersNotaryRepository{}, nil +} + +// LoadedWithNoSignersNotaryRepository is a mock Notary repository that is loaded with targets but no delegations +// it only contains the green target +type LoadedWithNoSignersNotaryRepository struct { + LoadedNotaryRepository +} + +// ListTargets lists all targets for the current repository. The list of +// roles should be passed in order from highest to lowest priority. +func (LoadedWithNoSignersNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { + filteredTargets := []*client.TargetWithRole{} + for _, tgt := range loadedTargets { + if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { + filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}) + } + } + return filteredTargets, nil +} + +// GetTargetByName returns a target by the given name. +func (LoadedWithNoSignersNotaryRepository) GetTargetByName(name string, _ ...data.RoleName) (*client.TargetWithRole, error) { + if name == "" || name == loadedGreenTarget.Name { + return &client.TargetWithRole{Target: loadedGreenTarget, Role: data.CanonicalTargetsRole}, nil + } + return nil, client.ErrNoSuchTarget(name) +} + +// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all +// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. +func (LoadedWithNoSignersNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { + if name == "" || name == loadedGreenTarget.Name { + return []client.TargetSignedStruct{{Target: loadedGreenTarget, Role: loadedTargetsRole}}, nil + } + return nil, client.ErrNoSuchTarget(name) +} + +// GetDelegationRoles returns the keys and roles of the repository's delegations +func (LoadedWithNoSignersNotaryRepository) GetDelegationRoles() ([]data.Role, error) { + return []data.Role{}, nil +} diff --git a/cmd/docker-trust/internal/test/randomid.go b/cmd/docker-trust/internal/test/randomid.go new file mode 100644 index 000000000000..fd5ed2ca73ba --- /dev/null +++ b/cmd/docker-trust/internal/test/randomid.go @@ -0,0 +1,15 @@ +package test + +import ( + "crypto/rand" + "encoding/hex" +) + +// RandomID returns a unique, 64-character ID consisting of a-z, 0-9. +func RandomID() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) // This shouldn't happen + } + return hex.EncodeToString(b) +} diff --git a/cmd/docker-trust/internal/trust/trust.go b/cmd/docker-trust/internal/trust/trust.go new file mode 100644 index 000000000000..a20cb273af64 --- /dev/null +++ b/cmd/docker-trust/internal/trust/trust.go @@ -0,0 +1,418 @@ +package trust + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strconv" + "time" + + "github.com/distribution/reference" + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cmd/docker-trust/internal/registry" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/go-connections/tlsconfig" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" + "github.com/theupdateframework/notary" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/passphrase" + "github.com/theupdateframework/notary/storage" + "github.com/theupdateframework/notary/trustmanager" + "github.com/theupdateframework/notary/trustpinning" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/signed" +) + +var ( + // ReleasesRole is the role named "releases" + ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases")) + // ActionsPullOnly defines the actions for read-only interactions with a Notary Repository + ActionsPullOnly = []string{"pull"} + // ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository + ActionsPushAndPull = []string{"pull", "push"} +) + +// Enabled returns whether content-trust is enabled through the DOCKER_CONTENT_TRUST env-var. +// +// IMPORTANT: this function is for internal use, and may be removed at any moment. +func Enabled() bool { + var enabled bool + if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { + if t, err := strconv.ParseBool(e); t || err != nil { + // treat any other value as true + enabled = true + } + } + return enabled +} + +// NotaryServer is the endpoint serving the Notary trust server +const NotaryServer = "https://notary.docker.io" + +// GetTrustDirectory returns the base trust directory name +func GetTrustDirectory() string { + return filepath.Join(config.Dir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(config.Dir(), "tls", u.Host), nil +} + +// Server returns the base URL for the trust server. +func Server(indexName string) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if indexName == "docker.io" || indexName == "index.docker.io" { + return NotaryServer, nil + } + return "https://" + indexName, nil +} + +type simpleCredentialStore struct { + auth registrytypes.AuthConfig +} + +func (scs simpleCredentialStore) Basic(*url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(*url.URL, string) string { + return scs.auth.IdentityToken +} + +func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {} + +const dctDeprecation = `WARNING: Docker is retiring DCT for Docker Official Images (DOI). + For details, refer to https://docs.docker.com/go/dct-deprecation/ + +` + +// GetNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates an HTTP transport providing authentication support. +func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { + server, err := Server(repoInfo.Index.Name) + if err != nil { + return nil, err + } + if server == NotaryServer { + _, _ = fmt.Fprint(os.Stderr, dctDeprecation) + } + + cfg := tlsconfig.ClientDefault() + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.Headers(userAgent, http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest(http.MethodGet, endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := challenge.NewSimpleManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: simpleCredentialStore{auth: *authConfig}, + Scopes: []auth.Scope{auth.RepositoryScope{ + Repository: repoInfo.Name.Name(), + Actions: actions, + }}, + ClientID: registry.AuthClientID, + }) + basicHandler := auth.NewBasicHandler(simpleCredentialStore{auth: *authConfig}) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) + + return client.NewFileCachedRepository( + GetTrustDirectory(), + data.GUN(repoInfo.Name.Name()), + server, + transport.NewTransport(base, modifiers...), + GetPassphraseRetriever(in, out), + trustpinning.TrustPinConfig{}) +} + +// GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars +func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +// NotaryError formats an error message received from the notary service +func NotaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address", repoName) + case signed.ErrExpired: + return fmt.Errorf("error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("error: signing keys for remote repository %s not found: %v", repoName, err) + case storage.NetworkError: + return fmt.Errorf("error: error contacting notary server: %v", err) + case storage.ErrMetaNotFound: + return fmt.Errorf("error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: + return fmt.Errorf("warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + default: + return err + } +} + +// AddToAllSignableRoles attempts to add the image target to all the top level +// delegation roles we can (based on whether we have the signing key and whether +// the role's path allows us to). +// +// If there are no delegation roles, we add to the targets role. +func AddToAllSignableRoles(repo client.Repository, target *client.Target) error { + signableRoles, err := GetSignableRoles(repo, target) + if err != nil { + return err + } + + return repo.AddTarget(target, signableRoles...) +} + +// GetSignableRoles returns a list of roles for which we have valid signing +// keys, given a notary repository and a target +func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) { + var signableRoles []data.RoleName + + // translate the full key names, which includes the GUN, into just the key IDs + allCanonicalKeyIDs := make(map[string]struct{}) + for fullKeyID := range repo.GetCryptoService().ListAllKeys() { + allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} + } + + allDelegationRoles, err := repo.GetDelegationRoles() + if err != nil { + return signableRoles, err + } + + // if there are no delegation roles, then just try to sign it into the targets role + if len(allDelegationRoles) == 0 { + signableRoles = append(signableRoles, data.CanonicalTargetsRole) + return signableRoles, nil + } + + // there are delegation roles, find every delegation role we have a key for, + // and attempt to sign in to all those roles. + for _, delegationRole := range allDelegationRoles { + // We do not support signing any delegation role that isn't a direct child of the targets role. + // Also don't bother checking the keys if we can't add the target + // to this role due to path restrictions + if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) { + continue + } + + for _, canonicalKeyID := range delegationRole.KeyIDs { + if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { + signableRoles = append(signableRoles, delegationRole.Name) + break + } + } + } + + if len(signableRoles) == 0 { + return signableRoles, errors.New("no valid signing keys for delegation roles") + } + + return signableRoles, nil +} + +// ImageRefAndAuth contains all reference information and the auth config for an image request +type ImageRefAndAuth struct { + original string + authConfig *registrytypes.AuthConfig + reference reference.Named + repoInfo *RepositoryInfo + tag string + digest digest.Digest +} + +// RepositoryInfo describes a repository +type RepositoryInfo struct { + Name reference.Named + // Index points to registry information + Index *registrytypes.IndexInfo +} + +// GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name +// as an ImageRefAndAuth struct +func GetImageReferencesAndAuth(ctx context.Context, + authResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig, + imgName string, +) (ImageRefAndAuth, error) { + ref, err := reference.ParseNormalizedNamed(imgName) + if err != nil { + return ImageRefAndAuth{}, err + } + + // Resolve the Repository name from fqn to RepositoryInfo, and create an + // IndexInfo. Docker Content Trust uses the IndexInfo.Official field to + // select the right domain for Docker Hub's Notary server; + // https://github.com/docker/cli/blob/v28.4.0/cli/trust/trust.go#L65-L79 + indexInfo := registry.NewIndexInfo(ref) + authConfig := authResolver(ctx, indexInfo) + return ImageRefAndAuth{ + original: imgName, + authConfig: &authConfig, + reference: ref, + repoInfo: &RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + }, + tag: getTag(ref), + digest: getDigest(ref), + }, nil +} + +func getTag(ref reference.Named) string { + switch x := ref.(type) { + case reference.Digested: + return "" // TODO(thaJeztah): is it intentional to discard the tag when "Tagged+Digested"? + case reference.Tagged: + return x.Tag() + default: + return "" + } +} + +func getDigest(ref reference.Named) digest.Digest { + switch x := ref.(type) { + case reference.Digested: + return x.Digest() + default: + return "" + } +} + +// AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) AuthConfig() *registrytypes.AuthConfig { + return imgRefAuth.authConfig +} + +// Reference returns the Image reference for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { + return imgRefAuth.reference +} + +// RepoInfo returns the repository information for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) RepoInfo() *RepositoryInfo { + return imgRefAuth.repoInfo +} + +// Tag returns the Image tag for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Tag() string { + return imgRefAuth.tag +} + +// Digest returns the Image digest for a given ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest { + return imgRefAuth.digest +} + +// Name returns the image name used to initialize the ImageRefAndAuth +func (imgRefAuth *ImageRefAndAuth) Name() string { + return imgRefAuth.original +} diff --git a/cli/trust/trust_push.go b/cmd/docker-trust/internal/trust/trust_push.go similarity index 81% rename from cli/trust/trust_push.go rename to cmd/docker-trust/internal/trust/trust_push.go index 1a8c5e4b7281..64f255964cf0 100644 --- a/cli/trust/trust_push.go +++ b/cmd/docker-trust/internal/trust/trust_push.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "sort" @@ -11,11 +12,8 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/jsonstream" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" + registrytypes "github.com/moby/moby/api/types/registry" "github.com/opencontainers/go-digest" - "github.com/pkg/errors" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" ) @@ -29,10 +27,19 @@ type Streams interface { Err() *streams.Out } +// PushResult contains the tag, manifest digest, and manifest size from the +// push. It's used to signal this information to the trust code in the client +// so it can sign the manifest if necessary. +type PushResult struct { + Tag string + Digest string + Size int +} + // PushTrustedReference pushes a canonical reference to the trust server. // //nolint:gocyclo -func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { +func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { // If it is a trusted push we would like to find the target entry which match the // tag provided in the function and then do an AddTarget later. notaryTarget := &client.Target{} @@ -46,7 +53,7 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *regi return } - var pushResult types.PushResult + var pushResult PushResult err := json.Unmarshal(*msg.Aux, &pushResult) if err == nil && pushResult.Tag != "" { if dgst, err := digest.Parse(pushResult.Digest); err == nil { @@ -64,9 +71,9 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *regi var tag string switch x := ref.(type) { - case reference.Canonical: + case reference.Digested: return errors.New("cannot push a digest reference") - case reference.NamedTagged: + case reference.Tagged: tag = x.Tag() default: // We want trust signatures to always take an explicit tag, @@ -83,18 +90,18 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *regi } if cnt > 1 { - return errors.Errorf("internal error: only one call to handleTarget expected") + return errors.New("internal error: only one call to handleTarget expected") } if notaryTarget == nil { - return errors.Errorf("no targets found, provide a specific tag in order to sign it") + return errors.New("no targets found, provide a specific tag in order to sign it") } _, _ = fmt.Fprintln(ioStreams.Out(), "Signing and pushing trust metadata") repo, err := GetNotaryRepository(ioStreams.In(), ioStreams.Out(), userAgent, repoInfo, &authConfig, "push", "pull") if err != nil { - return errors.Wrap(err, "error establishing connection to trust repository") + return fmt.Errorf("error establishing connection to trust repository: %w", err) } // get the latest repository metadata so we can figure out which roles to sign @@ -134,7 +141,7 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *regi } if err != nil { - err = errors.Wrapf(err, "failed to sign %s:%s", repoInfo.Name.Name(), tag) + err = fmt.Errorf("failed to sign %s:%s: %w", repoInfo.Name.Name(), tag, err) return NotaryError(repoInfo.Name.Name(), err) } diff --git a/cli/trust/trust_tag.go b/cmd/docker-trust/internal/trust/trust_tag.go similarity index 78% rename from cli/trust/trust_tag.go rename to cmd/docker-trust/internal/trust/trust_tag.go index 053f9317d164..3c39f1342b19 100644 --- a/cli/trust/trust_tag.go +++ b/cmd/docker-trust/internal/trust/trust_tag.go @@ -6,7 +6,7 @@ import ( "io" "github.com/distribution/reference" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) // TagTrusted tags a trusted ref. It is a shallow wrapper around [client.Client.ImageTag] @@ -18,5 +18,12 @@ func TagTrusted(ctx context.Context, apiClient client.ImageAPIClient, out io.Wri trustedFamiliarRef := reference.FamiliarString(trustedRef) _, _ = fmt.Fprintf(out, "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) - return apiClient.ImageTag(ctx, trustedFamiliarRef, familiarRef) + _, err := apiClient.ImageTag(ctx, client.ImageTagOptions{ + Source: trustedFamiliarRef, + Target: familiarRef, + }) + if err != nil { + return err + } + return nil } diff --git a/cmd/docker-trust/internal/trust/trust_test.go b/cmd/docker-trust/internal/trust/trust_test.go new file mode 100644 index 000000000000..f4d1bb256923 --- /dev/null +++ b/cmd/docker-trust/internal/trust/trust_test.go @@ -0,0 +1,87 @@ +package trust + +import ( + "testing" + + "github.com/distribution/reference" + "github.com/opencontainers/go-digest" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/trustpinning" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestGetTag(t *testing.T) { + ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") + assert.NilError(t, err) + tag := getTag(ref) + assert.Check(t, is.Equal("", tag)) + + ref, err = reference.ParseNormalizedNamed("alpine:latest") + assert.NilError(t, err) + tag = getTag(ref) + assert.Check(t, is.Equal(tag, "latest")) + + ref, err = reference.ParseNormalizedNamed("alpine") + assert.NilError(t, err) + tag = getTag(ref) + assert.Check(t, is.Equal(tag, "")) +} + +func TestGetDigest(t *testing.T) { + ref, err := reference.ParseNormalizedNamed("ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2") + assert.NilError(t, err) + d := getDigest(ref) + assert.Check(t, is.Equal(digest.Digest("sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"), d)) + + ref, err = reference.ParseNormalizedNamed("alpine:latest") + assert.NilError(t, err) + d = getDigest(ref) + assert.Check(t, is.Equal(digest.Digest(""), d)) + + ref, err = reference.ParseNormalizedNamed("alpine") + assert.NilError(t, err) + d = getDigest(ref) + assert.Check(t, is.Equal(digest.Digest(""), d)) +} + +func TestGetSignableRolesError(t *testing.T) { + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, nil, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + _, err = GetSignableRoles(notaryRepo, &client.Target{}) + const expected = "client is offline" + assert.Error(t, err, expected) +} + +func TestENVTrustServer(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.example.com:5000") + output, err := Server("testserver") + const expected = "https://notary-test.example.com:5000" + assert.NilError(t, err) + assert.Equal(t, output, expected) +} + +func TestHTTPENVTrustServer(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.example.com:5000") + _, err := Server("testserver") + const expected = "valid https URL required for trust server" + assert.ErrorContains(t, err, expected, "Expected error with invalid scheme") +} + +func TestOfficialTrustServer(t *testing.T) { + output, err := Server("docker.io") + const expected = NotaryServer + assert.NilError(t, err) + assert.Equal(t, output, expected) + + output, err = Server("index.docker.io") + assert.NilError(t, err) + assert.Equal(t, output, expected) +} + +func TestNonOfficialTrustServer(t *testing.T) { + output, err := Server("testserver") + const expected = "https://testserver" + assert.NilError(t, err) + assert.Equal(t, output, expected) +} diff --git a/cmd/docker-trust/internal/version/version.go b/cmd/docker-trust/internal/version/version.go new file mode 100644 index 000000000000..4e5b1326e889 --- /dev/null +++ b/cmd/docker-trust/internal/version/version.go @@ -0,0 +1,4 @@ +package version + +// Version is overridden at build-time via ldflags. +var Version = "unknown-version" diff --git a/cmd/docker-trust/main.go b/cmd/docker-trust/main.go new file mode 100644 index 000000000000..b4afbea5f131 --- /dev/null +++ b/cmd/docker-trust/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "syscall" + + cerrdefs "github.com/containerd/errdefs" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli-plugins/metadata" + "github.com/docker/cli/cli-plugins/plugin" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cmd/docker-trust/internal/version" + "github.com/docker/cli/cmd/docker-trust/trust" + "go.opentelemetry.io/otel" +) + +func runStandalone(cmd *command.DockerCli) error { + defer flushMetrics(cmd) + executable := os.Args[0] + rootCmd := trust.NewRootCmd(filepath.Base(executable), false, cmd) + return rootCmd.Execute() +} + +// flushMetrics will manually flush metrics from the configured +// meter provider. This is needed when running in standalone mode +// because the meter provider is initialized by the cli library, +// but the mechanism for forcing it to report is not presently +// exposed and not invoked when run in standalone mode. +// There are plans to fix that in the next release, but this is +// needed temporarily until the API for this is more thorough. +func flushMetrics(cmd *command.DockerCli) { + if mp, ok := cmd.MeterProvider().(command.MeterProvider); ok { + if err := mp.ForceFlush(context.Background()); err != nil { + otel.Handle(err) + } + } +} + +func runPlugin(cmd *command.DockerCli) error { + rootCmd := trust.NewRootCmd("trust", true, cmd) + return plugin.RunPlugin(cmd, rootCmd, metadata.Metadata{ + SchemaVersion: "0.1.0", + Vendor: "Docker Inc.", + Version: version.Version, + }) +} + +func run(cmd *command.DockerCli) error { + if plugin.RunningStandalone() { + return runStandalone(cmd) + } + return runPlugin(cmd) +} + +type errCtxSignalTerminated struct { + signal os.Signal +} + +func (errCtxSignalTerminated) Error() string { + return "" +} + +func main() { + cmd, err := command.NewDockerCli() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err = run(cmd); err == nil { + return + } + + if errors.As(err, &errCtxSignalTerminated{}) { + os.Exit(getExitCode(err)) + } + + if !cerrdefs.IsCanceled(err) { + if err.Error() != "" { + _, _ = fmt.Fprintln(cmd.Err(), err) + } + os.Exit(getExitCode(err)) + } +} + +// getExitCode returns the exit-code to use for the given error. +// If err is a [cli.StatusError] and has a StatusCode set, it uses the +// status-code from it, otherwise it returns "1" for any error. +func getExitCode(err error) int { + if err == nil { + return 0 + } + + var userTerminatedErr errCtxSignalTerminated + if errors.As(err, &userTerminatedErr) { + s, ok := userTerminatedErr.signal.(syscall.Signal) + if !ok { + return 1 + } + return 128 + int(s) + } + + var stErr cli.StatusError + if errors.As(err, &stErr) && stErr.StatusCode != 0 { // FIXME(thaJeztah): StatusCode should never be used with a zero status-code. Check if we do this anywhere. + return stErr.StatusCode + } + + // No status-code provided; all errors should have a non-zero exit code. + return 1 +} diff --git a/cmd/docker-trust/trust/cmd.go b/cmd/docker-trust/trust/cmd.go new file mode 100644 index 000000000000..55c80fe68a97 --- /dev/null +++ b/cmd/docker-trust/trust/cmd.go @@ -0,0 +1,32 @@ +package trust + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/internal/commands" + "github.com/spf13/cobra" +) + +func init() { + commands.Register(newTrustCommand) +} + +// newTrustCommand returns a cobra command for `trust` subcommands. +func newTrustCommand(dockerCLI command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "trust", + Short: "Manage trust on Docker images", + Args: cli.NoArgs, + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, + } + cmd.AddCommand( + newRevokeCommand(dockerCLI), + newSignCommand(dockerCLI), + newTrustKeyCommand(dockerCLI), + newTrustSignerCommand(dockerCLI), + newInspectCommand(dockerCLI), + ) + return cmd +} diff --git a/cmd/docker-trust/trust/commands.go b/cmd/docker-trust/trust/commands.go new file mode 100644 index 000000000000..cb0fc6fc4967 --- /dev/null +++ b/cmd/docker-trust/trust/commands.go @@ -0,0 +1,81 @@ +package trust + +import ( + "fmt" + + "github.com/docker/cli-docs-tool/annotation" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli-plugins/plugin" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/debug" + cliflags "github.com/docker/cli/cli/flags" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func NewRootCmd(name string, isPlugin bool, dockerCLI *command.DockerCli) *cobra.Command { + var opt rootOptions + cmd := &cobra.Command{ + Use: name, + Short: "Manage trust on Docker images", + Long: `Extended build capabilities with BuildKit`, + Annotations: map[string]string{ + annotation.CodeDelimiter: `"`, + }, + CompletionOptions: cobra.CompletionOptions{ + HiddenDefaultCmd: true, + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if opt.debug { + debug.Enable() + } + // cmd.SetContext(appcontext.Context()) + if !isPlugin { + // InstallFlags and SetDefaultOptions are necessary to match + // the plugin mode behavior to handle env vars such as + // DOCKER_TLS, DOCKER_TLS_VERIFY, ... and we also need to use a + // new flagset to avoid conflict with the global debug flag + // that we already handle in the root command otherwise it + // would panic. + nflags := pflag.NewFlagSet(cmd.DisplayName(), pflag.ContinueOnError) + options := cliflags.NewClientOptions() + options.InstallFlags(nflags) + options.SetDefaultOptions(nflags) + return dockerCLI.Initialize(options) + } + return plugin.PersistentPreRunE(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() + } + _ = cmd.Help() + return cli.StatusError{ + StatusCode: 1, + Status: fmt.Sprintf("ERROR: unknown command: %q", args[0]), + } + }, + } + if !isPlugin { + // match plugin behavior for standalone mode + // https://github.com/docker/cli/blob/6c9eb708fa6d17765d71965f90e1c59cea686ee9/cli-plugins/plugin/plugin.go#L117-L127 + cmd.SilenceUsage = true + cmd.SilenceErrors = true + cmd.TraverseChildren = true + cmd.DisableFlagsInUseLine = true + } + + cmd.AddCommand( + newRevokeCommand(dockerCLI), + newSignCommand(dockerCLI), + newTrustKeyCommand(dockerCLI), + newTrustSignerCommand(dockerCLI), + newInspectCommand(dockerCLI), + ) + + return cmd +} + +type rootOptions struct { + debug bool +} diff --git a/cmd/docker-trust/trust/common.go b/cmd/docker-trust/trust/common.go new file mode 100644 index 000000000000..f05e5772732d --- /dev/null +++ b/cmd/docker-trust/trust/common.go @@ -0,0 +1,211 @@ +package trust + +import ( + "context" + "encoding/hex" + "fmt" + "sort" + "strings" + + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cmd/docker-trust/internal/trust" + "github.com/fvbommel/sortorder" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/sirupsen/logrus" + "github.com/theupdateframework/notary" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +// trustTagKey represents a unique signed tag and hex-encoded hash pair +type trustTagKey struct { + SignedTag string + Digest string +} + +// trustTagRow encodes all human-consumable information for a signed tag, including signers +type trustTagRow struct { + trustTagKey + Signers []string +} + +// trustRepo represents consumable information about a trusted repository +type trustRepo struct { + Name string + SignedTags []trustTagRow + Signers []trustSigner + AdministrativeKeys []trustSigner +} + +// trustSigner represents a trusted signer in a trusted repository +// a signer is defined by a name and list of trustKeys +type trustSigner struct { + Name string `json:",omitempty"` + Keys []trustKey `json:",omitempty"` +} + +// trustKey contains information about trusted keys +type trustKey struct { + ID string `json:",omitempty"` +} + +// notaryClientProvider is used in tests to provide a dummy notary client. +type notaryClientProvider interface { + NotaryClient() (client.Repository, error) +} + +// newNotaryClient provides a Notary Repository to interact with signed metadata for an image. +func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth, actions []string) (client.Repository, error) { + if ncp, ok := cli.(notaryClientProvider); ok { + // notaryClientProvider is used in tests to provide a dummy notary client. + return ncp.NotaryClient() + } + return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...) +} + +// lookupTrustInfo returns processed signature and role information about a notary repository. +// This information is to be pretty printed or serialized into a machine-readable format. +func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) { + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), remote) + if err != nil { + return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + tag := imgRefAndAuth.Tag() + notaryRepo, err := newNotaryClient(cli, imgRefAndAuth, trust.ActionsPullOnly) + if err != nil { + return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err) + } + + if err = clearChangeList(notaryRepo); err != nil { + return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err + } + defer clearChangeList(notaryRepo) + + // Retrieve all released signatures, match them, and pretty print them + allSignedTargets, err := notaryRepo.GetAllTargetMetadataByName(tag) + if err != nil { + logrus.Debug(trust.NotaryError(remote, err)) + // print an empty table if we don't have signed targets, but have an initialized notary repo + if _, ok := err.(client.ErrNoSuchTarget); !ok { + return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("no signatures or cannot access %s", remote) + } + } + signatureRows := matchReleasedSignatures(allSignedTargets) + + // get the administrative roles + adminRolesWithSigs, err := notaryRepo.ListRoles() + if err != nil { + return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("no signers for %s", remote) + } + + // get delegation roles with the canonical key IDs + delegationRoles, err := notaryRepo.GetDelegationRoles() + if err != nil { + logrus.Debugf("no delegation roles found, or error fetching them for %s: %v", remote, err) + } + + return signatureRows, adminRolesWithSigs, delegationRoles, nil +} + +func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { + adminKeyList := roleWithSigs.KeyIDs + sort.Strings(adminKeyList) + + var role string + switch roleWithSigs.Name { + case data.CanonicalTargetsRole: + role = "Repository Key" + case data.CanonicalRootRole: + role = "Root Key" + default: + return "" + } + return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", ")) +} + +func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]string { + signerRoleToKeyIDs := make(map[string][]string) + for _, delRole := range rawDelegationRoles { + switch delRole.Name { + case trust.ReleasesRole, data.CanonicalRootRole, data.CanonicalSnapshotRole, data.CanonicalTargetsRole, data.CanonicalTimestampRole: + continue + default: + signerRoleToKeyIDs[notaryRoleToSigner(delRole.Name)] = delRole.KeyIDs + } + } + return signerRoleToKeyIDs +} + +// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been +// signed into the "targets" or "targets/releases" role. Output is sorted by tag name +func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagRow { + signatureRows := []trustTagRow{} + // do a first pass to get filter on tags signed into "targets" or "targets/releases" + releasedTargetRows := map[trustTagKey][]string{} + for _, tgt := range allTargets { + if isReleasedTarget(tgt.Role.Name) { + releasedKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} + releasedTargetRows[releasedKey] = []string{} + } + } + + // now fill out all signers on released keys + for _, tgt := range allTargets { + targetKey := trustTagKey{tgt.Target.Name, hex.EncodeToString(tgt.Target.Hashes[notary.SHA256])} + // only considered released targets + if _, ok := releasedTargetRows[targetKey]; ok && !isReleasedTarget(tgt.Role.Name) { + releasedTargetRows[targetKey] = append(releasedTargetRows[targetKey], notaryRoleToSigner(tgt.Role.Name)) + } + } + + // compile the final output as a sorted slice + for targetKey, signers := range releasedTargetRows { + signatureRows = append(signatureRows, trustTagRow{targetKey, signers}) + } + sort.Slice(signatureRows, func(i, j int) bool { + return sortorder.NaturalLess(signatureRows[i].SignedTag, signatureRows[j].SignedTag) + }) + return signatureRows +} + +// authResolver returns an auth resolver function from a [config.Provider]. +func authResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + return resolveAuthConfig(dockerCLI.ConfigFile(), index) + } +} + +// authConfigKey is the key used to store credentials for Docker Hub. It is +// a copy of [registry.IndexServer]. +// +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer +const authConfigKey = "https://index.docker.io/v1/" + +// resolveAuthConfig returns auth-config for the given registry from the +// credential-store. It returns an empty AuthConfig if no credentials were +// found. +// +// It is similar to [registry.ResolveAuthConfig], but uses the credentials- +// store, instead of looking up credentials from a map. +// +// [registry.ResolveAuthConfig]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#ResolveAuthConfig +func resolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + configKey := index.Name + if index.Official { + configKey = authConfigKey + } + + a, _ := cfg.GetAuthConfig(configKey) + return registrytypes.AuthConfig{ + Username: a.Username, + Password: a.Password, + ServerAddress: a.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: a.Auth, + IdentityToken: a.IdentityToken, + RegistryToken: a.RegistryToken, + } +} diff --git a/cli/command/trust/common_test.go b/cmd/docker-trust/trust/common_test.go similarity index 93% rename from cli/command/trust/common_test.go rename to cmd/docker-trust/trust/common_test.go index 13f24f50222b..a2e5f3b5f560 100644 --- a/cli/command/trust/common_test.go +++ b/cmd/docker-trust/trust/common_test.go @@ -3,7 +3,7 @@ package trust import ( "testing" - "github.com/docker/cli/cli/trust" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" "gotest.tools/v3/assert" diff --git a/cmd/docker-trust/trust/formatter.go b/cmd/docker-trust/trust/formatter.go new file mode 100644 index 000000000000..13e5712a2764 --- /dev/null +++ b/cmd/docker-trust/trust/formatter.go @@ -0,0 +1,125 @@ +package trust + +import ( + "sort" + "strings" + + "github.com/docker/cli/cli/command/formatter" +) + +const ( + defaultTrustTagTableFormat = "table {{.SignedTag}}\t{{.Digest}}\t{{.Signers}}" + signedTagNameHeader = "SIGNED TAG" + trustedDigestHeader = "DIGEST" + signersHeader = "SIGNERS" + defaultSignerInfoTableFormat = "table {{.Signer}}\t{{.Keys}}" + signerNameHeader = "SIGNER" + keysHeader = "KEYS" +) + +// signedTagInfo represents all formatted information needed to describe a signed tag: +// Name: name of the signed tag +// Digest: hex encoded digest of the contents +// Signers: list of entities who signed the tag +type signedTagInfo struct { + Name string + Digest string + Signers []string +} + +// signerInfo represents all formatted information needed to describe a signer: +// Name: name of the signer role +// Keys: the keys associated with the signer +type signerInfo struct { + Name string + Keys []string +} + +// tagWrite writes the context +func tagWrite(fmtCtx formatter.Context, signedTagInfoList []signedTagInfo) error { + trustTagCtx := &trustTagContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "SignedTag": signedTagNameHeader, + "Digest": trustedDigestHeader, + "Signers": signersHeader, + }, + }, + } + return fmtCtx.Write(trustTagCtx, func(format func(subContext formatter.SubContext) error) error { + for _, signedTag := range signedTagInfoList { + if err := format(&trustTagContext{s: signedTag}); err != nil { + return err + } + } + return nil + }) +} + +type trustTagContext struct { + formatter.HeaderContext + s signedTagInfo +} + +// SignedTag returns the name of the signed tag +func (c *trustTagContext) SignedTag() string { + return c.s.Name +} + +// Digest returns the hex encoded digest associated with this signed tag +func (c *trustTagContext) Digest() string { + return c.s.Digest +} + +// Signers returns the sorted list of entities who signed this tag +func (c *trustTagContext) Signers() string { + sort.Strings(c.s.Signers) + return strings.Join(c.s.Signers, ", ") +} + +// signerInfoWrite writes the context. +func signerInfoWrite(fmtCtx formatter.Context, signerInfoList []signerInfo) error { + signerInfoCtx := &signerInfoContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Signer": signerNameHeader, + "Keys": keysHeader, + }, + }, + } + return fmtCtx.Write(signerInfoCtx, func(format func(subContext formatter.SubContext) error) error { + for _, info := range signerInfoList { + if err := format(&signerInfoContext{ + trunc: fmtCtx.Trunc, + s: info, + }); err != nil { + return err + } + } + return nil + }) +} + +type signerInfoContext struct { + formatter.HeaderContext + trunc bool + s signerInfo +} + +// Keys returns the sorted list of keys associated with the signer +func (c *signerInfoContext) Keys() string { + sort.Strings(c.s.Keys) + truncatedKeys := []string{} + if c.trunc { + for _, keyID := range c.s.Keys { + truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID)) + } + return strings.Join(truncatedKeys, ", ") + } + return strings.Join(c.s.Keys, ", ") +} + +// Signer returns the name of the signer +func (c *signerInfoContext) Signer() string { + return c.s.Name +} diff --git a/cmd/docker-trust/trust/formatter_test.go b/cmd/docker-trust/trust/formatter_test.go new file mode 100644 index 000000000000..49c8ba24cfa2 --- /dev/null +++ b/cmd/docker-trust/trust/formatter_test.go @@ -0,0 +1,244 @@ +package trust + +import ( + "bytes" + "testing" + + "github.com/docker/cli/cli/command/formatter" + "github.com/docker/cli/cmd/docker-trust/internal/test" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestTrustTag(t *testing.T) { + digest := test.RandomID() + trustedTag := "tag" + + var ctx trustTagContext + + cases := []struct { + trustTagCtx trustTagContext + expValue string + call func() string + }{ + { + trustTagContext{ + s: signedTagInfo{ + Name: trustedTag, + Digest: digest, + Signers: nil, + }, + }, + digest, + ctx.Digest, + }, + { + trustTagContext{ + s: signedTagInfo{ + Name: trustedTag, + Digest: digest, + Signers: nil, + }, + }, + trustedTag, + ctx.SignedTag, + }, + // Empty signers makes a row with empty string + { + trustTagContext{ + s: signedTagInfo{ + Name: trustedTag, + Digest: digest, + Signers: nil, + }, + }, + "", + ctx.Signers, + }, + { + trustTagContext{ + s: signedTagInfo{ + Name: trustedTag, + Digest: digest, + Signers: []string{"alice", "bob", "claire"}, + }, + }, + "alice, bob, claire", + ctx.Signers, + }, + // alphabetic signing on Signers + { + trustTagContext{ + s: signedTagInfo{ + Name: trustedTag, + Digest: digest, + Signers: []string{"claire", "bob", "alice"}, + }, + }, + "alice, bob, claire", + ctx.Signers, + }, + } + + for _, c := range cases { + ctx = c.trustTagCtx + v := c.call() + if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestTrustTagContextWrite(t *testing.T) { + cases := []struct { + context formatter.Context + expected string + }{ + // Errors + { + formatter.Context{ + Format: "{{InvalidFunction}}", + }, + `template parsing error: template: :1: function "InvalidFunction" not defined`, + }, + { + formatter.Context{ + Format: "{{nil}}", + }, + `template parsing error: template: :1:2: executing "" at : nil is not a command`, + }, + // Table Format + { + formatter.Context{ + Format: defaultTrustTagTableFormat, + }, + `SIGNED TAG DIGEST SIGNERS +tag1 deadbeef alice +tag2 aaaaaaaa alice, bob +tag3 bbbbbbbb +`, + }, + } + + signedTags := []signedTagInfo{ + {Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}}, + {Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}}, + {Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}}, + } + + for _, tc := range cases { + t.Run(string(tc.context.Format), func(t *testing.T) { + var out bytes.Buffer + tc.context.Output = &out + + if err := tagWrite(tc.context, signedTags); err != nil { + assert.Error(t, err, tc.expected) + } else { + assert.Equal(t, out.String(), tc.expected) + } + }) + } +} + +// With no trust data, the formatWrite will print an empty table: +// it's up to the caller to decide whether or not to print this versus an error +func TestTrustTagContextEmptyWrite(t *testing.T) { + emptyCase := struct { + context formatter.Context + expected string + }{ + formatter.Context{ + Format: defaultTrustTagTableFormat, + }, + `SIGNED TAG DIGEST SIGNERS +`, + } + + emptySignedTags := []signedTagInfo{} + out := bytes.NewBufferString("") + emptyCase.context.Output = out + err := tagWrite(emptyCase.context, emptySignedTags) + assert.NilError(t, err) + assert.Check(t, is.Equal(emptyCase.expected, out.String())) +} + +func TestSignerInfoContextEmptyWrite(t *testing.T) { + emptyCase := struct { + context formatter.Context + expected string + }{ + formatter.Context{ + Format: defaultSignerInfoTableFormat, + }, + `SIGNER KEYS +`, + } + emptySignerInfo := []signerInfo{} + out := bytes.NewBufferString("") + emptyCase.context.Output = out + err := signerInfoWrite(emptyCase.context, emptySignerInfo) + assert.NilError(t, err) + assert.Check(t, is.Equal(emptyCase.expected, out.String())) +} + +func TestSignerInfoContextWrite(t *testing.T) { + cases := []struct { + context formatter.Context + expected string + }{ + // Errors + { + formatter.Context{ + Format: "{{InvalidFunction}}", + }, + `template parsing error: template: :1: function "InvalidFunction" not defined`, + }, + { + formatter.Context{ + Format: "{{nil}}", + }, + `template parsing error: template: :1:2: executing "" at : nil is not a command`, + }, + // Table Format + { + formatter.Context{ + Format: defaultSignerInfoTableFormat, + Trunc: true, + }, + `SIGNER KEYS +alice key11, key12 +bob key21 +eve foobarbazqux, key31, key32 +`, + }, + // No truncation + { + formatter.Context{ + Format: defaultSignerInfoTableFormat, + }, + `SIGNER KEYS +alice key11, key12 +bob key21 +eve foobarbazquxquux, key31, key32 +`, + }, + } + + signerInfo := []signerInfo{ + {Name: "alice", Keys: []string{"key11", "key12"}}, + {Name: "bob", Keys: []string{"key21"}}, + {Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}}, + } + for _, tc := range cases { + t.Run(string(tc.context.Format), func(t *testing.T) { + var out bytes.Buffer + tc.context.Output = &out + + if err := signerInfoWrite(tc.context, signerInfo); err != nil { + assert.Error(t, err, tc.expected) + } else { + assert.Equal(t, out.String(), tc.expected) + } + }) + } +} diff --git a/cmd/docker-trust/trust/helpers.go b/cmd/docker-trust/trust/helpers.go new file mode 100644 index 000000000000..ad7fa7f5be29 --- /dev/null +++ b/cmd/docker-trust/trust/helpers.go @@ -0,0 +1,55 @@ +package trust + +import ( + "strings" + + "github.com/docker/cli/cmd/docker-trust/internal/trust" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +const ( + releasedRoleName = "Repo Admin" + releasesRoleTUFName = "targets/releases" +) + +// isReleasedTarget checks if a role name is "released": +// either targets/releases or targets TUF roles +func isReleasedTarget(role data.RoleName) bool { + return role == data.CanonicalTargetsRole || role == trust.ReleasesRole +} + +// notaryRoleToSigner converts TUF role name to a human-understandable signer name +func notaryRoleToSigner(tufRole data.RoleName) string { + // don't show a signer for "targets" or "targets/releases" + if isReleasedTarget(data.RoleName(tufRole.String())) { + return releasedRoleName + } + return strings.TrimPrefix(tufRole.String(), "targets/") +} + +// clearChangeList clears the notary staging changelist. +func clearChangeList(notaryRepo client.Repository) error { + cl, err := notaryRepo.GetChangelist() + if err != nil { + return err + } + return cl.Clear("") +} + +// getOrGenerateRootKeyAndInitRepo initializes the notary repository +// with a remotely managed snapshot key. The initialization will use +// an existing root key if one is found, else a new one will be generated. +func getOrGenerateRootKeyAndInitRepo(notaryRepo client.Repository) error { + rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) + if err != nil { + return err + } + return notaryRepo.Initialize([]string{rootKey.ID()}, data.CanonicalSnapshotRole) +} + +const testPass = "password" + +func testPassRetriever(string, string, bool, int) (string, bool, error) { + return testPass, false, nil +} diff --git a/cmd/docker-trust/trust/inspect.go b/cmd/docker-trust/trust/inspect.go new file mode 100644 index 000000000000..5578b4b00975 --- /dev/null +++ b/cmd/docker-trust/trust/inspect.go @@ -0,0 +1,120 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +package trust + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/inspect" + "github.com/spf13/cobra" + "github.com/theupdateframework/notary/tuf/data" +) + +type inspectOptions struct { + remotes []string + // FIXME(n4ss): this is consistent with `docker service inspect` but we should provide + // a `--format` flag too. (format and pretty-print should be exclusive) + prettyPrint bool +} + +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { + options := inspectOptions{} + cmd := &cobra.Command{ + Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]", + Short: "Return low-level information about keys and signatures", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + options.remotes = args + + return runInspect(cmd.Context(), dockerCLI, options) + }, + DisableFlagsInUseLine: true, + } + + flags := cmd.Flags() + flags.BoolVar(&options.prettyPrint, "pretty", false, "Print the information in a human friendly format") + + return cmd +} + +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + if opts.prettyPrint { + var err error + + for index, remote := range opts.remotes { + if err = prettyPrintTrustInfo(ctx, dockerCLI, remote); err != nil { + return err + } + + // Additional separator between the inspection output of each image + if index < len(opts.remotes)-1 { + _, _ = fmt.Fprint(dockerCLI.Out(), "\n\n") + } + } + + return err + } + + getRefFunc := func(ref string) (any, []byte, error) { + i, err := getRepoTrustInfo(ctx, dockerCLI, ref) + return nil, i, err + } + return inspect.Inspect(dockerCLI.Out(), opts.remotes, "", getRefFunc) +} + +func getRepoTrustInfo(ctx context.Context, dockerCLI command.Cli, remote string) ([]byte, error) { + signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(ctx, dockerCLI, remote) + if err != nil { + return []byte{}, err + } + // process the signatures to include repo admin if signed by the base targets role + for idx, sig := range signatureRows { + if len(sig.Signers) == 0 { + signatureRows[idx].Signers = []string{releasedRoleName} + } + } + + signerList, adminList := []trustSigner{}, []trustSigner{} + + signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) + + for signerName, signerKeys := range signerRoleToKeyIDs { + signerKeyList := []trustKey{} + for _, keyID := range signerKeys { + signerKeyList = append(signerKeyList, trustKey{ID: keyID}) + } + signerList = append(signerList, trustSigner{signerName, signerKeyList}) + } + sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name }) + + for _, adminRole := range adminRolesWithSigs { + switch adminRole.Name { + case data.CanonicalRootRole: + rootKeys := []trustKey{} + for _, keyID := range adminRole.KeyIDs { + rootKeys = append(rootKeys, trustKey{ID: keyID}) + } + adminList = append(adminList, trustSigner{"Root", rootKeys}) + case data.CanonicalTargetsRole: + targetKeys := []trustKey{} + for _, keyID := range adminRole.KeyIDs { + targetKeys = append(targetKeys, trustKey{ID: keyID}) + } + adminList = append(adminList, trustSigner{"Repository", targetKeys}) + } + } + sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) + + return json.Marshal(trustRepo{ + Name: remote, + SignedTags: signatureRows, + Signers: signerList, + AdministrativeKeys: adminList, + }) +} diff --git a/cli/command/trust/inspect_pretty.go b/cmd/docker-trust/trust/inspect_pretty.go similarity index 87% rename from cli/command/trust/inspect_pretty.go rename to cmd/docker-trust/trust/inspect_pretty.go index b0b5e8c51c84..3eac2cfbe827 100644 --- a/cli/command/trust/inspect_pretty.go +++ b/cmd/docker-trust/trust/inspect_pretty.go @@ -56,33 +56,33 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) func printSignatures(out io.Writer, signatureRows []trustTagRow) error { trustTagCtx := formatter.Context{ Output: out, - Format: NewTrustTagFormat(), + Format: defaultTrustTagTableFormat, } // convert the formatted type before printing - formattedTags := []SignedTagInfo{} + formattedTags := []signedTagInfo{} for _, sigRow := range signatureRows { formattedSigners := sigRow.Signers if len(formattedSigners) == 0 { formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName)) } - formattedTags = append(formattedTags, SignedTagInfo{ + formattedTags = append(formattedTags, signedTagInfo{ Name: sigRow.SignedTag, Digest: sigRow.Digest, Signers: formattedSigners, }) } - return TagWrite(trustTagCtx, formattedTags) + return tagWrite(trustTagCtx, formattedTags) } func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { signerInfoCtx := formatter.Context{ Output: out, - Format: NewSignerInfoFormat(), + Format: defaultSignerInfoTableFormat, Trunc: true, } - formattedSignerInfo := []SignerInfo{} + formattedSignerInfo := []signerInfo{} for name, keyIDs := range roleToKeyIDs { - formattedSignerInfo = append(formattedSignerInfo, SignerInfo{ + formattedSignerInfo = append(formattedSignerInfo, signerInfo{ Name: name, Keys: keyIDs, }) @@ -90,5 +90,5 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { sort.Slice(formattedSignerInfo, func(i, j int) bool { return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name) }) - return SignerInfoWrite(signerInfoCtx, formattedSignerInfo) + return signerInfoWrite(signerInfoCtx, formattedSignerInfo) } diff --git a/cli/command/trust/inspect_pretty_test.go b/cmd/docker-trust/trust/inspect_pretty_test.go similarity index 91% rename from cli/command/trust/inspect_pretty_test.go rename to cmd/docker-trust/trust/inspect_pretty_test.go index 07e4edba41de..e083985d4c3b 100644 --- a/cli/command/trust/inspect_pretty_test.go +++ b/cmd/docker-trust/trust/inspect_pretty_test.go @@ -5,18 +5,16 @@ import ( "context" "encoding/hex" "io" + "net/http" "testing" - "github.com/docker/cli/cli/trust" - "github.com/docker/cli/internal/test" - notaryfake "github.com/docker/cli/internal/test/notary" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/client" + "github.com/docker/cli/cmd/docker-trust/internal/test" + notaryfake "github.com/docker/cli/cmd/docker-trust/internal/test/notary" + "github.com/docker/cli/cmd/docker-trust/internal/trust" + "github.com/moby/moby/client" "github.com/theupdateframework/notary" notaryclient "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" - "github.com/theupdateframework/notary/tuf/utils" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -28,16 +26,25 @@ type fakeClient struct { client.Client } -func (*fakeClient) Info(context.Context) (system.Info, error) { - return system.Info{}, nil +type fakeStreamResult struct { + io.ReadCloser + client.ImagePushResponse // same interface as [client.ImagePullResponse] } -func (*fakeClient) ImageInspect(context.Context, string, ...client.ImageInspectOption) (image.InspectResponse, error) { - return image.InspectResponse{}, nil +func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) } +func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() } + +func (*fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) { + return client.SystemInfoResult{}, nil +} + +func (*fakeClient) ImageInspect(context.Context, string, ...client.ImageInspectOption) (client.ImageInspectResult, error) { + return client.ImageInspectResult{}, nil } -func (*fakeClient) ImagePush(context.Context, string, image.PushOptions) (io.ReadCloser, error) { - return &utils.NoopCloser{Reader: bytes.NewBuffer([]byte{})}, nil +func (*fakeClient) ImagePush(context.Context, string, client.ImagePushOptions) (client.ImagePushResponse, error) { + // FIXME(thaJeztah): how to mock this? + return fakeStreamResult{ReadCloser: http.NoBody}, nil } func TestTrustInspectPrettyCommandErrors(t *testing.T) { @@ -67,7 +74,7 @@ func TestTrustInspectPrettyCommandErrors(t *testing.T) { cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) } } @@ -76,7 +83,7 @@ func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -85,7 +92,7 @@ func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) { cli = test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) cmd = newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -96,7 +103,7 @@ func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"reg/unsigned-img"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -105,7 +112,7 @@ func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) { cli = test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) cmd = newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"reg/unsigned-img:tag"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -116,7 +123,7 @@ func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"reg/img:unsigned-tag"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -127,7 +134,7 @@ func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) { cli = test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) cmd = newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"reg/img"}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -140,7 +147,7 @@ func TestTrustInspectPrettyCommandFullRepoWithoutSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"signed-repo"}) assert.NilError(t, cmd.Execute()) @@ -151,7 +158,7 @@ func TestTrustInspectPrettyCommandOneTagWithoutSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"signed-repo:green"}) assert.NilError(t, cmd.Execute()) @@ -162,7 +169,7 @@ func TestTrustInspectPrettyCommandFullRepoWithSigners(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"signed-repo"}) assert.NilError(t, cmd.Execute()) @@ -173,7 +180,7 @@ func TestTrustInspectPrettyCommandUnsignedTagInSignedRepo(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) cmd := newInspectCommand(cli) - cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Flags().Set("pretty", "true")) cmd.SetArgs([]string{"signed-repo:unsigned"}) assert.NilError(t, cmd.Execute()) diff --git a/cli/command/trust/inspect_test.go b/cmd/docker-trust/trust/inspect_test.go similarity index 94% rename from cli/command/trust/inspect_test.go rename to cmd/docker-trust/trust/inspect_test.go index 5823f329093f..60e771a5a104 100644 --- a/cli/command/trust/inspect_test.go +++ b/cmd/docker-trust/trust/inspect_test.go @@ -4,9 +4,8 @@ import ( "io" "testing" - "github.com/docker/cli/cli/trust" - "github.com/docker/cli/internal/test" - "github.com/docker/cli/internal/test/notary" + "github.com/docker/cli/cmd/docker-trust/internal/test" + "github.com/docker/cli/cmd/docker-trust/internal/test/notary" "github.com/theupdateframework/notary/client" "gotest.tools/v3/assert" "gotest.tools/v3/golden" @@ -48,7 +47,7 @@ func TestTrustInspectCommandRepositoryErrors(t *testing.T) { testCases := []struct { doc string args []string - notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error) + notaryRepository func() (client.Repository, error) err string golden string }{ @@ -100,7 +99,7 @@ func TestTrustInspectCommand(t *testing.T) { testCases := []struct { doc string args []string - notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error) + notaryRepository func() (client.Repository, error) golden string }{ { diff --git a/cmd/docker-trust/trust/key.go b/cmd/docker-trust/trust/key.go new file mode 100644 index 000000000000..bc1ede41680f --- /dev/null +++ b/cmd/docker-trust/trust/key.go @@ -0,0 +1,24 @@ +package trust + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +// newTrustKeyCommand returns a cobra command for `trust key` subcommands +func newTrustKeyCommand(dockerCLI command.Streams) *cobra.Command { + cmd := &cobra.Command{ + Use: "key", + Short: "Manage keys for signing Docker images", + Args: cli.NoArgs, + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, + } + cmd.AddCommand( + newKeyGenerateCommand(dockerCLI), + newKeyLoadCommand(dockerCLI), + ) + return cmd +} diff --git a/cli/command/trust/key_generate.go b/cmd/docker-trust/trust/key_generate.go similarity index 91% rename from cli/command/trust/key_generate.go rename to cmd/docker-trust/trust/key_generate.go index 9943c4772032..cf42ffc9bc96 100644 --- a/cli/command/trust/key_generate.go +++ b/cmd/docker-trust/trust/key_generate.go @@ -9,9 +9,8 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/trust" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/internal/lazyregexp" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" @@ -24,7 +23,7 @@ type keyGenerateOptions struct { directory string } -func newKeyGenerateCommand(dockerCli command.Streams) *cobra.Command { +func newKeyGenerateCommand(dockerCLI command.Streams) *cobra.Command { options := keyGenerateOptions{} cmd := &cobra.Command{ Use: "generate NAME", @@ -32,8 +31,9 @@ func newKeyGenerateCommand(dockerCli command.Streams) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.name = args[0] - return setupPassphraseAndGenerateKeys(dockerCli, options) + return setupPassphraseAndGenerateKeys(dockerCLI, options) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVar(&options.directory, "dir", "", "Directory to generate key in, defaults to current directory") @@ -88,7 +88,7 @@ func validateAndGenerateKey(streams command.Streams, keyName string, workingDir pubPEM, err := generateKeyAndOutputPubPEM(keyName, privKeyFileStore) if err != nil { _, _ = fmt.Fprint(streams.Out(), err) - return errors.Wrapf(err, "failed to generate key for %s", keyName) + return fmt.Errorf("failed to generate key for %s: %w", keyName, err) } // Output the public key to a file in the CWD or specified dir @@ -126,7 +126,7 @@ func writePubKeyPEMToDir(pubPEM pem.Block, keyName, workingDir string) (string, pubFileName := strings.Join([]string{keyName, "pub"}, ".") pubFilePath := filepath.Join(workingDir, pubFileName) if err := os.WriteFile(pubFilePath, pem.EncodeToMemory(&pubPEM), notary.PrivNoExecPerms); err != nil { - return "", errors.Wrapf(err, "failed to write public key to %s", pubFilePath) + return "", fmt.Errorf("failed to write public key to %s: %w", pubFilePath, err) } return pubFilePath, nil } diff --git a/cli/command/trust/key_generate_test.go b/cmd/docker-trust/trust/key_generate_test.go similarity index 98% rename from cli/command/trust/key_generate_test.go rename to cmd/docker-trust/trust/key_generate_test.go index 1efd1d31214c..f971dc238892 100644 --- a/cli/command/trust/key_generate_test.go +++ b/cmd/docker-trust/trust/key_generate_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/docker/cli/cli/config" - "github.com/docker/cli/internal/test" + "github.com/docker/cli/cmd/docker-trust/internal/test" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/trustmanager" tufutils "github.com/theupdateframework/notary/tuf/utils" diff --git a/cli/command/trust/key_load.go b/cmd/docker-trust/trust/key_load.go similarity index 89% rename from cli/command/trust/key_load.go rename to cmd/docker-trust/trust/key_load.go index b4ce1d4d51f6..7234dbc1a0e2 100644 --- a/cli/command/trust/key_load.go +++ b/cmd/docker-trust/trust/key_load.go @@ -3,6 +3,7 @@ package trust import ( "bytes" "encoding/pem" + "errors" "fmt" "io" "os" @@ -10,8 +11,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/trust" - "github.com/pkg/errors" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/spf13/cobra" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" @@ -27,15 +27,16 @@ type keyLoadOptions struct { keyName string } -func newKeyLoadCommand(dockerCli command.Streams) *cobra.Command { +func newKeyLoadCommand(dockerCLI command.Streams) *cobra.Command { var options keyLoadOptions cmd := &cobra.Command{ Use: "load [OPTIONS] KEYFILE", Short: "Load a private key file for signing", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return loadPrivKey(dockerCli, args[0], options) + return loadPrivKey(dockerCLI, args[0], options) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.StringVar(&options.keyName, "name", "signer", "Name for the loaded key") @@ -60,10 +61,10 @@ func loadPrivKey(streams command.Streams, keyPath string, options keyLoadOptions passRet := trust.GetPassphraseRetriever(streams.In(), streams.Out()) keyBytes, err := getPrivKeyBytesFromPath(keyPath) if err != nil { - return errors.Wrapf(err, "refusing to load key from %s", keyPath) + return fmt.Errorf("refusing to load key from %s: %w", keyPath, err) } if err := loadPrivKeyBytesToStore(keyBytes, privKeyImporters, keyPath, options.keyName, passRet); err != nil { - return errors.Wrapf(err, "error importing key from %s", keyPath) + return fmt.Errorf("error importing key from %s: %w", keyPath, err) } _, _ = fmt.Fprintln(streams.Out(), "Successfully imported key from", keyPath) return nil @@ -95,7 +96,7 @@ func loadPrivKeyBytesToStore(privKeyBytes []byte, privKeyImporters []trustmanage return fmt.Errorf("provided file %s is not a supported private key - to add a signer's public key use docker trust signer add", keyPath) } if privKeyBytes, err = decodePrivKeyIfNecessary(privKeyBytes, passRet); err != nil { - return errors.Wrapf(err, "cannot load key from provided file %s", keyPath) + return fmt.Errorf("cannot load key from provided file %s: %w", keyPath, err) } // Make a reader, rewind the file pointer return trustmanager.ImportKeys(bytes.NewReader(privKeyBytes), privKeyImporters, keyName, "", passRet) diff --git a/cli/command/trust/key_load_test.go b/cmd/docker-trust/trust/key_load_test.go similarity index 99% rename from cli/command/trust/key_load_test.go rename to cmd/docker-trust/trust/key_load_test.go index 2c004d38849d..3b3ec08e1eea 100644 --- a/cli/command/trust/key_load_test.go +++ b/cmd/docker-trust/trust/key_load_test.go @@ -10,7 +10,7 @@ import ( "testing" "github.com/docker/cli/cli/config" - "github.com/docker/cli/internal/test" + "github.com/docker/cli/cmd/docker-trust/internal/test" "github.com/theupdateframework/notary" "github.com/theupdateframework/notary/storage" "github.com/theupdateframework/notary/trustmanager" diff --git a/cli/command/trust/revoke.go b/cmd/docker-trust/trust/revoke.go similarity index 93% rename from cli/command/trust/revoke.go rename to cmd/docker-trust/trust/revoke.go index 303c7b8c2d68..b005d341b5a8 100644 --- a/cli/command/trust/revoke.go +++ b/cmd/docker-trust/trust/revoke.go @@ -2,14 +2,13 @@ package trust import ( "context" + "errors" "fmt" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/internal/prompt" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" @@ -28,6 +27,7 @@ func newRevokeCommand(dockerCLI command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return revokeTrust(cmd.Context(), dockerCLI, args[0], options) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&options.forceYes, "yes", "y", false, "Do not prompt for confirmation") @@ -35,7 +35,7 @@ func newRevokeCommand(dockerCLI command.Cli) *cobra.Command { } func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, options revokeOptions) error { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), remote) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), remote) if err != nil { return err } @@ -63,7 +63,7 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti } defer clearChangeList(notaryRepo) if err := revokeSignature(notaryRepo, tag); err != nil { - return errors.Wrapf(err, "could not remove signature for %s", remote) + return fmt.Errorf("could not remove signature for %s: %w", remote, err) } _, _ = fmt.Fprintf(dockerCLI.Out(), "Successfully deleted signature for %s\n", remote) return nil diff --git a/cmd/docker-trust/trust/revoke_test.go b/cmd/docker-trust/trust/revoke_test.go new file mode 100644 index 000000000000..7b3e887e61c6 --- /dev/null +++ b/cmd/docker-trust/trust/revoke_test.go @@ -0,0 +1,162 @@ +package trust + +import ( + "context" + "io" + "testing" + + "github.com/docker/cli/cmd/docker-trust/internal/test" + "github.com/docker/cli/cmd/docker-trust/internal/test/notary" + "github.com/theupdateframework/notary/client" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/golden" +) + +func TestTrustRevokeCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires 1 argument", + }, + { + name: "too-many-args", + args: []string{"remote1", "remote2"}, + expectedError: "requires 1 argument", + }, + { + name: "sha-reference", + args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, + expectedError: "invalid repository name", + }, + { + name: "invalid-img-reference", + args: []string{"ALPINE"}, + expectedError: "invalid reference format", + }, + { + name: "digest-reference", + args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, + expectedError: "cannot use a digest reference for IMAGE:TAG", + }, + } + for _, tc := range testCases { + cmd := newRevokeCommand( + test.NewFakeCli(&fakeClient{})) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestTrustRevokeCommand(t *testing.T) { + revokeCancelledError := "trust revoke has been cancelled" + + testCases := []struct { + doc string + notaryRepository func() (client.Repository, error) + args []string + expectedErr string + expectedMessage string + }{ + { + doc: "OfflineErrors_Confirm", + notaryRepository: notary.GetOfflineNotaryRepository, + args: []string{"reg-name.io/image"}, + expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", + expectedErr: revokeCancelledError, + }, + { + doc: "OfflineErrors_Offline", + notaryRepository: notary.GetOfflineNotaryRepository, + args: []string{"reg-name.io/image", "-y"}, + expectedErr: "could not remove signature for reg-name.io/image: client is offline", + }, + { + doc: "OfflineErrors_WithTag_Offline", + notaryRepository: notary.GetOfflineNotaryRepository, + args: []string{"reg-name.io/image:tag"}, + expectedErr: "could not remove signature for reg-name.io/image:tag: client is offline", + }, + { + doc: "UninitializedErrors_Confirm", + notaryRepository: notary.GetUninitializedNotaryRepository, + args: []string{"reg-name.io/image"}, + expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", + expectedErr: revokeCancelledError, + }, + { + doc: "UninitializedErrors_NoTrustData", + notaryRepository: notary.GetUninitializedNotaryRepository, + args: []string{"reg-name.io/image", "-y"}, + expectedErr: "could not remove signature for reg-name.io/image: does not have trust data for", + }, + { + doc: "UninitializedErrors_WithTag_NoTrustData", + notaryRepository: notary.GetUninitializedNotaryRepository, + args: []string{"reg-name.io/image:tag"}, + expectedErr: "could not remove signature for reg-name.io/image:tag: does not have trust data for", + }, + { + doc: "EmptyNotaryRepo_Confirm", + notaryRepository: notary.GetEmptyTargetsNotaryRepository, + args: []string{"reg-name.io/image"}, + expectedMessage: "Confirm you would like to delete all signature data for reg-name.io/image? [y/N] ", + expectedErr: revokeCancelledError, + }, + { + doc: "EmptyNotaryRepo_NoSignedTags", + notaryRepository: notary.GetEmptyTargetsNotaryRepository, + args: []string{"reg-name.io/image", "-y"}, + expectedErr: "could not remove signature for reg-name.io/image: no signed tags to remove", + }, + { + doc: "EmptyNotaryRepo_NoValidTrustData", + notaryRepository: notary.GetEmptyTargetsNotaryRepository, + args: []string{"reg-name.io/image:tag"}, + expectedErr: "could not remove signature for reg-name.io/image:tag: No valid trust data for tag", + }, + { + doc: "AllSigConfirmation", + notaryRepository: notary.GetEmptyTargetsNotaryRepository, + args: []string{"alpine"}, + expectedMessage: "Confirm you would like to delete all signature data for alpine? [y/N] ", + expectedErr: revokeCancelledError, + }, + } + + for _, tc := range testCases { + t.Run(tc.doc, func(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(tc.notaryRepository) + cmd := newRevokeCommand(cli) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + if tc.expectedErr != "" { + assert.ErrorContains(t, cmd.Execute(), tc.expectedErr) + } else { + assert.NilError(t, cmd.Execute()) + } + assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage)) + }) + } +} + +func TestRevokeTrustPromptTermination(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cli := test.NewFakeCli(&fakeClient{}) + cmd := newRevokeCommand(cli) + cmd.SetArgs([]string{"example/trust-demo"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + test.TerminatePrompt(ctx, t, cmd, cli) + golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden") +} diff --git a/cmd/docker-trust/trust/sign.go b/cmd/docker-trust/trust/sign.go new file mode 100644 index 000000000000..6b4b933f7242 --- /dev/null +++ b/cmd/docker-trust/trust/sign.go @@ -0,0 +1,262 @@ +package trust + +import ( + "context" + "errors" + "fmt" + "io" + "path" + "sort" + "strings" + + "github.com/distribution/reference" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cmd/docker-trust/internal/trust" + "github.com/moby/moby/api/pkg/authconfig" + "github.com/moby/moby/client" + "github.com/spf13/cobra" + notaryclient "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +type signOptions struct { + local bool + imageName string +} + +func newSignCommand(dockerCLI command.Cli) *cobra.Command { + options := signOptions{} + cmd := &cobra.Command{ + Use: "sign IMAGE:TAG", + Short: "Sign an image", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + options.imageName = args[0] + return runSignImage(cmd.Context(), dockerCLI, options) + }, + DisableFlagsInUseLine: true, + } + flags := cmd.Flags() + flags.BoolVar(&options.local, "local", false, "Sign a locally tagged image") + return cmd +} + +func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOptions) error { + imageName := options.imageName + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), imageName) + if err != nil { + return err + } + if err := validateTag(imgRefAndAuth); err != nil { + return err + } + + notaryRepo, err := newNotaryClient(dockerCLI, imgRefAndAuth, trust.ActionsPushAndPull) + if err != nil { + return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) + } + if err = clearChangeList(notaryRepo); err != nil { + return err + } + defer clearChangeList(notaryRepo) + + // get the latest repository metadata so we can figure out which roles to sign + if _, err = notaryRepo.ListTargets(); err != nil { + switch err.(type) { + case notaryclient.ErrRepoNotInitialized, notaryclient.ErrRepositoryNotExist: + // before initializing a new repo, check that the image exists locally: + if err := checkLocalImageExistence(ctx, dockerCLI.Client(), imageName); err != nil { + return err + } + + userRole := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), imgRefAndAuth.AuthConfig().Username)) + if err := initNotaryRepoWithSigners(notaryRepo, userRole); err != nil { + return trust.NotaryError(imgRefAndAuth.Reference().Name(), err) + } + + _, _ = fmt.Fprintln(dockerCLI.Out(), "Created signer:", imgRefAndAuth.AuthConfig().Username) + _, _ = fmt.Fprintln(dockerCLI.Out(), "Finished initializing signed repository for", imageName) + default: + return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err) + } + } + target, err := createTarget(notaryRepo, imgRefAndAuth.Tag()) + if err != nil || options.local { + switch err := err.(type) { + // If the error is nil then the local flag is set + case notaryclient.ErrNoSuchTarget, notaryclient.ErrRepositoryNotExist, nil: + // Fail fast if the image doesn't exist locally + if err := checkLocalImageExistence(ctx, dockerCLI.Client(), imageName); err != nil { + return err + } + _, _ = fmt.Fprintf(dockerCLI.Err(), "Signing and pushing trust data for local image %s, may overwrite remote trust data\n", imageName) + + authConfig := resolveAuthConfig(dockerCLI.ConfigFile(), imgRefAndAuth.RepoInfo().Index) + encodedAuth, err := authconfig.Encode(authConfig) + if err != nil { + return err + } + responseBody, err := dockerCLI.Client().ImagePush(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), client.ImagePushOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: nil, + }) + if err != nil { + return err + } + defer responseBody.Close() + return trust.PushTrustedReference(ctx, dockerCLI, imgRefAndAuth.RepoInfo(), imgRefAndAuth.Reference(), authConfig, responseBody, command.UserAgent()) + default: + return err + } + } + return signAndPublishToTarget(dockerCLI.Out(), imgRefAndAuth, notaryRepo, target) +} + +func signAndPublishToTarget(out io.Writer, imgRefAndAuth trust.ImageRefAndAuth, notaryRepo notaryclient.Repository, target notaryclient.Target) error { + tag := imgRefAndAuth.Tag() + _, _ = fmt.Fprintln(out, "Signing and pushing trust metadata for", imgRefAndAuth.Name()) + existingSigInfo, err := getExistingSignatureInfoForReleasedTag(notaryRepo, tag) + if err != nil { + return err + } + err = trust.AddToAllSignableRoles(notaryRepo, &target) + if err == nil { + prettyPrintExistingSignatureInfo(out, existingSigInfo) + err = notaryRepo.Publish() + } + if err != nil { + return fmt.Errorf("failed to sign %s:%s: %w", imgRefAndAuth.RepoInfo().Name.Name(), tag, err) + } + _, _ = fmt.Fprintf(out, "Successfully signed %s:%s\n", imgRefAndAuth.RepoInfo().Name.Name(), tag) + return nil +} + +func validateTag(imgRefAndAuth trust.ImageRefAndAuth) error { + tag := imgRefAndAuth.Tag() + if tag == "" { + if imgRefAndAuth.Digest() != "" { + return errors.New("cannot use a digest reference for IMAGE:TAG") + } + return fmt.Errorf("no tag specified for %s", imgRefAndAuth.Name()) + } + return nil +} + +func checkLocalImageExistence(ctx context.Context, apiClient client.APIClient, imageName string) error { + _, err := apiClient.ImageInspect(ctx, imageName) + return err +} + +func createTarget(notaryRepo notaryclient.Repository, tag string) (notaryclient.Target, error) { + target := ¬aryclient.Target{} + var err error + if tag == "" { + return *target, errors.New("no tag specified") + } + target.Name = tag + target.Hashes, target.Length, err = getSignedManifestHashAndSize(notaryRepo, tag) + return *target, err +} + +func getSignedManifestHashAndSize(notaryRepo notaryclient.Repository, tag string) (data.Hashes, int64, error) { + targets, err := notaryRepo.GetAllTargetMetadataByName(tag) + if err != nil { + return nil, 0, err + } + return getReleasedTargetHashAndSize(targets, tag) +} + +func getReleasedTargetHashAndSize(targets []notaryclient.TargetSignedStruct, tag string) (data.Hashes, int64, error) { + for _, tgt := range targets { + if isReleasedTarget(tgt.Role.Name) { + return tgt.Target.Hashes, tgt.Target.Length, nil + } + } + return nil, 0, notaryclient.ErrNoSuchTarget(tag) +} + +func getExistingSignatureInfoForReleasedTag(notaryRepo notaryclient.Repository, tag string) (trustTagRow, error) { + targets, err := notaryRepo.GetAllTargetMetadataByName(tag) + if err != nil { + return trustTagRow{}, err + } + releasedTargetInfoList := matchReleasedSignatures(targets) + if len(releasedTargetInfoList) == 0 { + return trustTagRow{}, nil + } + return releasedTargetInfoList[0], nil +} + +func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) { + sort.Strings(existingSigInfo.Signers) + joinedSigners := strings.Join(existingSigInfo.Signers, ", ") + _, _ = fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners) +} + +func initNotaryRepoWithSigners(notaryRepo notaryclient.Repository, newSigner data.RoleName) error { + rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) + if err != nil { + return err + } + rootKeyID := rootKey.ID() + + // Initialize the notary repository with a remotely managed snapshot key + if err := notaryRepo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { + return err + } + + signerKey, err := getOrGenerateNotaryKey(notaryRepo, newSigner) + if err != nil { + return err + } + if err := addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey}); err != nil { + return fmt.Errorf("could not add signer to repo: %s: %w", strings.TrimPrefix(newSigner.String(), "targets/"), err) + } + + return notaryRepo.Publish() +} + +// generates an ECDSA key without a GUN for the specified role +func getOrGenerateNotaryKey(notaryRepo notaryclient.Repository, role data.RoleName) (data.PublicKey, error) { + // use the signer name in the PEM headers if this is a delegation key + if data.IsDelegation(role) { + role = data.RoleName(notaryRoleToSigner(role)) + } + keys := notaryRepo.GetCryptoService().ListKeys(role) + var err error + var key data.PublicKey + // always select the first key by ID + if len(keys) > 0 { + sort.Strings(keys) + keyID := keys[0] + privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID) + if err != nil { + return nil, err + } + key = data.PublicKeyFromPrivate(privKey) + } else { + key, err = notaryRepo.GetCryptoService().Create(role, "", data.ECDSAKey) + if err != nil { + return nil, err + } + } + return key, nil +} + +// stages changes to add a signer with the specified name and key(s). Adds to targets/ and targets/releases +func addStagedSigner(notaryRepo notaryclient.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) error { + // create targets/ + if err := notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys); err != nil { + return err + } + if err := notaryRepo.AddDelegationPaths(newSigner, []string{""}); err != nil { + return err + } + + // create targets/releases + if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil { + return err + } + return notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}) +} diff --git a/cmd/docker-trust/trust/sign_test.go b/cmd/docker-trust/trust/sign_test.go new file mode 100644 index 000000000000..e7604c958419 --- /dev/null +++ b/cmd/docker-trust/trust/sign_test.go @@ -0,0 +1,282 @@ +package trust + +import ( + "bytes" + "encoding/json" + "io" + "runtime" + "testing" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cmd/docker-trust/internal/test" + notaryfake "github.com/docker/cli/cmd/docker-trust/internal/test/notary" + "github.com/docker/cli/cmd/docker-trust/internal/trust" + "github.com/theupdateframework/notary" + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/client/changelist" + "github.com/theupdateframework/notary/trustpinning" + "github.com/theupdateframework/notary/tuf/data" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/skip" +) + +func TestTrustSignCommandErrors(t *testing.T) { + testCases := []struct { + name string + args []string + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires 1 argument", + }, + { + name: "too-many-args", + args: []string{"image", "tag"}, + expectedError: "requires 1 argument", + }, + { + name: "sha-reference", + args: []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"}, + expectedError: "invalid repository name", + }, + { + name: "invalid-img-reference", + args: []string{"ALPINE:latest"}, + expectedError: "invalid reference format", + }, + { + name: "no-tag", + args: []string{"reg/img"}, + expectedError: "no tag specified for reg/img", + }, + { + name: "digest-reference", + args: []string{"ubuntu@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2"}, + expectedError: "cannot use a digest reference for IMAGE:TAG", + }, + } + // change to a tmpdir + config.SetDir(t.TempDir()) + for _, tc := range testCases { + cmd := newSignCommand( + test.NewFakeCli(&fakeClient{})) + cmd.SetArgs(tc.args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), tc.expectedError) + } +} + +func TestTrustSignCommandOfflineErrors(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) + cmd := newSignCommand(cli) + cmd.SetArgs([]string{"reg-name.io/image:tag"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), "client is offline") +} + +func TestGetOrGenerateNotaryKey(t *testing.T) { + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + + // repo is empty, try making a root key + rootKeyA, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) + assert.NilError(t, err) + assert.Check(t, rootKeyA != nil) + + // we should only have one newly generated key + allKeys := notaryRepo.GetCryptoService().ListAllKeys() + assert.Check(t, is.Len(allKeys, 1)) + assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyA.ID()) != nil) + + // this time we should get back the same key if we ask for another root key + rootKeyB, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole) + assert.NilError(t, err) + assert.Check(t, rootKeyB != nil) + + // we should only have one newly generated key + allKeys = notaryRepo.GetCryptoService().ListAllKeys() + assert.Check(t, is.Len(allKeys, 1)) + assert.Check(t, notaryRepo.GetCryptoService().GetKey(rootKeyB.ID()) != nil) + + // The key we retrieved should be identical to the one we generated + assert.Check(t, is.DeepEqual(rootKeyA.Public(), rootKeyB.Public())) + + // Now also try with a delegation key + releasesKey, err := getOrGenerateNotaryKey(notaryRepo, trust.ReleasesRole) + assert.NilError(t, err) + assert.Check(t, releasesKey != nil) + + // we should now have two keys + allKeys = notaryRepo.GetCryptoService().ListAllKeys() + assert.Check(t, is.Len(allKeys, 2)) + assert.Check(t, notaryRepo.GetCryptoService().GetKey(releasesKey.ID()) != nil) + // The key we retrieved should be identical to the one we generated + assert.Check(t, releasesKey != rootKeyA) + assert.Check(t, releasesKey != rootKeyB) +} + +func TestAddStageSigners(t *testing.T) { + skip.If(t, runtime.GOOS == "windows", "FIXME: not supported currently") + + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + + // stage targets/user + userRole := data.RoleName("targets/user") + userKey := data.NewPublicKey("algoA", []byte("a")) + err = addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey}) + assert.NilError(t, err) + // check the changelist for four total changes: two on targets/releases and two on targets/user + cl, err := notaryRepo.GetChangelist() + assert.NilError(t, err) + changeList := cl.List() + assert.Check(t, is.Len(changeList, 4)) + // ordering is deterministic: + + // first change is for targets/user key creation + newSignerKeyChange := changeList[0] + expectedJSON, err := json.Marshal(&changelist.TUFDelegation{ + NewThreshold: notary.MinThreshold, + AddKeys: []data.PublicKey{userKey}, + }) + assert.NilError(t, err) + expectedChange := changelist.NewTUFChange( + changelist.ActionCreate, + userRole, + changelist.TypeTargetsDelegation, + "", // no path for delegations + expectedJSON, + ) + assert.Check(t, is.DeepEqual(expectedChange, newSignerKeyChange)) + + // second change is for targets/user getting all paths + newSignerPathsChange := changeList[1] + expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ + AddPaths: []string{""}, + }) + assert.NilError(t, err) + expectedChange = changelist.NewTUFChange( + changelist.ActionCreate, + userRole, + changelist.TypeTargetsDelegation, + "", // no path for delegations + expectedJSON, + ) + assert.Check(t, is.DeepEqual(expectedChange, newSignerPathsChange)) + + releasesRole := data.RoleName("targets/releases") + + // third change is for targets/releases key creation + releasesKeyChange := changeList[2] + expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ + NewThreshold: notary.MinThreshold, + AddKeys: []data.PublicKey{userKey}, + }) + assert.NilError(t, err) + expectedChange = changelist.NewTUFChange( + changelist.ActionCreate, + releasesRole, + changelist.TypeTargetsDelegation, + "", // no path for delegations + expectedJSON, + ) + assert.Check(t, is.DeepEqual(expectedChange, releasesKeyChange)) + + // fourth change is for targets/releases getting all paths + releasesPathsChange := changeList[3] + expectedJSON, err = json.Marshal(&changelist.TUFDelegation{ + AddPaths: []string{""}, + }) + assert.NilError(t, err) + expectedChange = changelist.NewTUFChange( + changelist.ActionCreate, + releasesRole, + changelist.TypeTargetsDelegation, + "", // no path for delegations + expectedJSON, + ) + assert.Check(t, is.DeepEqual(expectedChange, releasesPathsChange)) +} + +func TestGetSignedManifestHashAndSize(t *testing.T) { + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + _, _, err = getSignedManifestHashAndSize(notaryRepo, "test") + assert.Error(t, err, "client is offline") +} + +func TestGetReleasedTargetHashAndSize(t *testing.T) { + oneReleasedTgt := []client.TargetSignedStruct{} + // make and append 3 non-released signatures on the "unreleased" target + unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}} + for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} { + oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt}) + } + _, _, err := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased") + assert.Error(t, err, "No valid trust data for unreleased") + releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}} + oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt}) + hash, _, _ := getReleasedTargetHashAndSize(oneReleasedTgt, "unreleased") + assert.Check(t, is.DeepEqual(data.Hashes{notary.SHA256: []byte("released-hash")}, hash)) +} + +func TestCreateTarget(t *testing.T) { + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + _, err = createTarget(notaryRepo, "") + assert.Error(t, err, "no tag specified") + _, err = createTarget(notaryRepo, "1") + assert.Error(t, err, "client is offline") +} + +func TestGetExistingSignatureInfoForReleasedTag(t *testing.T) { + notaryRepo, err := client.NewFileCachedRepository(t.TempDir(), "gun", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + _, err = getExistingSignatureInfoForReleasedTag(notaryRepo, "test") + assert.Error(t, err, "client is offline") +} + +func TestPrettyPrintExistingSignatureInfo(t *testing.T) { + buf := bytes.NewBuffer(nil) + signers := []string{"Bob", "Alice", "Carol"} + existingSig := trustTagRow{trustTagKey{"tagName", "abc123"}, signers} + prettyPrintExistingSignatureInfo(buf, existingSig) + + assert.Check(t, is.Contains(buf.String(), "Existing signatures for tag tagName digest abc123 from:\nAlice, Bob, Carol")) +} + +func TestSignCommandChangeListIsCleanedOnError(t *testing.T) { + tmpDir := t.TempDir() + + config.SetDir(tmpDir) + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) + cmd := newSignCommand(cli) + cmd.SetArgs([]string{"ubuntu:latest"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + + err := cmd.Execute() + assert.Assert(t, err != nil) + + notaryRepo, err := client.NewFileCachedRepository(tmpDir, "docker.io/library/ubuntu", "https://localhost", nil, testPassRetriever, trustpinning.TrustPinConfig{}) + assert.NilError(t, err) + cl, err := notaryRepo.GetChangelist() + assert.NilError(t, err) + assert.Check(t, is.Equal(len(cl.List()), 0)) +} + +func TestSignCommandLocalFlag(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{}) + cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) + cmd := newSignCommand(cli) + cmd.SetArgs([]string{"--local", "reg-name.io/image:red"}) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + assert.ErrorContains(t, cmd.Execute(), "error contacting notary server: dial tcp: lookup reg-name.io") +} diff --git a/cmd/docker-trust/trust/signer.go b/cmd/docker-trust/trust/signer.go new file mode 100644 index 000000000000..c765abeb1344 --- /dev/null +++ b/cmd/docker-trust/trust/signer.go @@ -0,0 +1,24 @@ +package trust + +import ( + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +// newTrustSignerCommand returns a cobra command for `trust signer` subcommands +func newTrustSignerCommand(dockerCLI command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "signer", + Short: "Manage entities who can sign Docker images", + Args: cli.NoArgs, + RunE: command.ShowHelp(dockerCLI.Err()), + + DisableFlagsInUseLine: true, + } + cmd.AddCommand( + newSignerAddCommand(dockerCLI), + newSignerRemoveCommand(dockerCLI), + ) + return cmd +} diff --git a/cli/command/trust/signer_add.go b/cmd/docker-trust/trust/signer_add.go similarity index 87% rename from cli/command/trust/signer_add.go rename to cmd/docker-trust/trust/signer_add.go index 155347074190..0d94b9c46fcd 100644 --- a/cli/command/trust/signer_add.go +++ b/cmd/docker-trust/trust/signer_add.go @@ -2,6 +2,7 @@ package trust import ( "context" + "errors" "fmt" "io" "os" @@ -10,11 +11,9 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/internal/lazyregexp" "github.com/docker/cli/opts" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" @@ -38,6 +37,7 @@ func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command { options.repos = args[1:] return addSigner(cmd.Context(), dockerCLI, options) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() options.keys = opts.NewListOpts(nil) @@ -80,7 +80,7 @@ func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOpti } func addSignerToRepo(ctx context.Context, dockerCLI command.Cli, signerName string, repoName string, signerPubKeys []data.PublicKey) error { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), repoName) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), repoName) if err != nil { return err } @@ -106,7 +106,7 @@ func addSignerToRepo(ctx context.Context, dockerCLI command.Cli, signerName stri newSignerRoleName := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), signerName)) if err := addStagedSigner(notaryRepo, newSignerRoleName, signerPubKeys); err != nil { - return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSignerRoleName.String(), "targets/")) + return fmt.Errorf("could not add signer to repo: %s: %w", strings.TrimPrefix(newSignerRoleName.String(), "targets/"), err) } return notaryRepo.Publish() @@ -118,20 +118,20 @@ func ingestPublicKeys(pubKeyPaths []string) ([]data.PublicKey, error) { // Read public key bytes from PEM file, limit to 1 KiB pubKeyFile, err := os.OpenFile(pubKeyPath, os.O_RDONLY, 0o666) if err != nil { - return nil, errors.Wrap(err, "unable to read public key from file") + return nil, fmt.Errorf("unable to read public key from file: %w", err) } defer pubKeyFile.Close() // limit to l := io.LimitReader(pubKeyFile, 1<<20) pubKeyBytes, err := io.ReadAll(l) if err != nil { - return nil, errors.Wrap(err, "unable to read public key from file") + return nil, fmt.Errorf("unable to read public key from file: %w", err) } // Parse PEM bytes into type PublicKey pubKey, err := tufutils.ParsePEMPublicKey(pubKeyBytes) if err != nil { - return nil, errors.Wrapf(err, "could not parse public key from file: %s", pubKeyPath) + return nil, fmt.Errorf("could not parse public key from file: %s: %w", pubKeyPath, err) } pubKeys = append(pubKeys, pubKey) } diff --git a/cli/command/trust/signer_add_test.go b/cmd/docker-trust/trust/signer_add_test.go similarity index 97% rename from cli/command/trust/signer_add_test.go rename to cmd/docker-trust/trust/signer_add_test.go index 31fd98361176..02858e2ddd90 100644 --- a/cli/command/trust/signer_add_test.go +++ b/cmd/docker-trust/trust/signer_add_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/docker/cli/cli/config" - "github.com/docker/cli/internal/test" - notaryfake "github.com/docker/cli/internal/test/notary" + "github.com/docker/cli/cmd/docker-trust/internal/test" + notaryfake "github.com/docker/cli/cmd/docker-trust/internal/test/notary" "github.com/theupdateframework/notary" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" diff --git a/cli/command/trust/signer_remove.go b/cmd/docker-trust/trust/signer_remove.go similarity index 86% rename from cli/command/trust/signer_remove.go rename to cmd/docker-trust/trust/signer_remove.go index 10d2c2933bad..e920ec645550 100644 --- a/cli/command/trust/signer_remove.go +++ b/cmd/docker-trust/trust/signer_remove.go @@ -2,15 +2,14 @@ package trust import ( "context" + "errors" "fmt" "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" - "github.com/docker/cli/cli/trust" + "github.com/docker/cli/cmd/docker-trust/internal/trust" "github.com/docker/cli/internal/prompt" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" @@ -22,7 +21,7 @@ type signerRemoveOptions struct { forceYes bool } -func newSignerRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newSignerRemoveCommand(dockerCLI command.Cli) *cobra.Command { options := signerRemoveOptions{} cmd := &cobra.Command{ Use: "remove [OPTIONS] NAME REPOSITORY [REPOSITORY...]", @@ -31,8 +30,9 @@ func newSignerRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { options.signer = args[0] options.repos = args[1:] - return removeSigner(cmd.Context(), dockerCli, options) + return removeSigner(cmd.Context(), dockerCLI, options) }, + DisableFlagsInUseLine: true, } flags := cmd.Flags() flags.BoolVarP(&options.forceYes, "force", "f", false, "Do not prompt for confirmation before removing the most recent signer") @@ -49,7 +49,7 @@ func removeSigner(ctx context.Context, dockerCLI command.Cli, options signerRemo } } if len(errRepos) > 0 { - return errors.Errorf("error removing signer from: %s", strings.Join(errRepos, ", ")) + return fmt.Errorf("error removing signer from: %s", strings.Join(errRepos, ", ")) } return nil } @@ -91,14 +91,14 @@ func maybePromptForSignerRemoval(ctx context.Context, dockerCLI command.Cli, rep // removeSingleSigner attempts to remove a single signer and returns whether signer removal happened. // The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation. func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, forceYes bool) (bool, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), repoName) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), repoName) if err != nil { return false, err } signerDelegation := data.RoleName("targets/" + signerName) if signerDelegation == releasesRoleTUFName { - return false, errors.Errorf("releases is a reserved keyword and cannot be removed") + return false, errors.New("releases is a reserved keyword and cannot be removed") } notaryRepo, err := newNotaryClient(dockerCLI, imgRefAndAuth, trust.ActionsPushAndPull) if err != nil { @@ -106,7 +106,7 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si } delegationRoles, err := notaryRepo.GetDelegationRoles() if err != nil { - return false, errors.Wrapf(err, "error retrieving signers for %s", repoName) + return false, fmt.Errorf("error retrieving signers for %s: %w", repoName, err) } var role data.Role for _, delRole := range delegationRoles { @@ -116,7 +116,7 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si } } if role.Name == "" { - return false, errors.Errorf("no signer %s for repository %s", signerName, repoName) + return false, fmt.Errorf("no signer %s for repository %s", signerName, repoName) } allRoles, err := notaryRepo.ListRoles() if err != nil { diff --git a/cli/command/trust/signer_remove_test.go b/cmd/docker-trust/trust/signer_remove_test.go similarity index 97% rename from cli/command/trust/signer_remove_test.go rename to cmd/docker-trust/trust/signer_remove_test.go index 402ddda991ca..64cede214e81 100644 --- a/cli/command/trust/signer_remove_test.go +++ b/cmd/docker-trust/trust/signer_remove_test.go @@ -5,8 +5,8 @@ import ( "io" "testing" - "github.com/docker/cli/internal/test" - notaryfake "github.com/docker/cli/internal/test/notary" + "github.com/docker/cli/cmd/docker-trust/internal/test" + notaryfake "github.com/docker/cli/cmd/docker-trust/internal/test/notary" "github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/tuf/data" "gotest.tools/v3/assert" diff --git a/cli/command/trust/testdata/trust-inspect-empty-repo.golden b/cmd/docker-trust/trust/testdata/trust-inspect-empty-repo.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-empty-repo.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-empty-repo.golden diff --git a/cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-full-repo-no-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-full-repo-no-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-full-repo-no-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-full-repo-with-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-full-repo-with-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-full-repo-with-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-multiple-repos-with-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-multiple-repos-with-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-multiple-repos-with-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-one-tag-no-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-one-tag-no-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-one-tag-no-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-pretty-full-repo-no-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-pretty-full-repo-no-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-pretty-full-repo-no-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-pretty-full-repo-no-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-pretty-full-repo-with-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-pretty-full-repo-with-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-pretty-full-repo-with-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-pretty-full-repo-with-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-pretty-one-tag-no-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-pretty-one-tag-no-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-pretty-one-tag-no-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-pretty-one-tag-no-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-pretty-unsigned-tag-with-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-pretty-unsigned-tag-with-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-pretty-unsigned-tag-with-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-pretty-unsigned-tag-with-signers.golden diff --git a/cli/command/trust/testdata/trust-inspect-uninitialized.golden b/cmd/docker-trust/trust/testdata/trust-inspect-uninitialized.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-uninitialized.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-uninitialized.golden diff --git a/cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden b/cmd/docker-trust/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden similarity index 100% rename from cli/command/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden rename to cmd/docker-trust/trust/testdata/trust-inspect-unsigned-tag-with-signers.golden diff --git a/cli/command/trust/testdata/trust-revoke-prompt-termination.golden b/cmd/docker-trust/trust/testdata/trust-revoke-prompt-termination.golden similarity index 100% rename from cli/command/trust/testdata/trust-revoke-prompt-termination.golden rename to cmd/docker-trust/trust/testdata/trust-revoke-prompt-termination.golden diff --git a/cmd/docker/builder.go b/cmd/docker/builder.go index cccae304fe5e..d6d74919bc28 100644 --- a/cmd/docker/builder.go +++ b/cmd/docker/builder.go @@ -8,10 +8,11 @@ import ( "strconv" "strings" + "github.com/containerd/errdefs" pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command" - "github.com/docker/docker/api/types/build" + "github.com/moby/moby/api/types/build" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -36,7 +37,7 @@ const ( ) func newBuilderError(errorMsg string, pluginLoadErr error) error { - if pluginmanager.IsNotFound(pluginLoadErr) { + if errdefs.IsNotFound(pluginLoadErr) { return errors.New(errorMsg) } if pluginLoadErr != nil { diff --git a/cmd/docker/builder_test.go b/cmd/docker/builder_test.go index 2b24db8140a0..f3688e0c8bbc 100644 --- a/cmd/docker/builder_test.go +++ b/cmd/docker/builder_test.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package main @@ -14,8 +14,7 @@ import ( "github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/flags" "github.com/docker/cli/internal/test/output" - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "gotest.tools/v3/assert" "gotest.tools/v3/fs" ) @@ -127,8 +126,8 @@ type fakeClient struct { client.Client } -func (*fakeClient) Ping(context.Context) (types.Ping, error) { - return types.Ping{OSType: "linux"}, nil +func (*fakeClient) Ping(context.Context, client.PingOptions) (client.PingResult, error) { + return client.PingResult{OSType: "linux"}, nil } func TestBuildkitDisabled(t *testing.T) { @@ -244,7 +243,12 @@ func TestBuilderBrokenEnforced(t *testing.T) { assert.DeepEqual(t, []string{"build", "."}, args) assert.Check(t, len(envs) == 0) - output.Assert(t, err.Error(), map[int]func(string) error{ + assert.Check(t, err != nil) + var errStr string + if err != nil { + errStr = err.Error() + } + output.Assert(t, errStr, map[int]func(string) error{ 0: output.Prefix("failed to fetch metadata:"), 2: output.Suffix("ERROR: BuildKit is enabled but the buildx component is missing or broken."), }) diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index ccd61845a1d2..5450ea05ec84 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -10,7 +10,7 @@ import ( "strings" "syscall" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/socket" @@ -20,7 +20,7 @@ import ( cliflags "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/version" platformsignals "github.com/docker/cli/cmd/docker/internal/signals" - "github.com/docker/docker/api/types/versions" + "github.com/moby/moby/client/pkg/versions" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -41,7 +41,7 @@ func main() { os.Exit(getExitCode(err)) } - if err != nil && !cerrdefs.IsCanceled(err) { + if err != nil && !errdefs.IsCanceled(err) { if err.Error() != "" { _, _ = fmt.Fprintln(os.Stderr, err) } @@ -144,6 +144,11 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "", }, } + + // Disable file-completion by default. Most commands and flags should not + // complete with filenames. + cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp) + cmd.SetIn(dockerCli.In()) cmd.SetOut(dockerCli.Out()) cmd.SetErr(dockerCli.Err()) @@ -163,8 +168,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { cmd.SetOut(dockerCli.Out()) commands.AddCommands(cmd, dockerCli) - cli.DisableFlagsInUseLine(cmd) - setValidateArgs(dockerCli, cmd) + visitAll(cmd, setValidateArgs(dockerCli)) // flags must be the top-level command flags, not cmd.Flags() return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) @@ -177,15 +181,9 @@ func setFlagErrorFunc(dockerCli command.Cli, cmd *cobra.Command) { // is called. flagErrorFunc := cmd.FlagErrorFunc() cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { - if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd.Root()); err != nil { - return err - } if err := isSupported(cmd, dockerCli); err != nil { return err } - if err := hideUnsupportedFeatures(cmd, dockerCli); err != nil { - return err - } return flagErrorFunc(cmd, err) }) } @@ -201,7 +199,7 @@ func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) { if err == nil { return helpcmd.Run() } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) } } @@ -240,7 +238,7 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) { if err == nil { return } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { ccmd.Println(err) return } @@ -256,23 +254,35 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) { ccmd.Println(err) return } - if err := hideUnsupportedFeatures(ccmd, dockerCli); err != nil { - ccmd.Println(err) - return - } + hideUnsupportedFeatures(ccmd, dockerCli) defaultHelpFunc(ccmd, args) }) } -func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) { - // The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. - // As a result, here we replace the existing Args validation func to a wrapper, - // where the wrapper will check to see if the feature is supported or not. - // The Args validation error will only be returned if the feature is supported. - cli.VisitAll(cmd, func(ccmd *cobra.Command) { +// visitAll traverses all commands from the root. +func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) { + for _, cmd := range root.Commands() { + visitAll(cmd, fns...) + } + for _, fn := range fns { + fn(root) + } +} + +// The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. +// As a result, here we replace the existing Args validation func to a wrapper, +// where the wrapper will check to see if the feature is supported or not. +// The Args validation error will only be returned if the feature is supported. +func setValidateArgs(dockerCLI versionDetails) func(*cobra.Command) { + return func(ccmd *cobra.Command) { // if there is no tags for a command or any of its parent, // there is no need to wrap the Args validation. + // + // FIXME(thaJeztah): can we memoize properties of the parent? + // visitAll traverses root -> all childcommands, and hasTags + // goes the reverse (cmd -> visit all parents), so we may + // end traversing two directions. if !hasTags(ccmd) { return } @@ -283,12 +293,12 @@ func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) { cmdArgs := ccmd.Args ccmd.Args = func(cmd *cobra.Command, args []string) error { - if err := isSupported(cmd, dockerCli); err != nil { + if err := isSupported(cmd, dockerCLI); err != nil { return err } return cmdArgs(cmd, args) } - }) + } } func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command, subcommand string, envs []string) error { @@ -321,19 +331,19 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command // signals to the subprocess because the shared // pgid makes the TTY a controlling terminal. // - // The plugin should have it's own copy of this + // The plugin should have its own copy of this // termination logic, and exit after 3 retries - // on it's own. + // on its own. if dockerCli.Out().IsTerminal() { return } - // Terminate the plugin server, which will - // close all connections with plugin - // subprocesses, and signal them to exit. + // Terminate the plugin server, which closes + // all connections with plugin subprocesses, + // and signal them to exit. // - // Repeated invocations will result in EINVAL, - // or EBADF; but that is fine for our purposes. + // Repeated invocations result in EINVAL or EBADF, + // but that is fine for our purposes. if srv != nil { _ = srv.Close() } @@ -351,15 +361,15 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command go func() { retries := 0 - force := false // catch the first signal through context cancellation <-ctx.Done() - tryTerminatePlugin(force) + tryTerminatePlugin(false) // register subsequent signals signals := make(chan os.Signal, exitLimit) signal.Notify(signals, platformsignals.TerminationSignals...) + force := false for range signals { retries++ // If we're still running after 3 interruptions @@ -440,7 +450,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { } }() } else { - fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed") + _, _ = fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed") } dockerCli.InstrumentCobraCommands(ctx, cmd) @@ -451,12 +461,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { return err } - if cli.HasCompletionArg(args) { + if hasCompletionArg(args) { // We add plugin command stubs early only for completion. We don't // want to add them for normal command execution as it would cause // a significant performance hit. - err = pluginmanager.AddPluginCommandStubs(dockerCli, cmd) - if err != nil { + if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil { return err } } @@ -473,7 +482,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { } return nil } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { // For plugin not found we fall through to // cmd.Execute() which deals with reporting // "command not found" in a consistent way. @@ -504,6 +513,16 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { return err } +// 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 +} + type versionDetails interface { CurrentVersion() string ServerInfo() command.ServerInfo @@ -535,7 +554,7 @@ func hideSubcommandIf(subcmd *cobra.Command, condition func(string) bool, annota } } -func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error { +func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { var ( notExperimental = func(_ string) bool { return !details.ServerInfo().HasExperimental } notOSType = func(v string) bool { return details.ServerInfo().OSType != "" && v != details.ServerInfo().OSType } @@ -591,7 +610,6 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error { hideSubcommandIf(subcmd, notSwarmStatus, "swarm") hideSubcommandIf(subcmd, versionOlderThan, "version") } - return nil } // Checks if a command or one of its ancestors is in the list diff --git a/cmd/docker/docker_test.go b/cmd/docker/docker_test.go index 48d37131c4a6..a4529cbb9db3 100644 --- a/cmd/docker/docker_test.go +++ b/cmd/docker/docker_test.go @@ -11,13 +11,31 @@ import ( "time" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/commands" "github.com/docker/cli/cli/debug" platformsignals "github.com/docker/cli/cmd/docker/internal/signals" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) +func TestDisableFlagsInUseLineIsSet(t *testing.T) { + dockerCli, err := command.NewDockerCli(command.WithBaseContext(context.TODO())) + assert.NilError(t, err) + rootCmd := &cobra.Command{DisableFlagsInUseLine: true} + commands.AddCommands(rootCmd, dockerCli) + + var errs []error + visitAll(rootCmd, func(c *cobra.Command) { + if !c.DisableFlagsInUseLine { + errs = append(errs, errors.New("DisableFlagsInUseLine is not set for "+c.CommandPath())) + } + }) + err = errors.Join(errs...) + assert.NilError(t, err) +} + func TestClientDebugEnabled(t *testing.T) { defer debug.Disable() ctx, cancel := context.WithCancel(context.TODO()) @@ -108,3 +126,21 @@ func TestUserTerminatedError(t *testing.T) { assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 143) } + +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"} + assert.DeepEqual(t, expected, visited) +} diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 595dec6ce132..388fd2165b0f 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -2803,7 +2803,6 @@ _docker_image_build() { " local boolean_options=" - --disable-content-trust=false --force-rm --help --no-cache diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index 10d72f28b5f3..6cb4df7856d4 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -139,7 +139,6 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpu-quota -d complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s c -l cpu-shares -d 'CPU shares (relative weight)' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpuset-cpus -d 'CPUs in which to allow execution (0-3, 0,1)' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpuset-mems -d 'MEMs in which to allow execution (0-3, 0,1)' -complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l disable-content-trust -d 'Skip image verification' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s f -l file -d "Name of the Dockerfile (Default is ‘PATH/Dockerfile’)" complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d 'Always remove intermediate containers' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage' diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 88350aabfb8e..6d9e4f9428a8 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -1005,7 +1005,6 @@ __docker_image_subcommand() { "($help)--cpu-rt-runtime=[Limit the CPU real-time runtime]:CPU real-time runtime in microseconds: " \ "($help)--cpuset-cpus=[CPUs in which to allow execution]:CPUs: " \ "($help)--cpuset-mems=[MEMs in which to allow execution]:MEMs: " \ - "($help)--disable-content-trust[Skip image verification]" \ "($help -f --file)"{-f=,--file=}"[Name of the Dockerfile]:Dockerfile:_files" \ "($help)--force-rm[Always remove intermediate containers]" \ "($help)--isolation=[Container isolation technology]:isolation:(default hyperv process)" \ @@ -2534,7 +2533,7 @@ __docker_volume_subcommand() { _arguments $(__docker_arguments) \ $opts_help \ "($help -f --force)"{-f,--force}"[Force the removal of one or more volumes]" \ - "($help -):volume:__docker_complete_volumes" && ret=0 + "($help -)*:volumes:__docker_complete_volumes" && ret=0 ;; (help) _arguments $(__docker_arguments) ":subcommand:__docker_volume_commands" && ret=0 diff --git a/docker-bake.hcl b/docker-bake.hcl index 670e31c1f5dc..ddcd9413835d 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,5 +1,5 @@ variable "GO_VERSION" { - default = "1.24.5" + default = "1.25.5" } variable "VERSION" { default = "" diff --git a/dockerfiles/Dockerfile.authors b/dockerfiles/Dockerfile.authors index e2a25e84165a..09d509d8113f 100644 --- a/dockerfiles/Dockerfile.authors +++ b/dockerfiles/Dockerfile.authors @@ -2,7 +2,7 @@ # ALPINE_VERSION sets the version of the alpine base image to use. # It must be a supported tag in the docker.io/library/alpine image repository. -ARG ALPINE_VERSION=3.21 +ARG ALPINE_VERSION=3.22 FROM alpine:${ALPINE_VERSION} AS gen RUN apk add --no-cache bash git diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 0b81e090c8b9..841b08147942 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -1,16 +1,16 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.25.5 # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # It must be a supported tag in the docker.io/library/alpine image repository # that's also available as alpine image variant for the Golang version used. -ARG ALPINE_VERSION=3.21 +ARG ALPINE_VERSION=3.22 # BUILDX_VERSION sets the version of buildx to install in the dev container. # It must be a valid tag in the docker.io/docker/buildx-bin image repository # on Docker Hub. -ARG BUILDX_VERSION=0.25.0 +ARG BUILDX_VERSION=0.29.1 FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang @@ -28,14 +28,16 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM golang AS gotestsum # GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container. # It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository. -ARG GOTESTSUM_VERSION=v1.12.3 +ARG GOTESTSUM_VERSION=v1.13.0 RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=tmpfs,target=/go/src/ \ GO111MODULE=on go install gotest.tools/gotestsum@${GOTESTSUM_VERSION} FROM golang AS goversioninfo -ARG GOVERSIONINFO_VERSION=v1.4.1 +# 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 RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=tmpfs,target=/go/src/ \ @@ -50,6 +52,7 @@ RUN apk add --no-cache \ coreutils \ curl \ git \ + git-daemon \ jq \ nano diff --git a/dockerfiles/Dockerfile.lint b/dockerfiles/Dockerfile.lint index 345943c2e5c0..ddba7e9b2772 100644 --- a/dockerfiles/Dockerfile.lint +++ b/dockerfiles/Dockerfile.lint @@ -1,22 +1,26 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.25.5 # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # It must be a supported tag in the docker.io/library/alpine image repository # that's also available as alpine image variant for the Golang version used. -ARG ALPINE_VERSION=3.21 -ARG GOLANGCI_LINT_VERSION=v2.1.5 +ARG ALPINE_VERSION=3.22 +# GOLANGCI_LINT_VERSION sets the version of the golangci/golangci-lint image to use. +ARG GOLANGCI_LINT_VERSION=v2.6.1 FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS lint ENV GOTOOLCHAIN=local -ENV GO111MODULE=off +ENV GO111MODULE=auto ENV CGO_ENABLED=0 ENV GOGC=75 WORKDIR /go/src/github.com/docker/cli COPY --link --from=golangci-lint /usr/bin/golangci-lint /usr/bin/golangci-lint -RUN --mount=type=bind,target=. \ +RUN --mount=type=bind,target=.,rw \ --mount=type=cache,target=/root/.cache \ + rm -f go.mod go.sum && \ + ln -s vendor.mod go.mod && \ + ln -s vendor.sum go.sum && \ golangci-lint run diff --git a/dockerfiles/Dockerfile.vendor b/dockerfiles/Dockerfile.vendor index e70a5921781d..ea9b9aba7d51 100644 --- a/dockerfiles/Dockerfile.vendor +++ b/dockerfiles/Dockerfile.vendor @@ -1,11 +1,11 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.25.5 # ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. # It must be a supported tag in the docker.io/library/alpine image repository # that's also available as alpine image variant for the Golang version used. -ARG ALPINE_VERSION=3.21 +ARG ALPINE_VERSION=3.22 ARG MODOUTDATED_VERSION=v0.8.0 FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base diff --git a/docs/deprecated.md b/docs/deprecated.md index 1a3c353da69f..4271bbf0ac78 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -53,6 +53,10 @@ The following table provides an overview of the current status of deprecated fea | Status | Feature | Deprecated | Remove | |------------|------------------------------------------------------------------------------------------------------------------------------------|------------|--------| +| Deprecated | [Support for cgroup v1](#support-for-cgroup-v1) | v29.0 | - | +| Deprecated | [`--pause` option on `docker commit`](#--pause-option-on-docker-commit) | v29.0 | v30.0 | +| Deprecated | [Legacy links environment variables](#legacy-links-environment-variables) | v28.4 | v30.0 | +| Deprecated | [Special handling for quoted values for TLS flags](#special-handling-for-quoted-values-for-tls-flags) | v28.4 | v29.0 | | Deprecated | [Empty/nil fields in image Config from inspect API](#emptynil-fields-in-image-config-from-inspect-api) | v28.3 | v29.0 | | Deprecated | [Configuration for pushing non-distributable artifacts](#configuration-for-pushing-non-distributable-artifacts) | v28.0 | v29.0 | | Deprecated | [`--time` option on `docker stop` and `docker restart`](#--time-option-on-docker-stop-and-docker-restart) | v28.0 | - | @@ -63,6 +67,7 @@ The following table provides an overview of the current status of deprecated fea | Removed | [`Container` and `ContainerConfig` fields in Image inspect](#container-and-containerconfig-fields-in-image-inspect) | v25.0 | v26.0 | | Removed | [Deprecate legacy API versions](#deprecate-legacy-api-versions) | v25.0 | v26.0 | | Removed | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 | +| Removed | [Mount `bind-nonrecursive` option](#mount-bind-nonrecursive-option) | v25.0 | v29.0 | | Removed | [IsAutomated field, and `is-automated` filter on `docker search`](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v28.2 | | Removed | [logentries logging driver](#logentries-logging-driver) | v24.0 | v25.0 | | Removed | [OOM-score adjust for the daemon](#oom-score-adjust-for-the-daemon) | v24.0 | v25.0 | @@ -121,10 +126,99 @@ The following table provides an overview of the current status of deprecated fea | Removed | [`--run` flag on `docker commit`](#--run-flag-on-docker-commit) | v0.10 | v1.13 | | Removed | [Three arguments form in `docker import`](#three-arguments-form-in-docker-import) | v0.6.7 | v1.12 | +### Support for cgroup v1 + +**Deprecated in release: v29.0** + +Support for cgroup v1 is deprecated in the v29.0 release, however, it will continue +to be supported until May 2029. +The latest release in May 2029 may not necessarily support cgroup v1, +but there will be at least one maintained branch with the support for cgroup v1. + +The cgroup version currently in use can be checked by running the `docker info` command: + +```console +$ docker info +<...> +Server: + <...> + Cgroup Version: 2 + <...> +``` + +### `--pause` option on `docker commit` + +**Deprecated in release: v29.0** + +**Target for removal in release: v30.0** + +The `--pause` option is enabled by default since Docker v1.1.0 to prevent +committing containers in an inconsistent state, but can be disabled by +setting the `--pause=false` option. In docker CLI v29.0 this flag is +replaced by a `--no-pause` flag instead. The `--pause` option is still +functional in the v29.0 release, printing a deprecation warning, but +will be removed in docker CLI v30. + +### Legacy links environment variables + +**Deprecated in release: v28.4** + +**Disabled by default in release: v29.0** + +**Target for removal in release: v30.0** + +Containers attached to the default bridge network can specify "legacy links" (e.g. +using `--links` on the CLI) to get access to other containers attached to that +network. The linking container (i.e., the container created with `--links`) automatically +gets environment variables that specify the IP address and port mappings of the linked +container. However, these environment variables are prefixed with the linked +container's names, making them impractical. + +Starting with Docker v29.0, these environment variables are no longer set by +default. Users who still depend on them can start Docker Engine with the +environment variable `DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS=1` set. + +Support for legacy links environment variables, as well as the `DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS` +will be removed in Docker Engine v30.0. + +### Special handling for quoted values for TLS flags + +**Deprecated in release: v28.4** + +**Target for removal in release: v29.0** + +The `--tlscacert`, `--tlscert`, and `--tlskey` command-line flags had +non-standard behavior for handling values contained in quotes (`"` or `'`). +Normally, quotes are handled by the shell, for example, in the following +example, the shell takes care of handling quotes before passing the values +to the `docker` CLI: + +```console +docker --some-option "some-value-in-quotes" ... +``` + +However, when passing values using an equal sign (`=`), this may not happen +and values may be handled including quotes; + +```console +docker --some-option="some-value-in-quotes" ... +``` + +This caused issues with "Docker Machine", which used this format as part +of its `docker-machine config` output, and the CLI carried special, non-standard +handling for these flags. + +Docker Machine reached EOL, and this special handling made the processing +of flag values inconsistent with other flags used, so this behavior is +deprecated. Users depending on this behavior are recommended to specify +the quoted values using a space between the flag and its value, as illustrated +above. + ### Empty/nil fields in image Config from inspect API -**Deprecated in Release: v28.3** -**Target For Removal In Release: v29.0** +**Deprecated in release: v28.3** + +**Target for removal in release: v29.0** The `Config` field returned by `docker image inspect` (and the `GET /images/{name}/json` API endpoint) currently includes certain fields even when they are empty or nil. @@ -150,8 +244,9 @@ API version for backward compatibility. ### Configuration for pushing non-distributable artifacts -**Deprecated in Release: v28.0** -**Target For Removal In Release: v29.0** +**Deprecated in release: v28.0** + +**Target for removal in release: v29.0** Non-distributable artifacts (also called foreign layers) were introduced in docker v1.12 to accommodate Windows images for which the EULA did not allow @@ -189,7 +284,7 @@ entirely. ### `--time` option on `docker stop` and `docker restart` -**Deprecated in Release: v28.0** +**Deprecated in release: v28.0** The `--time` option for the `docker stop`, `docker container stop`, `docker restart`, and `docker container restart` commands has been renamed to `--timeout` for @@ -199,8 +294,9 @@ Users are encouraged to migrate to using the `--timeout` option instead. ### Non-standard fields in image inspect -**Deprecated in Release: v27.0** -**Removed In Release: v28.2** +**Deprecated in release: v27.0** + +**Removed in release: v28.2** The `Config` field returned shown in `docker image inspect` (and as returned by the `GET /images/{name}/json` API endpoint) returns additional fields that are @@ -234,9 +330,11 @@ They continue to be included when using clients that use an older API version: ### Graphdriver plugins (experimental) -**Deprecated in Release: v27.0** -**Disabled by default in Release: v27.0** -**Target For Removal In Release: v28.0** +**Deprecated in**: v27.0**. + +**Disabled by default in release: v27.0** + +**Target for removal in release: v28.0** [Graphdriver plugins](https://github.com/docker/cli/blob/v26.1.4/docs/extend/plugins_graphdriver.md) were an experimental feature that allowed extending the Docker Engine with custom @@ -250,8 +348,10 @@ and a custom [snapshotter](https://github.com/containerd/containerd/tree/v1.7.18 ### API CORS headers -**Deprecated in Release: v27.0** -**Disabled by default in Release: v27.0** +**Deprecated in release: v27.0** + +**Disabled by default in release: v27.0** + **Removed in release: v28.0** The `api-cors-header` configuration option for the Docker daemon is insecure, @@ -271,8 +371,9 @@ If you need to access the API through a browser, use a reverse proxy. ### Unauthenticated TCP connections -**Deprecated in Release: v26.0** -**Target For Removal In Release: v28.0** +**Deprecated in release: v26.0** + +**Target for removal in release: v28.0** Configuring the Docker daemon to listen on a TCP address will require mandatory TLS verification. This change aims to ensure secure communication by preventing @@ -298,8 +399,9 @@ configuring TLS (or SSH) for the Docker daemon, refer to ### `Container` and `ContainerConfig` fields in Image inspect -**Deprecated in Release: v25.0** -**Removed In Release: v26.0** +**Deprecated in release: v25.0** + +**Removed in release: v26.0** The `Container` and `ContainerConfig` fields returned by `docker inspect` are mostly an implementation detail of the classic (non-BuildKit) image builder. @@ -311,8 +413,9 @@ you can obtain it from the `Config` field. ### Deprecate legacy API versions -**Deprecated in Release: v25.0** -**Target For Removal In Release: v26.0** +**Deprecated in release: v25.0** + +**Target for removal in release: v26.0** The Docker daemon provides a versioned API for backward compatibility with old clients. Docker clients can perform API-version negotiation to select the most @@ -368,8 +471,9 @@ old clients, and those clients must be supported. ### Container short ID in network Aliases field -**Deprecated in Release: v25.0** -**Removed In Release: v26.0** +**Deprecated in release: v25.0** + +**Removed in release: v26.0** The `Aliases` field returned by `docker inspect` contains the container short ID once the container is started. This behavior is deprecated in v25.0 but @@ -381,10 +485,32 @@ A new field `DNSNames` containing the container name (if one was specified), the hostname, the network aliases, as well as the container short ID, has been introduced in v25.0 and should be used instead of the `Aliases` field. +### Mount `bind-nonrecursive` option + +**Deprecated in release: v25.0** + +**Removed in release: v29.0** + +The `bind-nonrecursive` option was replaced with the [`bind-recursive`] +option (see [cli-4316], [cli-4671]). The option was still accepted, but +printed a deprecation warning: + +```console +bind-nonrecursive is deprecated, use bind-recursive=disabled instead +``` + +In the v29.0 release, this warning is removed, and returned as an error. +Users should use the equivalent `bind-recursive=disabled` option instead. + +[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts +[cli-4316]: https://github.com/docker/cli/pull/4316 +[cli-4671]: https://github.com/docker/cli/pull/4671 + ### IsAutomated field, and `is-automated` filter on `docker search` -**Deprecated in Release: v25.0** -**Removed In Release: v28.2** +**Deprecated in release: v25.0** + +**Removed in release: v28.2** The `is_automated` field has been deprecated by Docker Hub's search API. Consequently, the `IsAutomated` field in image search will always be set @@ -397,8 +523,9 @@ templating has been removed in v28.2. ### Logentries logging driver -**Deprecated in Release: v24.0** -**Removed in Release: v25.0** +**Deprecated in release: v24.0** + +**Removed in release: v25.0** The logentries service SaaS was shut down on November 15, 2022, rendering this logging driver non-functional. Users should no longer use this logging @@ -408,8 +535,9 @@ after upgrading. ### OOM-score adjust for the daemon -**Deprecated in Release: v24.0** -**Removed in Release: v25.0** +**Deprecated in release: v24.0** + +**Removed in release: v25.0** The `oom-score-adjust` option was added to prevent the daemon from being OOM-killed before other processes. This option was mostly added as a @@ -428,8 +556,9 @@ the daemon. ### BuildKit build information -**Deprecated in Release: v23.0** -**Removed in Release: v24.0** +**Deprecated in release: v23.0** + +**Removed in release: v24.0** [Build information](https://github.com/moby/buildkit/blob/v0.11/docs/buildinfo.md) structures have been introduced in [BuildKit v0.10.0](https://github.com/moby/buildkit/releases/tag/v0.10.0) @@ -440,7 +569,7 @@ information is also embedded into the image configuration if one is generated. ### Legacy builder for Linux images -**Deprecated in Release: v23.0** +**Deprecated in release: v23.0** Docker v23.0 now uses BuildKit by default to build Linux images, and uses the [Buildx](https://docs.docker.com/buildx/working-with-buildx/) CLI component for @@ -471,7 +600,7 @@ you to report issues in the [BuildKit issue tracker on GitHub](https://github.co ### Legacy builder fallback -**Deprecated in Release: v23.0** +**Deprecated in release: v23.0** [Docker v23.0 now uses BuildKit by default to build Linux images](#legacy-builder-for-linux-images), which requires the Buildx component to build images with BuildKit. There may be @@ -517,7 +646,7 @@ be possible in a future release. ### Btrfs storage driver on CentOS 7 and RHEL 7 -**Removed in Release: v23.0** +**Removed in release: v23.0** The `btrfs` storage driver on CentOS and RHEL was provided as a technology preview by CentOS and RHEL, but has been deprecated since the [Red Hat Enterprise Linux 7.4 release](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/storage_administration_guide/ch-btrfs), @@ -529,9 +658,9 @@ of Docker will no longer provide this driver. ### Support for encrypted TLS private keys -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** -**Removed in Release: v23.0** +**Removed in release: v23.0** Use of encrypted TLS private keys has been deprecated, and has been removed. Golang has deprecated support for legacy PEM encryption (as specified in @@ -545,8 +674,9 @@ to decrypt the private key, and store it un-encrypted to continue using it. ### Kubernetes stack and context support -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Following the deprecation of [Compose on Kubernetes](https://github.com/docker/compose-on-kubernetes), support for Kubernetes in the `stack` and `context` commands has been removed from @@ -574,8 +704,9 @@ CLI configuration file are no longer used, and ignored. ### Pulling images from non-compliant image registries -**Deprecated in Release: v20.10** -**Removed in Release: v28.2** +**Deprecated in release: v20.10** + +**Removed in release: v28.2** Docker Engine v20.10 and up includes optimizations to verify if images in the local image cache need updating before pulling, preventing the Docker Engine @@ -604,8 +735,9 @@ no longer needed. ### Linux containers on Windows (LCOW) (experimental) -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** The experimental feature to run Linux containers on Windows (LCOW) was introduced as a technical preview in Docker 17.09. While many enhancements were made after @@ -617,7 +749,7 @@ Developers who want to run Linux workloads on a Windows host are encouraged to u ### BLKIO weight options with cgroups v1 -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** Specifying blkio weight (`docker run --blkio-weight` and `docker run --blkio-weight-device`) is now marked as deprecated when using cgroups v1 because the corresponding features @@ -627,35 +759,23 @@ When using cgroups v2, the `--blkio-weight` options are implemented using ### Kernel memory limit -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Specifying kernel memory limit (`docker run --kernel-memory`) is no longer supported because the [Linux kernel deprecated `kmem.limit_in_bytes` in v5.4](https://github.com/torvalds/linux/commit/0158115f702b0ba208ab0b5adf44cae99b3ebcc7). -The OCI runtime specification now marks this option (as well as `--kernel-memory-tcp`) -as ["NOT RECOMMENDED"](https://github.com/opencontainers/runtime-spec/pull/1093), +The OCI runtime specification now marks this option as ["NOT RECOMMENDED"](https://github.com/opencontainers/runtime-spec/pull/1093), and OCI runtimes such as `runc` no longer support this option. -Docker API v1.42 and up now ignores this option when set. Older versions of the -API continue to accept the option, but depending on the OCI runtime used, may -take no effect. - -> [!NOTE] -> While not deprecated (yet) in Docker, the OCI runtime specification also -> deprecated the `memory.kmem.tcp.limit_in_bytes` option. When using `runc` as -> runtime, this option takes no effect. The Linux kernel did not explicitly -> deprecate this feature, and there is a tracking ticket in the `runc` issue -> tracker to determine if this option should be reinstated or if this was an -> oversight of the Linux kernel maintainers (see [opencontainers/runc#3174](https://github.com/opencontainers/runc/issues/3174)). -> -> The `memory.kmem.tcp.limit_in_bytes` option is only supported with cgroups v1, -> and not available on installations running with cgroups v2. This option is -> only supported by the API, and not exposed on the `docker` command-line. +The Docker API no longer handles the kernel-memory fields, and Docker CLI v29.0 +removes the `--kernel-memory` option. ### Classic Swarm and overlay networks using cluster store -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Standalone ("classic") Swarm has been deprecated, and with that the use of overlay networks using an external key/value store. The corresponding`--cluster-advertise`, @@ -663,8 +783,9 @@ networks using an external key/value store. The corresponding`--cluster-advertis ### Support for legacy `~/.dockercfg` configuration files -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** The Docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials after authenticating to a registry (`docker login`). Docker v1.7.0 replaced this @@ -679,9 +800,9 @@ been removed. ### Configuration options for experimental CLI features -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v23.0** +**Removed in release: v23.0** The `DOCKER_CLI_EXPERIMENTAL` environment variable and the corresponding `experimental` field in the CLI configuration file are deprecated. Experimental features are @@ -693,13 +814,13 @@ format. ### CLI plugins support -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** CLI Plugin API is now marked as deprecated. ### Dockerfile legacy `ENV name value` syntax -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** The Dockerfile `ENV` instruction allows values to be set using either `ENV name=value` or `ENV name value`. The latter (`ENV name value`) form can be ambiguous, for example, @@ -723,8 +844,9 @@ ENV ONE="" TWO="" THREE="world" ### `docker build --stream` flag (experimental) -**Deprecated in Release: v20.10** -**Removed in Release: v20.10** +**Deprecated in release: v20.10** + +**Removed in release: v20.10** Docker v17.07 introduced an experimental `--stream` flag on `docker build` which allowed the build-context to be incrementally sent to the daemon, instead of @@ -740,8 +862,9 @@ files. ### `fluentd-async-connect` log opt -**Deprecated in Release: v20.10** -**Removed in Release: v28.0** +**Deprecated in release: v20.10** + +**Removed in release: v28.0** The `--log-opt fluentd-async-connect` option for the fluentd logging driver is [deprecated in favor of `--log-opt fluentd-async`](https://github.com/moby/moby/pull/39086). @@ -756,11 +879,11 @@ for the old option has been removed. ### Pushing and pulling with image manifest v2 schema 1 -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Disabled by default in Release: v26.0** +**Disabled by default in release: v26.0** -**Removed in Release: v28.2** +**Removed in release: v28.2** The image manifest [v2 schema 1](https://distribution.github.io/distribution/spec/deprecated-schema-v1/) @@ -785,9 +908,9 @@ More information at https://docs.docker.com/go/deprecated-image-specs/ ### `docker engine` subcommands -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** The `docker engine activate`, `docker engine check`, and `docker engine update` provided an alternative installation method to upgrade Docker Community engines @@ -800,9 +923,9 @@ standard package managers. ### Top-level `docker deploy` subcommand (experimental) -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** The top-level `docker deploy` command (using the "Docker Application Bundle" (.dab) file format was introduced as an experimental feature in Docker 1.13 / @@ -811,9 +934,9 @@ subcommand. ### `docker stack deploy` using "dab" files (experimental) -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** With no development being done on this feature, and no active use of the file format, support for the DAB file format and the top-level `docker deploy` command @@ -822,8 +945,9 @@ using compose files. ### Support for the `overlay2.override_kernel_check` storage option -**Deprecated in Release: v19.03** -**Removed in Release: v24.0** +**Deprecated in release: v19.03** + +**Removed in release: v24.0** This daemon configuration option disabled the Linux kernel version check used to detect if the kernel supported OverlayFS with multiple lower dirs, which is @@ -833,8 +957,9 @@ option was no longer used. ### AuFS storage driver -**Deprecated in Release: v19.03** -**Removed in Release: v24.0** +**Deprecated in release: v19.03** + +**Removed in release: v24.0** The `aufs` storage driver is deprecated in favor of `overlay2`, and has been removed in a Docker Engine v24.0. Users of the `aufs` storage driver must @@ -852,8 +977,9 @@ maintenance of the `aufs` storage driver. ### Legacy overlay storage driver -**Deprecated in Release: v18.09** -**Removed in Release: v24.0** +**Deprecated in release: v18.09** + +**Removed in release: v24.0** The `overlay` storage driver is deprecated in favor of the `overlay2` storage driver, which has all the benefits of `overlay`, without its limitations (excessive @@ -868,9 +994,11 @@ backported), there is no reason to keep maintaining the `overlay` storage driver ### Device mapper storage driver -**Deprecated in Release: v18.09** -**Disabled by default in Release: v23.0** -**Removed in Release: v25.0** +**Deprecated in release: v18.09** + +**Disabled by default in release: v23.0** + +**Removed in release: v25.0** The `devicemapper` storage driver is deprecated in favor of `overlay2`, and has been removed in Docker Engine v25.0. Users of the `devicemapper` storage driver @@ -886,9 +1014,9 @@ is no reason to continue maintenance of the `devicemapper` storage driver. ### Use of reserved namespaces in engine labels -**Deprecated in Release: v18.06** +**Deprecated in release: v18.06** -**Removed In Release: v20.10** +**Removed in release: v20.10** The namespaces `com.docker.*`, `io.docker.*`, and `org.dockerproject.*` in engine labels were always documented to be reserved, but there was never any enforcement. @@ -900,7 +1028,7 @@ use, and will error instead in v20.10 and above. **Disabled In Release: v17.12** -**Removed In Release: v19.03** +**Removed in release: v19.03** The `--disable-legacy-registry` flag was disabled in Docker 17.12 and will print an error when used. For this error to be printed, the flag itself is still present, @@ -908,9 +1036,9 @@ but hidden. The flag has been removed in Docker 19.03. ### Interacting with V1 registries -**Disabled By Default In Release: v17.06** +**Disabled by default in release: v17.06** -**Removed In Release: v17.12** +**Removed in release: v17.12** Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the Docker daemon from `pull`, `push`, and `login` operations against v1 @@ -927,7 +1055,7 @@ start when set. ### Asynchronous `service create` and `service update` as default -**Deprecated In Release: v17.05** +**Deprecated in release: v17.05** **Disabled by default in release: [v17.10](https://github.com/docker/docker-ce/releases/tag/v17.10.0-ce)** @@ -941,9 +1069,9 @@ and `docker service scale` in Docker 17.10. ### `-g` and `--graph` flags on `dockerd` -**Deprecated In Release: v17.05** +**Deprecated in release: v17.05** -**Removed In Release: v23.0** +**Removed in release: v23.0** The `-g` or `--graph` flag for the `dockerd` or `docker daemon` command was used to indicate the directory in which to store persistent data and resource @@ -952,9 +1080,9 @@ flag. These flags were deprecated and hidden in v17.05, and removed in v23.0. ### Top-level network properties in NetworkSettings -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Target For Removal In Release: v17.12** +**Target for removal in release: v17.12** When inspecting a container, `NetworkSettings` contains top-level information about the default ("bridge") network; @@ -971,18 +1099,18 @@ information. ### `filter` option for `/images/json` endpoint -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v20.10** +**Removed in release: v20.10** The `filter` option to filter the list of image by reference (name or name:tag) is now implemented as a regular filter, named `reference`. ### `repository:shortid` image references -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The `repository:shortid` syntax for referencing images is very little used, collides with tag references, and can be confused with digest references. @@ -992,32 +1120,32 @@ in Docker 17.12. ### `docker daemon` subcommand -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The daemon is moved to a separate binary (`dockerd`), and should be used instead. ### Duplicate keys with conflicting values in engine labels -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** When setting duplicate keys with conflicting values, an error will be produced, and the daemon will fail to start. ### `MAINTAINER` in Dockerfile -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** `MAINTAINER` was an early very limited form of `LABEL` which should be used instead. ### API calls without a version -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Target For Removal In Release: v17.12** +**Target for removal in release: v17.12** API versions should be supplied to all API calls to ensure compatibility with future Engine versions. Instead of just requesting, for example, the URL @@ -1025,9 +1153,9 @@ future Engine versions. Instead of just requesting, for example, the URL ### Backing filesystem without `d_type` support for overlay/overlay2 -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The overlay and overlay2 storage driver does not work as expected if the backing filesystem does not support `d_type`. For example, XFS does not support `d_type` @@ -1041,18 +1169,18 @@ Refer to [#27358](https://github.com/docker/docker/issues/27358) for details. ### `--automated` and `--stars` flags on `docker search` -**Deprecated in Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Deprecated in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** -**Removed In Release: v20.10** +**Removed in release: v20.10** The `docker search --automated` and `docker search --stars` options are deprecated. Use `docker search --filter=is-automated=` and `docker search --filter=stars=...` instead. ### `-h` shorthand for `--help` -**Deprecated In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Deprecated in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** -**Target For Removal In Release: v17.09** +**Target for removal in release: v17.09** The shorthand (`-h`) is less common than `--help` on Linux and cannot be used on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on @@ -1061,58 +1189,58 @@ on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on ### `-e` and `--email` flags on `docker login` -**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** +**Deprecated in release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** -**Removed In Release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** +**Removed in release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** The `docker login` no longer automatically registers an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. ### Separator (`:`) of `--security-opt` flag on `docker run` -**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** +**Deprecated in release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** -**Target For Removal In Release: v17.06** +**Target for removal in release: v17.06** The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`. ### Ambiguous event fields in API -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure. See the events API documentation for the new format. ### `-f` flag on `docker tag` -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** To make tagging consistent across the various `docker` commands, the `-f` flag on the `docker tag` command is deprecated. It is no longer necessary to specify `-f` to move a tag from one image to another. Nor will `docker` generate an error if the `-f` flag is missing and the specified tag is already in use. ### HostConfig at API container start -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Passing an `HostConfig` to `POST /containers/{name}/start` is deprecated in favor of defining it at container creation (`POST /containers/create`). ### `--before` and `--since` flags on `docker ps` -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The `docker ps --before` and `docker ps --since` options are deprecated. Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead. ### Driver-specific log tags -**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** +**Deprecated in release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Log tags are now generated in a standard way across different logging drivers. Because of which, the driver specific log tag options `syslog-tag`, `gelf-tag` and @@ -1124,9 +1252,9 @@ $ docker --log-driver=syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}" ### Docker Content Trust ENV passphrase variables name change -**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** +**Deprecated in release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the Tagging key has been renamed to Repository key. Due to this renaming, we're also changing the corresponding environment variables @@ -1135,25 +1263,25 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the ### `/containers/(id or name)/copy` endpoint -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The endpoint `/containers/(id or name)/copy` is deprecated in favor of `/containers/(id or name)/archive`. ### LXC built-in exec driver -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Removed in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The built-in LXC execution driver, the lxc-conf flag, and API fields have been removed. ### Old Command Line Options -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Removed in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The flags `-d` and `--daemon` are deprecated. Use the separate `dockerd` binary instead. @@ -1197,34 +1325,34 @@ The following double-dash options are deprecated and have no replacement: - `docker ps --before-id` - `docker search --trusted` -**Deprecated In Release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)** +**Deprecated in release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The single-dash (`-help`) was removed, in favor of the double-dash `--help` ### `--api-enable-cors` flag on `dockerd` -**Deprecated In Release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)** +**Deprecated in release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)** -**Removed In Release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)** +**Removed in release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)** The flag `--api-enable-cors` is deprecated since v1.6.0. Use the flag `--api-cors-header` instead. ### `--run` flag on `docker commit` -**Deprecated In Release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)** +**Deprecated in release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)** -**Removed In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Removed in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** The flag `--run` of the `docker commit` command (and its short version `-run`) were deprecated in favor of the `--changes` flag that allows to pass `Dockerfile` commands. ### Three arguments form in `docker import` -**Deprecated In Release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)** +**Deprecated in release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no longer supported. diff --git a/docs/extend/plugins_network.md b/docs/extend/plugins_network.md index 8f94546b01b4..0ff0a64b1b6a 100644 --- a/docs/extend/plugins_network.md +++ b/docs/extend/plugins_network.md @@ -61,11 +61,4 @@ plugin protocol The network driver protocol, in addition to the plugin activation call, is documented as part of libnetwork: -[https://github.com/moby/moby/blob/master/libnetwork/docs/remote.md](https://github.com/moby/moby/blob/master/libnetwork/docs/remote.md). - -## Related Information - -To interact with the Docker maintainers and other interested users, see the IRC channel `#docker-network`. - -- [Docker networks feature overview](https://docs.docker.com/engine/userguide/networking/) -- The [LibNetwork](https://github.com/docker/libnetwork) project +[https://github.com/moby/moby/blob/master/daemon/libnetwork/docs/remote.md](https://github.com/moby/moby/blob/master/daemon/libnetwork/docs/remote.md). diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index dca5ee76ab84..491b0477dba5 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | diff --git a/docs/reference/commandline/builder_build.md b/docs/reference/commandline/builder_build.md index ad9c09532159..71589da94b93 100644 --- a/docs/reference/commandline/builder_build.md +++ b/docs/reference/commandline/builder_build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | diff --git a/docs/reference/commandline/commit.md b/docs/reference/commandline/commit.md index 02ae802b30c8..ad1371a0c004 100644 --- a/docs/reference/commandline/commit.md +++ b/docs/reference/commandline/commit.md @@ -14,7 +14,7 @@ Create a new image from a container's changes | `-a`, `--author` | `string` | | Author (e.g., `John Hannibal Smith `) | | `-c`, `--change` | `list` | | Apply Dockerfile instruction to the created image | | `-m`, `--message` | `string` | | Commit message | -| `-p`, `--pause` | `bool` | `true` | Pause container during commit | +| `--no-pause` | `bool` | | Disable pausing container during commit | diff --git a/docs/reference/commandline/container_commit.md b/docs/reference/commandline/container_commit.md index 636e85ba29bd..6aab8df4d0aa 100644 --- a/docs/reference/commandline/container_commit.md +++ b/docs/reference/commandline/container_commit.md @@ -14,7 +14,7 @@ Create a new image from a container's changes | `-a`, `--author` | `string` | | Author (e.g., `John Hannibal Smith `) | | [`-c`](#change), [`--change`](#change) | `list` | | Apply Dockerfile instruction to the created image | | `-m`, `--message` | `string` | | Commit message | -| `-p`, `--pause` | `bool` | `true` | Pause container during commit | +| `--no-pause` | `bool` | | Disable pausing container during commit | diff --git a/docs/reference/commandline/container_create.md b/docs/reference/commandline/container_create.md index 3b812a77747f..5bb5e18442db 100644 --- a/docs/reference/commandline/container_create.md +++ b/docs/reference/commandline/container_create.md @@ -37,7 +37,6 @@ Create a new container | `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device | | `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device | | `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--dns` | `list` | | Set custom DNS servers | | `--dns-option` | `list` | | Set DNS options | | `--dns-search` | `list` | | Set custom DNS search domains | @@ -60,11 +59,10 @@ Create a new container | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | -| `--kernel-memory` | `bytes` | `0` | Kernel memory limit | | `-l`, `--label` | `list` | | Set meta data on a container | | `--label-file` | `list` | | Read in a line delimited file of labels | | `--link` | `list` | | Add link to another container | diff --git a/docs/reference/commandline/container_run.md b/docs/reference/commandline/container_run.md index 7a991b1c766f..47ee86e6572f 100644 --- a/docs/reference/commandline/container_run.md +++ b/docs/reference/commandline/container_run.md @@ -39,7 +39,6 @@ Create and run a new container from an image | `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device | | `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device | | `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--dns` | `list` | | Set custom DNS servers | | `--dns-option` | `list` | | Set DNS options | | `--dns-search` | `list` | | Set custom DNS search domains | @@ -62,11 +61,10 @@ Create and run a new container from an image | [`-i`](#interactive), [`--interactive`](#interactive) | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | [`--ipc`](#ipc) | `string` | | IPC mode to use | | [`--isolation`](#isolation) | `string` | | Container isolation technology | -| `--kernel-memory` | `bytes` | `0` | Kernel memory limit | | [`-l`](#label), [`--label`](#label) | `list` | | Set meta data on a container | | `--label-file` | `list` | | Read in a line delimited file of labels | | `--link` | `list` | | Add link to another container | @@ -1284,7 +1282,7 @@ connect to services running on the host machine. It's conventional to use `host.docker.internal` as the hostname referring to `host-gateway`. Docker Desktop automatically resolves this hostname, see -[Explore networking features](https://docs.docker.com/desktop/features/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host). +[Explore networking features](https://docs.docker.com/desktop/features/networking/networking-how-tos/#i-want-to-connect-from-a-container-to-a-service-on-the-host). The following example shows how the special `host-gateway` value works. The example runs an HTTP server that serves a file from host to container over the diff --git a/docs/reference/commandline/create.md b/docs/reference/commandline/create.md index a3068217095e..5a7390b7f26c 100644 --- a/docs/reference/commandline/create.md +++ b/docs/reference/commandline/create.md @@ -37,7 +37,6 @@ Create a new container | `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device | | `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device | | `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--dns` | `list` | | Set custom DNS servers | | `--dns-option` | `list` | | Set DNS options | | `--dns-search` | `list` | | Set custom DNS search domains | @@ -60,11 +59,10 @@ Create a new container | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | -| `--kernel-memory` | `bytes` | `0` | Kernel memory limit | | `-l`, `--label` | `list` | | Set meta data on a container | | `--label-file` | `list` | | Read in a line delimited file of labels | | `--link` | `list` | | Add link to another container | diff --git a/docs/reference/commandline/docker.md b/docs/reference/commandline/docker.md index 69b1c91303be..b64330bdbe18 100644 --- a/docs/reference/commandline/docker.md +++ b/docs/reference/commandline/docker.md @@ -59,7 +59,6 @@ The base command for the Docker CLI. | [`system`](system.md) | Manage Docker | | [`tag`](tag.md) | Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE | | [`top`](top.md) | Display the running processes of a container | -| [`trust`](trust.md) | Manage trust on Docker images | | [`unpause`](unpause.md) | Unpause all processes within one or more containers | | [`update`](update.md) | Update configuration of one or more containers | | [`version`](version.md) | Show the Docker version information | @@ -74,7 +73,7 @@ The base command for the Docker CLI. | `--config` | `string` | `/root/.docker` | Location of client config files | | `-c`, `--context` | `string` | | Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with `docker context use`) | | `-D`, `--debug` | `bool` | | Enable debug mode | -| [`-H`](#host), [`--host`](#host) | `list` | | Daemon socket to connect to | +| [`-H`](#host), [`--host`](#host) | `string` | | Daemon socket to connect to | | `-l`, `--log-level` | `string` | `info` | Set the logging level (`debug`, `info`, `warn`, `error`, `fatal`) | | `--tls` | `bool` | | Use TLS; implied by --tlsverify | | `--tlscacert` | `string` | `/root/.docker/ca.pem` | Trust certs signed only by this CA | @@ -123,8 +122,6 @@ line: | `DOCKER_API_VERSION` | Override the negotiated API version to use for debugging (e.g. `1.19`) | | `DOCKER_CERT_PATH` | Location of your authentication keys. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | | `DOCKER_CONFIG` | The location of your client configuration files. | -| `DOCKER_CONTENT_TRUST_SERVER` | The URL of the Notary server to use. Defaults to the same URL as the registry. | -| `DOCKER_CONTENT_TRUST` | When set Docker uses notary to sign and verify images. Equates to `--disable-content-trust=false` for build, create, pull, push, run. | | `DOCKER_CONTEXT` | Name of the `docker context` to use (overrides `DOCKER_HOST` env var and default context set with `docker context use`) | | `DOCKER_CUSTOM_HEADERS` | (Experimental) Configure [custom HTTP headers](#custom-http-headers) to be sent by the client. Headers must be provided as a comma-separated list of `name=value` pairs. This is the equivalent to the `HttpHeaders` field in the configuration file. | | `DOCKER_DEFAULT_PLATFORM` | Default platform for commands that take the `--platform` flag. | @@ -133,6 +130,8 @@ line: | `DOCKER_TLS` | Enable TLS for connections made by the `docker` CLI (equivalent of the `--tls` command-line option). Set to a non-empty value to enable TLS. Note that TLS is enabled automatically if any of the other TLS options are set. | | `DOCKER_TLS_VERIFY` | When set Docker uses TLS and verifies the remote. This variable is used both by the `docker` CLI and the [`dockerd` daemon](https://docs.docker.com/reference/cli/dockerd/) | | `BUILDKIT_PROGRESS` | Set type of progress output (`auto`, `plain`, `tty`, `rawjson`) when [building](https://docs.docker.com/reference/cli/docker/image/build/) with [BuildKit backend](https://docs.docker.com/build/buildkit/). Use plain to show container output (default `auto`). | +| `NO_COLOR` | Disable any ANSI escape codes in the output in accordance with https://no-color.org/ + | Because Docker is developed using Go, you can also use any environment variables used by the Go runtime. In particular, you may find these useful: @@ -320,8 +319,8 @@ be set for each environment: These settings are used to configure proxy settings for containers only, and not used as proxy settings for the `docker` CLI or the `dockerd` daemon. Refer to the -[environment variables](#environment-variables) and [HTTP/HTTPS proxy](https://docs.docker.com/engine/daemon/proxy/#httphttps-proxy) -sections for configuring proxy settings for the CLI and daemon. +[environment variables](#environment-variables) section and the [Daemon proxy configuration](https://docs.docker.com/engine/daemon/proxy/) +guide for configuring proxy settings for the CLI and daemon. > [!WARNING] > Proxy settings may contain sensitive information (for example, if the proxy diff --git a/docs/reference/commandline/image_build.md b/docs/reference/commandline/image_build.md index bd9ae8ee3dff..db75ce398904 100644 --- a/docs/reference/commandline/image_build.md +++ b/docs/reference/commandline/image_build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | @@ -196,22 +195,22 @@ line in the `Engine` section: ```console Client: Docker Engine - Community - Version: 23.0.3 - API version: 1.42 - Go version: go1.19.7 - Git commit: 3e7cbfd - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: darwin/amd64 - Context: default + Version: 28.5.1 + API version: 1.51 + Go version: go1.24.8 + Git commit: e180ab8 + Built: Wed Oct 8 12:16:17 2025 + OS/Arch: darwin/arm64 + Context: desktop-linux Server: Docker Engine - Community Engine: - Version: 23.0.3 - API version: 1.42 (minimum version 1.12) - Go version: go1.19.7 - Git commit: 59118bf - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: linux/amd64 + Version: 28.5.1 + API version: 1.51 (minimum version 1.24) + Go version: go1.24.8 + Git commit: f8215cc + Built: Wed Oct 8 12:18:25 2025 + OS/Arch: linux/arm64 Experimental: true [...] ``` diff --git a/docs/reference/commandline/image_load.md b/docs/reference/commandline/image_load.md index bd104ed352f0..d2d11ef81d75 100644 --- a/docs/reference/commandline/image_load.md +++ b/docs/reference/commandline/image_load.md @@ -9,11 +9,11 @@ Load an image from a tar archive or STDIN ### Options -| Name | Type | Default | Description | -|:------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------| -| [`-i`](#input), [`--input`](#input) | `string` | | Read from tar archive file, instead of STDIN | -| [`--platform`](#platform) | `string` | | Load only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) | -| `-q`, `--quiet` | `bool` | | Suppress the load output | +| Name | Type | Default | Description | +|:------------------------------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------------------------------| +| [`-i`](#input), [`--input`](#input) | `string` | | Read from tar archive file, instead of STDIN | +| [`--platform`](#platform) | `stringSlice` | | Load only the given platform(s). Formatted as a comma-separated list of `os[/arch[/variant]]` (e.g., `linux/amd64,linux/arm64/v8`). | +| `-q`, `--quiet` | `bool` | | Suppress the load output | diff --git a/docs/reference/commandline/image_ls.md b/docs/reference/commandline/image_ls.md index 29174f171b46..925b6993bccd 100644 --- a/docs/reference/commandline/image_ls.md +++ b/docs/reference/commandline/image_ls.md @@ -342,6 +342,6 @@ To list all images in JSON format, use the `json` directive: ```console $ docker images --format json -{"Containers":"N/A","CreatedAt":"2021-03-04 03:24:42 +0100 CET","CreatedSince":"5 days ago","Digest":"\u003cnone\u003e","ID":"4dd97cefde62","Repository":"ubuntu","SharedSize":"N/A","Size":"72.9MB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"72.9MB"} -{"Containers":"N/A","CreatedAt":"2021-02-17 22:19:54 +0100 CET","CreatedSince":"2 weeks ago","Digest":"\u003cnone\u003e","ID":"28f6e2705743","Repository":"alpine","SharedSize":"N/A","Size":"5.61MB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"5.613MB"} +{"Containers":"N/A","CreatedAt":"2021-03-04 03:24:42 +0100 CET","CreatedSince":"5 days ago","Digest":"\u003cnone\u003e","ID":"4dd97cefde62","Repository":"ubuntu","SharedSize":"N/A","Size":"72.9MB","Tag":"latest","UniqueSize":"N/A"} +{"Containers":"N/A","CreatedAt":"2021-02-17 22:19:54 +0100 CET","CreatedSince":"2 weeks ago","Digest":"\u003cnone\u003e","ID":"28f6e2705743","Repository":"alpine","SharedSize":"N/A","Size":"5.61MB","Tag":"latest","UniqueSize":"N/A"} ``` diff --git a/docs/reference/commandline/image_pull.md b/docs/reference/commandline/image_pull.md index 3d390a294e49..aad73fe29ade 100644 --- a/docs/reference/commandline/image_pull.md +++ b/docs/reference/commandline/image_pull.md @@ -12,7 +12,6 @@ Download an image from a registry | Name | Type | Default | Description | |:---------------------------------------------|:---------|:--------|:-------------------------------------------------| | [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Download all tagged images in the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--platform` | `string` | | Set platform if server is multi-platform capable | | `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/image_push.md b/docs/reference/commandline/image_push.md index 91f6a41be1f0..543563398d7a 100644 --- a/docs/reference/commandline/image_push.md +++ b/docs/reference/commandline/image_push.md @@ -12,7 +12,6 @@ Upload an image to a registry | Name | Type | Default | Description | |:---------------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Push all tags of an image to the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image signing | | `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | | `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/image_save.md b/docs/reference/commandline/image_save.md index 0a01c45b93c6..25b18a58fee0 100644 --- a/docs/reference/commandline/image_save.md +++ b/docs/reference/commandline/image_save.md @@ -9,10 +9,10 @@ Save one or more images to a tar archive (streamed to STDOUT by default) ### Options -| Name | Type | Default | Description | -|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------| -| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | -| [`--platform`](#platform) | `string` | | Save only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) | +| Name | Type | Default | Description | +|:--------------------------|:--------------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------| +| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | +| [`--platform`](#platform) | `stringSlice` | | Save only the given platform(s). Formatted as a comma-separated list of `os[/arch[/variant]]` (e.g., `linux/amd64,linux/arm64/v8`) | diff --git a/docs/reference/commandline/load.md b/docs/reference/commandline/load.md index 41251b1b13a0..c1b94ca6785f 100644 --- a/docs/reference/commandline/load.md +++ b/docs/reference/commandline/load.md @@ -9,11 +9,11 @@ Load an image from a tar archive or STDIN ### Options -| Name | Type | Default | Description | -|:----------------|:---------|:--------|:-----------------------------------------------------------------------------------------------| -| `-i`, `--input` | `string` | | Read from tar archive file, instead of STDIN | -| `--platform` | `string` | | Load only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) | -| `-q`, `--quiet` | `bool` | | Suppress the load output | +| Name | Type | Default | Description | +|:----------------|:--------------|:--------|:------------------------------------------------------------------------------------------------------------------------------------| +| `-i`, `--input` | `string` | | Read from tar archive file, instead of STDIN | +| `--platform` | `stringSlice` | | Load only the given platform(s). Formatted as a comma-separated list of `os[/arch[/variant]]` (e.g., `linux/amd64,linux/arm64/v8`). | +| `-q`, `--quiet` | `bool` | | Suppress the load output | diff --git a/docs/reference/commandline/network_connect.md b/docs/reference/commandline/network_connect.md index 3a6514fab719..c3e373d5e867 100644 --- a/docs/reference/commandline/network_connect.md +++ b/docs/reference/commandline/network_connect.md @@ -10,10 +10,10 @@ Connect a container to a network | [`--alias`](#alias) | `stringSlice` | | Add network-scoped alias for the container | | `--driver-opt` | `stringSlice` | | driver options for the network | | `--gw-priority` | `int` | `0` | Highest gw-priority provides the default gateway. Accepts positive and negative values. | -| [`--ip`](#ip) | `string` | | IPv4 address (e.g., `172.30.100.104`) | -| `--ip6` | `string` | | IPv6 address (e.g., `2001:db8::33`) | +| [`--ip`](#ip) | `ip` | `` | IPv4 address (e.g., `172.30.100.104`) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., `2001:db8::33`) | | [`--link`](#link) | `list` | | Add link to another container | -| `--link-local-ip` | `stringSlice` | | Add a link-local address for the container | +| `--link-local-ip` | `ipSlice` | | Add a link-local address for the container | diff --git a/docs/reference/commandline/network_create.md b/docs/reference/commandline/network_create.md index 4c36adf9f487..ac493a844e10 100644 --- a/docs/reference/commandline/network_create.md +++ b/docs/reference/commandline/network_create.md @@ -12,10 +12,10 @@ Create a network | `--config-from` | `string` | | The network from which to copy the configuration | | `--config-only` | `bool` | | Create a configuration only network | | `-d`, `--driver` | `string` | `bridge` | Driver to manage the Network | -| `--gateway` | `stringSlice` | | IPv4 or IPv6 Gateway for the master subnet | +| `--gateway` | `ipSlice` | | IPv4 or IPv6 Gateway for the master subnet | | [`--ingress`](#ingress) | `bool` | | Create swarm routing-mesh network | | [`--internal`](#internal) | `bool` | | Restrict external access to the network | -| `--ip-range` | `stringSlice` | | Allocate container ip from a sub-range | +| `--ip-range` | `ipNetSlice` | | Allocate container ip from a sub-range | | `--ipam-driver` | `string` | `default` | IP Address Management Driver | | `--ipam-opt` | `map` | `map[]` | Set IPAM driver specific options | | `--ipv4` | `bool` | `true` | Enable or disable IPv4 address assignment | diff --git a/docs/reference/commandline/plugin_install.md b/docs/reference/commandline/plugin_install.md index 25ceb35c217e..38b619b51b23 100644 --- a/docs/reference/commandline/plugin_install.md +++ b/docs/reference/commandline/plugin_install.md @@ -9,7 +9,6 @@ Install a plugin |:--------------------------|:---------|:--------|:--------------------------------------------------| | `--alias` | `string` | | Local name for plugin | | `--disable` | `bool` | | Do not enable the plugin on install | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--grant-all-permissions` | `bool` | | Grant all permissions necessary to run the plugin | diff --git a/docs/reference/commandline/plugin_push.md b/docs/reference/commandline/plugin_push.md index 3fcfe47f6f2c..155d2d44d23c 100644 --- a/docs/reference/commandline/plugin_push.md +++ b/docs/reference/commandline/plugin_push.md @@ -3,12 +3,6 @@ Push a plugin to a registry -### Options - -| Name | Type | Default | Description | -|:--------------------------|:-------|:--------|:-------------------| -| `--disable-content-trust` | `bool` | `true` | Skip image signing | - diff --git a/docs/reference/commandline/plugin_upgrade.md b/docs/reference/commandline/plugin_upgrade.md index 39730104d4b7..df4094875b05 100644 --- a/docs/reference/commandline/plugin_upgrade.md +++ b/docs/reference/commandline/plugin_upgrade.md @@ -7,7 +7,6 @@ Upgrade an existing plugin | Name | Type | Default | Description | |:--------------------------|:-------|:--------|:----------------------------------------------------------------------| -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--grant-all-permissions` | `bool` | | Grant all permissions necessary to run the plugin | | `--skip-remote-check` | `bool` | | Do not check if specified remote plugin matches existing plugin image | diff --git a/docs/reference/commandline/pull.md b/docs/reference/commandline/pull.md index 66acb611da1b..3cc2b0f94f93 100644 --- a/docs/reference/commandline/pull.md +++ b/docs/reference/commandline/pull.md @@ -9,12 +9,11 @@ Download an image from a registry ### Options -| Name | Type | Default | Description | -|:--------------------------|:---------|:--------|:-------------------------------------------------| -| `-a`, `--all-tags` | `bool` | | Download all tagged images in the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | -| `--platform` | `string` | | Set platform if server is multi-platform capable | -| `-q`, `--quiet` | `bool` | | Suppress verbose output | +| Name | Type | Default | Description | +|:-------------------|:---------|:--------|:-------------------------------------------------| +| `-a`, `--all-tags` | `bool` | | Download all tagged images in the repository | +| `--platform` | `string` | | Set platform if server is multi-platform capable | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/push.md b/docs/reference/commandline/push.md index 9558d38e5ebc..61f48b22abfd 100644 --- a/docs/reference/commandline/push.md +++ b/docs/reference/commandline/push.md @@ -9,12 +9,11 @@ Upload an image to a registry ### Options -| Name | Type | Default | Description | -|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository | -| `--disable-content-trust` | `bool` | `true` | Skip image signing | -| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | -| `-q`, `--quiet` | `bool` | | Suppress verbose output | +| Name | Type | Default | Description | +|:-------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository | +| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) | +| `-q`, `--quiet` | `bool` | | Suppress verbose output | diff --git a/docs/reference/commandline/run.md b/docs/reference/commandline/run.md index 0e9a90ef3efd..d544a9cdf5fe 100644 --- a/docs/reference/commandline/run.md +++ b/docs/reference/commandline/run.md @@ -39,7 +39,6 @@ Create and run a new container from an image | `--device-read-iops` | `list` | | Limit read rate (IO per second) from a device | | `--device-write-bps` | `list` | | Limit write rate (bytes per second) to a device | | `--device-write-iops` | `list` | | Limit write rate (IO per second) to a device | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | `--dns` | `list` | | Set custom DNS servers | | `--dns-option` | `list` | | Set DNS options | | `--dns-search` | `list` | | Set custom DNS search domains | @@ -62,11 +61,10 @@ Create and run a new container from an image | `-i`, `--interactive` | `bool` | | Keep STDIN open even if not attached | | `--io-maxbandwidth` | `bytes` | `0` | Maximum IO bandwidth limit for the system drive (Windows only) | | `--io-maxiops` | `uint64` | `0` | Maximum IOps limit for the system drive (Windows only) | -| `--ip` | `string` | | IPv4 address (e.g., 172.30.100.104) | -| `--ip6` | `string` | | IPv6 address (e.g., 2001:db8::33) | +| `--ip` | `ip` | `` | IPv4 address (e.g., 172.30.100.104) | +| `--ip6` | `ip` | `` | IPv6 address (e.g., 2001:db8::33) | | `--ipc` | `string` | | IPC mode to use | | `--isolation` | `string` | | Container isolation technology | -| `--kernel-memory` | `bytes` | `0` | Kernel memory limit | | `-l`, `--label` | `list` | | Set meta data on a container | | `--label-file` | `list` | | Read in a line delimited file of labels | | `--link` | `list` | | Add link to another container | diff --git a/docs/reference/commandline/save.md b/docs/reference/commandline/save.md index 19e8c353dac0..8a2d75e75140 100644 --- a/docs/reference/commandline/save.md +++ b/docs/reference/commandline/save.md @@ -9,10 +9,10 @@ Save one or more images to a tar archive (streamed to STDOUT by default) ### Options -| Name | Type | Default | Description | -|:-----------------|:---------|:--------|:-----------------------------------------------------------------------------------------------| -| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | -| `--platform` | `string` | | Save only the given platform variant. Formatted as `os[/arch[/variant]]` (e.g., `linux/amd64`) | +| Name | Type | Default | Description | +|:-----------------|:--------------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------| +| `-o`, `--output` | `string` | | Write to a file, instead of STDOUT | +| `--platform` | `stringSlice` | | Save only the given platform(s). Formatted as a comma-separated list of `os[/arch[/variant]]` (e.g., `linux/amd64,linux/arm64/v8`) | diff --git a/docs/reference/commandline/service_create.md b/docs/reference/commandline/service_create.md index d059f141831c..fb550533c445 100644 --- a/docs/reference/commandline/service_create.md +++ b/docs/reference/commandline/service_create.md @@ -40,6 +40,8 @@ Create a new service | `--log-driver` | `string` | | Logging driver for service | | `--log-opt` | `list` | | Logging driver options | | `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) | +| `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) | +| `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default | | `--mode` | `string` | `replicated` | Service mode (`replicated`, `global`, `replicated-job`, `global-job`) | | [`--mount`](#mount) | `mount` | | Attach a filesystem mount to the service | | `--name` | `string` | | Service name | @@ -455,20 +457,6 @@ The following options can only be used for bind mounts (`type=bind`): When the option is not specified, the default behavior correponds to setting enabled. - - bind-nonrecursive - - bind-nonrecursive is deprecated since Docker Engine v25.0. - Use bind-recursiveinstead.
-
- A value is optional:
-
-
    -
  • true or 1: Equivalent to bind-recursive=disabled.
  • -
  • false or 0: Equivalent to bind-recursive=enabled.
  • -
- - ##### Bind propagation diff --git a/docs/reference/commandline/service_update.md b/docs/reference/commandline/service_update.md index f3564bcc1f24..64f81b07e80f 100644 --- a/docs/reference/commandline/service_update.md +++ b/docs/reference/commandline/service_update.md @@ -53,6 +53,8 @@ Update a service | `--log-driver` | `string` | | Logging driver for service | | `--log-opt` | `list` | | Logging driver options | | `--max-concurrent` | `uint` | | Number of job tasks to run concurrently (default equal to --replicas) | +| `--memory-swap` | `bytes` | `0` | Swap Bytes (-1 for unlimited) | +| `--memory-swappiness` | `int64` | `-1` | Tune memory swappiness (0-100), -1 to reset to default | | [`--mount-add`](#mount-add) | `mount` | | Add or update a mount on a service | | `--mount-rm` | `list` | | Remove a mount by its target path | | [`--network-add`](#network-add) | `network` | | Add a network | diff --git a/docs/reference/commandline/system_prune.md b/docs/reference/commandline/system_prune.md index 0d39d50feb99..39a58c54996c 100644 --- a/docs/reference/commandline/system_prune.md +++ b/docs/reference/commandline/system_prune.md @@ -72,7 +72,8 @@ my-network-a my-network-b Deleted Volumes: -named-vol +1e31bcd425e913d9f65ec0c3841e9c4ebb543aead2a1cfe0d95a7c5e88bb5026 +6a6ab3d6b8d740a1c1d4dbe36a9c5f043dd4bac5f78abfa7d1f2ae5789fe60b0 Deleted Images: untagged: my-curl:latest diff --git a/docs/reference/commandline/version.md b/docs/reference/commandline/version.md index 040ba1b5a436..1a19f88f1122 100644 --- a/docs/reference/commandline/version.md +++ b/docs/reference/commandline/version.md @@ -38,29 +38,29 @@ machine running Docker Desktop: $ docker version Client: Docker Engine - Community - Version: 23.0.3 - API version: 1.42 - Go version: go1.19.7 - Git commit: 3e7cbfd - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: darwin/amd64 - Context: default + Version: 28.5.1 + API version: 1.51 + Go version: go1.24.8 + Git commit: e180ab8 + Built: Wed Oct 8 12:16:17 2025 + OS/Arch: darwin/arm64 + Context: remote-test-server Server: Docker Desktop 4.19.0 (12345) Engine: - Version: 23.0.3 - API version: 1.42 (minimum version 1.12) - Go version: go1.19.7 - Git commit: 59118bf - Built: Tue Apr 4 22:05:41 2023 + Version: 27.5.1 + API version: 1.47 (minimum version 1.24) + Go version: go1.22.11 + Git commit: 4c9b3b0 + Built: Wed Jan 22 13:41:24 2025 OS/Arch: linux/amd64 - Experimental: false + Experimental: true containerd: - Version: 1.6.20 - GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38 + Version: v1.7.25 + GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb runc: - Version: 1.1.5 - GitCommit: v1.1.5-0-gf19387a + Version: 1.2.4 + GitCommit: v1.2.4-0-g6c52b3f docker-init: Version: 0.19.0 GitCommit: de40ad0 @@ -83,31 +83,32 @@ remote-test-server $ docker version Client: Docker Engine - Community - Version: 23.0.3 - API version: 1.40 (downgraded from 1.42) - Go version: go1.19.7 - Git commit: 3e7cbfd - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: darwin/amd64 + Version: 28.5.1 + API version: 1.51 + Go version: go1.24.8 + Git commit: e180ab8 + Built: Wed Oct 8 12:16:17 2025 + OS/Arch: darwin/arm64 Context: remote-test-server Server: Docker Engine - Community Engine: - Version: 19.03.8 - API version: 1.40 (minimum version 1.12) - Go version: go1.12.17 - Git commit: afacb8b - Built: Wed Mar 11 01:29:16 2020 + Version: 27.5.1 + API version: 1.47 (minimum version 1.24) + Go version: go1.22.11 + Git commit: 4c9b3b0 + Built: Wed Jan 22 13:41:24 2025 OS/Arch: linux/amd64 + Experimental: true containerd: - Version: v1.2.13 - GitCommit: 7ad184331fa3e55e52b890ea95e65ba581ae3429 + Version: v1.7.25 + GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb runc: - Version: 1.0.0-rc10 - GitCommit: dc9208a3303feef5b3839f4323d9beb36df0a9dd + Version: 1.2.4 + GitCommit: v1.2.4-0-g6c52b3f docker-init: - Version: 0.18.0 - GitCommit: fec3683 + Version: 0.19.0 + GitCommit: de40ad0 ``` ### API version and version negotiation @@ -117,14 +118,14 @@ CLI is connecting with. When connecting with the Docker Engine, the Docker CLI and Docker Engine perform API version negotiation, and select the highest API version that is supported by both the Docker CLI and the Docker Engine. -For example, if the CLI is connecting with Docker Engine version 19.03, it downgrades -to API version 1.40 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix) +For example, if the CLI is connecting with Docker Engine version 27.5, it downgrades +to API version 1.47 (refer to the [API version matrix](https://docs.docker.com/reference/api/engine/#api-version-matrix) to learn about the supported API versions for Docker Engine): ```console $ docker version --format '{{.Client.APIVersion}}' -1.40 +1.47 ``` Be aware that API version can also be overridden using the `DOCKER_API_VERSION` @@ -135,14 +136,14 @@ variable removes the override, and re-enables API version negotiation: ```console $ env | grep DOCKER_API_VERSION -DOCKER_API_VERSION=1.39 +DOCKER_API_VERSION=1.50 $ docker version --format '{{.Client.APIVersion}}' -1.39 +1.50 $ unset DOCKER_API_VERSION $ docker version --format '{{.Client.APIVersion}}' -1.42 +1.51 ``` ## Examples @@ -159,7 +160,7 @@ page for details of the format. ```console $ docker version --format '{{.Server.Version}}' -23.0.3 +28.5.1 ``` ### Get the client API version @@ -169,7 +170,7 @@ The following example prints the API version that is used by the client: ```console $ docker version --format '{{.Client.APIVersion}}' -1.42 +1.51 ``` The version shown is the API version that is negotiated between the client @@ -181,5 +182,5 @@ above for more information. ```console $ docker version --format '{{json .}}' -{"Client":"Version":"23.0.3","ApiVersion":"1.42", ...} +{"Client":"Version":"28.5.1","ApiVersion":"1.51", ...} ``` diff --git a/docs/reference/dockerd.md b/docs/reference/dockerd.md index 1e2b01633c5a..f2ed1911040b 100644 --- a/docs/reference/dockerd.md +++ b/docs/reference/dockerd.md @@ -29,6 +29,7 @@ Options: --bip string IPv4 address for the default bridge --bip6 string IPv6 address for the default bridge -b, --bridge string Attach containers to a network bridge + --bridge-accept-fwmark string In bridge networks, accept packets with this firewall mark/mask --cdi-spec-dir list CDI specification directories to use --cgroup-parent string Set parent cgroup for all containers --config-file string Daemon configuration file (default "/etc/docker/daemon.json") @@ -58,6 +59,7 @@ Options: --exec-root string Root directory for execution state files (default "/var/run/docker") --experimental Enable experimental features --feature map Enable feature in the daemon + --firewall-backend string Firewall backend to use, iptables or nftables --fixed-cidr string IPv4 subnet for the default bridge network --fixed-cidr-v6 string IPv6 subnet for the default bridge network -G, --group string Group for the unix socket (default "docker") @@ -1070,14 +1072,15 @@ The following is a full example of the allowed configuration options on Linux: "bip": "", "bip6": "", "bridge": "", + "bridge-accept-fwmark": "", "builder": { "gc": { "enabled": true, - "defaultKeepStorage": "10GB", + "defaultReservedSpace": "10GB", "policy": [ - { "keepStorage": "10GB", "filter": ["unused-for=2200h"] }, - { "keepStorage": "50GB", "filter": ["unused-for=3300h"] }, - { "keepStorage": "100GB", "all": true } + { "maxUsedSpace": "512MB", "keepDuration": "48h", "filter": [ "type=source.local" ] }, + { "reservedSpace": "10GB", "maxUsedSpace": "100GB", "keepDuration": "1440h" }, + { "reservedSpace": "50GB", "minFreeSpace": "20GB", "maxUsedSpace": "200GB", "all": true } ] } }, @@ -1120,6 +1123,7 @@ The following is a full example of the allowed configuration options on Linux: "cdi": true, "containerd-snapshotter": true }, + "firewall-backend": "", "fixed-cidr": "", "fixed-cidr-v6": "", "group": "", diff --git a/docs/reference/run.md b/docs/reference/run.md index db06ad71f669..9c8b8baf0f25 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -248,7 +248,7 @@ $ docker run -it --mount type=bind,source=[PATH],target=[PATH] busybox ``` In this case, the `--mount` flag takes three parameters. A type (`bind`), and -two paths. The `source` path is a the location on the host that you want to +two paths. The `source` path is the location on the host that you want to bind mount into the container. The `target` path is the mount destination inside the container. @@ -419,7 +419,7 @@ $ docker run -it -m 300M ubuntu:24.04 /bin/bash We set memory limit only, this means the processes in the container can use 300M memory and 300M swap memory, by default, the total virtual memory size -(--memory-swap) will be set as double of memory, in this case, memory + swap +(`--memory-swap`) will be set as double of memory, in this case, memory + swap would be 2*300M, so processes can use 300M swap memory as well. ```console @@ -1087,7 +1087,7 @@ Additionally, you can set any environment variable in the container by using one or more `-e` flags. You can even override the variables mentioned above, or variables defined using a Dockerfile `ENV` instruction when building the image. -If the you name an environment variable without specifying a value, the current +If you name an environment variable without specifying a value, the current value of the named variable on the host is propagated into the container's environment: diff --git a/e2e/cli-plugins/flags_test.go b/e2e/cli-plugins/flags_test.go index 69fe6d6ad3b4..0cb24ba2488e 100644 --- a/e2e/cli-plugins/flags_test.go +++ b/e2e/cli-plugins/flags_test.go @@ -1,6 +1,7 @@ package cliplugins import ( + "fmt" "os" "testing" @@ -91,9 +92,9 @@ func TestGlobalArgsOnlyParsedOnce(t *testing.T) { // This is checking the precondition wrt -H mentioned in the function comment name: "fails-if-H-used-twice", args: []string{"-H", dh, "-H", dh, "version", "-f", "{{.Client.Version}}"}, - expectedExitCode: 1, + expectedExitCode: 125, expectedOut: icmd.None, - expectedErr: "Specify only one -H", + expectedErr: fmt.Sprintf(`invalid argument %q for "-H, --host" flag: specify only one -H`, dh), }, { name: "builtin", diff --git a/e2e/cli-plugins/plugins/nopersistentprerun/main.go b/e2e/cli-plugins/plugins/nopersistentprerun/main.go index 35163d7a72d0..c6319ffa8a4b 100644 --- a/e2e/cli-plugins/plugins/nopersistentprerun/main.go +++ b/e2e/cli-plugins/plugins/nopersistentprerun/main.go @@ -6,6 +6,7 @@ import ( "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" + "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -17,7 +18,7 @@ func main() { // PersistentPreRunE: Not specified, we need to test that it works in the absence of an explicit call RunE: func(cmd *cobra.Command, args []string) error { cli := dockerCli.Client() - ping, err := cli.Ping(cmd.Context()) + ping, err := cli.Ping(cmd.Context(), client.PingOptions{}) if err != nil { return err } diff --git a/e2e/compose-env.yaml b/e2e/compose-env.yaml index df810df6cc9e..1d25623c2ded 100644 --- a/e2e/compose-env.yaml +++ b/e2e/compose-env.yaml @@ -1,7 +1,7 @@ services: registry: - image: 'registry:2' + image: 'registry:3' engine: image: 'docker:${ENGINE_VERSION:-28}-dind' @@ -9,19 +9,3 @@ services: command: ['--insecure-registry=registry:5000', '--experimental'] environment: - DOCKER_TLS_CERTDIR= - - notary-server: - build: - context: ./testdata - dockerfile: Dockerfile.notary-server - ports: - - 4443:4443 - command: ['notary-server', '-config=/fixtures/notary-config.json'] - - evil-notary-server: - build: - context: ./testdata - dockerfile: Dockerfile.evil-notary-server - ports: - - 4444:4443 - command: ['notary-server', '-config=/fixtures/notary-config.json'] diff --git a/e2e/container/create_test.go b/e2e/container/create_test.go index daf8e092f601..7a4bf67ae771 100644 --- a/e2e/container/create_test.go +++ b/e2e/container/create_test.go @@ -1,107 +1,12 @@ package container import ( - "fmt" "testing" "github.com/docker/cli/e2e/internal/fixtures" - "github.com/docker/cli/internal/test/environment" "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" ) -func TestCreateWithContentTrust(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-create", "latest") - - defer func() { - icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success) - }() - - result := icmd.RunCmd( - icmd.Command("docker", "create", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Expected{ - Err: fmt.Sprintf("Tagging %s@sha", image[:len(image)-7]), - }) -} - -func TestTrustedCreateFromUnreachableTrustServer(t *testing.T) { - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-create", "latest") - - result := icmd.RunCmd( - icmd.Command("docker", "create", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer("https://notary.invalid"), - ) - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "error contacting notary server", - }) -} - -func TestTrustedCreateFromBadTrustServer(t *testing.T) { - evilImageName := "registry:5000/evil-alpine:latest" - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - - // tag the image and upload it to the private registry - icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName), - fixtures.WithConfig(dir.Path()), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithTrust, - fixtures.WithNotary, - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // try create - icmd.RunCmd(icmd.Command("docker", "create", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // init a client with the evil-server and a new trust dir - evilNotaryDir := fixtures.SetupConfigWithNotaryURL(t, "evil-test", fixtures.EvilNotaryURL) - defer evilNotaryDir.Remove() - - // tag the same image and upload it to the private registry but signed with evil notary server - icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName), - fixtures.WithConfig(evilNotaryDir.Path()), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName), - fixtures.WithConfig(evilNotaryDir.Path()), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithTrust, - fixtures.WithNotaryServer(fixtures.EvilNotaryURL), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // try creating with the original client from the evil notary server. This should failed - // because the new root is invalid - icmd.RunCmd(icmd.Command("docker", "create", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer(fixtures.EvilNotaryURL), - ).Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "could not rotate trust to a new trusted root", - }) -} - func TestCreateWithEmptySourceVolume(t *testing.T) { icmd.RunCmd(icmd.Command("docker", "create", "-v", ":/volume", fixtures.AlpineImage)). Assert(t, icmd.Expected{ diff --git a/e2e/container/rename_test.go b/e2e/container/rename_test.go new file mode 100644 index 000000000000..c1879bd1150a --- /dev/null +++ b/e2e/container/rename_test.go @@ -0,0 +1,52 @@ +package container + +import ( + "strings" + "testing" + + "github.com/docker/cli/e2e/internal/fixtures" + "gotest.tools/v3/assert" + "gotest.tools/v3/icmd" +) + +func TestContainerRename(t *testing.T) { + oldName := "old_name_" + t.Name() + res := icmd.RunCommand("docker", "run", "-d", "--name", oldName, fixtures.AlpineImage, "sleep", "60") + res.Assert(t, icmd.Success) + cID := strings.TrimSpace(res.Stdout()) + t.Cleanup(func() { + icmd.RunCommand("docker", "container", "rm", "-f", cID).Assert(t, icmd.Success) + }) + + newName := "new_name_" + t.Name() + res = icmd.RunCommand("docker", "container", "rename", oldName, newName) + res.Assert(t, icmd.Success) + + res = icmd.RunCommand("docker", "container", "inspect", "--format", "{{.Name}}", cID) + res.Assert(t, icmd.Success) + assert.Equal(t, "/"+newName, strings.TrimSpace(res.Stdout())) +} + +func TestContainerRenameEmptyOldName(t *testing.T) { + res := icmd.RunCommand("docker", "container", "rename", "", "newName") + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "invalid container name or ID: value is empty", + }) +} + +func TestContainerRenameEmptyNewName(t *testing.T) { + oldName := "old_name_" + t.Name() + res := icmd.RunCommand("docker", "run", "-d", "--name", oldName, fixtures.AlpineImage, "sleep", "60") + res.Assert(t, icmd.Success) + cID := strings.TrimSpace(res.Stdout()) + t.Cleanup(func() { + icmd.RunCommand("docker", "container", "rm", "-f", cID).Assert(t, icmd.Success) + }) + + res = icmd.RunCommand("docker", "container", "rename", oldName, "") + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "new name cannot be blank", + }) +} diff --git a/e2e/container/run_test.go b/e2e/container/run_test.go index d279f2521bf2..d52230e23899 100644 --- a/e2e/container/run_test.go +++ b/e2e/container/run_test.go @@ -2,7 +2,6 @@ package container import ( "bytes" - "fmt" "io" "math/rand" "os/exec" @@ -14,7 +13,7 @@ import ( "github.com/creack/pty" "github.com/docker/cli/e2e/internal/fixtures" "github.com/docker/cli/internal/test/environment" - "github.com/docker/docker/api/types/versions" + "github.com/moby/moby/client/pkg/versions" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/golden" @@ -90,104 +89,6 @@ func TestRunInvalidEntrypointWithAutoremove(t *testing.T) { } } -func TestRunWithContentTrust(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-run", "latest") - - defer func() { - icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success) - }() - - result := icmd.RunCmd( - icmd.Command("docker", "run", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Expected{ - Err: fmt.Sprintf("Tagging %s@sha", image[:len(image)-7]), - }) -} - -func TestUntrustedRun(t *testing.T) { - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := registryPrefix + "/alpine:untrusted" - // tag the image and upload it to the private registry - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) - defer func() { - icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success) - }() - - // try trusted run on untrusted tag - result := icmd.RunCmd( - icmd.Command("docker", "run", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Expected{ - ExitCode: 125, - Err: "does not have trust data for", - }) -} - -func TestTrustedRunFromBadTrustServer(t *testing.T) { - evilImageName := registryPrefix + "/evil-alpine:latest" - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - - // tag the image and upload it to the private registry - icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName), - fixtures.WithConfig(dir.Path()), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithTrust, - fixtures.WithNotary, - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // try run - icmd.RunCmd(icmd.Command("docker", "run", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // init a client with the evil-server and a new trust dir - evilNotaryDir := fixtures.SetupConfigWithNotaryURL(t, "evil-test", fixtures.EvilNotaryURL) - defer evilNotaryDir.Remove() - - // tag the same image and upload it to the private registry but signed with evil notary server - icmd.RunCmd(icmd.Command("docker", "tag", fixtures.AlpineImage, evilImageName), - fixtures.WithConfig(evilNotaryDir.Path()), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "push", evilImageName), - fixtures.WithConfig(evilNotaryDir.Path()), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithTrust, - fixtures.WithNotaryServer(fixtures.EvilNotaryURL), - ).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "image", "rm", evilImageName)).Assert(t, icmd.Success) - - // try running with the original client from the evil notary server. This should failed - // because the new root is invalid - icmd.RunCmd(icmd.Command("docker", "run", evilImageName), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer(fixtures.EvilNotaryURL), - ).Assert(t, icmd.Expected{ - ExitCode: 125, - Err: "could not rotate trust to a new trusted root", - }) -} - // TODO: create this with registry API instead of engine API func createRemoteImage(t *testing.T) string { t.Helper() @@ -210,7 +111,6 @@ func TestRunWithCgroupNamespace(t *testing.T) { func TestMountSubvolume(t *testing.T) { skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.45")) - volName := "test-volume-" + t.Name() icmd.RunCommand("docker", "volume", "create", volName).Assert(t, icmd.Success) diff --git a/e2e/global/cli_test.go b/e2e/global/cli_test.go index 6d8f0d2dfed9..aea0ce841557 100644 --- a/e2e/global/cli_test.go +++ b/e2e/global/cli_test.go @@ -16,7 +16,6 @@ import ( "github.com/docker/cli/e2e/testutils" "github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test/environment" - "github.com/docker/docker/api/types/versions" "gotest.tools/v3/assert" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" @@ -90,7 +89,6 @@ func TestPromptExitCode(t *testing.T) { defaultCmdOpts := []icmd.CmdOp{ fixtures.WithConfig(dir.Path()), - fixtures.WithNotary, } testCases := []struct { @@ -132,27 +130,20 @@ func TestPromptExitCode(t *testing.T) { return icmd.Command("docker", "system", "prune") }, }, - { - name: "revoke trust", - run: func(t *testing.T) icmd.Cmd { - t.Helper() - return icmd.Command("docker", "trust", "revoke", "example/trust-demo") - }, - }, { name: "plugin install", run: func(t *testing.T) icmd.Cmd { t.Helper() - skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44")) + t.Skip("flaky test: see https://github.com/docker/cli/issues/6248") - pluginDir := testutils.SetupPlugin(t, ctx) - t.Cleanup(pluginDir.Remove) - - plugin := "registry:5000/plugin-content-trust-install:latest" + const plugin = "registry:5000/plugin-install-test:latest" - icmd.RunCommand("docker", "plugin", "create", plugin, pluginDir.Path()).Assert(t, icmd.Success) + pluginDir := testutils.SetupPlugin(t, ctx) + icmd.RunCommand("docker", "plugin", "create", plugin, pluginDir).Assert(t, icmd.Success) icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin), defaultCmdOpts...).Assert(t, icmd.Success) icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin), defaultCmdOpts...).Assert(t, icmd.Success) + + // Test prompt to grant privileges. return icmd.Command("docker", "plugin", "install", plugin) }, }, @@ -160,22 +151,24 @@ func TestPromptExitCode(t *testing.T) { name: "plugin upgrade", run: func(t *testing.T) icmd.Cmd { t.Helper() - skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44")) + t.Skip("flaky test: see https://github.com/docker/cli/issues/6248") - pluginLatestDir := testutils.SetupPlugin(t, ctx) - t.Cleanup(pluginLatestDir.Remove) - pluginNextDir := testutils.SetupPlugin(t, ctx) - t.Cleanup(pluginNextDir.Remove) - - plugin := "registry:5000/plugin-content-trust-upgrade" + const plugin = "registry:5000/plugin-upgrade-test" - icmd.RunCommand("docker", "plugin", "create", plugin+":latest", pluginLatestDir.Path()).Assert(t, icmd.Success) - icmd.RunCommand("docker", "plugin", "create", plugin+":next", pluginNextDir.Path()).Assert(t, icmd.Success) + pluginLatestDir := testutils.SetupPlugin(t, ctx) + icmd.RunCommand("docker", "plugin", "create", plugin+":latest", pluginLatestDir).Assert(t, icmd.Success) icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success) icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success) + + pluginNextDir := testutils.SetupPlugin(t, ctx) + icmd.RunCommand("docker", "plugin", "create", plugin+":next", pluginNextDir).Assert(t, icmd.Success) + icmd.RunCmd(icmd.Command("docker", "plugin", "push", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success) icmd.RunCmd(icmd.Command("docker", "plugin", "rm", "-f", plugin+":next"), defaultCmdOpts...).Assert(t, icmd.Success) + + // Using "--grant-all-permissions" to disable prompting for confirmation. icmd.RunCmd(icmd.Command("docker", "plugin", "install", "--disable", "--grant-all-permissions", plugin+":latest"), defaultCmdOpts...).Assert(t, icmd.Success) + + // Test prompting for upgrade. return icmd.Command("docker", "plugin", "upgrade", plugin+":latest", plugin+":next") }, }, diff --git a/e2e/image/build_test.go b/e2e/image/build_test.go index fadd13922406..a0af53eda09a 100644 --- a/e2e/image/build_test.go +++ b/e2e/image/build_test.go @@ -14,7 +14,6 @@ import ( is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" ) func TestBuildFromContextDirectoryWithTag(t *testing.T) { @@ -62,69 +61,8 @@ func TestBuildFromContextDirectoryWithTag(t *testing.T) { }) } -func TestTrustedBuild(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - t.Setenv("DOCKER_BUILDKIT", "0") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image1 := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-build1", "latest") - image2 := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-build2", "latest") - - buildDir := fs.NewDir(t, "test-trusted-build-context-dir", - fs.WithFile("Dockerfile", fmt.Sprintf(` - FROM %s as build-base - RUN echo ok > /foo - FROM %s - COPY --from=build-base foo bar - `, image1, image2))) - defer buildDir.Remove() - - result := icmd.RunCmd( - icmd.Command("docker", "build", "-t", "myimage", "."), - withWorkingDir(buildDir), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - - result.Assert(t, icmd.Expected{ - Out: fmt.Sprintf("FROM %s@sha", image1[:len(image1)-7]), - Err: fmt.Sprintf("Tagging %s@sha", image1[:len(image1)-7]), - }) - result.Assert(t, icmd.Expected{ - Out: fmt.Sprintf("FROM %s@sha", image2[:len(image2)-7]), - }) -} - -func TestTrustedBuildUntrustedImage(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - t.Setenv("DOCKER_BUILDKIT", "0") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - buildDir := fs.NewDir(t, "test-trusted-build-context-dir", - fs.WithFile("Dockerfile", fmt.Sprintf(` - FROM %s - RUN [] - `, fixtures.AlpineImage))) - defer buildDir.Remove() - - result := icmd.RunCmd( - icmd.Command("docker", "build", "-t", "myimage", "."), - withWorkingDir(buildDir), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "does not have trust data for", - }) -} - func TestBuildIidFileSquash(t *testing.T) { + t.Skip("Not implemented with containerd") environment.SkipIfNotExperimentalDaemon(t) t.Setenv("DOCKER_BUILDKIT", "0") diff --git a/e2e/image/pull_test.go b/e2e/image/pull_test.go index c739e7e5e410..383257bd609c 100644 --- a/e2e/image/pull_test.go +++ b/e2e/image/pull_test.go @@ -4,81 +4,16 @@ import ( "testing" "github.com/docker/cli/e2e/internal/fixtures" - "github.com/docker/cli/internal/test/environment" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/golden" "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" ) const registryPrefix = "registry:5000" -func TestPullWithContentTrust(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - // Digests in golden files are linux/amd64 specific. - // TODO: Fix this test and make it work on all platforms. - environment.SkipIfNotPlatform(t, "linux/amd64") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-pull", "latest") - defer func() { - icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success) - }() - - result := icmd.RunCmd(icmd.Command("docker", "pull", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Success) - golden.Assert(t, result.Stderr(), "pull-with-content-trust-err.golden") - golden.Assert(t, result.Stdout(), "pull-with-content-trust.golden") -} - func TestPullQuiet(t *testing.T) { result := icmd.RunCommand("docker", "pull", "--quiet", fixtures.AlpineImage) result.Assert(t, icmd.Success) - assert.Check(t, is.Equal(result.Stdout(), "registry:5000/alpine:frozen\n")) + assert.Check(t, is.Equal(result.Stdout(), registryPrefix+"/alpine:frozen\n")) assert.Check(t, is.Equal(result.Stderr(), "")) } - -func TestPullWithContentTrustUsesCacheWhenNotaryUnavailable(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-pull-unreachable", "latest") - defer func() { - icmd.RunCommand("docker", "image", "rm", image).Assert(t, icmd.Success) - }() - result := icmd.RunCmd(icmd.Command("docker", "pull", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer("https://invalidnotaryserver"), - ) - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "error contacting notary server", - }) - - // Do valid trusted pull to warm cache - result = icmd.RunCmd(icmd.Command("docker", "pull", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Success) - result = icmd.RunCommand("docker", "rmi", image) - result.Assert(t, icmd.Success) - - // Try pull again with invalid notary server, should use cache - result = icmd.RunCmd(icmd.Command("docker", "pull", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer("https://invalidnotaryserver"), - ) - result.Assert(t, icmd.Success) -} diff --git a/e2e/image/push_test.go b/e2e/image/push_test.go index 3ee86c535189..55f3d033092c 100644 --- a/e2e/image/push_test.go +++ b/e2e/image/push_test.go @@ -1,437 +1,18 @@ package image import ( - "fmt" - "os" - "strings" "testing" - "github.com/docker/cli/e2e/internal/fixtures" - "github.com/docker/cli/internal/test/environment" - "github.com/docker/cli/internal/test/output" "gotest.tools/v3/assert" - "gotest.tools/v3/fs" - "gotest.tools/v3/golden" + is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" ) -const ( - notary = "/usr/local/bin/notary" - - pubkey1 = "./testdata/notary/delgkey1.crt" - privkey1 = "./testdata/notary/delgkey1.key" - pubkey2 = "./testdata/notary/delgkey2.crt" - privkey2 = "./testdata/notary/delgkey2.key" - pubkey3 = "./testdata/notary/delgkey3.crt" - privkey3 = "./testdata/notary/delgkey3.key" - pubkey4 = "./testdata/notary/delgkey4.crt" - privkey4 = "./testdata/notary/delgkey4.key" -) - -func TestPushAllTags(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - // Compared digests are linux/amd64 specific. - // TODO: Fix this test and make it work on all platforms. - environment.SkipIfNotPlatform(t, "linux/amd64") - - _ = createImage(t, "push-all-tags", "latest", "v1", "v1.0", "v1.0.1") - result := icmd.RunCmd(icmd.Command("docker", "push", "--all-tags", registryPrefix+"/push-all-tags")) - - result.Assert(t, icmd.Success) - golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden") - output.Assert(t, result.Stdout(), map[int]func(string) error{ - 0: output.Equals("The push refers to repository [registry:5000/push-all-tags]"), - 1: output.Equals("7cd52847ad77: Preparing"), - 3: output.Equals("latest: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"), - 6: output.Equals("v1: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"), - 9: output.Equals("v1.0: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"), - 12: output.Equals("v1.0.1: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"), - }) -} - -func TestPushWithContentTrust(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - // Compared digests are linux/amd64 specific. - // TODO: Fix this test and make it work on all platforms. - environment.SkipIfNotPlatform(t, "linux/amd64") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := createImage(t, "trust-push", "latest") - - result := icmd.RunCmd(icmd.Command("docker", "push", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "bar"), - ) - result.Assert(t, icmd.Success) - golden.Assert(t, result.Stderr(), "push-with-content-trust-err.golden") - output.Assert(t, result.Stdout(), map[int]func(string) error{ - 0: output.Equals("The push refers to repository [registry:5000/trust-push]"), - 1: output.Equals("7cd52847ad77: Preparing"), - 3: output.Equals("latest: digest: sha256:e2e16842c9b54d985bf1ef9242a313f36b856181f188de21313820e177002501 size: 528"), - 4: output.Equals("Signing and pushing trust metadata"), - 5: output.Equals(`Finished initializing "registry:5000/trust-push"`), - 6: output.Equals("Successfully signed registry:5000/trust-push:latest"), - }) -} - func TestPushQuietErrors(t *testing.T) { result := icmd.RunCmd(icmd.Command("docker", "push", "--quiet", "nosuchimage")) - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "An image does not exist locally with the tag: nosuchimage", - }) -} - -func TestPushWithContentTrustUnreachableServer(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := createImage(t, "trust-push-unreachable", "latest") - - result := icmd.RunCmd(icmd.Command("docker", "push", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotaryServer("https://invalidnotaryserver"), - ) - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "error contacting notary server", - }) -} - -func TestPushWithContentTrustExistingTag(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image := createImage(t, "trust-push-existing", "latest") - - result := icmd.RunCmd(icmd.Command("docker", "push", image)) - result.Assert(t, icmd.Success) - - result = icmd.RunCmd(icmd.Command("docker", "push", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "bar"), - ) - result.Assert(t, icmd.Expected{ - Out: "Signing and pushing trust metadata", - }) - - // Re-push - result = icmd.RunCmd(icmd.Command("docker", "push", image), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "bar"), - ) - result.Assert(t, icmd.Expected{ - Out: "Signing and pushing trust metadata", - }) -} - -func TestPushWithContentTrustReleasesDelegationOnly(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - role := "targets/releases" - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - copyPrivateKey(t, dir.Join("trust", "private"), privkey1) - notaryDir := setupNotaryConfig(t, dir) - defer notaryDir.Remove() - homeDir := fs.NewDir(t, "push_test_home") - defer notaryDir.Remove() - - baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-delegation") - targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") - - // Init repository - notaryInit(t, notaryDir, homeDir, baseRef) - // Add delegation key (public key) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, role, pubkey1) - // Publish it - notaryPublish(t, notaryDir, homeDir, baseRef) - // Import private key - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, role, privkey1) - - // Tag & push with content trust - icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) - result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "foo"), - ) - result.Assert(t, icmd.Expected{ - Out: "Signing and pushing trust metadata", - }) - - targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, role) - assert.Assert(t, targetsInRole["latest"] == role, "%v", targetsInRole) - targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") - assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) - - result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Success) -} - -func TestPushWithContentTrustSignsAllFirstLevelRolesWeHaveKeysFor(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - copyPrivateKey(t, dir.Join("trust", "private"), privkey1) - copyPrivateKey(t, dir.Join("trust", "private"), privkey2) - copyPrivateKey(t, dir.Join("trust", "private"), privkey3) - notaryDir := setupNotaryConfig(t, dir) - defer notaryDir.Remove() - homeDir := fs.NewDir(t, "push_test_home") - defer notaryDir.Remove() - - baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-first-roles") - targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") - - // Init repository - notaryInit(t, notaryDir, homeDir, baseRef) - // Add delegation key (public key) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", pubkey3) - // Import private key - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1) - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2) - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1/subrole", privkey3) - // Publish it - notaryPublish(t, notaryDir, homeDir, baseRef) - - // Tag & push with content trust - icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) - result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "foo"), - ) - result.Assert(t, icmd.Expected{ - Out: "Signing and pushing trust metadata", - }) - - // check to make sure that the target has been added to targets/role1 and targets/role2, and - // not targets (because there are delegations) or targets/role3 (due to missing key) or - // targets/role1/subrole (due to it being a second level delegation) - targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1") - assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole) - targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role2") - assert.Assert(t, targetsInRole["latest"] == "targets/role2", "%v", targetsInRole) - targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") - assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) - - assert.NilError(t, os.RemoveAll(dir.Join("trust"))) - // Try to pull, should fail because non of these are the release role - // FIXME(vdemeester) should be unit test - result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - result.Assert(t, icmd.Expected{ - ExitCode: 1, - }) -} - -func TestPushWithContentTrustSignsForRolesWithKeysAndValidPaths(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - copyPrivateKey(t, dir.Join("trust", "private"), privkey1) - copyPrivateKey(t, dir.Join("trust", "private"), privkey2) - copyPrivateKey(t, dir.Join("trust", "private"), privkey3) - copyPrivateKey(t, dir.Join("trust", "private"), privkey4) - notaryDir := setupNotaryConfig(t, dir) - defer notaryDir.Remove() - homeDir := fs.NewDir(t, "push_test_home") - defer notaryDir.Remove() - - baseRef := fmt.Sprintf("%s/%s", registryPrefix, "trust-push-releases-keys-valid-paths") - targetRef := fmt.Sprintf("%s:%s", baseRef, "latest") - - // Init repository - notaryInit(t, notaryDir, homeDir, baseRef) - // Add delegation key (public key) - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role1", pubkey1, "l", "z") - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role2", pubkey2, "x", "y") - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role3", pubkey3, "latest") - notaryAddDelegation(t, notaryDir, homeDir, baseRef, "targets/role4", pubkey4, "latest") - // Import private keys (except 3rd key) - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role1", privkey1) - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role2", privkey2) - notaryImportPrivateKey(t, notaryDir, homeDir, baseRef, "targets/role4", privkey4) - // Publish it - notaryPublish(t, notaryDir, homeDir, baseRef) - - // Tag & push with content trust - icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, targetRef).Assert(t, icmd.Success) - result := icmd.RunCmd(icmd.Command("docker", "push", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - fixtures.WithPassphrase("foo", "foo"), - ) - result.Assert(t, icmd.Expected{ - Out: "Signing and pushing trust metadata", - }) - - // check to make sure that the target has been added to targets/role1 and targets/role4, and - // not targets (because there are delegations) or targets/role2 (due to path restrictions) or - // targets/role3 (due to missing key) - targetsInRole := notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role1") - assert.Assert(t, targetsInRole["latest"] == "targets/role1", "%v", targetsInRole) - targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets/role4") - assert.Assert(t, targetsInRole["latest"] == "targets/role4", "%v", targetsInRole) - targetsInRole = notaryListTargetsInRole(t, notaryDir, homeDir, baseRef, "targets") - assert.Assert(t, targetsInRole["latest"] != "targets", "%v", targetsInRole) - - assert.NilError(t, os.RemoveAll(dir.Join("trust"))) - // Try to pull, should fail because non of these are the release role - // FIXME(vdemeester) should be unit test - result = icmd.RunCmd(icmd.Command("docker", "pull", targetRef), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) result.Assert(t, icmd.Expected{ ExitCode: 1, }) -} - -func createImage(t *testing.T, repo string, tags ...string) string { - t.Helper() - icmd.RunCommand("docker", "pull", fixtures.AlpineImage).Assert(t, icmd.Success) - - for _, tag := range tags { - image := fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tag) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, image).Assert(t, icmd.Success) - } - return fmt.Sprintf("%s/%s:%s", registryPrefix, repo, tags[0]) -} - -//nolint:unparam -func withNotaryPassphrase(pwd string) func(*icmd.Cmd) { - return func(c *icmd.Cmd) { - c.Env = append(c.Env, []string{ - "NOTARY_ROOT_PASSPHRASE=" + pwd, - "NOTARY_TARGETS_PASSPHRASE=" + pwd, - "NOTARY_SNAPSHOT_PASSPHRASE=" + pwd, - "NOTARY_DELEGATION_PASSPHRASE=" + pwd, - }...) - } -} - -func notaryImportPrivateKey(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, privkey string) { - t.Helper() - icmd.RunCmd( - icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "key", "import", privkey, "-g", baseRef, "-r", role), - withNotaryPassphrase("foo"), - fixtures.WithHome(homeDir.Path()), - ).Assert(t, icmd.Success) -} - -func notaryPublish(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) { - t.Helper() - icmd.RunCmd( - icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "publish", baseRef), - withNotaryPassphrase("foo"), - fixtures.WithHome(homeDir.Path()), - ).Assert(t, icmd.Success) -} - -func notaryAddDelegation(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role, pubkey string, paths ...string) { - t.Helper() - pathsArg := "--all-paths" - if len(paths) > 0 { - pathsArg = "--paths=" + strings.Join(paths, ",") - } - icmd.RunCmd( - icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "delegation", "add", baseRef, role, pubkey, pathsArg), - withNotaryPassphrase("foo"), - fixtures.WithHome(homeDir.Path()), - ).Assert(t, icmd.Success) -} - -func notaryInit(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef string) { - t.Helper() - icmd.RunCmd( - icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "init", baseRef), - withNotaryPassphrase("foo"), - fixtures.WithHome(homeDir.Path()), - ).Assert(t, icmd.Success) -} - -func notaryListTargetsInRole(t *testing.T, notaryDir, homeDir *fs.Dir, baseRef, role string) map[string]string { - t.Helper() - result := icmd.RunCmd( - icmd.Command(notary, "-c", notaryDir.Join("client-config.json"), "list", baseRef, "-r", role), - fixtures.WithHome(homeDir.Path()), - ) - out := result.Combined() - - // should look something like: - // NAME DIGEST SIZE (BYTES) ROLE - // ------------------------------------------------------------------------------------------------------ - // latest 24a36bbc059b1345b7e8be0df20f1b23caa3602e85d42fff7ecd9d0bd255de56 1377 targets - - targets := make(map[string]string) - - // no target - lines := strings.Split(strings.TrimSpace(out), "\n") - if len(lines) == 1 && strings.Contains(out, "No targets present in this repository.") { - return targets - } - - // otherwise, there is at least one target - assert.Assert(t, len(lines) >= 3, "output is %s", out) - - for _, line := range lines[2:] { - tokens := strings.Fields(line) - assert.Assert(t, len(tokens) == 4) - targets[tokens[0]] = tokens[3] - } - - return targets -} - -func setupNotaryConfig(t *testing.T, dockerConfigDir fs.Dir) *fs.Dir { - t.Helper() - return fs.NewDir(t, "notary_test", fs.WithMode(0o700), - fs.WithFile("client-config.json", fmt.Sprintf(` -{ - "trust_dir": "%s", - "remote_server": { - "url": "%s" - } -}`, dockerConfigDir.Join("trust"), fixtures.NotaryURL)), - ) -} - -func copyPrivateKey(t *testing.T, dir, source string) { - t.Helper() - icmd.RunCommand("/bin/cp", source, dir).Assert(t, icmd.Success) + assert.Check(t, is.Contains(result.Stderr(), "does not exist")) + assert.Check(t, is.Contains(result.Stderr(), "nosuchimage")) } diff --git a/e2e/image/testdata/notary/delgkey1.crt b/e2e/image/testdata/notary/delgkey1.crt deleted file mode 100644 index 2218f23c89b1..000000000000 --- a/e2e/image/testdata/notary/delgkey1.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDhTCCAm2gAwIBAgIJAP2EcMN2UXPcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMQ8wDQYD -VQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb24wHhcNMTYwOTI4MTc0ODQ4 -WhcNMjYwNjI4MTc0ODQ4WjBXMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTAT -BgNVBAcTDFNhbkZyYW5jaXNjbzEPMA0GA1UEChMGRG9ja2VyMRMwEQYDVQQDEwpk -ZWxlZ2F0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvgewhaYs -Ke5s2AM7xxKrT4A6n7hW17qSnBjonCcPcwTFmYqIOdxWjYITgJuHrTwB4ZhBqWS7 -tTsUUu6hWLMeB7Uo5/GEQAAZspKkT9G/rNKF9lbWK9PPhGGkeR01c/Q932m92Hsn -fCQ0Pp/OzD3nVTh0v9HKk+PObNMOCcqG87eYs4ylPRxs0RrE/rP+bEGssKQSbeCZ -wazDnO+kiatVgKQZ2CK23iFdRE1z2rzqVDeaFWdvBqrRdWnkOZClhlLgEQ5nK2yV -B6tSqOiI3MmHyHzIkGOQJp2/s7Pe0ckEkzsjTsJW8oKHlBBl6pRxHIKzNN4VFbeB -vvYvrogrDrC/owIDAQABo1QwUjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF -oDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUFoHfukRa6qGk1ncON64Z -ASKlZdkwDQYJKoZIhvcNAQELBQADggEBAEq9Adpd03CPmpbRtTAJGAkjjLFr60sV -2r+/l/m9R31ZCN9ymM9nxToQ8zfMdeAh/nnPcErziil2gDVqXueCNDkRj09tmDIE -Q1Oc92uyNZNgcECow77cKZCTZSTku+qsJrYaykH5vSnia8ltcKj8inJedIcpBR+p -608HEQvF0Eg5eaLPJwH48BCb0Gqdri1dJgrNnqptz7MDr8M+u7tHVulbAd3YxLlq -JH1W2bkVUx6esbn/MUE5HL5iTuOYREEINvBSmLdmmFkampmCnCB/bDEyJeL9bAkt -ZPIi0UNSnqFKLSP1Vf8AGLXt6iO7+1OGvtsDXEEYdXVOMsSXZtUuT7A= ------END CERTIFICATE----- diff --git a/e2e/image/testdata/notary/delgkey1.key b/e2e/image/testdata/notary/delgkey1.key deleted file mode 100644 index cb37efc94a44..000000000000 --- a/e2e/image/testdata/notary/delgkey1.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAvgewhaYsKe5s2AM7xxKrT4A6n7hW17qSnBjonCcPcwTFmYqI -OdxWjYITgJuHrTwB4ZhBqWS7tTsUUu6hWLMeB7Uo5/GEQAAZspKkT9G/rNKF9lbW -K9PPhGGkeR01c/Q932m92HsnfCQ0Pp/OzD3nVTh0v9HKk+PObNMOCcqG87eYs4yl -PRxs0RrE/rP+bEGssKQSbeCZwazDnO+kiatVgKQZ2CK23iFdRE1z2rzqVDeaFWdv -BqrRdWnkOZClhlLgEQ5nK2yVB6tSqOiI3MmHyHzIkGOQJp2/s7Pe0ckEkzsjTsJW -8oKHlBBl6pRxHIKzNN4VFbeBvvYvrogrDrC/owIDAQABAoIBAB/o8KZwsgfUhqh7 -WoViSCwQb0e0z7hoFwhpUl4uXPTGf1v6HEgDDPG0PwwgkdbwNaypQZVtWevj4NTQ -R326jjdjH1xbfQa2PZpz722L3jDqJR6plEtFxRoIv3KrCffPsrgabIu2mnnJJpDB -ixtW5cq0sT4ov2i4H0i85CWWwbSY/G/MHsvCuK9PhoCj9uToVqrf1KrAESE5q4fh -mPSYUL99KVnj7SZkUz+79rc8sLLPVks3szZACMlm1n05ZTj/d6Nd2ZZUO45DllIj -1XJghfWmnChrB/P/KYXgQ3Y9BofIAw1ra2y3wOZeqRFNsbmojcGldfdtN/iQzhEj -uk4ThokCgYEA9FTmv36N8qSPWuqX/KzkixDQ8WrDGohcB54kK98Wx4ijXx3i38SY -tFjO8YUS9GVo1+UgmRjZbzVX7xeum6+TdBBwOjNOxEQ4tzwiQBWDdGpli8BccdJ2 -OOIVxSslWhiUWfpYloXVetrR88iHbT882g795pbonDaJdXSLnij4UW8CgYEAxxrr -QFpsmOEZvI/yPSOGdG7A1RIsCeH+cEOf4cKghs7+aCtAHlIweztNOrqirl3oKI1r -I0zQl46WsaW8S/y99v9lmmnZbWwqLa4vIu0NWs0zaZdzKZw3xljMhgp4Ge69hHa2 -utCtAxcX+7q/yLlHoTiYwKdxX54iLkheCB8csw0CgYEAleEG820kkjXUIodJ2JwO -Tihwo8dEC6CeI6YktizRgnEVFqH0rCOjMO5Rc+KX8AfNOrK5PnD54LguSuKSH7qi -j04OKgWTSd43lF90+y63RtCFnibQDpp2HwrBJAQFk7EEP/XMJfnPLN/SbuMSADgM -kg8kPTFRW5Iw3DYz9z9WpE0CgYAkn6/8Q2XMbUOFqti9JEa8Lg8sYk5VdwuNbPMA -3QMYKQUk9ieyLB4c3Nik3+XCuyVUKEc31A5egmz3umu7cn8i6vGuiJ/k/8t2YZ7s -Bry5Ihu95Yzab5DW3Eiqs0xKQN79ebS9AluAwQO5Wy2h52rknfuDHIm/M+BHsSoS -xl5KFQKBgQCokCsYuX1z2GojHw369/R2aX3ovCGuHqy4k7fWxUrpHTHvth2+qNPr -84qLJ9rLWoZE5sUiZ5YdwCgW877EdfkT+v4aaBX79ixso5VdqgJ/PdnoNntah/Vq -njQiW1skn6/P5V/eyimN2n0VsyBr/zMDEtYTRP/Tb1zi/njFLQkZEA== ------END RSA PRIVATE KEY----- diff --git a/e2e/image/testdata/notary/delgkey2.crt b/e2e/image/testdata/notary/delgkey2.crt deleted file mode 100644 index bec084790a59..000000000000 --- a/e2e/image/testdata/notary/delgkey2.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDhTCCAm2gAwIBAgIJAIq8naKlYAQfMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMQ8wDQYD -VQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb24wHhcNMTYwOTI4MTc0ODQ4 -WhcNMjYwNjI4MTc0ODQ4WjBXMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTAT -BgNVBAcTDFNhbkZyYW5jaXNjbzEPMA0GA1UEChMGRG9ja2VyMRMwEQYDVQQDEwpk -ZWxlZ2F0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyY2EWYTW -5VHipw08t675upmD6a+akiuZ1z+XpuOxZCgjZ0aHfoOe8wGKg3Ohz7UCBdD5Mob/ -L/qvRlsCaqPHGZKIyyX1HDO4mpuQQFBhYxt+ZAO3AaawEUOw2rwwMDEjLnDDTSZM -z8jxCMvsJjBDqgb8g3z+AmjducQ/OH6llldgHIBY8ioRbROCL2PGgqywWq2fThav -c70YMxtKviBGDNCouYeQ8JMK/PuLwPNDXNQAagFHVARXiUv/ILHk7ImYnSGJUcuk -JTUGN2MBnpY0eakg7i+4za8sjjqOdn+2I6aVzlGJDSiRP72nkg/cE4BqMl9FrMwK -9iS8xa9yMDLUvwIDAQABo1QwUjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF -oDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUvQzzFmh3Sv3HcdExY3wx -/1u6JLAwDQYJKoZIhvcNAQELBQADggEBAJcmDme2Xj/HPUPwaN/EyCmjhY73EiHO -x6Pm16tscg5JGn5A+u3CZ1DmxUYl8Hp6MaW/sWzdtL0oKJg76pynadCWh5EacFR8 -u+2GV/IcN9mSX6JQzvrqbjSqo5/FehqBD+W5h3euwwApWA3STAadYeyEfmdOA3SQ -W1vzrA1y7i8qgTqeJ7UX1sEAXlIhBK2zPYaMB+en+ZOiPyNxJYj6IDdGdD2paC9L -6H9wKC+GAUTSdCWp89HP7ETSXEGr94AXkrwU+qNsiN+OyK8ke0EMngEPh5IQoplw -/7zEZCth3oKxvR1/4S5LmTVaHI2ZlbU4q9bnY72G4tw8YQr2gcBGo4w= ------END CERTIFICATE----- diff --git a/e2e/image/testdata/notary/delgkey2.key b/e2e/image/testdata/notary/delgkey2.key deleted file mode 100644 index 5ccabe908fb1..000000000000 --- a/e2e/image/testdata/notary/delgkey2.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAyY2EWYTW5VHipw08t675upmD6a+akiuZ1z+XpuOxZCgjZ0aH -foOe8wGKg3Ohz7UCBdD5Mob/L/qvRlsCaqPHGZKIyyX1HDO4mpuQQFBhYxt+ZAO3 -AaawEUOw2rwwMDEjLnDDTSZMz8jxCMvsJjBDqgb8g3z+AmjducQ/OH6llldgHIBY -8ioRbROCL2PGgqywWq2fThavc70YMxtKviBGDNCouYeQ8JMK/PuLwPNDXNQAagFH -VARXiUv/ILHk7ImYnSGJUcukJTUGN2MBnpY0eakg7i+4za8sjjqOdn+2I6aVzlGJ -DSiRP72nkg/cE4BqMl9FrMwK9iS8xa9yMDLUvwIDAQABAoIBAHmffvzx7ydESWwa -zcfdu26BkptiTvjjfJrqEd4wSewxWGPKqJqMXE8xX99A2KTZClZuKuH1mmnecQQY -iRXGrK9ewFMuHYGeKEiLlPlqR8ohXhyGLVm+t0JDwaXMp5t9G0i73O5iLTm5fNGd -FGxa9YnVW20Q8MqNczbVGH1D1zInhxzzOyFzBd4bBBJ8PdrUdyLpd7+RxY2ghnbT -p9ZANR2vk5zmDLJgZx72n/u+miJWuhY6p0v3Vq4z/HHgdhf+K6vpDdzTcYlA0rO4 -c/c+RKED3ZadGUD5QoLsmEN0e3FVSMPN1kt4ZRTqWfH8f2X4mLz33aBryTjktP6+ -1rX6ThECgYEA74wc1Tq23B5R0/GaMm1AK3Ko2zzTD8wK7NSCElh2dls02B+GzrEB -aE3A2GMQSuzb+EA0zkipwANBaqs3ZemH5G1pu4hstQsXCMd4jAJn0TmTXlplXBCf -PSc8ZUU6XcJENRr9Q7O9/TGlgahX+z0ndxYx/CMCsSu7XsMg4IZsbAcCgYEA12Vb -wKOVG15GGp7pMshr+2rQfVimARUP4gf3JnQmenktI4PfdnMW3a4L3DEHfLhIerwT -6lRp/NpxSADmuT4h1UO1l2lc+gmTVPw0Vbl6VwHpgS5Kfu4ZyM6n3S66f/dE4nu7 -hQF9yZz7vn5Agghak4p6a1wC1gdMzR1tvxFzk4kCgYByBMTskWfcWeok8Yitm+bB -R3Ar+kWT7VD97SCETusD5uG+RTNLSmEbHnc+B9kHcLo67YS0800pAeOvPBPARGnU -RmffRU5I1iB+o0MzkSmNItSMQoagTaEd4IEUyuC/I+qHRHNsOC+kRm86ycAm67LP -MhdUpe1wGxqyPjp15EXTHQKBgDKzFu+3EWfJvvKRKQ7dAh3BvKVkcl6a2Iw5l8Ej -YdM+JpPPfI/i8yTmzL/dgoem0Nii4IUtrWzo9fUe0TAVId2S/HFRSaNJEbbVTnRH -HjbQqmfPv5U08jjD+9siHp/0UfCFc1QRT8xe+RqTmReCY9+KntoaZEiAm2FEZgqt -TukRAoGAf7QqbTP5/UH1KSkX89F5qy/6GS3pw6TLj9Ufm/l/NO8Um8gag6YhEKWR -7HpkpCqjfWj8Av8ESR9cqddPGrbdqXFm9z7dCjlAd5T3Q3h/h+v+JzLQWbsI6WOb -SsOSWNyE006ZZdIiFwO6GfxpLI24sVtYKgyob6Q71oxSqfnrnT0= ------END RSA PRIVATE KEY----- diff --git a/e2e/image/testdata/notary/delgkey3.crt b/e2e/image/testdata/notary/delgkey3.crt deleted file mode 100644 index f434b45fc8e9..000000000000 --- a/e2e/image/testdata/notary/delgkey3.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDhTCCAm2gAwIBAgIJAKHt/jxiWqMtMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMQ8wDQYD -VQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb24wHhcNMTYwOTI4MTc0ODQ5 -WhcNMjYwNjI4MTc0ODQ5WjBXMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTAT -BgNVBAcTDFNhbkZyYW5jaXNjbzEPMA0GA1UEChMGRG9ja2VyMRMwEQYDVQQDEwpk -ZWxlZ2F0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqfbJk2Dk -C9FJVjV2+Q2CQrJphG3vFc1Qlu9jgVA5RhGmF9jJzetsclsV/95nBhinIGcSmPQA -l318G7Bz/cG/6O2n5+hj+S1+YOvQweReZj3d4kCeS86SOyLNTpMD9gsF0S8nR1RN -h0jD4t1vxAVeGD1o61U8/k0O5eDoeOfOSWZagKk5PhyrMZgNip4IrG46umCkFlrw -zMMcgQdwTQXywPqkr/LmYpqT1WpMlzHYTQEY8rKorIJQbPtHVYdr4UxYnNmk6fbU -biEP1DQlwjBWcFTsDLqXKP/K+e3O0/e/hMB0y7Tj9fZ7Viw0t5IKXZPsxMhwknUT -9vmPzIJO6NiniwIDAQABo1QwUjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF -oDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUdTXRP1EzxQ+UDZSoheVo -Mobud1cwDQYJKoZIhvcNAQELBQADggEBADV9asTWWdbmpkeRuKyi0xGho39ONK88 -xxkFlco766BVgemo/rGQj3oPuw6M6SzHFoJ6JUPjmLiAQDIGEU/2/b6LcOuLjP+4 -YejCcDTY3lSW/HMNoAmzr2foo/LngNGfe/qhVFUqV7GjFT9+XzFFBfIZ1cQiL2ed -kc8rgQxFPwWXFCSwaENWeFnMDugkd+7xanoAHq8GsJpg5fTruDTmJkUqC2RNiMLn -WM7QaqW7+lmUnMnc1IBoz0hFhgoiadWM/1RQxx51zTVw6Au1koIm4ZXu5a+/WyC8 -K1+HyUbc0AVaDaRBpRSOR9aHRwLGh6WQ4aUZQNyJroc999qfYrDEEV8= ------END CERTIFICATE----- diff --git a/e2e/image/testdata/notary/delgkey3.key b/e2e/image/testdata/notary/delgkey3.key deleted file mode 100644 index a61d18cc3d48..000000000000 --- a/e2e/image/testdata/notary/delgkey3.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAqfbJk2DkC9FJVjV2+Q2CQrJphG3vFc1Qlu9jgVA5RhGmF9jJ -zetsclsV/95nBhinIGcSmPQAl318G7Bz/cG/6O2n5+hj+S1+YOvQweReZj3d4kCe -S86SOyLNTpMD9gsF0S8nR1RNh0jD4t1vxAVeGD1o61U8/k0O5eDoeOfOSWZagKk5 -PhyrMZgNip4IrG46umCkFlrwzMMcgQdwTQXywPqkr/LmYpqT1WpMlzHYTQEY8rKo -rIJQbPtHVYdr4UxYnNmk6fbUbiEP1DQlwjBWcFTsDLqXKP/K+e3O0/e/hMB0y7Tj -9fZ7Viw0t5IKXZPsxMhwknUT9vmPzIJO6NiniwIDAQABAoIBAQCAr/ed3A2umO7T -FDYZik3nXBiiiW4t7r+nGGgZ3/kNgY1lnuHlROxehXLZwbX1mrLnyML/BjhwezV9 -7ZNVPd6laVPpNj6DyxtWHRZ5yARlm1Al39E7CpQTrF0QsiWcpGnqIa62xjDRTpnq -askV/Q5qggyvqmE9FnFCQpEiAjlhvp7F0kVHVJm9s3MK3zSyR0UTZ3cpYus2Jr2z -OotHgAMHq5Hgb3dvxOeE2xRMeYAVDujbkNzXm2SddAtiRdLhWDh7JIr3zXhp0HyN -4rLOyhlgz00oIGeDt/C0q3fRmghr3iZOG+7m2sUx0FD1Ru1dI9v2A+jYmIVNW6+x -YJk5PzxJAoGBANDj7AGdcHSci/LDBPoTTUiz3uucAd27/IJma/iy8mdbVfOAb0Fy -PRSPvoozlpZyOxg2J4eH/o4QxQR4lVKtnLKZLNHK2tg3LarwyBX1LiI3vVlB+DT1 -AmV8i5bJAckDhqFeEH5qdWZFi03oZsSXWEqX5iMYCrdK5lTZggcrFZeHAoGBANBL -fkk3knAdcVfTYpmHx18GBi2AsCWTd20KD49YBdbVy0Y2Jaa1EJAmGWpTUKdYx40R -H5CuGgcAviXQz3bugdTU1I3tAclBtpJNU7JkhuE+Epz0CM/6WERJrE0YxcGQA5ui -6fOguFyiXD1/85jrDBOKy74aoS7lYz9r/a6eqmjdAoGBAJpm/nmrIAZx+Ff2ouUe -A1Ar9Ch/Zjm5zEmu3zwzOU4AiyWz14iuoktifNq2iyalRNz+mnVpplToPFizsNwu -C9dPtXtU0DJlhtIFrD/evLz6KnGhe4/ZUm4lgyBvb2xfuNHqL5Lhqelwmil6EQxb -Oh3Y7XkfOjyFln89TwlxZUJdAoGAJRMa4kta7EvBTeGZLjyltvsqhFTghX+vBSCC -ToBbYbbiHJgssXSPAylU4sD7nR3HPwuqM6VZip+OOMrm8oNXZpuPTce+xqTEq1vK -JvmPrG3RAFDLdMFZjqYSXhKnuGE60yv3Ol8EEbDwfB3XLQPBPYU56Jdy0xcPSE2f -dMJXEJ0CgYEAisZw0nXw6lFeYecu642EGuU0wv1O9i21p7eho9QwOcsoTl4Q9l+M -M8iBv+qTHO+D19l4JbkGvy2H2diKoYduUFACcuiFYs8fjrT+4Z6DyOQAQGAf6Ylw -BFbU15k6KbA9v4mZDfd1tY9x62L/XO55ZxYG+J+q0e26tEThgD8cEog= ------END RSA PRIVATE KEY----- diff --git a/e2e/image/testdata/notary/delgkey4.crt b/e2e/image/testdata/notary/delgkey4.crt deleted file mode 100644 index c8cbe46bdfa6..000000000000 --- a/e2e/image/testdata/notary/delgkey4.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDhTCCAm2gAwIBAgIJANae++ZkUEWMMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV -BAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMQ8wDQYD -VQQKEwZEb2NrZXIxEzARBgNVBAMTCmRlbGVnYXRpb24wHhcNMTYwOTI4MTc0ODQ5 -WhcNMjYwNjI4MTc0ODQ5WjBXMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFTAT -BgNVBAcTDFNhbkZyYW5jaXNjbzEPMA0GA1UEChMGRG9ja2VyMRMwEQYDVQQDEwpk -ZWxlZ2F0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqULAjgba -Y2I10WfqdmYnPfEqEe6iMDbzcgECb2xKafXcI4ltkQj1iO4zBTs0Ft9EzXFc5ZBh -pTjZrL6vrIa0y/CH2BiIHBJ0wRHx/40HXp4DSj3HZpVOlEMI3npRfBGNIBllUaRN -PWG7zL7DcKMIepBfPXyjBsxzH3yNiISq0W5hSiy+ImhSo3aipJUHHcp9Z9NgvpNC -3QvnxsGKRnECmDRDlxkq+FQu9Iqs/HWFYWgyfcsw+YTrWZq3qVnnqUouHO//c9PG -Ry3sZSDU97MwvkjvWys1e01Xvd3AbHx08YAsxih58i/OBKe81eD9NuZDP2KrjTxI -5xkXKhj6DV2NnQIDAQABo1QwUjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIF -oDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUDt95hiqbQvi0KcvZGAUu -VisnztQwDQYJKoZIhvcNAQELBQADggEBAGi7qHai7MWbfeu6SlXhzIP3AIMa8TMi -lp/+mvPUFPswIVqYJ71MAN8uA7CTH3z50a2vYupGeOEtZqVJeRf+xgOEpwycncxp -Qz6wc6TWPVIoT5q1Hqxw1RD2MyKL+Y+QBDYwFxFkthpDMlX48I9frcqoJUWFxBF2 -lnRr/cE7BbPE3sMbXV3wGPlH7+eUf+CgzXJo2HB6THzagyEgNrDiz/0rCQa1ipFd -mNU3D/U6BFGmJNxhvSOtXX9escg8yjr05YwwzokHS2K4jE0ZuJPBd50C/Rvo3Mf4 -0h7/2Q95e7d42zPe9WYPu2F8KTWsf4r+6ddhKrKhYzXIcTAfHIOiO+U= ------END CERTIFICATE----- diff --git a/e2e/image/testdata/notary/delgkey4.key b/e2e/image/testdata/notary/delgkey4.key deleted file mode 100644 index f473cc495a72..000000000000 --- a/e2e/image/testdata/notary/delgkey4.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAqULAjgbaY2I10WfqdmYnPfEqEe6iMDbzcgECb2xKafXcI4lt -kQj1iO4zBTs0Ft9EzXFc5ZBhpTjZrL6vrIa0y/CH2BiIHBJ0wRHx/40HXp4DSj3H -ZpVOlEMI3npRfBGNIBllUaRNPWG7zL7DcKMIepBfPXyjBsxzH3yNiISq0W5hSiy+ -ImhSo3aipJUHHcp9Z9NgvpNC3QvnxsGKRnECmDRDlxkq+FQu9Iqs/HWFYWgyfcsw -+YTrWZq3qVnnqUouHO//c9PGRy3sZSDU97MwvkjvWys1e01Xvd3AbHx08YAsxih5 -8i/OBKe81eD9NuZDP2KrjTxI5xkXKhj6DV2NnQIDAQABAoIBAGK0ZKnuYSiXux60 -5MvK4pOCsa/nY3mOcgVHhW4IzpRgJdIrcFOlz9ncXrBsSAIWjX7o3u2Ydvjs4DOW -t8d6frB3QiDInYcRVDjLCD6otWV97Bk9Ua0G4N4hAWkMF7ysV4oihS1JDSoAdo39 -qOdki6s9yeyHZGKwk2oHLlowU5TxQMBA8DHmxqBII1HTm+8xRz45bcEqRXydYSUn -P1JuSU9jFqdylxU+Nrq6ehslMQ3y7qNWQyiLGxu6EmR+vgrzSU0s3iAOqCHthaOS -VBBXPL3DNEYUS+0QGnGrACuJhanOMBfdiO6Orelx6ZzWZm38PNGv0yBt0WCM+8/A -TtQNGkECgYEA1LqR6AH9XikUQ0+rM4526BgVuYqtjw21h4Lj9alaA+YTQntBBJOv -iAcUpnJiV4T8jzAMLeqpK8R/rbxRnK5S9jOV2gr+puk4L6tH46cgahBUESDigDp8 -6vK8ur6ubBcXNPh3AT6rsPj+Ph2EU3raqiYdouvCdga/OCYZb+jr6UkCgYEAy7Cr -l8WssI/8/ORcQ4MFJFNyfz/Y2beNXyLd1PX0H+wRSiGcKzeUuTHNtzFFpMbrK/nx -ZOPCT2ROdHsBHzp1L+WquCb0fyMVSiYiXBU+VCFDbUU5tBr3ycTc7VwuFPENOiha -IdlWgew/aW110FQHIaqe9g+htRe+mXe++faZtbUCgYB/MSJmNzJX53XvHSZ/CBJ+ -iVAMBSfq3caJRLCqRNzGcf1YBbwFUYxlZ95n+wJj0+byckcF+UW3HqE8rtmZNf3y -qTtTCLnj8JQgpGeybU4LPMIXD7N9+fqQvBwuCC7gABpnGJyHCQK9KNNTLnDdPRqb -G3ki3ZYC3dvdZaJV8E2FyQKBgQCMa5Mf4kqWvezueo+QizZ0QILibqWUEhIH0AWV -1qkhiKCytlDvCjYhJdBnxjP40Jk3i+t6XfmKud/MNTAk0ywOhQoYQeKz8v+uSnPN -f2ekn/nXzq1lGGJSWsDjcXTjQvqXaVIZm7cjgjaE+80IfaUc9H75qvUT3vaq3f5u -XC7DMQKBgQDMAzCCpWlEPbZoFMl6F49+7jG0/TiqM/WRUSQnNtufPMbrR9Je4QM1 -L1UCANCPaHFOncKYer15NfIV1ctt5MZKImevDsUaQO8CUlO+dzd5H8KvHw9E29gA -B22v8k3jIjsYeRL+UJ/sBnWHgxdAe/NEM+TdlP2oP9D1gTifutPqAg== ------END RSA PRIVATE KEY----- diff --git a/e2e/image/testdata/notary/gen.sh b/e2e/image/testdata/notary/gen.sh deleted file mode 100755 index 8d6381cec4cf..000000000000 --- a/e2e/image/testdata/notary/gen.sh +++ /dev/null @@ -1,18 +0,0 @@ -for selfsigned in delgkey1 delgkey2 delgkey3 delgkey4; do - subj='/C=US/ST=CA/L=SanFrancisco/O=Docker/CN=delegation' - - openssl genrsa -out "${selfsigned}.key" 2048 - openssl req -new -key "${selfsigned}.key" -out "${selfsigned}.csr" -sha256 -subj "${subj}" - cat > "${selfsigned}.cnf" <= v24 # see https://github.com/docker-library/docker/pull/470 groupadd -f docker && \ diff --git a/e2e/testdata/Dockerfile.evil-notary-server b/e2e/testdata/Dockerfile.evil-notary-server deleted file mode 100644 index 97f234e849bf..000000000000 --- a/e2e/testdata/Dockerfile.evil-notary-server +++ /dev/null @@ -1,7 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG NOTARY_VERSION=0.6.1 - -FROM notary:server-${NOTARY_VERSION} - -COPY ./notary-evil/ /fixtures/ diff --git a/e2e/testdata/Dockerfile.gencerts b/e2e/testdata/Dockerfile.gencerts deleted file mode 100644 index 390ed58e0b47..000000000000 --- a/e2e/testdata/Dockerfile.gencerts +++ /dev/null @@ -1,20 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG GO_VERSION=1.24.5 - -FROM golang:${GO_VERSION}-alpine AS generated -ENV GOTOOLCHAIN=local -RUN go install github.com/dmcgowan/quicktls@master -WORKDIR /tmp/gencerts/notary -RUN --mount=type=bind,source=e2e/testdata/notary,target=/tmp/gencerts/notary,rw <> notary-server.cert - mv ca.pem root-ca.cert - cp notary-server.cert notary-server.key root-ca.cert ../notary-evil - cp -r /tmp/gencerts/notary* /out/ -EOT - -FROM scratch -COPY --from=generated /out / diff --git a/e2e/testdata/Dockerfile.notary-server b/e2e/testdata/Dockerfile.notary-server deleted file mode 100644 index 846253e2fba6..000000000000 --- a/e2e/testdata/Dockerfile.notary-server +++ /dev/null @@ -1,7 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG NOTARY_VERSION=0.6.1 - -FROM notary:server-${NOTARY_VERSION} - -COPY ./notary/ /fixtures/ diff --git a/e2e/testdata/notary-evil/notary-config.json b/e2e/testdata/notary-evil/notary-config.json deleted file mode 100644 index f3345c080a31..000000000000 --- a/e2e/testdata/notary-evil/notary-config.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "server": { - "http_addr": "evil-notary-server:4444", - "tls_key_file": "./notary-server.key", - "tls_cert_file": "./notary-server.cert" - }, - "trust_service": { - "type": "local", - "hostname": "", - "port": "", - "key_algorithm": "ed25519" - }, - "logging": { - "level": "debug" - }, - "storage": { - "backend": "memory" - } -} diff --git a/e2e/testdata/notary-evil/notary-server.cert b/e2e/testdata/notary-evil/notary-server.cert deleted file mode 100644 index eee2b3463d0d..000000000000 --- a/e2e/testdata/notary-evil/notary-server.cert +++ /dev/null @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIQTujwx+1xxXeI5AbzAQ379TANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCkxDzANBgNVBAoTBkRvY2tlcjEWMBQGA1UEAxMN -bm90YXJ5LXNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPhZ -pU7DRK/2nwbTu+kVYhU/XARDleVSiLrQ5RMR1Cz2xC4LWkOEVSj4aCBo85O66JAx -p+WRVwoVEU2rdkK3k4983Xr34+7q5Hv4hmwlg6I7QLRRJapEgK5G5RB/9aQntolx -h5E0KaoF4PJP25y4FHCUr4td4QyitaICsCpuOAN6XgmE9sM9TBf+AEjTSxwwvgEz -DqHvyovl7pA+pQP2oTKBrf6KN8hHDOXmm9gd8ST9yKLrsYWhqExLLPnAD4lQEcKZ -29g+iTd4eNoJUXctpuY+3IpqBcQSLq35mNKBP/FQco6g3q26/cB4zWGxTr3jGJqs -ms8qdFLGZ2KiBCt+oDMCAwEAAaOBoTCBnjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0T -AQH/BAIwADAfBgNVHSMEGDAWgBTxYMNqgy2wkgmPZL/+bTCTQo6ulTBdBgNVHREE -VjBUgg1ub3Rhcnktc2VydmVyggxub3RhcnlzZXJ2ZXKCEmV2aWwtbm90YXJ5LXNl -cnZlcoIQZXZpbG5vdGFyeXNlcnZlcoIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 -DQEBCwUAA4IBAQCDMjuZnNINFfqstFwhEEvAgWbjYW26ZQxhQffaqDlbMIQkWoXj -8inld9bma9Mf5i/GAkUwFqCnEHD4LQ6vDgfAgL+pSOv9VI5SBEuk/gLqvIBUeIRu -uHo1jWtll2Fr7eDLVdD4mPRPFC7V6mv6sFa9EN4tBN8eheQxHJvzwnnU7X28prfI -/hWnwPWScVvttqBSsq1h2CUpVu2zGVToeCJ9xl4r/NyDtM5TyMgz7RLrer0p8NSu -4Qp4ZXtxHDLduWcyMUHLGTprW05yjj9UVq89xfaCOqFSpx5i4oxotYm1PoOacHmN -RMp9vaYMAmopoxIEYX6fDg5T3sQ5cidZJEvU ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDEDCCAfigAwIBAgIQdxGVILXsVcogexr+Ia2MZDANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCIxDzANBgNVBAoTBkRvY2tlcjEPMA0GA1UEAxMG -RG9ja2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3sA/g7Srrkz -uEf1Qa2jAw93EfEJvxU1ZmZ30aB7KLgHN2TznxAGYtNekAu88CV4H3PKS44BZOar -wOo3KL4wQffLt7lmsRJG1KOfyiAmjmvidP5JSeRdTiBtj4CCVoi3EE6BZXPpZjst -9OSOlld2bWWXHb2ZdoY3ZAhZ9rn3tVwyfoLKpuESp1WZSFHPIdcuoMmZPtqD0bSi -5hc4gVFNLlZOAILvUkXxcHKUgLHZg0YEDQWsYjqh8EYp5LUK2tt4Mpz0HwAt9siE -VxHGIsiEqG1ajmxZiS28nlRWc4JRlOdmy5x1TPzJTDy+49gxB4njp1nRUtUgzmaG -QHhml35xHQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU8WDDaoMtsJIJj2S//m0wk0KOrpUwDQYJKoZIhvcNAQELBQAD -ggEBAKZJfQjjfqn0K/UlzmrGcRkhrLbJNUfCD6TvxD75MoGtEe+VUEjljm1JHSbj -DrevDyTnak1W4o5/dcy0h6kI6lhHgObbcoAV5CxQ4+HHmeowA/fzedbnIdnHwtNg -SUJEslqoJSiYiiFQLV/yWWfBCWpbIgpDrADU7x9Ccxt6INuxrxOQwf1LZnmVbYs0 -1Mb/O1UFnvW7MeVSR4Nb/4lw6lol+mrR1iF8tTQ+rk4sBdCxw2aU48x3Pjqm+XpV -PIm9uRUr4tRDyQfmBZuxWTNJ9NSx5zVpLEPhDmyOW5wlSw+aKGscu9+RjBx/gXPk -sK8jZi441ojEJ7OaggGPheO3mCU= ------END CERTIFICATE----- diff --git a/e2e/testdata/notary-evil/notary-server.key b/e2e/testdata/notary-evil/notary-server.key deleted file mode 100644 index 7f7562a9defd..000000000000 --- a/e2e/testdata/notary-evil/notary-server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA+FmlTsNEr/afBtO76RViFT9cBEOV5VKIutDlExHULPbELgta -Q4RVKPhoIGjzk7rokDGn5ZFXChURTat2QreTj3zdevfj7urke/iGbCWDojtAtFEl -qkSArkblEH/1pCe2iXGHkTQpqgXg8k/bnLgUcJSvi13hDKK1ogKwKm44A3peCYT2 -wz1MF/4ASNNLHDC+ATMOoe/Ki+XukD6lA/ahMoGt/oo3yEcM5eab2B3xJP3Iouux -haGoTEss+cAPiVARwpnb2D6JN3h42glRdy2m5j7cimoFxBIurfmY0oE/8VByjqDe -rbr9wHjNYbFOveMYmqyazyp0UsZnYqIEK36gMwIDAQABAoIBAQDy7W2f763+mbTQ -zshepQX+Vq3BlgLIAMWyR6fr0WLEYNVhXMV8ibNrkiD4ovCwLwJSGeBr1JFZUWZN -nUze0gdLMg7LvDN/ftDk2yNSIhfy1xbhywaW2M8uqjZiv2genKIXK7A6PtYKdBmn -rKnbUMzdmvNj1f7Ph1E4Gn0L5ChybJDJrq6wQjuTdZ6RmkGkbid0L+47Uv+6xBm9 -hgBPVXd8auQAYGmyXZwvfga5ZjfRMI4wvWkvjOAQcJtxxgOnLT1KDjYV+L70PWul -bYoKX0sNkFEP9tOq2pD9XVBuTVQxcYeztv0Vz+kG66Ju1KKCAnUYFhRt055zZLfm -WDYlWm0BAoGBAPvGW9LvzwCDE9QUcR46nG1ZihheJyGKwWVK+ZjYkUU9nLbrIpOD -/jmihoHHhKBC6YOfHHY73LtZ22fgXEu6ivDzZtTxBErXbdRpEKktJebRK7gPkfsB -PNQ8CRd/DxRC/JuVFR76OPsbZWhXCaeC7PRdyAtvU9toT1jIQf+a4OhBAoGBAPyE -kxEoNO1KhWtgByUlsPzvq9PaTjwW/LpmEoo0FBUhYRPxYzVuYrE0BBflDR6JcMRR -oE9CXYGjtVPB44gT7pHVP09f3Ugrxk7X+t8wy3PWUTaTprmmEGqF0TzfdH4oQz0Y -v1khwuIu6rRlddGEiCKldXxn+gJy9E70yO4bm4tzAoGAL/XFIBVWVT6i1E9gjOWV -Tq8zwxiMU7Ney7DQgvEeGxZ1d9Kqr3cBQnFXNfmPpgeY+92fSlZ04atoRA1VB4ft -V6DGAeI3cxo+bavl5JQZGDLYJSOyJyJBOByHjtZBRRbNj8WCVHhNymeZlZqe2C30 -fUgwBx2Z172y/7KF/+680QECgYEA1GhUKQ9wDdYsiliZSgb9bJXSLH8qZeNULRrl -J3mNFwUf2p2mvPAgdjxx4QOb2H716z1aIrGJZB4nzc9/LBzQBb2h5ouV4DpqMjH8 -5bbuvH6fi9ABY5Irpt7vVUwFeoU1ofPqKPh8LLQYWywpQddAiBwzyjTQGTVHCg9f -4OI6Ib8CgYAptl24MGOc6BminKgsux+vNS9X1WwIADiHDyWBPHeQgLX8bYegswq9 -/6uGXJQgdFBhfLuoTBBN0ia/0QQhDezzrqnERddciuL2zxFxEETdpIuxm4lhieX7 -9LqnFcjxM4sLCg4SDSRX+nburiCnLDQiaBzhARooMJO48luTZUiWYQ== ------END RSA PRIVATE KEY----- diff --git a/e2e/testdata/notary-evil/root-ca.cert b/e2e/testdata/notary-evil/root-ca.cert deleted file mode 100644 index e7411c14bef8..000000000000 --- a/e2e/testdata/notary-evil/root-ca.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEDCCAfigAwIBAgIQdxGVILXsVcogexr+Ia2MZDANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCIxDzANBgNVBAoTBkRvY2tlcjEPMA0GA1UEAxMG -RG9ja2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3sA/g7Srrkz -uEf1Qa2jAw93EfEJvxU1ZmZ30aB7KLgHN2TznxAGYtNekAu88CV4H3PKS44BZOar -wOo3KL4wQffLt7lmsRJG1KOfyiAmjmvidP5JSeRdTiBtj4CCVoi3EE6BZXPpZjst -9OSOlld2bWWXHb2ZdoY3ZAhZ9rn3tVwyfoLKpuESp1WZSFHPIdcuoMmZPtqD0bSi -5hc4gVFNLlZOAILvUkXxcHKUgLHZg0YEDQWsYjqh8EYp5LUK2tt4Mpz0HwAt9siE -VxHGIsiEqG1ajmxZiS28nlRWc4JRlOdmy5x1TPzJTDy+49gxB4njp1nRUtUgzmaG -QHhml35xHQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU8WDDaoMtsJIJj2S//m0wk0KOrpUwDQYJKoZIhvcNAQELBQAD -ggEBAKZJfQjjfqn0K/UlzmrGcRkhrLbJNUfCD6TvxD75MoGtEe+VUEjljm1JHSbj -DrevDyTnak1W4o5/dcy0h6kI6lhHgObbcoAV5CxQ4+HHmeowA/fzedbnIdnHwtNg -SUJEslqoJSiYiiFQLV/yWWfBCWpbIgpDrADU7x9Ccxt6INuxrxOQwf1LZnmVbYs0 -1Mb/O1UFnvW7MeVSR4Nb/4lw6lol+mrR1iF8tTQ+rk4sBdCxw2aU48x3Pjqm+XpV -PIm9uRUr4tRDyQfmBZuxWTNJ9NSx5zVpLEPhDmyOW5wlSw+aKGscu9+RjBx/gXPk -sK8jZi441ojEJ7OaggGPheO3mCU= ------END CERTIFICATE----- diff --git a/e2e/testdata/notary/notary-config.json b/e2e/testdata/notary/notary-config.json deleted file mode 100644 index a4aed592a6b5..000000000000 --- a/e2e/testdata/notary/notary-config.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "server": { - "http_addr": "notary-server:4443", - "tls_key_file": "./notary-server.key", - "tls_cert_file": "./notary-server.cert" - }, - "trust_service": { - "type": "local", - "hostname": "", - "port": "", - "key_algorithm": "ed25519" - }, - "logging": { - "level": "debug" - }, - "storage": { - "backend": "memory" - } -} diff --git a/e2e/testdata/notary/notary-server.cert b/e2e/testdata/notary/notary-server.cert deleted file mode 100644 index eee2b3463d0d..000000000000 --- a/e2e/testdata/notary/notary-server.cert +++ /dev/null @@ -1,40 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIQTujwx+1xxXeI5AbzAQ379TANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCkxDzANBgNVBAoTBkRvY2tlcjEWMBQGA1UEAxMN -bm90YXJ5LXNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPhZ -pU7DRK/2nwbTu+kVYhU/XARDleVSiLrQ5RMR1Cz2xC4LWkOEVSj4aCBo85O66JAx -p+WRVwoVEU2rdkK3k4983Xr34+7q5Hv4hmwlg6I7QLRRJapEgK5G5RB/9aQntolx -h5E0KaoF4PJP25y4FHCUr4td4QyitaICsCpuOAN6XgmE9sM9TBf+AEjTSxwwvgEz -DqHvyovl7pA+pQP2oTKBrf6KN8hHDOXmm9gd8ST9yKLrsYWhqExLLPnAD4lQEcKZ -29g+iTd4eNoJUXctpuY+3IpqBcQSLq35mNKBP/FQco6g3q26/cB4zWGxTr3jGJqs -ms8qdFLGZ2KiBCt+oDMCAwEAAaOBoTCBnjAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0T -AQH/BAIwADAfBgNVHSMEGDAWgBTxYMNqgy2wkgmPZL/+bTCTQo6ulTBdBgNVHREE -VjBUgg1ub3Rhcnktc2VydmVyggxub3RhcnlzZXJ2ZXKCEmV2aWwtbm90YXJ5LXNl -cnZlcoIQZXZpbG5vdGFyeXNlcnZlcoIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3 -DQEBCwUAA4IBAQCDMjuZnNINFfqstFwhEEvAgWbjYW26ZQxhQffaqDlbMIQkWoXj -8inld9bma9Mf5i/GAkUwFqCnEHD4LQ6vDgfAgL+pSOv9VI5SBEuk/gLqvIBUeIRu -uHo1jWtll2Fr7eDLVdD4mPRPFC7V6mv6sFa9EN4tBN8eheQxHJvzwnnU7X28prfI -/hWnwPWScVvttqBSsq1h2CUpVu2zGVToeCJ9xl4r/NyDtM5TyMgz7RLrer0p8NSu -4Qp4ZXtxHDLduWcyMUHLGTprW05yjj9UVq89xfaCOqFSpx5i4oxotYm1PoOacHmN -RMp9vaYMAmopoxIEYX6fDg5T3sQ5cidZJEvU ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIDEDCCAfigAwIBAgIQdxGVILXsVcogexr+Ia2MZDANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCIxDzANBgNVBAoTBkRvY2tlcjEPMA0GA1UEAxMG -RG9ja2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3sA/g7Srrkz -uEf1Qa2jAw93EfEJvxU1ZmZ30aB7KLgHN2TznxAGYtNekAu88CV4H3PKS44BZOar -wOo3KL4wQffLt7lmsRJG1KOfyiAmjmvidP5JSeRdTiBtj4CCVoi3EE6BZXPpZjst -9OSOlld2bWWXHb2ZdoY3ZAhZ9rn3tVwyfoLKpuESp1WZSFHPIdcuoMmZPtqD0bSi -5hc4gVFNLlZOAILvUkXxcHKUgLHZg0YEDQWsYjqh8EYp5LUK2tt4Mpz0HwAt9siE -VxHGIsiEqG1ajmxZiS28nlRWc4JRlOdmy5x1TPzJTDy+49gxB4njp1nRUtUgzmaG -QHhml35xHQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU8WDDaoMtsJIJj2S//m0wk0KOrpUwDQYJKoZIhvcNAQELBQAD -ggEBAKZJfQjjfqn0K/UlzmrGcRkhrLbJNUfCD6TvxD75MoGtEe+VUEjljm1JHSbj -DrevDyTnak1W4o5/dcy0h6kI6lhHgObbcoAV5CxQ4+HHmeowA/fzedbnIdnHwtNg -SUJEslqoJSiYiiFQLV/yWWfBCWpbIgpDrADU7x9Ccxt6INuxrxOQwf1LZnmVbYs0 -1Mb/O1UFnvW7MeVSR4Nb/4lw6lol+mrR1iF8tTQ+rk4sBdCxw2aU48x3Pjqm+XpV -PIm9uRUr4tRDyQfmBZuxWTNJ9NSx5zVpLEPhDmyOW5wlSw+aKGscu9+RjBx/gXPk -sK8jZi441ojEJ7OaggGPheO3mCU= ------END CERTIFICATE----- diff --git a/e2e/testdata/notary/notary-server.key b/e2e/testdata/notary/notary-server.key deleted file mode 100644 index 7f7562a9defd..000000000000 --- a/e2e/testdata/notary/notary-server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA+FmlTsNEr/afBtO76RViFT9cBEOV5VKIutDlExHULPbELgta -Q4RVKPhoIGjzk7rokDGn5ZFXChURTat2QreTj3zdevfj7urke/iGbCWDojtAtFEl -qkSArkblEH/1pCe2iXGHkTQpqgXg8k/bnLgUcJSvi13hDKK1ogKwKm44A3peCYT2 -wz1MF/4ASNNLHDC+ATMOoe/Ki+XukD6lA/ahMoGt/oo3yEcM5eab2B3xJP3Iouux -haGoTEss+cAPiVARwpnb2D6JN3h42glRdy2m5j7cimoFxBIurfmY0oE/8VByjqDe -rbr9wHjNYbFOveMYmqyazyp0UsZnYqIEK36gMwIDAQABAoIBAQDy7W2f763+mbTQ -zshepQX+Vq3BlgLIAMWyR6fr0WLEYNVhXMV8ibNrkiD4ovCwLwJSGeBr1JFZUWZN -nUze0gdLMg7LvDN/ftDk2yNSIhfy1xbhywaW2M8uqjZiv2genKIXK7A6PtYKdBmn -rKnbUMzdmvNj1f7Ph1E4Gn0L5ChybJDJrq6wQjuTdZ6RmkGkbid0L+47Uv+6xBm9 -hgBPVXd8auQAYGmyXZwvfga5ZjfRMI4wvWkvjOAQcJtxxgOnLT1KDjYV+L70PWul -bYoKX0sNkFEP9tOq2pD9XVBuTVQxcYeztv0Vz+kG66Ju1KKCAnUYFhRt055zZLfm -WDYlWm0BAoGBAPvGW9LvzwCDE9QUcR46nG1ZihheJyGKwWVK+ZjYkUU9nLbrIpOD -/jmihoHHhKBC6YOfHHY73LtZ22fgXEu6ivDzZtTxBErXbdRpEKktJebRK7gPkfsB -PNQ8CRd/DxRC/JuVFR76OPsbZWhXCaeC7PRdyAtvU9toT1jIQf+a4OhBAoGBAPyE -kxEoNO1KhWtgByUlsPzvq9PaTjwW/LpmEoo0FBUhYRPxYzVuYrE0BBflDR6JcMRR -oE9CXYGjtVPB44gT7pHVP09f3Ugrxk7X+t8wy3PWUTaTprmmEGqF0TzfdH4oQz0Y -v1khwuIu6rRlddGEiCKldXxn+gJy9E70yO4bm4tzAoGAL/XFIBVWVT6i1E9gjOWV -Tq8zwxiMU7Ney7DQgvEeGxZ1d9Kqr3cBQnFXNfmPpgeY+92fSlZ04atoRA1VB4ft -V6DGAeI3cxo+bavl5JQZGDLYJSOyJyJBOByHjtZBRRbNj8WCVHhNymeZlZqe2C30 -fUgwBx2Z172y/7KF/+680QECgYEA1GhUKQ9wDdYsiliZSgb9bJXSLH8qZeNULRrl -J3mNFwUf2p2mvPAgdjxx4QOb2H716z1aIrGJZB4nzc9/LBzQBb2h5ouV4DpqMjH8 -5bbuvH6fi9ABY5Irpt7vVUwFeoU1ofPqKPh8LLQYWywpQddAiBwzyjTQGTVHCg9f -4OI6Ib8CgYAptl24MGOc6BminKgsux+vNS9X1WwIADiHDyWBPHeQgLX8bYegswq9 -/6uGXJQgdFBhfLuoTBBN0ia/0QQhDezzrqnERddciuL2zxFxEETdpIuxm4lhieX7 -9LqnFcjxM4sLCg4SDSRX+nburiCnLDQiaBzhARooMJO48luTZUiWYQ== ------END RSA PRIVATE KEY----- diff --git a/e2e/testdata/notary/root-ca.cert b/e2e/testdata/notary/root-ca.cert deleted file mode 100644 index e7411c14bef8..000000000000 --- a/e2e/testdata/notary/root-ca.cert +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEDCCAfigAwIBAgIQdxGVILXsVcogexr+Ia2MZDANBgkqhkiG9w0BAQsFADAi -MQ8wDQYDVQQKEwZEb2NrZXIxDzANBgNVBAMTBkRvY2tlcjAeFw0yMzAzMjcxMTA5 -NTBaFw0zMzAzMjQxMTA5NTBaMCIxDzANBgNVBAoTBkRvY2tlcjEPMA0GA1UEAxMG -RG9ja2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3sA/g7Srrkz -uEf1Qa2jAw93EfEJvxU1ZmZ30aB7KLgHN2TznxAGYtNekAu88CV4H3PKS44BZOar -wOo3KL4wQffLt7lmsRJG1KOfyiAmjmvidP5JSeRdTiBtj4CCVoi3EE6BZXPpZjst -9OSOlld2bWWXHb2ZdoY3ZAhZ9rn3tVwyfoLKpuESp1WZSFHPIdcuoMmZPtqD0bSi -5hc4gVFNLlZOAILvUkXxcHKUgLHZg0YEDQWsYjqh8EYp5LUK2tt4Mpz0HwAt9siE -VxHGIsiEqG1ajmxZiS28nlRWc4JRlOdmy5x1TPzJTDy+49gxB4njp1nRUtUgzmaG -QHhml35xHQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB -/zAdBgNVHQ4EFgQU8WDDaoMtsJIJj2S//m0wk0KOrpUwDQYJKoZIhvcNAQELBQAD -ggEBAKZJfQjjfqn0K/UlzmrGcRkhrLbJNUfCD6TvxD75MoGtEe+VUEjljm1JHSbj -DrevDyTnak1W4o5/dcy0h6kI6lhHgObbcoAV5CxQ4+HHmeowA/fzedbnIdnHwtNg -SUJEslqoJSiYiiFQLV/yWWfBCWpbIgpDrADU7x9Ccxt6INuxrxOQwf1LZnmVbYs0 -1Mb/O1UFnvW7MeVSR4Nb/4lw6lol+mrR1iF8tTQ+rk4sBdCxw2aU48x3Pjqm+XpV -PIm9uRUr4tRDyQfmBZuxWTNJ9NSx5zVpLEPhDmyOW5wlSw+aKGscu9+RjBx/gXPk -sK8jZi441ojEJ7OaggGPheO3mCU= ------END CERTIFICATE----- diff --git a/e2e/testutils/plugins.go b/e2e/testutils/plugins.go index 1485f425b1fa..3dde6ba9349d 100644 --- a/e2e/testutils/plugins.go +++ b/e2e/testutils/plugins.go @@ -12,7 +12,7 @@ import ( "path/filepath" "testing" - "github.com/docker/docker/api/types" + "github.com/moby/moby/api/types/plugin" "gotest.tools/v3/assert" "gotest.tools/v3/fs" "gotest.tools/v3/icmd" @@ -22,17 +22,23 @@ import ( var plugins embed.FS // SetupPlugin builds a plugin and creates a temporary -// directory with the plugin's config.json and rootfs. -func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir { +// directory with the plugin's config.json and rootfs, +// which will be removed in [t.Cleanup]. It returns +// the location of the temporary directory. +func SetupPlugin(t *testing.T, ctx context.Context) (pluginDir string) { t.Helper() - p := &types.PluginConfig{ - Linux: types.PluginConfigLinux{ + p := &plugin.Config{ + Linux: plugin.LinuxConfig{ Capabilities: []string{"CAP_SYS_ADMIN"}, }, - Interface: types.PluginConfigInterface{ + Interface: plugin.Interface{ Socket: "basic.sock", - Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}}, + Types: []plugin.CapabilityID{{ + Capability: "dummy", + Prefix: "docker", + Version: "1.0", + }}, }, Entrypoint: []string{"/basic"}, } @@ -48,7 +54,7 @@ func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir { ) icmd.RunCommand("/bin/cp", binPath, dir.Join("rootfs", p.Entrypoint[0])).Assert(t, icmd.Success) - return dir + return dir.Path() } // buildPlugin uses Go to build a plugin from one of the source files in the plugins directory. diff --git a/e2e/trust/main_test.go b/e2e/trust/main_test.go deleted file mode 100644 index 5881adcdac8a..000000000000 --- a/e2e/trust/main_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package trust - -import ( - "fmt" - "os" - "testing" - - "github.com/docker/cli/internal/test/environment" -) - -func TestMain(m *testing.M) { - if err := environment.Setup(); err != nil { - fmt.Println(err.Error()) - os.Exit(3) - } - os.Exit(m.Run()) -} diff --git a/e2e/trust/revoke_test.go b/e2e/trust/revoke_test.go deleted file mode 100644 index 8ee5dc4ad973..000000000000 --- a/e2e/trust/revoke_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package trust - -import ( - "testing" - - "github.com/docker/cli/e2e/internal/fixtures" - "github.com/docker/cli/internal/test/environment" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/fs" - "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" -) - -const ( - revokeImage = "registry:5000/revoke:v1" - revokeRepo = "registry:5000/revokerepo" -) - -func TestRevokeImage(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - setupTrustedImagesForRevoke(t, dir) - result := icmd.RunCmd( - icmd.Command("docker", "trust", "revoke", revokeImage), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithNotary, fixtures.WithConfig(dir.Path())) - result.Assert(t, icmd.Success) - assert.Check(t, is.Contains(result.Stdout(), "Successfully deleted signature for registry:5000/revoke:v1")) -} - -func TestRevokeRepo(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - setupTrustedImagesForRevokeRepo(t, dir) - result := icmd.RunCmd( - icmd.Command("docker", "trust", "revoke", revokeRepo, "-y"), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithNotary, fixtures.WithConfig(dir.Path())) - result.Assert(t, icmd.Success) - assert.Check(t, is.Contains(result.Stdout(), "Successfully deleted signature for registry:5000/revoke")) -} - -func setupTrustedImagesForRevoke(t *testing.T, dir fs.Dir) { - t.Helper() - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, revokeImage).Assert(t, icmd.Success) - icmd.RunCmd( - icmd.Command("docker", "-D", "trust", "sign", revokeImage), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success) -} - -func setupTrustedImagesForRevokeRepo(t *testing.T, dir fs.Dir) { - t.Helper() - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, revokeRepo+":v1").Assert(t, icmd.Success) - icmd.RunCmd( - icmd.Command("docker", "-D", "trust", "sign", revokeRepo+":v1"), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.BusyboxImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, revokeRepo+":v2").Assert(t, icmd.Success) - icmd.RunCmd( - icmd.Command("docker", "-D", "trust", "sign", revokeRepo+":v2"), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary).Assert(t, icmd.Success) -} diff --git a/e2e/trust/sign_test.go b/e2e/trust/sign_test.go deleted file mode 100644 index 44bb007826c2..000000000000 --- a/e2e/trust/sign_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package trust - -import ( - "testing" - - "github.com/docker/cli/e2e/internal/fixtures" - "github.com/docker/cli/internal/test/environment" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" - "gotest.tools/v3/fs" - "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" -) - -const ( - localImage = "registry:5000/signlocal:v1" - signImage = "registry:5000/sign:v1" -) - -func TestSignLocalImage(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - // Digests in golden files are linux/amd64 specific. - // TODO: Fix this test and make it work on all platforms. - environment.SkipIfNotPlatform(t, "linux/amd64") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, signImage).Assert(t, icmd.Success) - result := icmd.RunCmd( - icmd.Command("docker", "trust", "sign", signImage), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary) - result.Assert(t, icmd.Success) - assert.Check(t, is.Contains(result.Stdout(), "v1: digest: sha256:"+fixtures.AlpineSha)) -} - -func TestSignWithLocalFlag(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - // Digests in golden files are linux/amd64 specific. - // TODO: Fix this test and make it work on all platforms. - environment.SkipIfNotPlatform(t, "linux/amd64") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - setupTrustedImageForOverwrite(t, dir) - result := icmd.RunCmd( - icmd.Command("docker", "trust", "sign", "--local", localImage), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary) - result.Assert(t, icmd.Success) - assert.Check(t, is.Contains(result.Stdout(), "v1: digest: sha256:"+fixtures.BusyboxSha)) -} - -func setupTrustedImageForOverwrite(t *testing.T, dir fs.Dir) { - t.Helper() - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.AlpineImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.AlpineImage, localImage).Assert(t, icmd.Success) - result := icmd.RunCmd( - icmd.Command("docker", "-D", "trust", "sign", localImage), - fixtures.WithPassphrase("root_password", "repo_password"), - fixtures.WithConfig(dir.Path()), fixtures.WithNotary) - result.Assert(t, icmd.Success) - assert.Check(t, is.Contains(result.Stdout(), "v1: digest: sha256:"+fixtures.AlpineSha)) - icmd.RunCmd(icmd.Command("docker", "pull", fixtures.BusyboxImage)).Assert(t, icmd.Success) - icmd.RunCommand("docker", "tag", fixtures.BusyboxImage, localImage).Assert(t, icmd.Success) -} diff --git a/internal/commands/commands.go b/internal/commands/commands.go new file mode 100644 index 000000000000..2a1750625c09 --- /dev/null +++ b/internal/commands/commands.go @@ -0,0 +1,39 @@ +package commands + +import ( + "os" + + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +var commands []func(command.Cli) *cobra.Command + +// Register pushes the passed in command into an internal queue which can +// be retrieved using the [Commands] function. It is designed to be called +// in an init function and is not safe for concurrent use. +func Register(f func(command.Cli) *cobra.Command) { + commands = append(commands, f) +} + +// RegisterLegacy functions similarly to [Register], but it checks the +// "DOCKER_HIDE_LEGACY_COMMANDS" environment variable and if it has been +// set and is non-empty, the command will be hidden. It is designed to be called +// in an init function and is not safe for concurrent use. +func RegisterLegacy(f func(command.Cli) *cobra.Command) { + commands = append(commands, func(c command.Cli) *cobra.Command { + if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" { + return f(c) + } + cmd := f(c) + cmd.Hidden = true + cmd.Aliases = []string{} + return cmd + }) +} + +// Commands returns the internal queue holding registered commands added +// via [Register] and [RegisterLegacy]. +func Commands() []func(command.Cli) *cobra.Command { + return commands +} diff --git a/internal/jsonstream/display.go b/internal/jsonstream/display.go index 8981eca3f6f6..de9d1e2cbcc2 100644 --- a/internal/jsonstream/display.go +++ b/internal/jsonstream/display.go @@ -4,14 +4,15 @@ import ( "context" "io" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/cli/cli/streams" + "github.com/moby/moby/api/types/jsonstream" + "github.com/moby/moby/client/pkg/jsonmessage" ) type ( - Stream = jsonmessage.Stream - JSONMessage = jsonmessage.JSONMessage - JSONError = jsonmessage.JSONError - JSONProgress = jsonmessage.JSONProgress + JSONError = jsonstream.Error + JSONMessage = jsonstream.Message + JSONProgress = jsonstream.Progress ) type ctxReader struct { @@ -46,7 +47,7 @@ func WithAuxCallback(cb func(JSONMessage)) Options { // "context aware" and appropriately returns why the function was canceled. // // It returns an error if the context is canceled, but not if the input reader / stream is closed. -func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error { +func Display(ctx context.Context, in io.Reader, stream *streams.Out, opts ...Options) error { if ctx.Err() != nil { return ctx.Err() } diff --git a/internal/jsonstream/display_test.go b/internal/jsonstream/display_test.go index 7b14db2a9fac..05bed621e2b2 100644 --- a/internal/jsonstream/display_test.go +++ b/internal/jsonstream/display_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/docker/cli/cli/streams" + "github.com/moby/moby/api/types/jsonstream" "gotest.tools/v3/assert" ) @@ -30,11 +31,9 @@ func TestDisplay(t *testing.T) { return default: err := enc.Encode(JSONMessage{ - Status: "Downloading", - ID: fmt.Sprintf("id-%d", i), - TimeNano: time.Now().UnixNano(), - Time: time.Now().Unix(), - Progress: &JSONProgress{ + Status: "Downloading", + ID: fmt.Sprintf("id-%d", i), + Progress: &jsonstream.Progress{ Current: int64(i), Total: 100, Start: 0, diff --git a/internal/oauth/api/api.go b/internal/oauth/api/api.go index e41a75f41de5..13d107db6973 100644 --- a/internal/oauth/api/api.go +++ b/internal/oauth/api/api.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package api diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 115006436404..96deb61b1325 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -14,8 +14,8 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/oauth" "github.com/docker/cli/internal/oauth/api" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/tui" - "github.com/docker/docker/registry" "github.com/morikuni/aec" "github.com/sirupsen/logrus" diff --git a/internal/registry/auth.go b/internal/registry/auth.go new file mode 100644 index 000000000000..e15a139cb471 --- /dev/null +++ b/internal/registry/auth.go @@ -0,0 +1,126 @@ +package registry + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + "github.com/containerd/log" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" + "github.com/moby/moby/api/types/registry" +) + +// AuthClientID is used the ClientID used for the token server +const AuthClientID = "docker" + +type loginCredentialStore struct { + authConfig *registry.AuthConfig +} + +func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { + return lcs.authConfig.Username, lcs.authConfig.Password +} + +func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { + return lcs.authConfig.IdentityToken +} + +func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { + lcs.authConfig.IdentityToken = token +} + +// loginV2 tries to login to the v2 registry server. The given registry +// endpoint will be pinged to get authorization challenges. These challenges +// will be used to authenticate against the registry to validate credentials. +func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) { + endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" + log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint") + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody) + if err != nil { + return "", err + } + + var ( + modifiers = Headers(userAgent, nil) + authTrans = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) + credentialAuthConfig = *authConfig + creds = loginCredentialStore{authConfig: &credentialAuthConfig} + ) + + loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil) + if err != nil { + return "", err + } + + resp, err := loginClient.Do(req) + if err != nil { + err = translateV2AuthError(err) + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + // TODO(dmcgowan): Attempt to further interpret result, status code and error code string + return "", fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return credentialAuthConfig.IdentityToken, nil +} + +func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { + challengeManager, err := PingV2Registry(endpoint, authTransport) + if err != nil { + return nil, err + } + + authHandlers := []auth.AuthenticationHandler{ + auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + OfflineAccess: true, + ClientID: AuthClientID, + Scopes: scopes, + }), + auth.NewBasicHandler(creds), + } + + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...)) + + return &http.Client{ + Transport: transport.NewTransport(authTransport, modifiers...), + Timeout: 15 * time.Second, + }, nil +} + +// PingV2Registry attempts to ping a v2 registry and on success return a +// challenge manager for the supported authentication types. +// If a response is received but cannot be interpreted, a PingResponseError will be returned. +func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) { + endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" + req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody) + if err != nil { + return nil, err + } + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 15 * time.Second, + } + resp, err := pingClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + challengeManager := challenge.NewSimpleManager() + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + + return challengeManager, nil +} diff --git a/internal/registry/config.go b/internal/registry/config.go new file mode 100644 index 000000000000..ef68ebf8b421 --- /dev/null +++ b/internal/registry/config.go @@ -0,0 +1,310 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +package registry + +import ( + "context" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/containerd/log" + "github.com/distribution/reference" + "github.com/moby/moby/api/types/registry" +) + +// ServiceOptions holds command line options. +// +// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically. +type ServiceOptions struct { + InsecureRegistries []string `json:"insecure-registries,omitempty"` +} + +// serviceConfig holds daemon configuration for the registry service. +// +// It's a reduced version of [registry.ServiceConfig] for the CLI. +type serviceConfig struct { + insecureRegistryCIDRs []*net.IPNet + indexConfigs map[string]*registry.IndexInfo +} + +// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains +// are here for historic reasons and backward-compatibility. These domains +// are still supported by Docker Hub (and will continue to be supported), but +// there are new domains already in use, and plans to consolidate all legacy +// domains to new "canonical" domains. Once those domains are decided on, we +// should update these consts (but making sure to preserve compatibility with +// existing installs, clients, and user configuration). +const ( + // DefaultNamespace is the default namespace + DefaultNamespace = "docker.io" + // IndexHostname is the index hostname, used for authentication and image search. + IndexHostname = "index.docker.io" + // IndexServer is used for user auth and image search + IndexServer = "https://index.docker.io/v1/" + // IndexName is the name of the index + IndexName = "docker.io" +) + +var ( + // DefaultV2Registry is the URI of the default (Docker Hub) registry + // used for pushing and pulling images. This hostname is hard-coded to handle + // the conversion from image references without registry name (e.g. "ubuntu", + // or "ubuntu:latest"), as well as references using the "docker.io" domain + // name, which is used as canonical reference for images on Docker Hub, but + // does not match the domain-name of Docker Hub's registry. + DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"} + + validHostPortRegex = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) + }) +) + +// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], +// but inlining it to prevent adding that as a dependency for docker/cli. +// +// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 +func runningWithRootlessKit() bool { + return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != "" +} + +// CertsDir is the directory where certificates are stored. +// +// - Linux: "/etc/docker/certs.d/" +// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/" +// - Windows: "%PROGRAMDATA%/docker/certs.d/" +// +// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless. +func CertsDir() string { + certsDir := "/etc/docker/certs.d" + if runningWithRootlessKit() { + if configHome, _ := os.UserConfigDir(); configHome != "" { + certsDir = filepath.Join(configHome, "docker", "certs.d") + } + } else if runtime.GOOS == "windows" { + certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d") + } + return certsDir +} + +// newServiceConfig creates a new service config with the given options. +func newServiceConfig(registries []string) (*serviceConfig, error) { + if len(registries) == 0 { + return &serviceConfig{}, nil + } + // Localhost is by default considered as an insecure registry. This is a + // stop-gap for people who are running a private registry on localhost. + registries = append(registries, "::1/128", "127.0.0.0/8") + + var ( + insecureRegistryCIDRs = make([]*net.IPNet, 0) + indexConfigs = make(map[string]*registry.IndexInfo) + ) + +skip: + for _, r := range registries { + if scheme, host, ok := strings.Cut(r, "://"); ok { + switch strings.ToLower(scheme) { + case "http", "https": + log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme) + r = host + default: + // unsupported scheme + return nil, invalidParam(fmt.Errorf("insecure registry %s should not contain '://'", r)) + } + } + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. + for _, value := range insecureRegistryCIDRs { + if value.IP.String() == ipnet.IP.String() && value.Mask.String() == ipnet.Mask.String() { + continue skip + } + } + // ipnet is not found, add it in config.InsecureRegistryCIDRs + insecureRegistryCIDRs = append(insecureRegistryCIDRs, ipnet) + } else { + if err := validateHostPort(r); err != nil { + return nil, invalidParam(fmt.Errorf("insecure registry %s is not valid: %w", r, err)) + } + // Assume `host:port` if not CIDR. + indexConfigs[r] = ®istry.IndexInfo{ + Name: r, + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + indexConfigs[IndexName] = ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + + return &serviceConfig{ + indexConfigs: indexConfigs, + insecureRegistryCIDRs: insecureRegistryCIDRs, + }, nil +} + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +func (config *serviceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides newIndexInfo, in order to honor per-index configurations. + if index, ok := config.indexConfigs[indexName]; ok { + return index.Secure + } + + return !isCIDRMatch(config.insecureRegistryCIDRs, indexName) +} + +// for mocking in unit tests. +var lookupIP = net.LookupIP + +// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host") +// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be +// resolved to IP addresses for matching. If resolution fails, false is returned. +func isCIDRMatch(cidrs []*net.IPNet, urlHost string) bool { + if len(cidrs) == 0 { + return false + } + + host, _, err := net.SplitHostPort(urlHost) + if err != nil { + // Assume urlHost is a host without port and go on. + host = urlHost + } + + var addresses []net.IP + if ip := net.ParseIP(host); ip != nil { + // Host is an IP-address. + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-address. + addresses, err = lookupIP(host) + if err != nil { + // We failed to resolve the host; assume there's no match. + return false + } + } + + for _, addr := range addresses { + for _, ipnet := range cidrs { + // check if the addr falls in the subnet + if ipnet.Contains(addr) { + return true + } + } + } + + return false +} + +func normalizeIndexName(val string) string { + if val == "index.docker.io" { + return "docker.io" + } + return val +} + +func validateHostPort(s string) error { + // Split host and port, and in case s can not be split, assume host only + host, port, err := net.SplitHostPort(s) + if err != nil { + host = s + port = "" + } + // If match against the `host:port` pattern fails, + // it might be `IPv6:port`, which will be captured by net.ParseIP(host) + if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil { + return invalidParamf("invalid host %q", host) + } + if port != "" { + v, err := strconv.Atoi(port) + if err != nil { + return err + } + if v < 0 || v > 65535 { + return invalidParamf("invalid port %q", port) + } + } + return nil +} + +// NewIndexInfo creates a new [registry.IndexInfo] or the given +// repository-name, and detects whether the registry is considered +// "secure" (non-localhost). +func NewIndexInfo(reposName reference.Named) *registry.IndexInfo { + indexName := normalizeIndexName(reference.Domain(reposName)) + if indexName == IndexName { + return ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + } + + return ®istry.IndexInfo{ + Name: indexName, + Secure: !isInsecure(indexName), + } +} + +// isInsecure is used to detect whether a registry domain or IP-address is allowed +// to use an insecure (non-TLS, or self-signed cert) connection according to the +// defaults, which allows for insecure connections with registries running on a +// loopback address ("localhost", "::1/128", "127.0.0.0/8"). +// +// It is used in situations where we don't have access to the daemon's configuration, +// for example, when used from the client / CLI. +func isInsecure(hostNameOrIP string) bool { + // Attempt to strip port if present; this also strips brackets for + // IPv6 addresses with a port (e.g. "[::1]:5000"). + // + // This is best-effort; we'll continue using the address as-is if it fails. + if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { + hostNameOrIP = host + } + if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { + // Fast path; no need to resolve these, assuming nobody overrides + // "localhost" for anything else than a loopback address (sorry, not sorry). + return true + } + + var addresses []net.IP + if ip := net.ParseIP(hostNameOrIP); ip != nil { + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-addresses. + addrs, _ := lookupIP(hostNameOrIP) + addresses = append(addresses, addrs...) + } + + for _, addr := range addresses { + if addr.IsLoopback() { + return true + } + } + return false +} diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go new file mode 100644 index 000000000000..9c21929f281b --- /dev/null +++ b/internal/registry/config_test.go @@ -0,0 +1,92 @@ +package registry + +import ( + "testing" + + "github.com/containerd/errdefs" + "gotest.tools/v3/assert" +) + +func TestLoadInsecureRegistries(t *testing.T) { + testCases := []struct { + registries []string + index string + err string + }{ + { + registries: []string{"127.0.0.1"}, + index: "127.0.0.1", + }, + { + registries: []string{"127.0.0.1:8080"}, + index: "127.0.0.1:8080", + }, + { + registries: []string{"2001:db8::1"}, + index: "2001:db8::1", + }, + { + registries: []string{"[2001:db8::1]:80"}, + index: "[2001:db8::1]:80", + }, + { + registries: []string{"http://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"https://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"HTTP://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"svn://myregistry.example.com"}, + err: "insecure registry svn://myregistry.example.com should not contain '://'", + }, + { + registries: []string{`mytest-.com`}, + err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`, + }, + { + registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`}, + err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`, + }, + { + registries: []string{`myregistry.example.com:500000`}, + err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`, + }, + { + registries: []string{`"myregistry.example.com"`}, + err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`, + }, + { + registries: []string{`"myregistry.example.com:5000"`}, + err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`, + }, + } + for _, testCase := range testCases { + config, err := newServiceConfig(testCase.registries) + if testCase.err == "" { + if err != nil { + t.Fatalf("expect no error, got '%s'", err) + } + match := false + for index := range config.indexConfigs { + if index == testCase.index { + match = true + } + } + if !match { + t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.indexConfigs) + } + } else { + if err == nil { + t.Fatalf("expect error '%s', got no error", testCase.err) + } + assert.ErrorContains(t, err, testCase.err) + assert.Check(t, errdefs.IsInvalidArgument(err)) + } + } +} diff --git a/internal/registry/doc.go b/internal/registry/doc.go new file mode 100644 index 000000000000..0b6a24767c84 --- /dev/null +++ b/internal/registry/doc.go @@ -0,0 +1,12 @@ +// Package registry is a fork of [github.com/docker/docker/registry], taken +// at commit [moby@49306c6]. Git history was not preserved in this fork, +// but can be found using the URLs provided. +// +// This fork was created to remove the dependency on the "Moby" codebase, +// and because the CLI only needs a subset of its features. The original +// package was written specifically for use in the daemon code, and includes +// functionality that cannot be used in the CLI. +// +// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry +// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry +package registry diff --git a/internal/registry/errors.go b/internal/registry/errors.go new file mode 100644 index 000000000000..05638eb7088a --- /dev/null +++ b/internal/registry/errors.go @@ -0,0 +1,51 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + +package registry + +import ( + "errors" + "fmt" + "net/url" + + "github.com/docker/distribution/registry/api/errcode" +) + +func translateV2AuthError(err error) error { + var e *url.Error + if errors.As(err, &e) { + var e2 errcode.Error + if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) { + return unauthorizedErr{err} + } + } + return err +} + +func invalidParam(err error) error { + return invalidParameterErr{err} +} + +func invalidParamf(format string, args ...any) error { + return invalidParameterErr{fmt.Errorf(format, args...)} +} + +type unauthorizedErr struct{ error } + +func (unauthorizedErr) Unauthorized() {} + +func (e unauthorizedErr) Cause() error { + return e.error +} + +func (e unauthorizedErr) Unwrap() error { + return e.error +} + +type invalidParameterErr struct{ error } + +func (invalidParameterErr) InvalidParameter() {} + +func (e invalidParameterErr) Unwrap() error { + return e.error +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go new file mode 100644 index 000000000000..7832529ee671 --- /dev/null +++ b/internal/registry/registry.go @@ -0,0 +1,149 @@ +// Package registry contains client primitives to interact with a remote Docker registry. +package registry + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/containerd/log" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/go-connections/tlsconfig" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// hostCertsDir returns the config directory for a specific host. +func hostCertsDir(hostnameAndPort string) string { + if runtime.GOOS == "windows" { + // Ensure that a directory name is valid; hostnameAndPort may contain + // a colon (:) if a port is included, and Windows does not allow colons + // in directory names. + hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", "")) + } + return filepath.Join(CertsDir(), hostnameAndPort) +} + +// newTLSConfig constructs a client TLS configuration based on server defaults +func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) { + // PreferredServerCipherSuites should have no effect + tlsConfig := tlsconfig.ServerDefault() + tlsConfig.InsecureSkipVerify = !isSecure + + if isSecure { + hostDir := hostCertsDir(hostname) + log.G(ctx).Debugf("hostDir: %s", hostDir) + if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil { + return nil, err + } + } + + return tlsConfig, nil +} + +func hasFile(files []os.DirEntry, name string) bool { + for _, f := range files { + if f.Name() == name { + return true + } + } + return false +} + +// loadTLSConfig reads the directory for TLS certificates including roots and +// certificate pairs, and updates the provided TLS configuration. +func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error { + fs, err := os.ReadDir(directory) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return invalidParam(err) + } + + for _, f := range fs { + if ctx.Err() != nil { + return ctx.Err() + } + switch filepath.Ext(f.Name()) { + case ".crt": + if tlsConfig.RootCAs == nil { + systemPool, err := tlsconfig.SystemCertPool() + if err != nil { + return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err)) + } + tlsConfig.RootCAs = systemPool + } + fileName := filepath.Join(directory, f.Name()) + log.G(ctx).Debugf("crt: %s", fileName) + data, err := os.ReadFile(fileName) + if err != nil { + return err + } + tlsConfig.RootCAs.AppendCertsFromPEM(data) + case ".cert": + certName := f.Name() + keyName := certName[:len(certName)-5] + ".key" + log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName)) + if !hasFile(fs, keyName) { + return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName) + } + cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) + if err != nil { + return err + } + tlsConfig.Certificates = append(tlsConfig.Certificates, cert) + case ".key": + keyName := f.Name() + certName := keyName[:len(keyName)-4] + ".cert" + log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName)) + if !hasFile(fs, certName) { + return invalidParamf("missing client certificate %s for key %s", certName, keyName) + } + } + } + + return nil +} + +// Headers returns request modifiers with a User-Agent and metaHeaders +func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier { + modifiers := []transport.RequestModifier{} + if userAgent != "" { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{ + "User-Agent": []string{userAgent}, + })) + } + if metaHeaders != nil { + modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) + } + return modifiers +} + +// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the +// default TLS configuration. +func newTransport(tlsConfig *tls.Config) http.RoundTripper { + if tlsConfig == nil { + tlsConfig = tlsconfig.ServerDefault() + } + + return otelhttp.NewTransport( + &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: tlsConfig, + // TODO(dmcgowan): Call close idle connections when complete and use keep alive + DisableKeepAlives: true, + }, + ) +} diff --git a/internal/registry/registry_mock_test.go b/internal/registry/registry_mock_test.go new file mode 100644 index 000000000000..024f6a83f00d --- /dev/null +++ b/internal/registry/registry_mock_test.go @@ -0,0 +1,92 @@ +package registry + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containerd/log" + "github.com/moby/moby/api/types/registry" + "gotest.tools/v3/assert" +) + +var testHTTPServer *httptest.Server + +func init() { + r := http.NewServeMux() + + // /v1/ + r.HandleFunc("/v1/_ping", handlerGetPing) + r.HandleFunc("/v1/search", handlerSearch) + + // /v2/ + r.HandleFunc("/v2/version", handlerGetPing) + + testHTTPServer = httptest.NewServer(handlerAccessLog(r)) +} + +func handlerAccessLog(handler http.Handler) http.Handler { + logHandler := func(w http.ResponseWriter, r *http.Request) { + log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + } + return http.HandlerFunc(logHandler) +} + +func makeURL(req string) string { + return testHTTPServer.URL + req +} + +func writeHeaders(w http.ResponseWriter) { + h := w.Header() + h.Add("Server", "docker-tests/mock") + h.Add("Expires", "-1") + h.Add("Content-Type", "application/json") + h.Add("Pragma", "no-cache") + h.Add("Cache-Control", "no-cache") +} + +func writeResponse(w http.ResponseWriter, message any, code int) { + writeHeaders(w) + w.WriteHeader(code) + body, err := json.Marshal(message) + if err != nil { + _, _ = io.WriteString(w, err.Error()) + return + } + _, _ = w.Write(body) +} + +func handlerGetPing(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + writeResponse(w, true, http.StatusOK) +} + +func handlerSearch(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + result := ®istry.SearchResults{ + Query: "fakequery", + NumResults: 1, + Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}}, + } + writeResponse(w, result, http.StatusOK) +} + +func TestPing(t *testing.T) { + res, err := http.Get(makeURL("/v1/_ping")) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, res.StatusCode, http.StatusOK, "") + assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock") + _ = res.Body.Close() +} diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go new file mode 100644 index 000000000000..676076fc93d4 --- /dev/null +++ b/internal/registry/registry_test.go @@ -0,0 +1,281 @@ +package registry + +import ( + "testing" + + "github.com/distribution/reference" + "github.com/moby/moby/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestNewIndexInfo(t *testing.T) { + type staticRepositoryInfo struct { + Index *registry.IndexInfo + RemoteName string + CanonicalName string + LocalName string + } + + tests := map[string]staticRepositoryInfo{ + "fooo/bar": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "docker.io/fooo/bar", + }, + "library/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "nonlibrary/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "docker.io/nonlibrary/ubuntu", + }, + "ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "other/library": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "docker.io/other/library", + }, + "127.0.0.1:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + }, + "127.0.0.1:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + }, + "[::1]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "[::1]:8000/private/moonbase", + CanonicalName: "[::1]:8000/private/moonbase", + }, + "[::1]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "[::1]:8000/privatebase", + CanonicalName: "[::1]:8000/privatebase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "[::2]:8000/private/moonbase", + CanonicalName: "[::2]:8000/private/moonbase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "[::2]:8000/privatebase", + CanonicalName: "[::2]:8000/privatebase", + }, + "localhost:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + }, + "localhost:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + }, + "example.com/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + }, + "example.com/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + }, + "example.com:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + }, + "example.com:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + }, + "localhost/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + }, + "localhost/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + }, + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "index." + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + "index." + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + } + + for reposName, expected := range tests { + t.Run(reposName, func(t *testing.T) { + named, err := reference.ParseNormalizedNamed(reposName) + assert.NilError(t, err) + + indexInfo := NewIndexInfo(named) + repoInfoName := reference.TrimNamed(named) + + assert.Check(t, is.DeepEqual(indexInfo, expected.Index)) + assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName)) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName)) + assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName)) + }) + } +} diff --git a/internal/registry/service.go b/internal/registry/service.go new file mode 100644 index 000000000000..dbb5a4d31b3e --- /dev/null +++ b/internal/registry/service.go @@ -0,0 +1,86 @@ +package registry + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/containerd/errdefs" + "github.com/containerd/log" + "github.com/moby/moby/api/types/registry" +) + +// Service is a registry service. It tracks configuration data such as a list +// of mirrors. +type Service struct { + config *serviceConfig +} + +// NewService returns a new instance of [Service] ready to be installed into +// an engine. +func NewService(options ServiceOptions) (*Service, error) { + config, err := newServiceConfig(options.InsecureRegistries) + if err != nil { + return nil, err + } + return &Service{config: config}, nil +} + +// Auth contacts the public registry with the provided credentials, +// and returns OK if authentication was successful. +// It can be used to verify the validity of a client's credentials. +func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (token string, _ error) { + registryHostName := IndexHostname + + if authConfig.ServerAddress != "" { + serverAddress := authConfig.ServerAddress + if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { + serverAddress = "https://" + serverAddress + } + u, err := url.Parse(serverAddress) + if err != nil { + return "", invalidParam(fmt.Errorf("unable to parse server address: %w", err)) + } + registryHostName = u.Host + } + + // Lookup endpoints for authentication. + endpoints, err := s.Endpoints(ctx, registryHostName) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return "", err + } + return "", invalidParam(err) + } + + var lastErr error + for _, endpoint := range endpoints { + authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errdefs.IsUnauthorized(err) { + // Failed to authenticate; don't continue with (non-TLS) endpoints. + return "", err + } + // Try next endpoint + log.G(ctx).WithFields(log.Fields{ + "error": err, + "endpoint": endpoint, + }).Infof("Error logging in to endpoint, trying next endpoint") + lastErr = err + continue + } + + return authToken, nil + } + + return "", lastErr +} + +// APIEndpoint represents a remote API endpoint +type APIEndpoint struct { + URL *url.URL + TLSConfig *tls.Config +} diff --git a/internal/registry/service_v2.go b/internal/registry/service_v2.go new file mode 100644 index 000000000000..ccfb5ac50959 --- /dev/null +++ b/internal/registry/service_v2.go @@ -0,0 +1,37 @@ +package registry + +import ( + "context" + "net/url" + + "github.com/docker/go-connections/tlsconfig" +) + +func (s *Service) Endpoints(ctx context.Context, hostname string) ([]APIEndpoint, error) { + if hostname == DefaultNamespace || hostname == IndexHostname { + return []APIEndpoint{{ + URL: DefaultV2Registry, + TLSConfig: tlsconfig.ServerDefault(), + }}, nil + } + + tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) + if err != nil { + return nil, err + } + + endpoints := []APIEndpoint{{ + URL: &url.URL{Scheme: "https", Host: hostname}, + TLSConfig: tlsConfig, + }} + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ + URL: &url.URL{Scheme: "http", Host: hostname}, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + }) + } + + return endpoints, nil +} diff --git a/internal/registryclient/client.go b/internal/registryclient/client.go new file mode 100644 index 000000000000..20f0eda11d7f --- /dev/null +++ b/internal/registryclient/client.go @@ -0,0 +1,196 @@ +package registryclient + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/distribution/reference" + manifesttypes "github.com/docker/cli/cli/manifest/types" + "github.com/docker/distribution" + distributionclient "github.com/docker/distribution/registry/client" + registrytypes "github.com/moby/moby/api/types/registry" + "github.com/opencontainers/go-digest" + "github.com/sirupsen/logrus" +) + +// RegistryClient is a client used to communicate with a Docker distribution +// registry +type RegistryClient interface { + GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) + GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) + MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error + PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) +} + +// NewRegistryClient returns a new RegistryClient with a resolver +func NewRegistryClient(resolver AuthConfigResolver, userAgent string, insecure bool) RegistryClient { + return &client{ + authConfigResolver: resolver, + insecureRegistry: insecure, + userAgent: userAgent, + } +} + +// AuthConfigResolver returns Auth Configuration for an index +type AuthConfigResolver func(ctx context.Context, hostName string) registrytypes.AuthConfig + +type client struct { + authConfigResolver AuthConfigResolver + insecureRegistry bool + userAgent string +} + +// ErrBlobCreated returned when a blob mount request was created +type ErrBlobCreated struct { + From reference.Named + Target reference.Named +} + +func (err ErrBlobCreated) Error() string { + return fmt.Sprintf("blob mounted from: %v to: %v", + err.From, err.Target) +} + +// httpProtoError returned if attempting to use TLS with a non-TLS registry +type httpProtoError struct { + cause error +} + +func (e httpProtoError) Error() string { + return e.cause.Error() +} + +var _ RegistryClient = &client{} + +// MountBlob into the registry, so it can be referenced by a manifest +func (c *client) MountBlob(ctx context.Context, sourceRef reference.Canonical, targetRef reference.Named) error { + repoEndpoint, err := newDefaultRepositoryEndpoint(targetRef, c.insecureRegistry) + if err != nil { + return err + } + repoEndpoint.actions = []string{"pull", "push"} + repo, err := c.getRepositoryForReference(ctx, targetRef, repoEndpoint) + if err != nil { + return err + } + lu, err := repo.Blobs(ctx).Create(ctx, distributionclient.WithMountFrom(sourceRef)) + switch err.(type) { + case distribution.ErrBlobMounted: + logrus.Debugf("mount of blob %s succeeded", sourceRef) + return nil + case nil: + default: + return fmt.Errorf("failed to mount blob %s to %s: %w", sourceRef, targetRef, err) + } + _ = lu.Cancel(ctx) + logrus.Debugf("mount of blob %s created", sourceRef) + return ErrBlobCreated{From: sourceRef, Target: targetRef} +} + +// PutManifest sends the manifest to a registry and returns the new digest +func (c *client) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) { + repoEndpoint, err := newDefaultRepositoryEndpoint(ref, c.insecureRegistry) + if err != nil { + return "", err + } + + repoEndpoint.actions = []string{"pull", "push"} + repo, err := c.getRepositoryForReference(ctx, ref, repoEndpoint) + if err != nil { + return "", err + } + + manifestService, err := repo.Manifests(ctx) + if err != nil { + return "", err + } + + _, opts, err := getManifestOptionsFromReference(ref) + if err != nil { + return "", err + } + + dgst, err := manifestService.Put(ctx, manifest, opts...) + if err != nil { + return dgst, fmt.Errorf("failed to put manifest %s: %w", ref, err) + } + return dgst, nil +} + +func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Named, repoEndpoint repositoryEndpoint) (distribution.Repository, error) { + repoName, err := reference.WithName(repoEndpoint.repoName) + if err != nil { + return nil, fmt.Errorf("failed to parse repo name from %s: %w", ref, err) + } + httpTransport, err := c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) + if err != nil { + if !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { + return nil, err + } + if !repoEndpoint.endpoint.TLSConfig.InsecureSkipVerify { + return nil, httpProtoError{cause: err} + } + // --insecure was set; fall back to plain HTTP + if url := repoEndpoint.endpoint.URL; url != nil && url.Scheme == "https" { + url.Scheme = "http" + httpTransport, err = c.getHTTPTransportForRepoEndpoint(ctx, repoEndpoint) + if err != nil { + return nil, err + } + } + } + return distributionclient.NewRepository(repoName, repoEndpoint.BaseURL(), httpTransport) +} + +func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) { + httpTransport, err := getHTTPTransport( + c.authConfigResolver(ctx, repoEndpoint.indexInfo.Name), + repoEndpoint.endpoint, + repoEndpoint.repoName, + c.userAgent, + repoEndpoint.actions, + ) + if err != nil { + return nil, fmt.Errorf("failed to configure transport: %w", err) + } + return httpTransport, nil +} + +// GetManifest returns an ImageManifest for the reference +func (c *client) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) { + var result manifesttypes.ImageManifest + fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { + var err error + result, err = fetchManifest(ctx, repo, ref) + return result.Ref != nil, err + } + + err := c.iterateEndpoints(ctx, ref, fetch) + return result, err +} + +// GetManifestList returns a list of ImageManifest for the reference +func (c *client) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) { + result := []manifesttypes.ImageManifest{} + fetch := func(ctx context.Context, repo distribution.Repository, ref reference.Named) (bool, error) { + var err error + result, err = fetchList(ctx, repo, ref) + return len(result) > 0, err + } + + err := c.iterateEndpoints(ctx, ref, fetch) + return result, err +} + +func getManifestOptionsFromReference(ref reference.Named) (digest.Digest, []distribution.ManifestServiceOption, error) { + if tagged, isTagged := ref.(reference.NamedTagged); isTagged { + tag := tagged.Tag() + return "", []distribution.ManifestServiceOption{distribution.WithTag(tag)}, nil + } + if digested, isDigested := ref.(reference.Canonical); isDigested { + return digested.Digest(), []distribution.ManifestServiceOption{}, nil + } + return "", nil, fmt.Errorf("%s no tag or digest", ref) +} diff --git a/internal/registryclient/endpoint.go b/internal/registryclient/endpoint.go new file mode 100644 index 000000000000..6cef45350db3 --- /dev/null +++ b/internal/registryclient/endpoint.go @@ -0,0 +1,133 @@ +package registryclient + +import ( + "context" + "fmt" + "net" + "net/http" + "net/url" + "time" + + "github.com/distribution/reference" + "github.com/docker/cli/internal/registry" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + registrytypes "github.com/moby/moby/api/types/registry" +) + +type repositoryEndpoint struct { + repoName string + indexInfo *registrytypes.IndexInfo + endpoint registry.APIEndpoint + actions []string +} + +// BaseURL returns the endpoint url +func (r repositoryEndpoint) BaseURL() string { + return r.endpoint.URL.String() +} + +func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { + indexInfo := registry.NewIndexInfo(ref) + endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure) + if err != nil { + return repositoryEndpoint{}, err + } + if insecure { + endpoint.TLSConfig.InsecureSkipVerify = true + } + return repositoryEndpoint{ + repoName: reference.Path(reference.TrimNamed(ref)), + indexInfo: indexInfo, + endpoint: endpoint, + }, nil +} + +func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEndpoint, error) { + registryService, err := registry.NewService(registry.ServiceOptions{}) + if err != nil { + return registry.APIEndpoint{}, err + } + endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoName)) + if err != nil { + return registry.APIEndpoint{}, err + } + // Default to the highest priority endpoint to return + endpoint := endpoints[0] + if insecure { + for _, ep := range endpoints { + if ep.URL.Scheme == "http" { + endpoint = ep + } + } + } + return endpoint, nil +} + +// getHTTPTransport builds a transport for use in communicating with a registry +func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.APIEndpoint, repoName, userAgent string, actions []string) (http.RoundTripper, error) { + // get the http transport, this will be used in a client to upload manifest + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: endpoint.TLSConfig, + DisableKeepAlives: true, + } + + modifiers := registry.Headers(userAgent, http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + challengeManager, err := registry.PingV2Registry(endpoint.URL, authTransport) + if err != nil { + return nil, fmt.Errorf("error pinging v2 registry: %w", err) + } + if authConfig.RegistryToken != "" { + passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken} + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler)) + } else { + if len(actions) == 0 { + actions = []string{"pull"} + } + creds := &staticCredentialStore{authConfig: &authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) + } + return transport.NewTransport(base, modifiers...), nil +} + +type existingTokenHandler struct { + token string +} + +func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string]string) error { + req.Header.Set("Authorization", "Bearer "+th.token) + return nil +} + +func (*existingTokenHandler) Scheme() string { + return "bearer" +} + +type staticCredentialStore struct { + authConfig *registrytypes.AuthConfig +} + +func (scs staticCredentialStore) Basic(*url.URL) (string, string) { + if scs.authConfig == nil { + return "", "" + } + return scs.authConfig.Username, scs.authConfig.Password +} + +func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { + if scs.authConfig == nil { + return "" + } + return scs.authConfig.IdentityToken +} + +func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {} diff --git a/cli/registry/client/fetcher.go b/internal/registryclient/fetcher.go similarity index 90% rename from cli/registry/client/fetcher.go rename to internal/registryclient/fetcher.go index f270d494324f..42ef0dec5423 100644 --- a/cli/registry/client/fetcher.go +++ b/internal/registryclient/fetcher.go @@ -1,11 +1,14 @@ -package client +package registryclient import ( "context" "encoding/json" + "errors" + "fmt" "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" + "github.com/docker/cli/internal/registry" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/ocischema" @@ -13,10 +16,8 @@ import ( "github.com/docker/distribution/registry/api/errcode" v2 "github.com/docker/distribution/registry/api/v2" distclient "github.com/docker/distribution/registry/client" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -35,9 +36,9 @@ func fetchManifest(ctx context.Context, repo distribution.Repository, ref refere case *ocischema.DeserializedManifest: return pullManifestOCISchema(ctx, ref, repo, *v) case *manifestlist.DeserializedManifestList: - return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref) + return types.ImageManifest{}, fmt.Errorf("%s is a manifest list", ref) } - return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref) + return types.ImageManifest{}, fmt.Errorf("%s is not a manifest", ref) } func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) { @@ -50,7 +51,7 @@ func fetchList(ctx context.Context, repo distribution.Repository, ref reference. case *manifestlist.DeserializedManifestList: return pullManifestList(ctx, ref, repo, *v) default: - return nil, errors.Errorf("unsupported manifest format: %v", v) + return nil, fmt.Errorf("unsupported manifest format: %v", v) } } @@ -62,7 +63,7 @@ func getManifest(ctx context.Context, repo distribution.Repository, ref referenc dgst, opts, err := getManifestOptionsFromReference(ref) if err != nil { - return nil, errors.Errorf("image manifest for %q does not exist", ref) + return nil, fmt.Errorf("image manifest for %q does not exist", ref) } return manSvc.Get(ctx, dgst, opts...) } @@ -123,7 +124,7 @@ func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, re return nil, err } if !verifier.Verified() { - return nil, errors.Errorf("image config verification failed for digest %s", dgst) + return nil, fmt.Errorf("image config verification failed for digest %s", dgst) } return configJSON, nil } @@ -143,7 +144,7 @@ func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (oc // If pull by digest, then verify the manifest digest. if digested, isDigested := ref.(reference.Canonical); isDigested && digested.Digest() != desc.Digest { - return ocispec.Descriptor{}, errors.Errorf("manifest verification failed for digest %s", digested.Digest()) + return ocispec.Descriptor{}, fmt.Errorf("manifest verification failed for digest %s", digested.Digest()) } return desc, nil @@ -179,7 +180,7 @@ func pullManifestList(ctx context.Context, ref reference.Named, repo distributio case *ocischema.DeserializedManifest: imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v) default: - err = errors.Errorf("unsupported manifest type: %T", manifest) + err = fmt.Errorf("unsupported manifest type: %T", manifest) } if err != nil { return nil, err @@ -220,9 +221,8 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, return err } - repoName := reference.TrimNamed(namedRef) - repoInfo, _ := registry.ParseRepositoryInfo(namedRef) - indexInfo := repoInfo.Index + repoName := reference.Path(reference.TrimNamed(namedRef)) + indexInfo := registry.NewIndexInfo(namedRef) confirmedTLSRegistries := make(map[string]bool) for _, endpoint := range endpoints { @@ -283,10 +283,9 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi } registryService, err := registry.NewService(serviceOpts) if err != nil { - return []registry.APIEndpoint{}, err + return nil, err } - repoInfo, _ := registry.ParseRepositoryInfo(namedRef) - endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) + endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(namedRef)) logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) return endpoints, err } diff --git a/internal/test/builders/config.go b/internal/test/builders/config.go index 53aeb43e2921..1c7e336e9f96 100644 --- a/internal/test/builders/config.go +++ b/internal/test/builders/config.go @@ -3,7 +3,7 @@ package builders import ( "time" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // Config creates a config with default values. diff --git a/internal/test/builders/container.go b/internal/test/builders/container.go index df67f60a0749..43dbc5603141 100644 --- a/internal/test/builders/container.go +++ b/internal/test/builders/container.go @@ -1,9 +1,10 @@ package builders import ( + "net/netip" "time" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" ) // Container creates a container with default values. @@ -45,12 +46,12 @@ func WithName(name string) func(*container.Summary) { } // WithPort adds a port mapping to the container -func WithPort(privatePort, publicPort uint16, builders ...func(*container.Port)) func(*container.Summary) { +func WithPort(privatePort, publicPort uint16, builders ...func(summary *container.PortSummary)) func(*container.Summary) { return func(c *container.Summary) { if c.Ports == nil { - c.Ports = []container.Port{} + c.Ports = []container.PortSummary{} } - port := &container.Port{ + port := &container.PortSummary{ PrivatePort: privatePort, PublicPort: publicPort, } @@ -71,18 +72,18 @@ func WithSize(size int64) func(*container.Summary) { } // IP sets the ip of the port -func IP(ip string) func(*container.Port) { - return func(p *container.Port) { - p.IP = ip +func IP(ip string) func(*container.PortSummary) { + return func(p *container.PortSummary) { + p.IP = netip.MustParseAddr(ip) } } // TCP sets the port to tcp -func TCP(p *container.Port) { +func TCP(p *container.PortSummary) { p.Type = "tcp" } // UDP sets the port to udp -func UDP(p *container.Port) { +func UDP(p *container.PortSummary) { p.Type = "udp" } diff --git a/internal/test/builders/network.go b/internal/test/builders/network.go index 73ceba46a5cf..4071786ebcfb 100644 --- a/internal/test/builders/network.go +++ b/internal/test/builders/network.go @@ -1,7 +1,7 @@ package builders import ( - "github.com/docker/docker/api/types/network" + "github.com/moby/moby/api/types/network" ) // NetworkResource creates a network resource with default values. diff --git a/internal/test/builders/node.go b/internal/test/builders/node.go index 4e3eb06dd1fb..e57674e2eda1 100644 --- a/internal/test/builders/node.go +++ b/internal/test/builders/node.go @@ -3,7 +3,7 @@ package builders import ( "time" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // Node creates a node with default values. diff --git a/internal/test/builders/secret.go b/internal/test/builders/secret.go index 500193cab6d6..2f608e04d2cb 100644 --- a/internal/test/builders/secret.go +++ b/internal/test/builders/secret.go @@ -3,7 +3,7 @@ package builders import ( "time" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // Secret creates a secret with default values. diff --git a/internal/test/builders/service.go b/internal/test/builders/service.go index 654880676922..c188fb055161 100644 --- a/internal/test/builders/service.go +++ b/internal/test/builders/service.go @@ -1,7 +1,7 @@ package builders import ( - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // Service creates a service with default values. diff --git a/internal/test/builders/swarm.go b/internal/test/builders/swarm.go index 986976fd5d2e..0322e26d59a5 100644 --- a/internal/test/builders/swarm.go +++ b/internal/test/builders/swarm.go @@ -3,7 +3,7 @@ package builders import ( "time" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // Swarm creates a swarm with default values. diff --git a/internal/test/builders/task.go b/internal/test/builders/task.go index 1d0c796b89a9..94279be9c206 100644 --- a/internal/test/builders/task.go +++ b/internal/test/builders/task.go @@ -3,7 +3,7 @@ package builders import ( "time" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) var defaultTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) diff --git a/internal/test/builders/volume.go b/internal/test/builders/volume.go index 90b697810b6f..ed5bab27bb65 100644 --- a/internal/test/builders/volume.go +++ b/internal/test/builders/volume.go @@ -1,11 +1,11 @@ package builders -import "github.com/docker/docker/api/types/volume" +import "github.com/moby/moby/api/types/volume" // Volume creates a volume with default values. // Any number of volume function builder can be passed to augment it. -func Volume(builders ...func(vol *volume.Volume)) *volume.Volume { - vol := &volume.Volume{ +func Volume(builders ...func(vol *volume.Volume)) volume.Volume { + vol := volume.Volume{ Name: "volume", Driver: "local", Mountpoint: "/data/volume", @@ -13,7 +13,7 @@ func Volume(builders ...func(vol *volume.Volume)) *volume.Volume { } for _, builder := range builders { - builder(vol) + builder(&vol) } return vol diff --git a/internal/test/cli.go b/internal/test/cli.go index 8b506e14149c..4a5b0bf9a0fc 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -2,7 +2,6 @@ package test import ( "bytes" - "errors" "io" "strings" @@ -11,35 +10,27 @@ import ( "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" 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/trust" - "github.com/docker/docker/api" - "github.com/docker/docker/client" - notaryclient "github.com/theupdateframework/notary/client" + "github.com/docker/cli/internal/registryclient" + "github.com/moby/moby/client" ) -// NotaryClientFuncType defines a function that returns a fake notary client -type NotaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) - // FakeCli emulates the default DockerCli type FakeCli struct { command.DockerCli - client client.APIClient - configfile *configfile.ConfigFile - out *streams.Out - outBuffer *bytes.Buffer - err *streams.Out - errBuffer *bytes.Buffer - in *streams.In - server command.ServerInfo - notaryClientFunc NotaryClientFuncType - manifestStore manifeststore.Store - registryClient registryclient.RegistryClient - contentTrust bool - contextStore store.Store - currentContext string - dockerEndpoint docker.Endpoint + client client.APIClient + configfile *configfile.ConfigFile + out *streams.Out + outBuffer *bytes.Buffer + err *streams.Out + errBuffer *bytes.Buffer + in *streams.In + server command.ServerInfo + manifestStore manifeststore.Store + registryClient registryclient.RegistryClient + contextStore store.Store + currentContext string + dockerEndpoint docker.Endpoint } // NewFakeCli returns a fake for the command.Cli interface @@ -106,7 +97,7 @@ func (c *FakeCli) Client() client.APIClient { // CurrentVersion returns the API version used by FakeCli. func (*FakeCli) CurrentVersion() string { - return api.DefaultVersion + return client.MaxAPIVersion } // Out returns the output stream (stdout) the cli should write on @@ -165,19 +156,6 @@ func (c *FakeCli) ResetOutputBuffers() { c.errBuffer.Reset() } -// SetNotaryClient sets the internal getter for retrieving a NotaryClient -func (c *FakeCli) SetNotaryClient(notaryClientFunc NotaryClientFuncType) { - c.notaryClientFunc = notaryClientFunc -} - -// NotaryClient returns an err for testing unless defined -func (c *FakeCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) { - if c.notaryClientFunc != nil { - return c.notaryClientFunc(imgRefAndAuth, actions) - } - return nil, errors.New("no notary client available unless defined") -} - // ManifestStore returns a fake store used for testing func (c *FakeCli) ManifestStore() manifeststore.Store { return c.manifestStore @@ -198,16 +176,6 @@ func (c *FakeCli) SetRegistryClient(registryClient registryclient.RegistryClient c.registryClient = registryClient } -// ContentTrustEnabled on the fake cli -func (c *FakeCli) ContentTrustEnabled() bool { - return c.contentTrust -} - -// EnableContentTrust on the fake cli -func EnableContentTrust(c *FakeCli) { - c.contentTrust = true -} - // BuildKitEnabled on the fake cli func (*FakeCli) BuildKitEnabled() (bool, error) { return true, nil diff --git a/internal/test/environment/testenv.go b/internal/test/environment/testenv.go index 9c5d06db34f9..db45edc9e7b6 100644 --- a/internal/test/environment/testenv.go +++ b/internal/test/environment/testenv.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/docker/docker/client" + "github.com/moby/moby/client" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" "gotest.tools/v3/skip" diff --git a/internal/test/network/client.go b/internal/test/network/client.go index 2d1bc656dda1..7bf6ffd5474a 100644 --- a/internal/test/network/client.go +++ b/internal/test/network/client.go @@ -3,20 +3,19 @@ package network import ( "context" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/client" + "github.com/moby/moby/client" ) // FakeClient is a fake NetworkAPIClient type FakeClient struct { client.NetworkAPIClient - NetworkInspectFunc func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) + NetworkInspectFunc func(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) } // NetworkInspect fakes inspecting a network -func (c *FakeClient) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { +func (c *FakeClient) NetworkInspect(ctx context.Context, networkID string, options client.NetworkInspectOptions) (client.NetworkInspectResult, error) { if c.NetworkInspectFunc != nil { return c.NetworkInspectFunc(ctx, networkID, options) } - return network.Inspect{}, nil + return client.NetworkInspectResult{}, nil } diff --git a/internal/test/notary/client.go b/internal/test/notary/client.go deleted file mode 100644 index a948ad62cf54..000000000000 --- a/internal/test/notary/client.go +++ /dev/null @@ -1,556 +0,0 @@ -package notary - -import ( - "github.com/docker/cli/cli/trust" - "github.com/theupdateframework/notary/client" - "github.com/theupdateframework/notary/client/changelist" - "github.com/theupdateframework/notary/cryptoservice" - "github.com/theupdateframework/notary/storage" - "github.com/theupdateframework/notary/trustmanager" - "github.com/theupdateframework/notary/tuf/data" - "github.com/theupdateframework/notary/tuf/signed" -) - -// GetOfflineNotaryRepository returns a OfflineNotaryRepository -func GetOfflineNotaryRepository(trust.ImageRefAndAuth, []string) (client.Repository, error) { - return OfflineNotaryRepository{}, nil -} - -// OfflineNotaryRepository is a mock Notary repository that is offline -type OfflineNotaryRepository struct{} - -// Initialize creates a new repository by using rootKey as the root Key for the -// TUF repository. -func (OfflineNotaryRepository) Initialize([]string, ...data.RoleName) error { - return storage.ErrOffline{} -} - -// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates -func (OfflineNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { - return storage.ErrOffline{} -} - -// Publish pushes the local changes in signed material to the remote notary-server -// Conceptually it performs an operation similar to a `git rebase` -func (OfflineNotaryRepository) Publish() error { - return storage.ErrOffline{} -} - -// AddTarget creates new changelist entries to add a target to the given roles -// in the repository when the changelist gets applied at publish time. -func (OfflineNotaryRepository) AddTarget(*client.Target, ...data.RoleName) error { - return nil -} - -// RemoveTarget creates new changelist entries to remove a target from the given -// roles in the repository when the changelist gets applied at publish time. -func (OfflineNotaryRepository) RemoveTarget(string, ...data.RoleName) error { - return nil -} - -// ListTargets lists all targets for the current repository. The list of -// roles should be passed in order from highest to lowest priority. -func (OfflineNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { - return nil, storage.ErrOffline{} -} - -// GetTargetByName returns a target by the given name. -func (OfflineNotaryRepository) GetTargetByName(string, ...data.RoleName) (*client.TargetWithRole, error) { - return nil, storage.ErrOffline{} -} - -// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all -// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. -func (OfflineNotaryRepository) GetAllTargetMetadataByName(string) ([]client.TargetSignedStruct, error) { - return nil, storage.ErrOffline{} -} - -// GetChangelist returns the list of the repository's unpublished changes -func (OfflineNotaryRepository) GetChangelist() (changelist.Changelist, error) { - return changelist.NewMemChangelist(), nil -} - -// ListRoles returns a list of RoleWithSignatures objects for this repo -func (OfflineNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { - return nil, storage.ErrOffline{} -} - -// GetDelegationRoles returns the keys and roles of the repository's delegations -func (OfflineNotaryRepository) GetDelegationRoles() ([]data.Role, error) { - return nil, storage.ErrOffline{} -} - -// AddDelegation creates changelist entries to add provided delegation public keys and paths. -func (OfflineNotaryRepository) AddDelegation(data.RoleName, []data.PublicKey, []string) error { - return nil -} - -// AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. -func (OfflineNotaryRepository) AddDelegationRoleAndKeys(data.RoleName, []data.PublicKey) error { - return nil -} - -// AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. -func (OfflineNotaryRepository) AddDelegationPaths(data.RoleName, []string) error { - return nil -} - -// RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths. -func (OfflineNotaryRepository) RemoveDelegationKeysAndPaths(data.RoleName, []string, []string) error { - return nil -} - -// RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety. -func (OfflineNotaryRepository) RemoveDelegationRole(data.RoleName) error { - return nil -} - -// RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. -func (OfflineNotaryRepository) RemoveDelegationPaths(data.RoleName, []string) error { - return nil -} - -// RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. -func (OfflineNotaryRepository) RemoveDelegationKeys(data.RoleName, []string) error { - return nil -} - -// ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. -func (OfflineNotaryRepository) ClearDelegationPaths(data.RoleName) error { - return nil -} - -// Witness creates change objects to witness (i.e. re-sign) the given -// roles on the next publish. One change is created per role -func (OfflineNotaryRepository) Witness(...data.RoleName) ([]data.RoleName, error) { - return nil, nil -} - -// RotateKey rotates a private key and returns the public component from the remote server -func (OfflineNotaryRepository) RotateKey(data.RoleName, bool, []string) error { - return storage.ErrOffline{} -} - -// GetCryptoService is the getter for the repository's CryptoService -func (OfflineNotaryRepository) GetCryptoService() signed.CryptoService { - return nil -} - -// SetLegacyVersions allows the number of legacy versions of the root -// to be inspected for old signing keys to be configured. -func (OfflineNotaryRepository) SetLegacyVersions(int) {} - -// GetGUN is a getter for the GUN object from a Repository -func (OfflineNotaryRepository) GetGUN() data.GUN { - return data.GUN("gun") -} - -// GetUninitializedNotaryRepository returns an UninitializedNotaryRepository -func GetUninitializedNotaryRepository(trust.ImageRefAndAuth, []string) (client.Repository, error) { - return UninitializedNotaryRepository{}, nil -} - -// UninitializedNotaryRepository is a mock Notary repository that is uninintialized -// it builds on top of the OfflineNotaryRepository, instead returning ErrRepositoryNotExist -// for any online operation -type UninitializedNotaryRepository struct { - OfflineNotaryRepository -} - -// Initialize creates a new repository by using rootKey as the root Key for the -// TUF repository. -func (UninitializedNotaryRepository) Initialize([]string, ...data.RoleName) error { - return client.ErrRepositoryNotExist{} -} - -// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates -func (UninitializedNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { - return client.ErrRepositoryNotExist{} -} - -// Publish pushes the local changes in signed material to the remote notary-server -// Conceptually it performs an operation similar to a `git rebase` -func (UninitializedNotaryRepository) Publish() error { - return client.ErrRepositoryNotExist{} -} - -// ListTargets lists all targets for the current repository. The list of -// roles should be passed in order from highest to lowest priority. -func (UninitializedNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { - return nil, client.ErrRepositoryNotExist{} -} - -// GetTargetByName returns a target by the given name. -func (UninitializedNotaryRepository) GetTargetByName(string, ...data.RoleName) (*client.TargetWithRole, error) { - return nil, client.ErrRepositoryNotExist{} -} - -// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all -// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. -func (UninitializedNotaryRepository) GetAllTargetMetadataByName(string) ([]client.TargetSignedStruct, error) { - return nil, client.ErrRepositoryNotExist{} -} - -// ListRoles returns a list of RoleWithSignatures objects for this repo -func (UninitializedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { - return nil, client.ErrRepositoryNotExist{} -} - -// GetDelegationRoles returns the keys and roles of the repository's delegations -func (UninitializedNotaryRepository) GetDelegationRoles() ([]data.Role, error) { - return nil, client.ErrRepositoryNotExist{} -} - -// RotateKey rotates a private key and returns the public component from the remote server -func (UninitializedNotaryRepository) RotateKey(data.RoleName, bool, []string) error { - return client.ErrRepositoryNotExist{} -} - -// GetEmptyTargetsNotaryRepository returns an EmptyTargetsNotaryRepository -func GetEmptyTargetsNotaryRepository(trust.ImageRefAndAuth, []string) (client.Repository, error) { - return EmptyTargetsNotaryRepository{}, nil -} - -// EmptyTargetsNotaryRepository is a mock Notary repository that is initialized -// but does not have any signed targets -type EmptyTargetsNotaryRepository struct { - OfflineNotaryRepository -} - -// Initialize creates a new repository by using rootKey as the root Key for the -// TUF repository. -func (EmptyTargetsNotaryRepository) Initialize([]string, ...data.RoleName) error { - return nil -} - -// InitializeWithCertificate initializes the repository with root keys and their corresponding certificates -func (EmptyTargetsNotaryRepository) InitializeWithCertificate([]string, []data.PublicKey, ...data.RoleName) error { - return nil -} - -// Publish pushes the local changes in signed material to the remote notary-server -// Conceptually it performs an operation similar to a `git rebase` -func (EmptyTargetsNotaryRepository) Publish() error { - return nil -} - -// ListTargets lists all targets for the current repository. The list of -// roles should be passed in order from highest to lowest priority. -func (EmptyTargetsNotaryRepository) ListTargets(...data.RoleName) ([]*client.TargetWithRole, error) { - return []*client.TargetWithRole{}, nil -} - -// GetTargetByName returns a target by the given name. -func (EmptyTargetsNotaryRepository) GetTargetByName(name string, _ ...data.RoleName) (*client.TargetWithRole, error) { - return nil, client.ErrNoSuchTarget(name) -} - -// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all -// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. -func (EmptyTargetsNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { - return nil, client.ErrNoSuchTarget(name) -} - -// ListRoles returns a list of RoleWithSignatures objects for this repo -func (EmptyTargetsNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { - rootRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"rootID"}, - Threshold: 1, - }, - Name: data.CanonicalRootRole, - } - - targetsRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"targetsID"}, - Threshold: 1, - }, - Name: data.CanonicalTargetsRole, - } - return []client.RoleWithSignatures{ - {Role: rootRole}, - {Role: targetsRole}, - }, nil -} - -// GetDelegationRoles returns the keys and roles of the repository's delegations -func (EmptyTargetsNotaryRepository) GetDelegationRoles() ([]data.Role, error) { - return []data.Role{}, nil -} - -// RotateKey rotates a private key and returns the public component from the remote server -func (EmptyTargetsNotaryRepository) RotateKey(data.RoleName, bool, []string) error { - return nil -} - -// GetLoadedNotaryRepository returns a LoadedNotaryRepository -func GetLoadedNotaryRepository(trust.ImageRefAndAuth, []string) (client.Repository, error) { - return LoadedNotaryRepository{}, nil -} - -// LoadedNotaryRepository is a mock Notary repository that is loaded with targets, delegations, and keys -type LoadedNotaryRepository struct { - EmptyTargetsNotaryRepository - statefulCryptoService signed.CryptoService -} - -// LoadedNotaryRepository has three delegations: -// - targets/releases: includes keys A and B -// - targets/alice: includes key A -// - targets/bob: includes key B -var loadedReleasesRole = data.DelegationRole{ - BaseRole: data.BaseRole{ - Name: "targets/releases", - Keys: map[string]data.PublicKey{"A": nil, "B": nil}, - Threshold: 1, - }, -} - -var loadedAliceRole = data.DelegationRole{ - BaseRole: data.BaseRole{ - Name: "targets/alice", - Keys: map[string]data.PublicKey{"A": nil}, - Threshold: 1, - }, -} - -var loadedBobRole = data.DelegationRole{ - BaseRole: data.BaseRole{ - Name: "targets/bob", - Keys: map[string]data.PublicKey{"B": nil}, - Threshold: 1, - }, -} - -var loadedDelegationRoles = []data.Role{ - { - Name: loadedReleasesRole.Name, - RootRole: data.RootRole{ - KeyIDs: []string{"A", "B"}, - Threshold: 1, - }, - }, - { - Name: loadedAliceRole.Name, - RootRole: data.RootRole{ - KeyIDs: []string{"A"}, - Threshold: 1, - }, - }, - { - Name: loadedBobRole.Name, - RootRole: data.RootRole{ - KeyIDs: []string{"B"}, - Threshold: 1, - }, - }, -} - -var loadedTargetsRole = data.DelegationRole{ - BaseRole: data.BaseRole{ - Name: data.CanonicalTargetsRole, - Keys: map[string]data.PublicKey{"C": nil}, - Threshold: 1, - }, -} - -// LoadedNotaryRepository has three targets: -// - red: signed by targets/releases, targets/alice, targets/bob -// - blue: signed by targets/releases, targets/alice -// - green: signed by targets/releases -var loadedRedTarget = client.Target{ - Name: "red", - Hashes: data.Hashes{"sha256": []byte("red-digest")}, -} - -var loadedBlueTarget = client.Target{ - Name: "blue", - Hashes: data.Hashes{"sha256": []byte("blue-digest")}, -} - -var loadedGreenTarget = client.Target{ - Name: "green", - Hashes: data.Hashes{"sha256": []byte("green-digest")}, -} - -var loadedTargets = []client.TargetSignedStruct{ - // red is signed by all three delegations - {Target: loadedRedTarget, Role: loadedReleasesRole}, - {Target: loadedRedTarget, Role: loadedAliceRole}, - {Target: loadedRedTarget, Role: loadedBobRole}, - - // blue is signed by targets/releases, targets/alice - {Target: loadedBlueTarget, Role: loadedReleasesRole}, - {Target: loadedBlueTarget, Role: loadedAliceRole}, - - // green is signed by targets/releases - {Target: loadedGreenTarget, Role: loadedReleasesRole}, -} - -// ListRoles returns a list of RoleWithSignatures objects for this repo -func (LoadedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error) { - rootRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"rootID"}, - Threshold: 1, - }, - Name: data.CanonicalRootRole, - } - - targetsRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"targetsID"}, - Threshold: 1, - }, - Name: data.CanonicalTargetsRole, - } - - aliceRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"A"}, - Threshold: 1, - }, - Name: data.RoleName("targets/alice"), - } - - bobRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"B"}, - Threshold: 1, - }, - Name: data.RoleName("targets/bob"), - } - - releasesRole := data.Role{ - RootRole: data.RootRole{ - KeyIDs: []string{"A", "B"}, - Threshold: 1, - }, - Name: data.RoleName("targets/releases"), - } - // have releases only signed off by Alice last - releasesSig := []data.Signature{{KeyID: "A"}} - - return []client.RoleWithSignatures{ - {Role: rootRole}, - {Role: targetsRole}, - {Role: aliceRole}, - {Role: bobRole}, - {Role: releasesRole, Signatures: releasesSig}, - }, nil -} - -// ListTargets lists all targets for the current repository. The list of -// roles should be passed in order from highest to lowest priority. -func (LoadedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { - filteredTargets := []*client.TargetWithRole{} - for _, tgt := range loadedTargets { - if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { - filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}) - } - } - return filteredTargets, nil -} - -// GetTargetByName returns a target by the given name. -func (LoadedNotaryRepository) GetTargetByName(name string, roles ...data.RoleName) (*client.TargetWithRole, error) { - for _, tgt := range loadedTargets { - if name == tgt.Target.Name { - if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { - return &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}, nil - } - } - } - return nil, client.ErrNoSuchTarget(name) -} - -// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all -// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. -func (LoadedNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { - if name == "" { - return loadedTargets, nil - } - filteredTargets := []client.TargetSignedStruct{} - for _, tgt := range loadedTargets { - if name == tgt.Target.Name { - filteredTargets = append(filteredTargets, tgt) - } - } - if len(filteredTargets) == 0 { - return nil, client.ErrNoSuchTarget(name) - } - return filteredTargets, nil -} - -// GetGUN is a getter for the GUN object from a Repository -func (LoadedNotaryRepository) GetGUN() data.GUN { - return "signed-repo" -} - -// GetDelegationRoles returns the keys and roles of the repository's delegations -func (LoadedNotaryRepository) GetDelegationRoles() ([]data.Role, error) { - return loadedDelegationRoles, nil -} - -const testPass = "password" - -func testPassRetriever(string, string, bool, int) (string, bool, error) { - return testPass, false, nil -} - -// GetCryptoService is the getter for the repository's CryptoService -func (l LoadedNotaryRepository) GetCryptoService() signed.CryptoService { - if l.statefulCryptoService == nil { - // give it an in-memory cryptoservice with a root key and targets key - l.statefulCryptoService = cryptoservice.NewCryptoService(trustmanager.NewKeyMemoryStore(testPassRetriever)) - l.statefulCryptoService.AddKey(data.CanonicalRootRole, l.GetGUN(), nil) - l.statefulCryptoService.AddKey(data.CanonicalTargetsRole, l.GetGUN(), nil) - } - return l.statefulCryptoService -} - -// GetLoadedWithNoSignersNotaryRepository returns a LoadedWithNoSignersNotaryRepository -func GetLoadedWithNoSignersNotaryRepository(trust.ImageRefAndAuth, []string) (client.Repository, error) { - return LoadedWithNoSignersNotaryRepository{}, nil -} - -// LoadedWithNoSignersNotaryRepository is a mock Notary repository that is loaded with targets but no delegations -// it only contains the green target -type LoadedWithNoSignersNotaryRepository struct { - LoadedNotaryRepository -} - -// ListTargets lists all targets for the current repository. The list of -// roles should be passed in order from highest to lowest priority. -func (LoadedWithNoSignersNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) { - filteredTargets := []*client.TargetWithRole{} - for _, tgt := range loadedTargets { - if len(roles) == 0 || (len(roles) > 0 && roles[0] == tgt.Role.Name) { - filteredTargets = append(filteredTargets, &client.TargetWithRole{Target: tgt.Target, Role: tgt.Role.Name}) - } - } - return filteredTargets, nil -} - -// GetTargetByName returns a target by the given name. -func (LoadedWithNoSignersNotaryRepository) GetTargetByName(name string, _ ...data.RoleName) (*client.TargetWithRole, error) { - if name == "" || name == loadedGreenTarget.Name { - return &client.TargetWithRole{Target: loadedGreenTarget, Role: data.CanonicalTargetsRole}, nil - } - return nil, client.ErrNoSuchTarget(name) -} - -// GetAllTargetMetadataByName searches the entire delegation role tree to find the specified target by name for all -// roles, and returns a list of TargetSignedStructs for each time it finds the specified target. -func (LoadedWithNoSignersNotaryRepository) GetAllTargetMetadataByName(name string) ([]client.TargetSignedStruct, error) { - if name == "" || name == loadedGreenTarget.Name { - return []client.TargetSignedStruct{{Target: loadedGreenTarget, Role: loadedTargetsRole}}, nil - } - return nil, client.ErrNoSuchTarget(name) -} - -// GetDelegationRoles returns the keys and roles of the repository's delegations -func (LoadedWithNoSignersNotaryRepository) GetDelegationRoles() ([]data.Role, error) { - return []data.Role{}, nil -} diff --git a/internal/test/store.go b/internal/test/store.go deleted file mode 100644 index 802ea6a026a6..000000000000 --- a/internal/test/store.go +++ /dev/null @@ -1,79 +0,0 @@ -package test - -import ( - "github.com/docker/cli/cli/config/credentials" - "github.com/docker/cli/cli/config/types" -) - -// FakeStore implements a credentials.Store that only acts as an in memory map -type FakeStore struct { - store map[string]types.AuthConfig - eraseFunc func(serverAddress string) error - getFunc func(serverAddress string) (types.AuthConfig, error) - getAllFunc func() (map[string]types.AuthConfig, error) - storeFunc func(authConfig types.AuthConfig) error -} - -// NewFakeStore creates a new file credentials store. -func NewFakeStore() credentials.Store { - return &FakeStore{store: map[string]types.AuthConfig{}} -} - -// SetStore is used to overrides Set function -func (c *FakeStore) SetStore(store map[string]types.AuthConfig) { - c.store = store -} - -// SetEraseFunc is used to overrides Erase function -func (c *FakeStore) SetEraseFunc(eraseFunc func(string) error) { - c.eraseFunc = eraseFunc -} - -// SetGetFunc is used to overrides Get function -func (c *FakeStore) SetGetFunc(getFunc func(string) (types.AuthConfig, error)) { - c.getFunc = getFunc -} - -// SetGetAllFunc is used to overrides GetAll function -func (c *FakeStore) SetGetAllFunc(getAllFunc func() (map[string]types.AuthConfig, error)) { - c.getAllFunc = getAllFunc -} - -// SetStoreFunc is used to override Store function -func (c *FakeStore) SetStoreFunc(storeFunc func(types.AuthConfig) error) { - c.storeFunc = storeFunc -} - -// Erase removes the given credentials from the map store -func (c *FakeStore) Erase(serverAddress string) error { - if c.eraseFunc != nil { - return c.eraseFunc(serverAddress) - } - delete(c.store, serverAddress) - return nil -} - -// Get retrieves credentials for a specific server from the map store. -func (c *FakeStore) Get(serverAddress string) (types.AuthConfig, error) { - if c.getFunc != nil { - return c.getFunc(serverAddress) - } - return c.store[serverAddress], nil -} - -// GetAll returns the key value pairs of ServerAddress => Username -func (c *FakeStore) GetAll() (map[string]types.AuthConfig, error) { - if c.getAllFunc != nil { - return c.getAllFunc() - } - return c.store, nil -} - -// Store saves the given credentials in the map store. -func (c *FakeStore) Store(authConfig types.AuthConfig) error { - if c.storeFunc != nil { - return c.storeFunc(authConfig) - } - c.store[authConfig.ServerAddress] = authConfig - return nil -} diff --git a/internal/tui/chip.go b/internal/tui/chip.go index 02a9b8b8bb65..c48158e75d07 100644 --- a/internal/tui/chip.go +++ b/internal/tui/chip.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui diff --git a/internal/tui/colors.go b/internal/tui/colors.go index d82d61dd74b1..6f1d8e6b797f 100644 --- a/internal/tui/colors.go +++ b/internal/tui/colors.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui diff --git a/internal/tui/count.go b/internal/tui/count.go index 5d7ebd9444c4..a626b24f83dd 100644 --- a/internal/tui/count.go +++ b/internal/tui/count.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui diff --git a/internal/tui/note.go b/internal/tui/note.go index d2bc0b9af85c..759d8d0e295b 100644 --- a/internal/tui/note.go +++ b/internal/tui/note.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui @@ -28,7 +28,7 @@ func withHeader(header Str) noteOptions { } func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) { - if o.isTerminal { + if !o.noColor { // TODO: Handle all flags format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform")) } @@ -51,7 +51,7 @@ func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOpti } l := line - if o.isTerminal { + if !o.noColor { l = aec.Italic.Apply(l) } _, _ = fmt.Fprintln(o, l) diff --git a/internal/tui/output.go b/internal/tui/output.go index 1f526d3fbacf..34bc13a3a09e 100644 --- a/internal/tui/output.go +++ b/internal/tui/output.go @@ -1,10 +1,11 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui import ( "fmt" + "os" "github.com/docker/cli/cli/streams" "github.com/morikuni/aec" @@ -12,7 +13,7 @@ import ( type Output struct { *streams.Out - isTerminal bool + noColor bool } type terminalPrintable interface { @@ -20,24 +21,28 @@ type terminalPrintable interface { } func NewOutput(out *streams.Out) Output { + noColor := !out.IsTerminal() + if os.Getenv("NO_COLOR") != "" { + noColor = true + } return Output{ - Out: out, - isTerminal: out.IsTerminal(), + Out: out, + noColor: noColor, } } func (o Output) Color(clr aec.ANSI) aec.ANSI { - if o.isTerminal { - return clr + if o.noColor { + return ColorNone } - return ColorNone + return clr } func (o Output) Sprint(all ...any) string { var out []any for _, p := range all { if s, ok := p.(terminalPrintable); ok { - out = append(out, s.String(o.isTerminal)) + out = append(out, s.String(!o.noColor)) } else { out = append(out, p) } @@ -47,7 +52,7 @@ func (o Output) Sprint(all ...any) string { func (o Output) PrintlnWithColor(clr aec.ANSI, args ...any) { msg := o.Sprint(args...) - if o.isTerminal { + if !o.noColor { msg = clr.Apply(msg) } _, _ = fmt.Fprintln(o.Out, msg) diff --git a/internal/tui/str.go b/internal/tui/str.go index c1ea9c95d3f9..82fc59f1caec 100644 --- a/internal/tui/str.go +++ b/internal/tui/str.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package tui diff --git a/internal/volumespec/types.go b/internal/volumespec/types.go new file mode 100644 index 000000000000..7eb9a5006ef0 --- /dev/null +++ b/internal/volumespec/types.go @@ -0,0 +1,40 @@ +package volumespec + +// VolumeConfig are references to a volume used by a service +type VolumeConfig struct { + Type string `yaml:",omitempty" json:"type,omitempty"` + Source string `yaml:",omitempty" json:"source,omitempty"` + Target string `yaml:",omitempty" json:"target,omitempty"` + ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` + Consistency string `yaml:",omitempty" json:"consistency,omitempty"` + Bind *BindOpts `yaml:",omitempty" json:"bind,omitempty"` + Volume *VolumeOpts `yaml:",omitempty" json:"volume,omitempty"` + Image *ImageOpts `yaml:",omitempty" json:"image,omitempty"` + Tmpfs *TmpFsOpts `yaml:",omitempty" json:"tmpfs,omitempty"` + Cluster *ClusterOpts `yaml:",omitempty" json:"cluster,omitempty"` +} + +// BindOpts are options for a service volume of type bind +type BindOpts struct { + Propagation string `yaml:",omitempty" json:"propagation,omitempty"` +} + +// VolumeOpts are options for a service volume of type volume +type VolumeOpts struct { + NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` + Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` +} + +// ImageOpts are options for a service volume of type image +type ImageOpts struct { + Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` +} + +// TmpFsOpts are options for a service volume of type tmpfs +type TmpFsOpts struct { + Size int64 `yaml:",omitempty" json:"size,omitempty"` +} + +// ClusterOpts are options for a service volume of type cluster. +// Deliberately left blank for future options, but unused now. +type ClusterOpts struct{} diff --git a/internal/volumespec/volumespec.go b/internal/volumespec/volumespec.go new file mode 100644 index 000000000000..791805edff38 --- /dev/null +++ b/internal/volumespec/volumespec.go @@ -0,0 +1,125 @@ +package volumespec + +import ( + "errors" + "fmt" + "strings" + "unicode" + "unicode/utf8" + + "github.com/moby/moby/api/types/mount" +) + +const endOfSpec = rune(0) + +// Parse parses a volume spec without any knowledge of the target platform +func Parse(spec string) (VolumeConfig, error) { + volume := VolumeConfig{} + + switch len(spec) { + case 0: + return volume, errors.New("invalid empty volume spec") + case 1, 2: + volume.Target = spec + volume.Type = string(mount.TypeVolume) + return volume, nil + } + + var buffer []rune + for _, char := range spec + string(endOfSpec) { + switch { + case isWindowsDrive(buffer, char): + buffer = append(buffer, char) + case char == ':' || char == endOfSpec: + if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { + populateType(&volume) + return volume, fmt.Errorf("invalid spec: %s: %w", spec, err) + } + buffer = []rune{} + default: + buffer = append(buffer, char) + } + } + + populateType(&volume) + return volume, nil +} + +func isWindowsDrive(buffer []rune, char rune) bool { + return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) +} + +func populateFieldFromBuffer(char rune, buffer []rune, volume *VolumeConfig) error { + strBuffer := string(buffer) + switch { + case len(buffer) == 0: + return errors.New("empty section between colons") + // Anonymous volume + case volume.Source == "" && char == endOfSpec: + volume.Target = strBuffer + return nil + case volume.Source == "": + volume.Source = strBuffer + return nil + case volume.Target == "": + volume.Target = strBuffer + return nil + case char == ':': + return errors.New("too many colons") + } + for _, option := range strings.Split(strBuffer, ",") { + switch option { + case "ro": + volume.ReadOnly = true + case "rw": + volume.ReadOnly = false + case "nocopy": + volume.Volume = &VolumeOpts{NoCopy: true} + default: + if isBindOption(option) { + volume.Bind = &BindOpts{Propagation: option} + } + // ignore unknown options + } + } + return nil +} + +func isBindOption(option string) bool { + for _, propagation := range mount.Propagations { + if mount.Propagation(option) == propagation { + return true + } + } + return false +} + +func populateType(volume *VolumeConfig) { + switch { + // Anonymous volume + case volume.Source == "": + volume.Type = string(mount.TypeVolume) + case isFilePath(volume.Source): + volume.Type = string(mount.TypeBind) + default: + volume.Type = string(mount.TypeVolume) + } +} + +func isFilePath(source string) bool { + switch source[0] { + case '.', '/', '~': + return true + } + if len([]rune(source)) == 1 { + return false + } + + // windows named pipes + if strings.HasPrefix(source, `\\`) { + return true + } + + first, nextIndex := utf8.DecodeRuneInString(source) + return isWindowsDrive([]rune{first}, rune(source[nextIndex])) +} diff --git a/internal/volumespec/volumespec_test.go b/internal/volumespec/volumespec_test.go new file mode 100644 index 000000000000..b4d356fffaf7 --- /dev/null +++ b/internal/volumespec/volumespec_test.go @@ -0,0 +1,229 @@ +package volumespec + +import ( + "fmt" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestParseVolumeAnonymousVolume(t *testing.T) { + for _, path := range []string{"/path", "/path/foo"} { + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { + for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeTooManyColons(t *testing.T) { + _, err := Parse("/foo:/foo:ro:foo") + assert.Error(t, err, "invalid spec: /foo:/foo:ro:foo: too many colons") +} + +func TestParseVolumeShortVolumes(t *testing.T) { + for _, path := range []string{".", "/a"} { + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeMissingSource(t *testing.T) { + for _, spec := range []string{":foo", "/foo::ro"} { + _, err := Parse(spec) + assert.ErrorContains(t, err, "empty section between colons") + } +} + +func TestParseVolumeBindMount(t *testing.T) { + for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { + volume, err := Parse(path + ":/target") + expected := VolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeRelativeBindMountWindows(t *testing.T) { + for _, path := range []string{ + "./foo", + "~/thing", + "../other", + "D:\\path", "/home/user", + } { + volume, err := Parse(path + ":d:\\target") + expected := VolumeConfig{ + Type: "bind", + Source: path, + Target: "d:\\target", + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeWithBindOptions(t *testing.T) { + volume, err := Parse("/source:/target:slave") + expected := VolumeConfig{ + Type: "bind", + Source: "/source", + Target: "/target", + Bind: &BindOpts{Propagation: "slave"}, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) +} + +func TestParseVolumeWithBindOptionsWindows(t *testing.T) { + volume, err := Parse("C:\\source\\foo:D:\\target:ro,rprivate") + expected := VolumeConfig{ + Type: "bind", + Source: "C:\\source\\foo", + Target: "D:\\target", + ReadOnly: true, + Bind: &BindOpts{Propagation: "rprivate"}, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) +} + +func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { + _, err := Parse("name:/target:bogus") + assert.NilError(t, err) +} + +func TestParseVolumeWithVolumeOptions(t *testing.T) { + volume, err := Parse("name:/target:nocopy") + expected := VolumeConfig{ + Type: "volume", + Source: "name", + Target: "/target", + Volume: &VolumeOpts{NoCopy: true}, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) +} + +func TestParseVolumeWithReadOnly(t *testing.T) { + for _, path := range []string{"./foo", "/home/user"} { + volume, err := Parse(path + ":/target:ro") + expected := VolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + ReadOnly: true, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeWithRW(t *testing.T) { + for _, path := range []string{"./foo", "/home/user"} { + volume, err := Parse(path + ":/target:rw") + expected := VolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + ReadOnly: false, + } + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(expected, volume)) + } +} + +func TestParseVolumeWindowsNamedPipe(t *testing.T) { + volume, err := Parse(`\\.\pipe\docker_engine:\\.\pipe\inside`) + assert.NilError(t, err) + expected := VolumeConfig{ + Type: "bind", + Source: `\\.\pipe\docker_engine`, + Target: `\\.\pipe\inside`, + } + assert.Check(t, is.DeepEqual(expected, volume)) +} + +func TestIsFilePath(t *testing.T) { + assert.Check(t, !isFilePath("a界")) + assert.Check(t, !isFilePath("1")) + assert.Check(t, !isFilePath("c")) +} + +// Preserve the test cases for VolumeSplitN +func TestParseVolumeSplitCases(t *testing.T) { + for casenumber, x := range []struct { + input string + n int + expected []string + }{ + {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, + {`:C:\foo:d:`, -1, nil}, + {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, + {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, + {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, + {`d:\`, -1, []string{`d:\`}}, + {`d:`, -1, []string{`d:`}}, + {`d:\path`, -1, []string{`d:\path`}}, + {`d:\path with space`, -1, []string{`d:\path with space`}}, + {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, + + {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, + {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, + {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, + {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, + {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, + {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, + {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, + {`name:D:`, -1, []string{`name`, `D:`}}, + {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, + {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, + + {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, + {`c:\Windows`, -1, []string{`c:\Windows`}}, + {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, + {``, -1, nil}, + {`.`, -1, []string{`.`}}, + {`..\`, -1, []string{`..\`}}, + {`c:\:..\`, -1, []string{`c:\`, `..\`}}, + {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, + // Cover directories with one-character name + {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, + } { + parsed, _ := Parse(x.input) + + expected := len(x.expected) > 1 + msg := fmt.Sprintf("Case %d: %s", casenumber, x.input) + assert.Check(t, is.Equal(expected, parsed.Source != ""), msg) + } +} + +func TestParseVolumeInvalidEmptySpec(t *testing.T) { + _, err := Parse("") + assert.ErrorContains(t, err, "invalid empty volume spec") +} + +func TestParseVolumeInvalidSections(t *testing.T) { + _, err := Parse("/foo::rw") + assert.ErrorContains(t, err, "invalid spec") +} + +func TestParseVolumeWithEmptySource(t *testing.T) { + _, err := Parse(":/vol") + assert.ErrorContains(t, err, "empty section between colons") +} diff --git a/man/docker-run.1.md b/man/docker-run.1.md index 89e6b1f94bc2..ca3ca72af673 100644 --- a/man/docker-run.1.md +++ b/man/docker-run.1.md @@ -479,8 +479,6 @@ according to RFC4862. If set to `disabled`, submounts are not recursively bind-mounted. If set to `writable`, submounts are recursively bind-mounted but not made recursively read-only. If set to `readonly`, submounts are recursively bind-mounted and forcibly made recursively read-only. - * `bind-nonrecursive` (Deprecated): `true` or `false` (default). Setting `true` equates to `bind-recursive=disabled`. - Setting `false` equates to `bind-recursive=enabled`. Options specific to `volume`: diff --git a/man/src/image/ls.md b/man/src/image/ls.md index 463fc75fbae3..a171976d4415 100644 --- a/man/src/image/ls.md +++ b/man/src/image/ls.md @@ -114,9 +114,9 @@ Valid template placeholders are listed above. To list all images in JSON format you can use: docker image ls --format json - {"Containers":"N/A","CreatedAt":"2021-01-18 11:29:06 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"fbcf509fa16f","Repository":"docker","SharedSize":"N/A","Size":"235MB","Tag":"stable-dind","UniqueSize":"N/A","VirtualSize":"235.5MB"} - {"Containers":"N/A","CreatedAt":"2021-01-18 11:24:48 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"08656a69ab2b","Repository":"docker-cli-e2e","SharedSize":"N/A","Size":"1.21GB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"1.207GB"} - {"Containers":"N/A","CreatedAt":"2021-01-18 10:43:44 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"abca5c07c1ba","Repository":"docker-cli-dev","SharedSize":"N/A","Size":"608MB","Tag":"latest","UniqueSize":"N/A","VirtualSize":"607.8MB"} + {"Containers":"N/A","CreatedAt":"2021-01-18 11:29:06 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"fbcf509fa16f","Repository":"docker","SharedSize":"N/A","Size":"235MB","Tag":"stable-dind","UniqueSize":"N/A"} + {"Containers":"N/A","CreatedAt":"2021-01-18 11:24:48 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"08656a69ab2b","Repository":"docker-cli-e2e","SharedSize":"N/A","Size":"1.21GB","Tag":"latest","UniqueSize":"N/A"} + {"Containers":"N/A","CreatedAt":"2021-01-18 10:43:44 +0100 CET","CreatedSince":"24 hours ago","Digest":"\u003cnone\u003e","ID":"abca5c07c1ba","Repository":"docker-cli-dev","SharedSize":"N/A","Size":"608MB","Tag":"latest","UniqueSize":"N/A"} ## Listing only the shortened image IDs diff --git a/man/src/inspect.md b/man/src/inspect.md index d92dde73aa72..c39827560584 100644 --- a/man/src/inspect.md +++ b/man/src/inspect.md @@ -42,23 +42,6 @@ To get information on a container use its ID or instance name: }, "Image": "ded7cd95e059788f2586a51c275a4f151653779d6a7f4dad77c2bd34601d94e4", "NetworkSettings": { - "Bridge": "", - "SandboxID": "6b4851d1903e16dd6a567bd526553a86664361f31036eaaa2f8454d6f4611f6f", - "HairpinMode": false, - "LinkLocalIPv6Address": "", - "LinkLocalIPv6PrefixLen": 0, - "Ports": {}, - "SandboxKey": "/var/run/docker/netns/6b4851d1903e", - "SecondaryIPAddresses": null, - "SecondaryIPv6Addresses": null, - "EndpointID": "7587b82f0dada3656fda26588aee72630c6fab1536d36e394b2bfbcf898c971d", - "Gateway": "172.17.0.1", - "GlobalIPv6Address": "", - "GlobalIPv6PrefixLen": 0, - "IPAddress": "172.17.0.2", - "IPPrefixLen": 16, - "IPv6Gateway": "", - "MacAddress": "02:42:ac:12:00:02", "Networks": { "bridge": { "NetworkID": "7ea29fc1412292a2d7bba362f9253545fecdfa8ce9a6e37dd10ba8bee7129812", @@ -71,8 +54,21 @@ To get information on a container use its ID or instance name: "GlobalIPv6PrefixLen": 0, "MacAddress": "02:42:ac:12:00:02" } - } - + }, + "Ports": { + "80/tcp": [ + { + "HostIp": "0.0.0.0", + "HostPort": "8080" + }, + { + "HostIp": "::", + "HostPort": "8080" + } + ] + }, + "SandboxID": "6b4851d1903e16dd6a567bd526553a86664361f31036eaaa2f8454d6f4611f6f", + "SandboxKey": "/var/run/docker/netns/6b4851d1903e" }, "ResolvConfPath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/resolv.conf", "HostnamePath": "/var/lib/docker/containers/d2cc496561d6d520cbc0236b4ba88c362c446a7619992123f11c809cded25b47/hostname", @@ -212,76 +208,47 @@ https://pkg.go.dev/text/template. Use an image's ID or name (e.g., repository/name[:tag]) to get information about the image: - $ docker inspect ded7cd95e059 - [{ - "Id": "ded7cd95e059788f2586a51c275a4f151653779d6a7f4dad77c2bd34601d94e4", - "Parent": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", - "Comment": "", - "Created": "2015-05-27T16:58:22.937503085Z", - "Container": "76cf7f67d83a7a047454b33007d03e32a8f474ad332c3a03c94537edd22b312b", - "ContainerConfig": { - "Hostname": "76cf7f67d83a", - "Domainname": "", - "User": "", - "AttachStdin": false, - "AttachStdout": false, - "AttachStderr": false, - "ExposedPorts": null, - "Tty": false, - "OpenStdin": false, - "StdinOnce": false, - "Env": null, - "Cmd": [ - "/bin/sh", - "-c", - "#(nop) ADD file:4be46382bcf2b095fcb9fe8334206b584eff60bb3fad8178cbd97697fcb2ea83 in /" - ], - "Image": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", - "Volumes": null, - "VolumeDriver": "", - "WorkingDir": "", - "Entrypoint": null, - "NetworkDisabled": false, - "MacAddress": "", - "OnBuild": null, - "Labels": {} - }, - "DockerVersion": "1.6.0", - "Author": "Lokesh Mandvekar \u003clsm5@fedoraproject.org\u003e", - "Config": { - "Hostname": "76cf7f67d83a", - "Domainname": "", - "User": "", - "AttachStdin": false, - "AttachStdout": false, - "AttachStderr": false, - "ExposedPorts": null, - "Tty": false, - "OpenStdin": false, - "StdinOnce": false, - "Env": null, - "Cmd": null, - "Image": "48ecf305d2cf7046c1f5f8fcbcd4994403173441d4a7f125b1bb0ceead9de731", - "Volumes": null, - "VolumeDriver": "", - "WorkingDir": "", - "Entrypoint": null, - "NetworkDisabled": false, - "MacAddress": "", - "OnBuild": null, - "Labels": {} - }, - "Architecture": "amd64", - "Os": "linux", - "Size": 186507296, - "VirtualSize": 186507296, - "GraphDriver": { - "Data": { - "LowerDir": "/var/lib/docker/overlay2/44b1d1f04db6b1b73a86f9a62678673bf5d16d9a6b62c13e859aa34a99cce5ea/diff:/var/lib/docker/overlay2/ef637181eb13e30e84b7382183364ed7fd7ff7be22d8bb87049e36b75fb89a86/diff:/var/lib/docker/overlay2/64fb0f850b1289cd09cbc3b077cab2c0f59a4f540c67f997b094fc3652b9b0d6/diff:/var/lib/docker/overlay2/68c4d1411addc2b2bd07e900ca3a059c9c5f9fa2607efd87d8d715a0080ed242/diff", - "MergedDir": "/var/lib/docker/overlay2/c7846fe68c6f18247ab9b8672114dde9f506bc164081a895c465716eeb10f2bc/merged", - "UpperDir": "/var/lib/docker/overlay2/c7846fe68c6f18247ab9b8672114dde9f506bc164081a895c465716eeb10f2bc/diff", - "WorkDir": "/var/lib/docker/overlay2/c7846fe68c6f18247ab9b8672114dde9f506bc164081a895c465716eeb10f2bc/work" - }, - "Name": "overlay2" - } - }] + docker inspect hello-world + [ + { + "Id": "sha256:54e66cc1dd1fcb1c3c58bd8017914dbed8701e2d8c74d9262e26bd9cc1642d31", + "RepoTags": [ + "hello-world:latest" + ], + "RepoDigests": [ + "hello-world@sha256:54e66cc1dd1fcb1c3c58bd8017914dbed8701e2d8c74d9262e26bd9cc1642d31" + ], + "Parent": "", + "Comment": "buildkit.dockerfile.v0", + "Created": "2025-08-08T19:05:17Z", + "DockerVersion": "", + "Author": "", + "Config": { + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/hello" + ], + "WorkingDir": "/" + }, + "Architecture": "arm64", + "Variant": "v8", + "Os": "linux", + "Size": 16963, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:50163a6b11927e67829dd6ba5d5ba2b52fae0a17adb18c1967e24c13a62bfffa" + ] + }, + "Metadata": { + "LastTagTime": "2025-09-30T14:06:24.148634215Z" + }, + "Descriptor": { + "mediaType": "application/vnd.oci.image.index.v1+json", + "digest": "sha256:54e66cc1dd1fcb1c3c58bd8017914dbed8701e2d8c74d9262e26bd9cc1642d31", + "size": 12341 + } + } + ] diff --git a/man/src/version.md b/man/src/version.md index 4d5ff070e322..250f814ab4c1 100644 --- a/man/src/version.md +++ b/man/src/version.md @@ -17,29 +17,29 @@ machine running Docker Desktop: $ docker version Client: Docker Engine - Community - Version: 23.0.3 - API version: 1.42 - Go version: go1.19.7 - Git commit: 3e7cbfd - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: darwin/amd64 - Context: default + Version: 28.5.1 + API version: 1.51 + Go version: go1.24.8 + Git commit: e180ab8 + Built: Wed Oct 8 12:16:17 2025 + OS/Arch: darwin/arm64 + Context: desktop-linux - Server: Docker Desktop 4.19.0 (12345) + Server: Docker Desktop 4.49.0 (12345) Engine: - Version: 23.0.3 - API version: 1.42 (minimum version 1.12) - Go version: go1.19.7 - Git commit: 59118bf - Built: Tue Apr 4 22:05:41 2023 - OS/Arch: linux/amd64 + Version: 28.5.1 + API version: 1.51 (minimum version 1.24) + Go version: go1.24.8 + Git commit: f8215cc + Built: Wed Oct 8 12:18:25 2025 + OS/Arch: linux/arm64 Experimental: false containerd: - Version: 1.6.20 - GitCommit: 2806fc1057397dbaeefbea0e4e17bddfbd388f38 + Version: 1.7.27 + GitCommit: 05044ec0a9a75232cad458027ca83437aae3f4da runc: - Version: 1.1.5 - GitCommit: v1.1.5-0-gf19387a + Version: 1.2.5 + GitCommit: v1.2.5-0-g59923ef docker-init: Version: 0.19.0 GitCommit: de40ad0 @@ -47,11 +47,11 @@ machine running Docker Desktop: Get server version: $ docker version --format '{{.Server.Version}}' - 23.0.3 + 28.5.1 Dump raw data: To view all available fields, you can use the format `{{json .}}`. $ docker version --format '{{json .}}' - {"Client":"Version":"23.0.3","ApiVersion":"1.42", ...} + {"Client":"Version":"28.5.1","ApiVersion":"1.51", ...} diff --git a/opts/env.go b/opts/env.go index 675ddda96229..2a21394ba5ba 100644 --- a/opts/env.go +++ b/opts/env.go @@ -7,13 +7,14 @@ import ( ) // ValidateEnv validates an environment variable and returns it. -// If no value is specified, it obtains its value from the current environment +// If no value is specified, it obtains its value from the current environment. // -// As on ParseEnvFile and related to #16585, environment variable names -// are not validated, and it's up to the application inside the container -// to validate them or not. +// Environment variable names are not validated, and it's up to the application +// inside the container to validate them (see [moby-16585]). The only validation +// here is to check if name is empty, per [moby-25099]. // -// The only validation here is to check if name is empty, per #25099 +// [moby-16585]: https://github.com/moby/moby/issues/16585 +// [moby-25099]: https://github.com/moby/moby/issues/25099 func ValidateEnv(val string) (string, error) { k, _, hasValue := strings.Cut(val, "=") if k == "" { diff --git a/opts/envfile.go b/opts/envfile.go deleted file mode 100644 index 3a16e6c189bc..000000000000 --- a/opts/envfile.go +++ /dev/null @@ -1,24 +0,0 @@ -package opts - -import ( - "os" - - "github.com/docker/cli/pkg/kvfile" -) - -// ParseEnvFile reads a file with environment variables enumerated by lines -// -// “Environment variable names used by the utilities in the Shell and -// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase -// letters, digits, and the '_' (underscore) from the characters defined in -// Portable Character Set and do not begin with a digit. *But*, other -// characters may be permitted by an implementation; applications shall -// tolerate the presence of such names.” -// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html -// -// As of #16585, it's up to application inside docker to validate or not -// environment variables, that's why we just strip leading whitespace and -// nothing more. -func ParseEnvFile(filename string) ([]string, error) { - return kvfile.Parse(filename, os.LookupEnv) -} diff --git a/opts/gpus.go b/opts/gpus.go index 6a56c49c4a81..b39a3f14e727 100644 --- a/opts/gpus.go +++ b/opts/gpus.go @@ -7,7 +7,7 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" ) // GpuOpts is a Value type for parsing mounts diff --git a/opts/gpus_test.go b/opts/gpus_test.go index ea177a19717e..ebbad8f8e931 100644 --- a/opts/gpus_test.go +++ b/opts/gpus_test.go @@ -3,7 +3,7 @@ package opts import ( "testing" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) diff --git a/opts/hosts.go b/opts/hosts.go index 552ab6b4a552..dcbbb7e78166 100644 --- a/opts/hosts.go +++ b/opts/hosts.go @@ -32,23 +32,6 @@ const ( hostGatewayName = "host-gateway" ) -// ValidateHost validates that the specified string is a valid host and returns it. -// -// TODO(thaJeztah): ValidateHost appears to be unused; deprecate it. -func ValidateHost(val string) (string, error) { - host := strings.TrimSpace(val) - // The empty string means default and is not handled by parseDockerDaemonHost - if host != "" { - _, err := parseDockerDaemonHost(host) - if err != nil { - return val, err - } - } - // Note: unlike most flag validators, we don't return the mutated value here - // we need to know what the user entered later (using ParseHost) to adjust for TLS - return val, nil -} - // ParseHost and set defaults for a Daemon host string func ParseHost(defaultToTLS bool, val string) (string, error) { host := strings.TrimSpace(val) diff --git a/opts/mount.go b/opts/mount.go index 05c1cd0b03bd..0ac252f31187 100644 --- a/opts/mount.go +++ b/opts/mount.go @@ -9,9 +9,8 @@ import ( "strconv" "strings" - mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/go-units" - "github.com/sirupsen/logrus" + mounttypes "github.com/moby/moby/api/types/mount" ) // MountOpt is a Value type for parsing mounts @@ -88,8 +87,7 @@ func (m *MountOpt) Set(value string) error { volumeOptions().NoCopy = true continue case "bind-nonrecursive": - bindOptions().NonRecursive = true - continue + return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead") default: return fmt.Errorf("invalid field '%s' must be a key=value pair", field) } @@ -117,16 +115,12 @@ func (m *MountOpt) Set(value string) error { case "bind-propagation": bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(val)) case "bind-nonrecursive": - bindOptions().NonRecursive, err = strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("invalid value for %s: %s", key, val) - } - logrus.Warn("bind-nonrecursive is deprecated, use bind-recursive=disabled instead") + return errors.New("bind-nonrecursive is deprecated, use bind-recursive=disabled instead") case "bind-recursive": switch val { case "enabled": // read-only mounts are recursively read-only if Engine >= v25 && kernel >= v5.12, otherwise writable // NOP - case "disabled": // alias of bind-nonrecursive=true + case "disabled": // previously "bind-nonrecursive=true" bindOptions().NonRecursive = true case "writable": // conforms to the default read-only bind-mount of Docker v24; read-only mounts are recursively mounted but not recursively read-only bindOptions().ReadOnlyNonRecursive = true diff --git a/opts/mount_test.go b/opts/mount_test.go index f51bdc40bc19..756ad25ca0ed 100644 --- a/opts/mount_test.go +++ b/opts/mount_test.go @@ -5,28 +5,28 @@ import ( "path/filepath" "testing" - mounttypes "github.com/docker/docker/api/types/mount" + "github.com/moby/moby/api/types/mount" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestMountOptString(t *testing.T) { - mount := MountOpt{ - values: []mounttypes.Mount{ + m := MountOpt{ + values: []mount.Mount{ { - Type: mounttypes.TypeBind, + Type: mount.TypeBind, Source: "/home/path", Target: "/target", }, { - Type: mounttypes.TypeVolume, + Type: mount.TypeVolume, Source: "foo", Target: "/target/foo", }, }, } expected := "bind /home/path /target, volume foo /target/foo" - assert.Check(t, is.Equal(expected, mount.String())) + assert.Check(t, is.Equal(expected, m.String())) } func TestMountRelative(t *testing.T) { @@ -57,15 +57,15 @@ func TestMountRelative(t *testing.T) { }, } { t.Run(testcase.name, func(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set(testcase.bind)) + var m MountOpt + assert.NilError(t, m.Set(testcase.bind)) - mounts := mount.Value() + mounts := m.Value() assert.Assert(t, is.Len(mounts, 1)) abs, err := filepath.Abs(testcase.path) assert.NilError(t, err) - assert.Check(t, is.DeepEqual(mounttypes.Mount{ - Type: mounttypes.TypeBind, + assert.Check(t, is.DeepEqual(mount.Mount{ + Type: mount.TypeBind, Source: abs, Target: "/target", }, mounts[0])) @@ -73,77 +73,83 @@ func TestMountRelative(t *testing.T) { } } +// TestMountOptSetBindNoErrorBind tests several aliases that should have +// the same result. func TestMountOptSetBindNoErrorBind(t *testing.T) { - for _, testcase := range []string{ - // tests several aliases that should have same result. + for _, tc := range []string{ "type=bind,target=/target,source=/source", "type=bind,src=/source,dst=/target", "type=bind,source=/source,dst=/target", "type=bind,src=/source,target=/target", } { - var mount MountOpt + t.Run(tc, func(t *testing.T) { + var m MountOpt - assert.NilError(t, mount.Set(testcase)) + assert.NilError(t, m.Set(tc)) - mounts := mount.Value() - assert.Assert(t, is.Len(mounts, 1)) - assert.Check(t, is.DeepEqual(mounttypes.Mount{ - Type: mounttypes.TypeBind, - Source: "/source", - Target: "/target", - }, mounts[0])) + mounts := m.Value() + assert.Assert(t, is.Len(mounts, 1)) + assert.Check(t, is.DeepEqual(mount.Mount{ + Type: mount.TypeBind, + Source: "/source", + Target: "/target", + }, mounts[0])) + }) } } +// TestMountOptSetVolumeNoError tests several aliases that should have +// the same result. func TestMountOptSetVolumeNoError(t *testing.T) { - for _, testcase := range []string{ - // tests several aliases that should have same result. + for _, tc := range []string{ "type=volume,target=/target,source=/source", "type=volume,src=/source,dst=/target", "type=volume,source=/source,dst=/target", "type=volume,src=/source,target=/target", } { - var mount MountOpt + t.Run(tc, func(t *testing.T) { + var m MountOpt - assert.NilError(t, mount.Set(testcase)) + assert.NilError(t, m.Set(tc)) - mounts := mount.Value() - assert.Assert(t, is.Len(mounts, 1)) - assert.Check(t, is.DeepEqual(mounttypes.Mount{ - Type: mounttypes.TypeVolume, - Source: "/source", - Target: "/target", - }, mounts[0])) + mounts := m.Value() + assert.Assert(t, is.Len(mounts, 1)) + assert.Check(t, is.DeepEqual(mount.Mount{ + Type: mount.TypeVolume, + Source: "/source", + Target: "/target", + }, mounts[0])) + }) } } // TestMountOptDefaultType ensures that a mount without the type defaults to a // volume mount. func TestMountOptDefaultType(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set("target=/target,source=/foo")) - assert.Check(t, is.Equal(mounttypes.TypeVolume, mount.values[0].Type)) + var m MountOpt + assert.NilError(t, m.Set("target=/target,source=/foo")) + assert.Check(t, is.Equal(mount.TypeVolume, m.values[0].Type)) } func TestMountOptSetErrorNoTarget(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required") + var m MountOpt + assert.Error(t, m.Set("type=volume,source=/foo"), "target is required") } func TestMountOptSetErrorInvalidKey(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus' in 'bogus=foo'") + var m MountOpt + assert.Error(t, m.Set("type=volume,bogus=foo"), "unexpected key 'bogus' in 'bogus=foo'") } func TestMountOptSetErrorInvalidField(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus' must be a key=value pair") + var m MountOpt + assert.Error(t, m.Set("type=volume,bogus"), "invalid field 'bogus' must be a key=value pair") } func TestMountOptSetErrorInvalidReadOnly(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no") - assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid") + var m MountOpt + assert.Error(t, m.Set("type=volume,readonly=no"), "invalid value for readonly: no") + assert.Error(t, m.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid") } func TestMountOptDefaultEnableReadOnly(t *testing.T) { @@ -200,46 +206,49 @@ func TestMountOptTypeConflict(t *testing.T) { } func TestMountOptSetImageNoError(t *testing.T) { - for _, testcase := range []string{ + for _, tc := range []string{ "type=image,source=foo,target=/target,image-subpath=/bar", } { - var mount MountOpt + var m MountOpt - assert.NilError(t, mount.Set(testcase)) + assert.NilError(t, m.Set(tc)) - mounts := mount.Value() + mounts := m.Value() assert.Assert(t, is.Len(mounts, 1)) - assert.Check(t, is.DeepEqual(mounttypes.Mount{ - Type: mounttypes.TypeImage, + assert.Check(t, is.DeepEqual(mount.Mount{ + Type: mount.TypeImage, Source: "foo", Target: "/target", - ImageOptions: &mounttypes.ImageOptions{ + ImageOptions: &mount.ImageOptions{ Subpath: "/bar", }, }, mounts[0])) } } +// TestMountOptSetTmpfsNoError tests several aliases that should have +// the same result. func TestMountOptSetTmpfsNoError(t *testing.T) { - for _, testcase := range []string{ - // tests several aliases that should have same result. + for _, tc := range []string{ "type=tmpfs,target=/target,tmpfs-size=1m,tmpfs-mode=0700", "type=tmpfs,target=/target,tmpfs-size=1MB,tmpfs-mode=700", } { - var mount MountOpt + t.Run(tc, func(t *testing.T) { + var m MountOpt - assert.NilError(t, mount.Set(testcase)) + assert.NilError(t, m.Set(tc)) - mounts := mount.Value() - assert.Assert(t, is.Len(mounts, 1)) - assert.Check(t, is.DeepEqual(mounttypes.Mount{ - Type: mounttypes.TypeTmpfs, - Target: "/target", - TmpfsOptions: &mounttypes.TmpfsOptions{ - SizeBytes: 1024 * 1024, // not 1000 * 1000 - Mode: os.FileMode(0o700), - }, - }, mounts[0])) + mounts := m.Value() + assert.Assert(t, is.Len(mounts, 1)) + assert.Check(t, is.DeepEqual(mount.Mount{ + Type: mount.TypeTmpfs, + Target: "/target", + TmpfsOptions: &mount.TmpfsOptions{ + SizeBytes: 1024 * 1024, // not 1000 * 1000 + Mode: os.FileMode(0o700), + }, + }, mounts[0])) + }) } } @@ -250,85 +259,70 @@ func TestMountOptSetTmpfsError(t *testing.T) { assert.ErrorContains(t, m.Set("type=tmpfs"), "target is required") } -func TestMountOptSetBindNonRecursive(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-nonrecursive")) - assert.Check(t, is.DeepEqual([]mounttypes.Mount{ - { - Type: mounttypes.TypeBind, - Source: "/foo", - Target: "/bar", - BindOptions: &mounttypes.BindOptions{ - NonRecursive: true, - }, - }, - }, mount.Value())) -} - func TestMountOptSetBindRecursive(t *testing.T) { t.Run("enabled", func(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=enabled")) - assert.Check(t, is.DeepEqual([]mounttypes.Mount{ + var m MountOpt + assert.NilError(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=enabled")) + assert.Check(t, is.DeepEqual([]mount.Mount{ { - Type: mounttypes.TypeBind, + Type: mount.TypeBind, Source: "/foo", Target: "/bar", }, - }, mount.Value())) + }, m.Value())) }) t.Run("disabled", func(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=disabled")) - assert.Check(t, is.DeepEqual([]mounttypes.Mount{ + var m MountOpt + assert.NilError(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=disabled")) + assert.Check(t, is.DeepEqual([]mount.Mount{ { - Type: mounttypes.TypeBind, + Type: mount.TypeBind, Source: "/foo", Target: "/bar", - BindOptions: &mounttypes.BindOptions{ + BindOptions: &mount.BindOptions{ NonRecursive: true, }, }, - }, mount.Value())) + }, m.Value())) }) t.Run("writable", func(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=writable"), + var m MountOpt + assert.Error(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=writable"), "option 'bind-recursive=writable' requires 'readonly' to be specified in conjunction") - assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=writable,readonly")) - assert.Check(t, is.DeepEqual([]mounttypes.Mount{ + assert.NilError(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=writable,readonly")) + assert.Check(t, is.DeepEqual([]mount.Mount{ { - Type: mounttypes.TypeBind, + Type: mount.TypeBind, Source: "/foo", Target: "/bar", ReadOnly: true, - BindOptions: &mounttypes.BindOptions{ + BindOptions: &mount.BindOptions{ ReadOnlyNonRecursive: true, }, }, - }, mount.Value())) + }, m.Value())) }) t.Run("readonly", func(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly"), + var m MountOpt + assert.Error(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly"), "option 'bind-recursive=readonly' requires 'readonly' to be specified in conjunction") - assert.Error(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly,readonly"), + assert.Error(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly,readonly"), "option 'bind-recursive=readonly' requires 'bind-propagation=rprivate' to be specified in conjunction") - assert.NilError(t, mount.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly,readonly,bind-propagation=rprivate")) - assert.Check(t, is.DeepEqual([]mounttypes.Mount{ + assert.NilError(t, m.Set("type=bind,source=/foo,target=/bar,bind-recursive=readonly,readonly,bind-propagation=rprivate")) + assert.Check(t, is.DeepEqual([]mount.Mount{ { - Type: mounttypes.TypeBind, + Type: mount.TypeBind, Source: "/foo", Target: "/bar", ReadOnly: true, - BindOptions: &mounttypes.BindOptions{ + BindOptions: &mount.BindOptions{ ReadOnlyForceRecursive: true, - Propagation: mounttypes.PropagationRPrivate, + Propagation: mount.PropagationRPrivate, }, }, - }, mount.Value())) + }, m.Value())) }) } diff --git a/opts/network.go b/opts/network.go index 43b3a09d4151..489ef8be3971 100644 --- a/opts/network.go +++ b/opts/network.go @@ -4,6 +4,7 @@ import ( "encoding/csv" "errors" "fmt" + "net/netip" "regexp" "strconv" "strings" @@ -26,9 +27,9 @@ type NetworkAttachmentOpts struct { Aliases []string DriverOpts map[string]string Links []string // TODO add support for links in the csv notation of `--network` - IPv4Address string - IPv6Address string - LinkLocalIPs []string + IPv4Address netip.Addr + IPv6Address netip.Addr + LinkLocalIPs []netip.Addr MacAddress string GwPriority int } @@ -70,13 +71,23 @@ func (n *NetworkOpt) Set(value string) error { //nolint:gocyclo case networkOptAlias: netOpt.Aliases = append(netOpt.Aliases, val) case networkOptIPv4Address: - netOpt.IPv4Address = val + netOpt.IPv4Address, err = netip.ParseAddr(val) + if err != nil { + return err + } case networkOptIPv6Address: - netOpt.IPv6Address = val + netOpt.IPv6Address, err = netip.ParseAddr(val) + if err != nil { + return err + } case networkOptMacAddress: netOpt.MacAddress = val case networkOptLinkLocalIP: - netOpt.LinkLocalIPs = append(netOpt.LinkLocalIPs, val) + a, err := netip.ParseAddr(val) + if err != nil { + return err + } + netOpt.LinkLocalIPs = append(netOpt.LinkLocalIPs, a) case driverOpt: key, val, err = parseDriverOpt(val) if err != nil { diff --git a/opts/network_test.go b/opts/network_test.go index ffdcbf26cd80..ad7f50ece7b5 100644 --- a/opts/network_test.go +++ b/opts/network_test.go @@ -1,8 +1,10 @@ package opts import ( + "net/netip" "testing" + "github.com/google/go-cmp/cmp/cmpopts" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -24,7 +26,7 @@ func TestNetworkOptLegacySyntax(t *testing.T) { for _, tc := range testCases { var network NetworkOpt assert.NilError(t, network.Set(tc.value)) - assert.Check(t, is.DeepEqual(tc.expected, network.Value())) + assert.Check(t, is.DeepEqual(tc.expected, network.Value(), cmpopts.EquateComparable(netip.Addr{}))) } } @@ -64,8 +66,8 @@ func TestNetworkOptAdvancedSyntax(t *testing.T) { { Target: "docknet1", Aliases: []string{}, - IPv4Address: "172.20.88.22", - IPv6Address: "2001:db8::8822", + IPv4Address: netip.MustParseAddr("172.20.88.22"), + IPv6Address: netip.MustParseAddr("2001:db8::8822"), }, }, }, @@ -94,7 +96,7 @@ func TestNetworkOptAdvancedSyntax(t *testing.T) { { Target: "docknet1", Aliases: []string{}, - LinkLocalIPs: []string{"169.254.169.254", "169.254.10.10"}, + LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254"), netip.MustParseAddr("169.254.10.10")}, }, }, }, @@ -127,7 +129,7 @@ func TestNetworkOptAdvancedSyntax(t *testing.T) { t.Run(tc.value, func(t *testing.T) { var network NetworkOpt assert.NilError(t, network.Set(tc.value)) - assert.Check(t, is.DeepEqual(tc.expected, network.Value())) + assert.Check(t, is.DeepEqual(tc.expected, network.Value(), cmpopts.EquateComparable(netip.Addr{}))) }) } } diff --git a/opts/opts.go b/opts/opts.go index 1a885db30eff..0b8979b6b6c6 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -1,6 +1,7 @@ package opts import ( + "encoding/json" "errors" "fmt" "math/big" @@ -9,8 +10,8 @@ import ( "strings" "github.com/docker/cli/internal/lazyregexp" - "github.com/docker/docker/api/types/filters" "github.com/docker/go-units" + "github.com/moby/moby/client" ) var ( @@ -60,6 +61,8 @@ func (opts *ListOpts) Set(value string) error { } // Delete removes the specified element from the slice. +// +// Deprecated: this method is no longer used and will be removed in the next release. func (opts *ListOpts) Delete(key string) { for i, k := range *opts.values { if k == key { @@ -79,13 +82,6 @@ func (opts *ListOpts) GetMap() map[string]struct{} { return ret } -// GetAll returns the values of slice. -// -// Deprecated: use [ListOpts.GetSlice] instead. This method will be removed in a future release. -func (opts *ListOpts) GetAll() []string { - return *opts.values -} - // GetSlice returns the values of slice. // // It implements [cobra.SliceValue] to allow shell completion to be provided @@ -132,35 +128,6 @@ func (opts *ListOpts) WithValidator(validator ValidatorFctType) *ListOpts { return opts } -// NamedOption is an interface that list and map options -// with names implement. -type NamedOption interface { - Name() string -} - -// NamedListOpts is a ListOpts with a configuration name. -// This struct is useful to keep reference to the assigned -// field name in the internal configuration struct. -type NamedListOpts struct { - name string - ListOpts -} - -var _ NamedOption = &NamedListOpts{} - -// NewNamedListOptsRef creates a reference to a new NamedListOpts struct. -func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts { - return &NamedListOpts{ - name: name, - ListOpts: *NewListOptsRef(values, validator), - } -} - -// Name returns the name of the NamedListOpts in the configuration. -func (o *NamedListOpts) Name() string { - return o.name -} - // MapOpts holds a map of values and a validation function. type MapOpts struct { values map[string]string @@ -207,29 +174,6 @@ func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts { } } -// NamedMapOpts is a MapOpts struct with a configuration name. -// This struct is useful to keep reference to the assigned -// field name in the internal configuration struct. -type NamedMapOpts struct { - name string - MapOpts -} - -var _ NamedOption = &NamedMapOpts{} - -// NewNamedMapOpts creates a reference to a new NamedMapOpts struct. -func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts { - return &NamedMapOpts{ - name: name, - MapOpts: *NewMapOpts(values, validator), - } -} - -// Name returns the name of the NamedMapOpts in the configuration. -func (o *NamedMapOpts) Name() string { - return o.name -} - // ValidatorFctType defines a validator function that returns a validated string and/or an error. type ValidatorFctType func(val string) (string, error) @@ -250,6 +194,8 @@ func ValidateIPAddress(val string) (string, error) { } // ValidateMACAddress validates a MAC address. +// +// Deprecated: use [net.ParseMAC]. This function will be removed in the next release. func ValidateMACAddress(val string) (string, error) { _, err := net.ParseMAC(strings.TrimSpace(val)) if err != nil { @@ -336,20 +282,23 @@ func ValidateSysctl(val string) (string, error) { // FilterOpt is a flag type for validating filters type FilterOpt struct { - filter filters.Args + filter client.Filters } // NewFilterOpt returns a new FilterOpt func NewFilterOpt() FilterOpt { - return FilterOpt{filter: filters.NewArgs()} + return FilterOpt{filter: make(client.Filters)} } func (o *FilterOpt) String() string { - repr, err := filters.ToJSON(o.filter) + if o == nil || len(o.filter) == 0 { + return "" + } + repr, err := json.Marshal(o.filter) if err != nil { return "invalid filters" } - return repr + return string(repr) } // Set sets the value of the opt by parsing the command line value @@ -375,7 +324,7 @@ func (*FilterOpt) Type() string { } // Value returns the value of this option -func (o *FilterOpt) Value() filters.Args { +func (o *FilterOpt) Value() client.Filters { return o.filter } diff --git a/opts/opts_test.go b/opts/opts_test.go index 7dc87cad9e83..e978c2ea8799 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -112,7 +112,6 @@ func TestMapOpts(t *testing.T) { } } -//nolint:gocyclo // ignore "cyclomatic complexity 17 is too high" func TestListOptsWithoutValidator(t *testing.T) { o := NewListOpts(nil) err := o.Set("foo") @@ -142,18 +141,11 @@ func TestListOptsWithoutValidator(t *testing.T) { if o.Get("baz") { t.Error(`o.Get("baz") == true`) } - o.Delete("foo") - if o.String() != "[bar bar]" { - t.Errorf("%s != [bar bar]", o.String()) + if listOpts := o.GetSlice(); len(listOpts) != 3 || listOpts[0] != "foo" || listOpts[1] != "bar" || listOpts[2] != "bar" { + t.Errorf("Expected [[foo bar bar]], got [%v]", listOpts) } - if listOpts := o.GetAll(); len(listOpts) != 2 || listOpts[0] != "bar" || listOpts[1] != "bar" { - t.Errorf("Expected [[bar bar]], got [%v]", listOpts) - } - if listOpts := o.GetSlice(); len(listOpts) != 2 || listOpts[0] != "bar" || listOpts[1] != "bar" { - t.Errorf("Expected [[bar bar]], got [%v]", listOpts) - } - if mapListOpts := o.GetMap(); len(mapListOpts) != 1 { - t.Errorf("Expected [map[bar:{}]], got [%v]", mapListOpts) + if mapListOpts := o.GetMap(); len(mapListOpts) != 2 { + t.Errorf("Expected [map[bar:{} foo:{}]], got [%v]", mapListOpts) } } @@ -186,9 +178,8 @@ func TestListOptsWithValidator(t *testing.T) { if o.Get("baz") { t.Error(`o.Get("baz") == true`) } - o.Delete("valid-option2=2") - if o.String() != "" { - t.Errorf(`%s != ""`, o.String()) + if expected := "[valid-option2=2]"; o.String() != expected { + t.Errorf(`%s != %q`, o.String(), expected) } } @@ -364,52 +355,6 @@ func sampleValidator(val string) (string, error) { return "", fmt.Errorf("invalid key %s", k) } -func TestNamedListOpts(t *testing.T) { - var v []string - o := NewNamedListOptsRef("foo-name", &v, nil) - - o.Set("foo") - if o.String() != "[foo]" { - t.Errorf("%s != [foo]", o.String()) - } - if o.Name() != "foo-name" { - t.Errorf("%s != foo-name", o.Name()) - } - if len(v) != 1 { - t.Errorf("expected foo to be in the values, got %v", v) - } -} - -func TestNamedMapOpts(t *testing.T) { - tmpMap := make(map[string]string) - o := NewNamedMapOpts("max-name", tmpMap, nil) - - o.Set("max-size=1") - if o.String() != "map[max-size:1]" { - t.Errorf("%s != [map[max-size:1]", o.String()) - } - if o.Name() != "max-name" { - t.Errorf("%s != max-name", o.Name()) - } - if _, exist := tmpMap["max-size"]; !exist { - t.Errorf("expected map-size to be in the values, got %v", tmpMap) - } -} - -func TestValidateMACAddress(t *testing.T) { - if _, err := ValidateMACAddress(`92:d0:c6:0a:29:33`); err != nil { - t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:29:33`) got %s", err) - } - - if _, err := ValidateMACAddress(`92:d0:c6:0a:33`); err == nil { - t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:33`) succeeded; expected failure on invalid MAC") - } - - if _, err := ValidateMACAddress(`random invalid string`); err == nil { - t.Fatalf("ValidateMACAddress(`random invalid string`) succeeded; expected failure on invalid MAC") - } -} - func TestValidateLink(t *testing.T) { valid := []string{ "name", diff --git a/opts/parse.go b/opts/parse.go index 996d4d7e7a2d..c04fc7d4b8bb 100644 --- a/opts/parse.go +++ b/opts/parse.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/docker/cli/pkg/kvfile" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" ) // ReadKVStrings reads a file of line terminated key=value pairs, and overrides any keys diff --git a/opts/quotedstring.go b/opts/quotedstring.go deleted file mode 100644 index eb2ac7fbc8af..000000000000 --- a/opts/quotedstring.go +++ /dev/null @@ -1,40 +0,0 @@ -package opts - -// QuotedString is a string that may have extra quotes around the value. The -// quotes are stripped from the value. -type QuotedString struct { - value *string -} - -// Set sets a new value -func (s *QuotedString) Set(val string) error { - *s.value = trimQuotes(val) - return nil -} - -// Type returns the type of the value -func (*QuotedString) Type() string { - return "string" -} - -func (s *QuotedString) String() string { - return *s.value -} - -func trimQuotes(value string) string { - if len(value) < 2 { - return value - } - lastIndex := len(value) - 1 - for _, char := range []byte{'\'', '"'} { - if value[0] == char && value[lastIndex] == char { - return value[1:lastIndex] - } - } - return value -} - -// NewQuotedString returns a new quoted string option -func NewQuotedString(value *string) *QuotedString { - return &QuotedString{value: value} -} diff --git a/opts/quotedstring_test.go b/opts/quotedstring_test.go deleted file mode 100644 index f56868b1c63e..000000000000 --- a/opts/quotedstring_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package opts - -import ( - "testing" - - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestQuotedStringSetWithQuotes(t *testing.T) { - value := "" - qs := NewQuotedString(&value) - assert.NilError(t, qs.Set(`"something"`)) - assert.Check(t, is.Equal("something", qs.String())) - assert.Check(t, is.Equal("something", value)) -} - -func TestQuotedStringSetWithMismatchedQuotes(t *testing.T) { - value := "" - qs := NewQuotedString(&value) - assert.NilError(t, qs.Set(`"something'`)) - assert.Check(t, is.Equal(`"something'`, qs.String())) -} - -func TestQuotedStringSetWithNoQuotes(t *testing.T) { - value := "" - qs := NewQuotedString(&value) - assert.NilError(t, qs.Set("something")) - assert.Check(t, is.Equal("something", qs.String())) -} - -func TestQuotedStringShort(t *testing.T) { - value := "" - qs := NewQuotedString(&value) - assert.NilError(t, qs.Set(`"`)) - assert.Check(t, is.Equal(`"`, qs.String())) - - assert.NilError(t, qs.Set(`'`)) - assert.Check(t, is.Equal(`'`, qs.String())) -} diff --git a/opts/swarmopts/config.go b/opts/swarmopts/config.go index ff137304ace0..78e8f886b649 100644 --- a/opts/swarmopts/config.go +++ b/opts/swarmopts/config.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // ConfigOpt is a Value type for parsing configs diff --git a/opts/swarmopts/port.go b/opts/swarmopts/port.go index e15c6b83023c..2a8c2b71c039 100644 --- a/opts/swarmopts/port.go +++ b/opts/swarmopts/port.go @@ -9,8 +9,9 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-connections/nat" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" "github.com/sirupsen/logrus" ) @@ -41,7 +42,10 @@ func (p *PortOpt) Set(value string) error { return err } - pConfig := swarm.PortConfig{} + pConfig := swarm.PortConfig{ + Protocol: network.TCP, + PublishMode: swarm.PortConfigPublishModeIngress, + } for _, field := range fields { // TODO(thaJeztah): these options should not be case-insensitive. key, val, ok := strings.Cut(strings.ToLower(field), "=") @@ -50,17 +54,19 @@ func (p *PortOpt) Set(value string) error { } switch key { case portOptProtocol: - if val != string(swarm.PortConfigProtocolTCP) && val != string(swarm.PortConfigProtocolUDP) && val != string(swarm.PortConfigProtocolSCTP) { + switch proto := network.IPProtocol(val); proto { + case network.TCP, network.UDP, network.SCTP: + pConfig.Protocol = proto + default: return fmt.Errorf("invalid protocol value '%s'", val) } - - pConfig.Protocol = swarm.PortConfigProtocol(val) case portOptMode: - if val != string(swarm.PortConfigPublishModeIngress) && val != string(swarm.PortConfigPublishModeHost) { + switch swarm.PortConfigPublishMode(val) { + case swarm.PortConfigPublishModeIngress, swarm.PortConfigPublishModeHost: + pConfig.PublishMode = swarm.PortConfigPublishMode(val) + default: return fmt.Errorf("invalid publish mode value (%s): must be either '%s' or '%s'", val, swarm.PortConfigPublishModeIngress, swarm.PortConfigPublishModeHost) } - - pConfig.PublishMode = swarm.PortConfigPublishMode(val) case portOptTargetPort: tPort, err := strconv.ParseUint(val, 10, 16) if err != nil { @@ -92,18 +98,11 @@ func (p *PortOpt) Set(value string) error { return fmt.Errorf("missing mandatory field '%s'", portOptTargetPort) } - if pConfig.PublishMode == "" { - pConfig.PublishMode = swarm.PortConfigPublishModeIngress - } - - if pConfig.Protocol == "" { - pConfig.Protocol = swarm.PortConfigProtocolTCP - } - p.ports = append(p.ports, pConfig) } else { - // short syntax - portConfigs := []swarm.PortConfig{} + // short syntax ([ip:]public:private[/proto]) + // + // TODO(thaJeztah): we need an equivalent that handles the "ip-address" part without depending on the nat package. ports, portBindingMap, err := nat.ParsePortSpecs([]string{value}) if err != nil { return err @@ -116,8 +115,13 @@ func (p *PortOpt) Set(value string) error { } } + var portConfigs []swarm.PortConfig for port := range ports { - portConfig, err := ConvertPortToPortConfig(port, portBindingMap) + portProto, err := network.ParsePort(string(port)) + if err != nil { + return err + } + portConfig, err := ConvertPortToPortConfig(portProto, portBindingMap) if err != nil { return err } @@ -135,7 +139,7 @@ func (*PortOpt) Type() string { // String returns a string repr of this option func (p *PortOpt) String() string { - ports := []string{} + ports := make([]string, 0, len(p.ports)) for _, port := range p.ports { repr := fmt.Sprintf("%v:%v/%s/%s", port.PublishedPort, port.TargetPort, port.Protocol, port.PublishMode) ports = append(ports, repr) @@ -150,29 +154,27 @@ func (p *PortOpt) Value() []swarm.PortConfig { // ConvertPortToPortConfig converts ports to the swarm type func ConvertPortToPortConfig( - port nat.Port, + portProto network.Port, portBindings map[nat.Port][]nat.PortBinding, ) ([]swarm.PortConfig, error) { - ports := []swarm.PortConfig{} - - for _, binding := range portBindings[port] { + ports := make([]swarm.PortConfig, 0, len(portBindings)) + for _, binding := range portBindings[nat.Port(portProto.String())] { if p := net.ParseIP(binding.HostIP); p != nil && !p.IsUnspecified() { // TODO(thaJeztah): use context-logger, so that this output can be suppressed (in tests). - logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), port) + logrus.Warnf("ignoring IP-address (%s:%s) service will listen on '0.0.0.0'", net.JoinHostPort(binding.HostIP, binding.HostPort), portProto.String()) } - startHostPort, endHostPort, err := nat.ParsePortRange(binding.HostPort) - + pr, err := network.ParsePortRange(binding.HostPort) if err != nil && binding.HostPort != "" { - return nil, fmt.Errorf("invalid hostport binding (%s) for port (%s)", binding.HostPort, port.Port()) + return nil, fmt.Errorf("invalid hostport binding (%s) for port (%d)", binding.HostPort, portProto.Num()) } - for i := startHostPort; i <= endHostPort; i++ { + for p := range pr.All() { ports = append(ports, swarm.PortConfig{ // TODO Name: ? - Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())), - TargetPort: uint32(port.Int()), - PublishedPort: uint32(i), + Protocol: portProto.Proto(), + TargetPort: uint32(portProto.Num()), + PublishedPort: uint32(p.Num()), PublishMode: swarm.PortConfigPublishModeIngress, }) } diff --git a/opts/swarmopts/port_test.go b/opts/swarmopts/port_test.go index c13c6cd4d000..5c283f2b7a33 100644 --- a/opts/swarmopts/port_test.go +++ b/opts/swarmopts/port_test.go @@ -5,8 +5,9 @@ import ( "os" "testing" - "github.com/docker/docker/api/types/swarm" "github.com/docker/go-connections/nat" + "github.com/moby/moby/api/types/network" + "github.com/moby/moby/api/types/swarm" "github.com/sirupsen/logrus" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -136,12 +137,14 @@ func TestPortOptValidSimpleSyntax(t *testing.T) { }, } for _, tc := range testCases { - var port PortOpt - assert.NilError(t, port.Set(tc.value)) - assert.Check(t, is.Len(port.Value(), len(tc.expected))) - for _, expectedPortConfig := range tc.expected { - assertContains(t, port.Value(), expectedPortConfig) - } + t.Run(tc.value, func(t *testing.T) { + var port PortOpt + assert.NilError(t, port.Set(tc.value)) + assert.Check(t, is.Len(port.Value(), len(tc.expected))) + for _, expectedPortConfig := range tc.expected { + assertContains(t, port.Value(), expectedPortConfig) + } + }) } } @@ -227,12 +230,14 @@ func TestPortOptValidComplexSyntax(t *testing.T) { }, } for _, tc := range testCases { - var port PortOpt - assert.NilError(t, port.Set(tc.value)) - assert.Check(t, is.Len(port.Value(), len(tc.expected))) - for _, expectedPortConfig := range tc.expected { - assertContains(t, port.Value(), expectedPortConfig) - } + t.Run(tc.value, func(t *testing.T) { + var port PortOpt + assert.NilError(t, port.Set(tc.value)) + assert.Check(t, is.Len(port.Value(), len(tc.expected))) + for _, expectedPortConfig := range tc.expected { + assertContains(t, port.Value(), expectedPortConfig) + } + }) } } @@ -309,7 +314,8 @@ func TestPortOptInvalidSimpleSyntax(t *testing.T) { }, { value: "", - expectedError: "no port specified: ", + expectedError: "invalid proto: ", + // expectedError: "no port specified: ", // FIXME(thaJeztah): re-enable once https://github.com/docker/go-connections/pull/143 is in a go-connections release. }, { value: "1.1.1.1:80:80", @@ -317,8 +323,10 @@ func TestPortOptInvalidSimpleSyntax(t *testing.T) { }, } for _, tc := range testCases { - var port PortOpt - assert.Error(t, port.Set(tc.value), tc.expectedError) + t.Run(tc.value, func(t *testing.T) { + var port PortOpt + assert.Error(t, port.Set(tc.value), tc.expectedError) + }) } } @@ -347,7 +355,7 @@ func TestConvertPortToPortConfigWithIP(t *testing.T) { logrus.SetOutput(&b) for _, tc := range testCases { t.Run(tc.value, func(t *testing.T) { - _, err := ConvertPortToPortConfig("80/tcp", map[nat.Port][]nat.PortBinding{ + _, err := ConvertPortToPortConfig(network.MustParsePort("80/tcp"), map[nat.Port][]nat.PortBinding{ "80/tcp": {{HostIP: tc.value, HostPort: "2345"}}, }) assert.NilError(t, err) diff --git a/opts/swarmopts/secret.go b/opts/swarmopts/secret.go index 9f97627a5b9a..b28970af9ae8 100644 --- a/opts/swarmopts/secret.go +++ b/opts/swarmopts/secret.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/swarm" + "github.com/moby/moby/api/types/swarm" ) // SecretOpt is a Value type for parsing secrets diff --git a/opts/throttledevice.go b/opts/throttledevice.go index 46b09185c72a..bc759820e02d 100644 --- a/opts/throttledevice.go +++ b/opts/throttledevice.go @@ -5,8 +5,8 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/blkiodev" "github.com/docker/go-units" + "github.com/moby/moby/api/types/blkiodev" ) // ValidatorThrottleFctType defines a validator function that returns a validated struct and/or an error. diff --git a/opts/ulimit.go b/opts/ulimit.go index 48052c887c21..aa88bce71a24 100644 --- a/opts/ulimit.go +++ b/opts/ulimit.go @@ -4,8 +4,8 @@ import ( "fmt" "sort" - "github.com/docker/docker/api/types/container" "github.com/docker/go-units" + "github.com/moby/moby/api/types/container" ) // UlimitOpt defines a map of Ulimits diff --git a/opts/ulimit_test.go b/opts/ulimit_test.go index 3a0753d15901..72c07024a4de 100644 --- a/opts/ulimit_test.go +++ b/opts/ulimit_test.go @@ -3,7 +3,7 @@ package opts import ( "testing" - "github.com/docker/docker/api/types/container" + "github.com/moby/moby/api/types/container" "gotest.tools/v3/assert" ) diff --git a/opts/weightdevice.go b/opts/weightdevice.go index 036c7c8c50c8..4476548fd36e 100644 --- a/opts/weightdevice.go +++ b/opts/weightdevice.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "github.com/docker/docker/api/types/blkiodev" + "github.com/moby/moby/api/types/blkiodev" ) // ValidatorWeightFctType defines a validator function that returns a validated struct and/or an error. diff --git a/scripts/build/.variables b/scripts/build/.variables index c42d305a3b1d..d37c945893c6 100755 --- a/scripts/build/.variables +++ b/scripts/build/.variables @@ -96,11 +96,11 @@ if test -n "${PLATFORM}"; then GO_LDFLAGS="$GO_LDFLAGS -X \"github.com/docker/cli/cli/version.PlatformName=${PLATFORM}\"" fi if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then - GO_LDFLAGS="$GO_LDFLAGS -extldflags -static" + GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static" fi if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ]; then - # compiling statically with CGO enabled requires osusergo to be set. - GO_BUILDTAGS="$GO_BUILDTAGS osusergo" + # compiling statically with CGO enabled requires osusergo and netgo to be set. + GO_BUILDTAGS="$GO_BUILDTAGS osusergo netgo" fi if [ -n "$GO_STRIP" ]; then # if stripping enabled and building with llvm < 12 against darwin/amd64 diff --git a/scripts/build/binary b/scripts/build/binary index 44cd4f14fdd8..139cc517fbdf 100755 --- a/scripts/build/binary +++ b/scripts/build/binary @@ -7,10 +7,6 @@ set -eu . ./scripts/build/.variables -if [ "$CGO_ENABLED" = "1" ] && [ "$(go env GOOS)" != "windows" ]; then - GO_BUILDTAGS="$GO_BUILDTAGS pkcs11" -fi - echo "Building $GO_LINKMODE $(basename "${TARGET}")" export GO111MODULE=auto diff --git a/scripts/build/mkversioninfo b/scripts/build/mkversioninfo index b349e9226664..1cbc6dcc5379 100755 --- a/scripts/build/mkversioninfo +++ b/scripts/build/mkversioninfo @@ -20,7 +20,7 @@ VERSION_QUAD=$(printf "%s" "$VERSION" | sed -re 's/^([0-9.]*).*$/\1/' | sed -re # Generate versioninfo.json to be able to create a syso file which contains # Microsoft Windows Version Information and an icon using goversioninfo. # https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block -# https://github.com/josephspurrier/goversioninfo/blob/master/testdata/resource/versioninfo.json +# https://github.com/josephspurrier/goversioninfo/blob/v1.5.0/testdata/resource/versioninfo.json cat > ./cmd/docker/winresources/versioninfo.json <&2 <<- EOF module github.com/docker/cli - go 1.23.0 + go 1.24.0 EOF trap 'rm -f "${ROOTDIR}/go.mod"' EXIT fi diff --git a/templates/templates.go b/templates/templates.go index f0726eec95c5..e339050abb3f 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -1,5 +1,5 @@ // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: -//go:build go1.23 +//go:build go1.24 package templates @@ -13,18 +13,7 @@ import ( // basicFunctions are the set of initial // functions provided to every template. var basicFunctions = template.FuncMap{ - "json": func(v any) string { - buf := &bytes.Buffer{} - enc := json.NewEncoder(buf) - enc.SetEscapeHTML(false) - err := enc.Encode(v) - if err != nil { - panic(err) - } - - // Remove the trailing new line added by the encoder - return strings.TrimSpace(buf.String()) - }, + "json": formatJSON, "split": strings.Split, "join": strings.Join, "title": strings.Title, //nolint:nolintlint,staticcheck // strings.Title is deprecated, but we only use it for ASCII, so replacing with golang.org/x/text is out of scope @@ -71,7 +60,7 @@ var HeaderFunctions = template.FuncMap{ // Parse creates a new anonymous template with the basic functions // and parses the given format. func Parse(format string) (*template.Template, error) { - return NewParse("", format) + return template.New("").Funcs(basicFunctions).Parse(format) } // New creates a new empty template with the provided tag and built-in @@ -80,12 +69,6 @@ func New(tag string) *template.Template { return template.New(tag).Funcs(basicFunctions) } -// NewParse creates a new tagged template with the basic functions -// and parses the given format. -func NewParse(tag, format string) (*template.Template, error) { - return New(tag).Parse(format) -} - // padWithSpace adds whitespace to the input if the input is non-empty func padWithSpace(source string, prefix, suffix int) string { if source == "" { @@ -101,3 +84,16 @@ func truncateWithLength(source string, length int) string { } return source[:length] } + +func formatJSON(v any) string { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err := enc.Encode(v) + if err != nil { + panic(err) + } + + // Remove the trailing new line added by the encoder + return strings.TrimSpace(buf.String()) +} diff --git a/templates/templates_test.go b/templates/templates_test.go index 608fe72a2004..e9dbaefd0e5e 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -30,7 +30,7 @@ func TestParseStringFunctions(t *testing.T) { } func TestNewParse(t *testing.T) { - tm, err := NewParse("foo", "this is a {{ . }}") + tm, err := New("foo").Parse("this is a {{ . }}") assert.NilError(t, err) var b bytes.Buffer diff --git a/vendor.mod b/vendor.mod index 68d14b00251c..e21c8bb8a9c5 100644 --- a/vendor.mod +++ b/vendor.mod @@ -4,86 +4,86 @@ module github.com/docker/cli // There is no 'go.mod' file, as that would imply opting in for all the rules // around SemVer, which this repo cannot abide by as it uses CalVer. -go 1.23.0 +go 1.24.0 require ( - dario.cat/mergo v1.0.1 + dario.cat/mergo v1.0.2 github.com/containerd/errdefs v1.0.0 - github.com/containerd/platforms v1.0.0-rc.1 + github.com/containerd/log v0.1.0 + github.com/containerd/platforms v1.0.0-rc.2 github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/creack/pty v1.1.24 github.com/distribution/reference v0.6.0 - github.com/docker/cli-docs-tool v0.10.0 + github.com/docker/cli-docs-tool v0.11.0 github.com/docker/distribution v2.8.3+incompatible - github.com/docker/docker v28.3.1+incompatible - github.com/docker/docker-credential-helpers v0.9.3 - github.com/docker/go-connections v0.5.0 + github.com/docker/docker-credential-helpers v0.9.4 + github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fvbommel/sortorder v1.1.0 - github.com/go-jose/go-jose/v4 v4.0.5 - github.com/go-viper/mapstructure/v2 v2.2.1 + github.com/go-jose/go-jose/v4 v4.1.3 + github.com/go-viper/mapstructure/v2 v2.4.0 github.com/gogo/protobuf v1.3.2 github.com/google/go-cmp v0.7.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 - github.com/mattn/go-runewidth v0.0.16 + github.com/mattn/go-runewidth v0.0.19 github.com/moby/go-archive v0.1.0 + github.com/moby/moby/api v1.52.0 + github.com/moby/moby/client v0.2.1 github.com/moby/patternmatcher v0.6.0 - github.com/moby/swarmkit/v2 v2.0.0 + github.com/moby/swarmkit/v2 v2.1.1 github.com/moby/sys/atomicwriter v0.1.0 github.com/moby/sys/capability v0.4.0 github.com/moby/sys/sequential v0.6.0 github.com/moby/sys/signal v0.7.1 + github.com/moby/sys/symlink v0.3.0 github.com/moby/term v0.5.2 - github.com/morikuni/aec v1.0.0 + github.com/morikuni/aec v1.1.0 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c - github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.6 - github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 github.com/xeipuuv/gojsonschema v1.2.0 - go.opentelemetry.io/otel v1.35.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 - go.opentelemetry.io/otel/metric v1.35.0 - go.opentelemetry.io/otel/sdk v1.35.0 - go.opentelemetry.io/otel/sdk/metric v1.35.0 - go.opentelemetry.io/otel/trace v1.35.0 - golang.org/x/sync v0.14.0 - golang.org/x/sys v0.33.0 - golang.org/x/term v0.31.0 - golang.org/x/text v0.24.0 - gopkg.in/yaml.v3 v3.0.1 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 + go.opentelemetry.io/otel v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 + go.opentelemetry.io/otel/metric v1.38.0 + go.opentelemetry.io/otel/sdk v1.38.0 + go.opentelemetry.io/otel/sdk/metric v1.38.0 + go.opentelemetry.io/otel/trace v1.38.0 + go.yaml.in/yaml/v3 v3.0.4 + golang.org/x/sync v0.18.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 gotest.tools/v3 v3.5.2 - tags.cncf.io/container-device-interface v0.8.0 + tags.cncf.io/container-device-interface v1.0.1 ) require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect - github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect github.com/docker/go-metrics v0.0.1 // indirect + github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/miekg/pkcs11 v1.1.1 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/symlink v0.3.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -91,21 +91,17 @@ require ( github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.etcd.io/etcd/raft/v3 v3.5.16 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.5.0 // indirect - golang.org/x/crypto v0.37.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/time v0.11.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.2 // indirect - google.golang.org/protobuf v1.36.6 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/vendor.sum b/vendor.sum index 3aa9706f4a08..5bbfc2493f2f 100644 --- a/vendor.sum +++ b/vendor.sum @@ -1,116 +1,82 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6DKrR4/ljxn6SF6nURyu785wKMuQcjt7H3VCQ= -github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bugsnag/bugsnag-go v1.0.5 h1:NIoY2u+am1/GRgUZa+ata8UUrRBuCK4pLq0/lcvMF7M= -github.com/bugsnag/bugsnag-go v1.0.5/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8= -github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= -github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU= -github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE= +github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY= -github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= -github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= -github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI= +github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4= +github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= -github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -119,63 +85,42 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= -github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= -github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= -github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= -github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.5.3 h1:C8fxWnhYyME3n0klPOhVM7PtYUB3eV1W3DeFmN3j53Y= -github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I= -github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= +github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k= +github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/swarmkit/v2 v2.0.0 h1:jkWQKQaJ4ltA61/mC9UdPe1McLma55RUcacTO+pPweY= -github.com/moby/swarmkit/v2 v2.0.0/go.mod h1:mTTGIAz/59OGZR5Qe+QByIe3Nxc+sSuJkrsStFhr6Lg= +github.com/moby/swarmkit/v2 v2.1.1 h1:yvTJ8MMCc3f0qTA44J6R59EZ5yZawdYopkpuLk4+ICU= +github.com/moby/swarmkit/v2 v2.1.1/go.mod h1:mTTGIAz/59OGZR5Qe+QByIe3Nxc+sSuJkrsStFhr6Lg= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= @@ -196,91 +141,59 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94 h1:JmfC365KywYwHB946TTiQWEb8kqPY+pybPLoGE9GgVk= -github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431 h1:XTHrT015sxHyJ5FnQ0AeemSspZWaDq7DoTRW0EVsDCE= -github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg= -github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a h1:tlJ7tGUHvcvL1v3yR6NcCc9nOqh2L+CG6HWrYQtwzQ0= -github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a/go.mod h1:Y94A6rPp2OwNfP/7vmf8O2xx2IykP8pPXQ1DLouGnEw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 h1:TvtdmeYsYEij78hS4oxnwikoiLdIrgav3BA+CbhaDAI= github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ= -github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b h1:FsyNrX12e5BkplJq7wKOLk0+C6LZ+KGXvuEcKUYm5ss= -github.com/weppos/publicsuffix-go v0.15.1-0.20210511084619-b1f36a2d6c0b/go.mod h1:HYux0V0Zi04bHNwOHy4cXJVz/TQjYonnF6aoYhj+3QE= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -290,89 +203,74 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAraH/3D8njopUR/pARHv4tZZ6pw= -github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk= -github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0= -github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU= go.etcd.io/etcd/raft/v3 v3.5.16 h1:zBXA3ZUpYs1AwiLGPafYAKKl/CORn/uaxYDwlNwndAk= go.etcd.io/etcd/raft/v3 v3.5.16/go.mod h1:P4UP14AxofMJ/54boWilabqqWoW9eLodl6I5GdGzazI= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -381,40 +279,27 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= -google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= -google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY= +google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= -gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= -gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= -k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc= -tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= +tags.cncf.io/container-device-interface v1.0.1/go.mod h1:JojJIOeW3hNbcnOH2q0NrWNha/JuHoDZcmYxAZwb2i0= diff --git a/vendor/dario.cat/mergo/FUNDING.json b/vendor/dario.cat/mergo/FUNDING.json new file mode 100644 index 000000000000..0585e1fe13f7 --- /dev/null +++ b/vendor/dario.cat/mergo/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930" + } + } +} diff --git a/vendor/dario.cat/mergo/README.md b/vendor/dario.cat/mergo/README.md index 0b3c488893b0..0e4a59afd9ac 100644 --- a/vendor/dario.cat/mergo/README.md +++ b/vendor/dario.cat/mergo/README.md @@ -85,7 +85,6 @@ Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/depend * [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser) * [go-micro/go-micro](https://github.com/go-micro/go-micro) * [grafana/loki](https://github.com/grafana/loki) -* [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) * [masterminds/sprig](github.com/Masterminds/sprig) * [moby/moby](https://github.com/moby/moby) * [slackhq/nebula](https://github.com/slackhq/nebula) @@ -191,10 +190,6 @@ func main() { } ``` -Note: if test are failing due missing package, please execute: - - go get gopkg.in/yaml.v3 - ### Transformers Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? diff --git a/vendor/dario.cat/mergo/SECURITY.md b/vendor/dario.cat/mergo/SECURITY.md index a5de61f77ba7..3788fcc1c212 100644 --- a/vendor/dario.cat/mergo/SECURITY.md +++ b/vendor/dario.cat/mergo/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 0.3.x | :white_check_mark: | -| < 0.3 | :x: | +| 1.x.x | :white_check_mark: | +| < 1.0 | :x: | ## Security contact information diff --git a/vendor/github.com/cenkalti/backoff/v4/README.md b/vendor/github.com/cenkalti/backoff/v4/README.md deleted file mode 100644 index 9433004a2809..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Coverage Status][coveralls image]][coveralls] - -This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. - -[Exponential backoff][exponential backoff wiki] -is an algorithm that uses feedback to multiplicatively decrease the rate of some process, -in order to gradually find an acceptable rate. -The retries exponentially increase and stop increasing when a certain threshold is met. - -## Usage - -Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end. - -Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation. - -## Contributing - -* I would like to keep this library as small as possible. -* Please don't send a PR without opening an issue and discussing it first. -* If proposed change is not a common use case, I will probably not accept it. - -[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4 -[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png -[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master -[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master - -[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java -[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff - -[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples diff --git a/vendor/github.com/cenkalti/backoff/v4/context.go b/vendor/github.com/cenkalti/backoff/v4/context.go deleted file mode 100644 index 48482330eb76..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/context.go +++ /dev/null @@ -1,62 +0,0 @@ -package backoff - -import ( - "context" - "time" -) - -// BackOffContext is a backoff policy that stops retrying after the context -// is canceled. -type BackOffContext interface { // nolint: golint - BackOff - Context() context.Context -} - -type backOffContext struct { - BackOff - ctx context.Context -} - -// WithContext returns a BackOffContext with context ctx -// -// ctx must not be nil -func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint - if ctx == nil { - panic("nil context") - } - - if b, ok := b.(*backOffContext); ok { - return &backOffContext{ - BackOff: b.BackOff, - ctx: ctx, - } - } - - return &backOffContext{ - BackOff: b, - ctx: ctx, - } -} - -func getContext(b BackOff) context.Context { - if cb, ok := b.(BackOffContext); ok { - return cb.Context() - } - if tb, ok := b.(*backOffTries); ok { - return getContext(tb.delegate) - } - return context.Background() -} - -func (b *backOffContext) Context() context.Context { - return b.ctx -} - -func (b *backOffContext) NextBackOff() time.Duration { - select { - case <-b.ctx.Done(): - return Stop - default: - return b.BackOff.NextBackOff() - } -} diff --git a/vendor/github.com/cenkalti/backoff/v4/exponential.go b/vendor/github.com/cenkalti/backoff/v4/exponential.go deleted file mode 100644 index aac99f196ad5..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/exponential.go +++ /dev/null @@ -1,216 +0,0 @@ -package backoff - -import ( - "math/rand" - "time" -) - -/* -ExponentialBackOff is a backoff implementation that increases the backoff -period for each retry attempt using a randomization function that grows exponentially. - -NextBackOff() is calculated using the following formula: - - randomized interval = - RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) - -In other words NextBackOff() will range between the randomization factor -percentage below and above the retry interval. - -For example, given the following parameters: - - RetryInterval = 2 - RandomizationFactor = 0.5 - Multiplier = 2 - -the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, -multiplied by the exponential, that is, between 2 and 6 seconds. - -Note: MaxInterval caps the RetryInterval and not the randomized interval. - -If the time elapsed since an ExponentialBackOff instance is created goes past the -MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. - -The elapsed time can be reset by calling Reset(). - -Example: Given the following default arguments, for 10 tries the sequence will be, -and assuming we go over the MaxElapsedTime on the 10th try: - - Request # RetryInterval (seconds) Randomized Interval (seconds) - - 1 0.5 [0.25, 0.75] - 2 0.75 [0.375, 1.125] - 3 1.125 [0.562, 1.687] - 4 1.687 [0.8435, 2.53] - 5 2.53 [1.265, 3.795] - 6 3.795 [1.897, 5.692] - 7 5.692 [2.846, 8.538] - 8 8.538 [4.269, 12.807] - 9 12.807 [6.403, 19.210] - 10 19.210 backoff.Stop - -Note: Implementation is not thread-safe. -*/ -type ExponentialBackOff struct { - InitialInterval time.Duration - RandomizationFactor float64 - Multiplier float64 - MaxInterval time.Duration - // After MaxElapsedTime the ExponentialBackOff returns Stop. - // It never stops if MaxElapsedTime == 0. - MaxElapsedTime time.Duration - Stop time.Duration - Clock Clock - - currentInterval time.Duration - startTime time.Time -} - -// Clock is an interface that returns current time for BackOff. -type Clock interface { - Now() time.Time -} - -// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options. -type ExponentialBackOffOpts func(*ExponentialBackOff) - -// Default values for ExponentialBackOff. -const ( - DefaultInitialInterval = 500 * time.Millisecond - DefaultRandomizationFactor = 0.5 - DefaultMultiplier = 1.5 - DefaultMaxInterval = 60 * time.Second - DefaultMaxElapsedTime = 15 * time.Minute -) - -// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. -func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff { - b := &ExponentialBackOff{ - InitialInterval: DefaultInitialInterval, - RandomizationFactor: DefaultRandomizationFactor, - Multiplier: DefaultMultiplier, - MaxInterval: DefaultMaxInterval, - MaxElapsedTime: DefaultMaxElapsedTime, - Stop: Stop, - Clock: SystemClock, - } - for _, fn := range opts { - fn(b) - } - b.Reset() - return b -} - -// WithInitialInterval sets the initial interval between retries. -func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.InitialInterval = duration - } -} - -// WithRandomizationFactor sets the randomization factor to add jitter to intervals. -func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.RandomizationFactor = randomizationFactor - } -} - -// WithMultiplier sets the multiplier for increasing the interval after each retry. -func WithMultiplier(multiplier float64) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.Multiplier = multiplier - } -} - -// WithMaxInterval sets the maximum interval between retries. -func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.MaxInterval = duration - } -} - -// WithMaxElapsedTime sets the maximum total time for retries. -func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.MaxElapsedTime = duration - } -} - -// WithRetryStopDuration sets the duration after which retries should stop. -func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.Stop = duration - } -} - -// WithClockProvider sets the clock used to measure time. -func WithClockProvider(clock Clock) ExponentialBackOffOpts { - return func(ebo *ExponentialBackOff) { - ebo.Clock = clock - } -} - -type systemClock struct{} - -func (t systemClock) Now() time.Time { - return time.Now() -} - -// SystemClock implements Clock interface that uses time.Now(). -var SystemClock = systemClock{} - -// Reset the interval back to the initial retry interval and restarts the timer. -// Reset must be called before using b. -func (b *ExponentialBackOff) Reset() { - b.currentInterval = b.InitialInterval - b.startTime = b.Clock.Now() -} - -// NextBackOff calculates the next backoff interval using the formula: -// Randomized interval = RetryInterval * (1 ± RandomizationFactor) -func (b *ExponentialBackOff) NextBackOff() time.Duration { - // Make sure we have not gone over the maximum elapsed time. - elapsed := b.GetElapsedTime() - next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) - b.incrementCurrentInterval() - if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime { - return b.Stop - } - return next -} - -// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance -// is created and is reset when Reset() is called. -// -// The elapsed time is computed using time.Now().UnixNano(). It is -// safe to call even while the backoff policy is used by a running -// ticker. -func (b *ExponentialBackOff) GetElapsedTime() time.Duration { - return b.Clock.Now().Sub(b.startTime) -} - -// Increments the current interval by multiplying it with the multiplier. -func (b *ExponentialBackOff) incrementCurrentInterval() { - // Check for overflow, if overflow is detected set the current interval to the max interval. - if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { - b.currentInterval = b.MaxInterval - } else { - b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) - } -} - -// Returns a random value from the following interval: -// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval]. -func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { - if randomizationFactor == 0 { - return currentInterval // make sure no randomness is used when randomizationFactor is 0. - } - var delta = randomizationFactor * float64(currentInterval) - var minInterval = float64(currentInterval) - delta - var maxInterval = float64(currentInterval) + delta - - // Get a random value from the range [minInterval, maxInterval]. - // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then - // we want a 33% chance for selecting either 1, 2 or 3. - return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) -} diff --git a/vendor/github.com/cenkalti/backoff/v4/retry.go b/vendor/github.com/cenkalti/backoff/v4/retry.go deleted file mode 100644 index b9c0c51cd755..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/retry.go +++ /dev/null @@ -1,146 +0,0 @@ -package backoff - -import ( - "errors" - "time" -) - -// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData(). -// The operation will be retried using a backoff policy if it returns an error. -type OperationWithData[T any] func() (T, error) - -// An Operation is executing by Retry() or RetryNotify(). -// The operation will be retried using a backoff policy if it returns an error. -type Operation func() error - -func (o Operation) withEmptyData() OperationWithData[struct{}] { - return func() (struct{}, error) { - return struct{}{}, o() - } -} - -// Notify is a notify-on-error function. It receives an operation error and -// backoff delay if the operation failed (with an error). -// -// NOTE that if the backoff policy stated to stop retrying, -// the notify function isn't called. -type Notify func(error, time.Duration) - -// Retry the operation o until it does not return error or BackOff stops. -// o is guaranteed to be run at least once. -// -// If o returns a *PermanentError, the operation is not retried, and the -// wrapped error is returned. -// -// Retry sleeps the goroutine for the duration returned by BackOff after a -// failed operation returns. -func Retry(o Operation, b BackOff) error { - return RetryNotify(o, b, nil) -} - -// RetryWithData is like Retry but returns data in the response too. -func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) { - return RetryNotifyWithData(o, b, nil) -} - -// RetryNotify calls notify function with the error and wait duration -// for each failed attempt before sleep. -func RetryNotify(operation Operation, b BackOff, notify Notify) error { - return RetryNotifyWithTimer(operation, b, notify, nil) -} - -// RetryNotifyWithData is like RetryNotify but returns data in the response too. -func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) { - return doRetryNotify(operation, b, notify, nil) -} - -// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer -// for each failed attempt before sleep. -// A default timer that uses system timer is used when nil is passed. -func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error { - _, err := doRetryNotify(operation.withEmptyData(), b, notify, t) - return err -} - -// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too. -func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) { - return doRetryNotify(operation, b, notify, t) -} - -func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) { - var ( - err error - next time.Duration - res T - ) - if t == nil { - t = &defaultTimer{} - } - - defer func() { - t.Stop() - }() - - ctx := getContext(b) - - b.Reset() - for { - res, err = operation() - if err == nil { - return res, nil - } - - var permanent *PermanentError - if errors.As(err, &permanent) { - return res, permanent.Err - } - - if next = b.NextBackOff(); next == Stop { - if cerr := ctx.Err(); cerr != nil { - return res, cerr - } - - return res, err - } - - if notify != nil { - notify(err, next) - } - - t.Start(next) - - select { - case <-ctx.Done(): - return res, ctx.Err() - case <-t.C(): - } - } -} - -// PermanentError signals that the operation should not be retried. -type PermanentError struct { - Err error -} - -func (e *PermanentError) Error() string { - return e.Err.Error() -} - -func (e *PermanentError) Unwrap() error { - return e.Err -} - -func (e *PermanentError) Is(target error) bool { - _, ok := target.(*PermanentError) - return ok -} - -// Permanent wraps the given err in a *PermanentError. -func Permanent(err error) error { - if err == nil { - return nil - } - return &PermanentError{ - Err: err, - } -} diff --git a/vendor/github.com/cenkalti/backoff/v4/timer.go b/vendor/github.com/cenkalti/backoff/v4/timer.go deleted file mode 100644 index 8120d0213c58..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/timer.go +++ /dev/null @@ -1,35 +0,0 @@ -package backoff - -import "time" - -type Timer interface { - Start(duration time.Duration) - Stop() - C() <-chan time.Time -} - -// defaultTimer implements Timer interface using time.Timer -type defaultTimer struct { - timer *time.Timer -} - -// C returns the timers channel which receives the current time when the timer fires. -func (t *defaultTimer) C() <-chan time.Time { - return t.timer.C -} - -// Start starts the timer to fire after the given duration -func (t *defaultTimer) Start(duration time.Duration) { - if t.timer == nil { - t.timer = time.NewTimer(duration) - } else { - t.timer.Reset(duration) - } -} - -// Stop is called when the timer is not used anymore and resources may be freed. -func (t *defaultTimer) Stop() { - if t.timer != nil { - t.timer.Stop() - } -} diff --git a/vendor/github.com/cenkalti/backoff/v4/tries.go b/vendor/github.com/cenkalti/backoff/v4/tries.go deleted file mode 100644 index 28d58ca37c68..000000000000 --- a/vendor/github.com/cenkalti/backoff/v4/tries.go +++ /dev/null @@ -1,38 +0,0 @@ -package backoff - -import "time" - -/* -WithMaxRetries creates a wrapper around another BackOff, which will -return Stop if NextBackOff() has been called too many times since -the last time Reset() was called - -Note: Implementation is not thread-safe. -*/ -func WithMaxRetries(b BackOff, max uint64) BackOff { - return &backOffTries{delegate: b, maxTries: max} -} - -type backOffTries struct { - delegate BackOff - maxTries uint64 - numTries uint64 -} - -func (b *backOffTries) NextBackOff() time.Duration { - if b.maxTries == 0 { - return Stop - } - if b.maxTries > 0 { - if b.maxTries <= b.numTries { - return Stop - } - b.numTries++ - } - return b.delegate.NextBackOff() -} - -func (b *backOffTries) Reset() { - b.numTries = 0 - b.delegate.Reset() -} diff --git a/vendor/github.com/cenkalti/backoff/v4/.gitignore b/vendor/github.com/cenkalti/backoff/v5/.gitignore similarity index 100% rename from vendor/github.com/cenkalti/backoff/v4/.gitignore rename to vendor/github.com/cenkalti/backoff/v5/.gitignore diff --git a/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md b/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md new file mode 100644 index 000000000000..658c37436d9c --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [5.0.0] - 2024-12-19 + +### Added + +- RetryAfterError can be returned from an operation to indicate how long to wait before the next retry. + +### Changed + +- Retry function now accepts additional options for specifying max number of tries and max elapsed time. +- Retry function now accepts a context.Context. +- Operation function signature changed to return result (any type) and error. + +### Removed + +- RetryNotify* and RetryWithData functions. Only single Retry function remains. +- Optional arguments from ExponentialBackoff constructor. +- Clock and Timer interfaces. + +### Fixed + +- The original error is returned from Retry if there's a PermanentError. (#144) +- The Retry function respects the wrapped PermanentError. (#140) diff --git a/vendor/github.com/cenkalti/backoff/v4/LICENSE b/vendor/github.com/cenkalti/backoff/v5/LICENSE similarity index 100% rename from vendor/github.com/cenkalti/backoff/v4/LICENSE rename to vendor/github.com/cenkalti/backoff/v5/LICENSE diff --git a/vendor/github.com/cenkalti/backoff/v5/README.md b/vendor/github.com/cenkalti/backoff/v5/README.md new file mode 100644 index 000000000000..4611b1d1701a --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/README.md @@ -0,0 +1,31 @@ +# Exponential Backoff [![GoDoc][godoc image]][godoc] + +This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. + +[Exponential backoff][exponential backoff wiki] +is an algorithm that uses feedback to multiplicatively decrease the rate of some process, +in order to gradually find an acceptable rate. +The retries exponentially increase and stop increasing when a certain threshold is met. + +## Usage + +Import path is `github.com/cenkalti/backoff/v5`. Please note the version part at the end. + +For most cases, use `Retry` function. See [example_test.go][example] for an example. + +If you have specific needs, copy `Retry` function (from [retry.go][retry-src]) into your code and modify it as needed. + +## Contributing + +* I would like to keep this library as small as possible. +* Please don't send a PR without opening an issue and discussing it first. +* If proposed change is not a common use case, I will probably not accept it. + +[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v5 +[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png + +[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java +[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff + +[retry-src]: https://github.com/cenkalti/backoff/blob/v5/retry.go +[example]: https://github.com/cenkalti/backoff/blob/v5/example_test.go diff --git a/vendor/github.com/cenkalti/backoff/v4/backoff.go b/vendor/github.com/cenkalti/backoff/v5/backoff.go similarity index 87% rename from vendor/github.com/cenkalti/backoff/v4/backoff.go rename to vendor/github.com/cenkalti/backoff/v5/backoff.go index 3676ee405d87..dd2b24ca7350 100644 --- a/vendor/github.com/cenkalti/backoff/v4/backoff.go +++ b/vendor/github.com/cenkalti/backoff/v5/backoff.go @@ -15,16 +15,16 @@ import "time" // BackOff is a backoff policy for retrying an operation. type BackOff interface { // NextBackOff returns the duration to wait before retrying the operation, - // or backoff. Stop to indicate that no more retries should be made. + // backoff.Stop to indicate that no more retries should be made. // // Example usage: // - // duration := backoff.NextBackOff(); - // if (duration == backoff.Stop) { - // // Do not retry operation. - // } else { - // // Sleep for duration and retry operation. - // } + // duration := backoff.NextBackOff() + // if duration == backoff.Stop { + // // Do not retry operation. + // } else { + // // Sleep for duration and retry operation. + // } // NextBackOff() time.Duration diff --git a/vendor/github.com/cenkalti/backoff/v5/error.go b/vendor/github.com/cenkalti/backoff/v5/error.go new file mode 100644 index 000000000000..beb2b38a23d7 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/error.go @@ -0,0 +1,46 @@ +package backoff + +import ( + "fmt" + "time" +) + +// PermanentError signals that the operation should not be retried. +type PermanentError struct { + Err error +} + +// Permanent wraps the given err in a *PermanentError. +func Permanent(err error) error { + if err == nil { + return nil + } + return &PermanentError{ + Err: err, + } +} + +// Error returns a string representation of the Permanent error. +func (e *PermanentError) Error() string { + return e.Err.Error() +} + +// Unwrap returns the wrapped error. +func (e *PermanentError) Unwrap() error { + return e.Err +} + +// RetryAfterError signals that the operation should be retried after the given duration. +type RetryAfterError struct { + Duration time.Duration +} + +// RetryAfter returns a RetryAfter error that specifies how long to wait before retrying. +func RetryAfter(seconds int) error { + return &RetryAfterError{Duration: time.Duration(seconds) * time.Second} +} + +// Error returns a string representation of the RetryAfter error. +func (e *RetryAfterError) Error() string { + return fmt.Sprintf("retry after %s", e.Duration) +} diff --git a/vendor/github.com/cenkalti/backoff/v5/exponential.go b/vendor/github.com/cenkalti/backoff/v5/exponential.go new file mode 100644 index 000000000000..79d425e87469 --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/exponential.go @@ -0,0 +1,118 @@ +package backoff + +import ( + "math/rand/v2" + "time" +) + +/* +ExponentialBackOff is a backoff implementation that increases the backoff +period for each retry attempt using a randomization function that grows exponentially. + +NextBackOff() is calculated using the following formula: + + randomized interval = + RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + +In other words NextBackOff() will range between the randomization factor +percentage below and above the retry interval. + +For example, given the following parameters: + + RetryInterval = 2 + RandomizationFactor = 0.5 + Multiplier = 2 + +the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, +multiplied by the exponential, that is, between 2 and 6 seconds. + +Note: MaxInterval caps the RetryInterval and not the randomized interval. + +Example: Given the following default arguments, for 9 tries the sequence will be: + + Request # RetryInterval (seconds) Randomized Interval (seconds) + + 1 0.5 [0.25, 0.75] + 2 0.75 [0.375, 1.125] + 3 1.125 [0.562, 1.687] + 4 1.687 [0.8435, 2.53] + 5 2.53 [1.265, 3.795] + 6 3.795 [1.897, 5.692] + 7 5.692 [2.846, 8.538] + 8 8.538 [4.269, 12.807] + 9 12.807 [6.403, 19.210] + +Note: Implementation is not thread-safe. +*/ +type ExponentialBackOff struct { + InitialInterval time.Duration + RandomizationFactor float64 + Multiplier float64 + MaxInterval time.Duration + + currentInterval time.Duration +} + +// Default values for ExponentialBackOff. +const ( + DefaultInitialInterval = 500 * time.Millisecond + DefaultRandomizationFactor = 0.5 + DefaultMultiplier = 1.5 + DefaultMaxInterval = 60 * time.Second +) + +// NewExponentialBackOff creates an instance of ExponentialBackOff using default values. +func NewExponentialBackOff() *ExponentialBackOff { + return &ExponentialBackOff{ + InitialInterval: DefaultInitialInterval, + RandomizationFactor: DefaultRandomizationFactor, + Multiplier: DefaultMultiplier, + MaxInterval: DefaultMaxInterval, + } +} + +// Reset the interval back to the initial retry interval and restarts the timer. +// Reset must be called before using b. +func (b *ExponentialBackOff) Reset() { + b.currentInterval = b.InitialInterval +} + +// NextBackOff calculates the next backoff interval using the formula: +// +// Randomized interval = RetryInterval * (1 ± RandomizationFactor) +func (b *ExponentialBackOff) NextBackOff() time.Duration { + if b.currentInterval == 0 { + b.currentInterval = b.InitialInterval + } + + next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) + b.incrementCurrentInterval() + return next +} + +// Increments the current interval by multiplying it with the multiplier. +func (b *ExponentialBackOff) incrementCurrentInterval() { + // Check for overflow, if overflow is detected set the current interval to the max interval. + if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { + b.currentInterval = b.MaxInterval + } else { + b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) + } +} + +// Returns a random value from the following interval: +// +// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval]. +func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { + if randomizationFactor == 0 { + return currentInterval // make sure no randomness is used when randomizationFactor is 0. + } + var delta = randomizationFactor * float64(currentInterval) + var minInterval = float64(currentInterval) - delta + var maxInterval = float64(currentInterval) + delta + + // Get a random value from the range [minInterval, maxInterval]. + // The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then + // we want a 33% chance for selecting either 1, 2 or 3. + return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) +} diff --git a/vendor/github.com/cenkalti/backoff/v5/retry.go b/vendor/github.com/cenkalti/backoff/v5/retry.go new file mode 100644 index 000000000000..32a7f988347d --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/retry.go @@ -0,0 +1,139 @@ +package backoff + +import ( + "context" + "errors" + "time" +) + +// DefaultMaxElapsedTime sets a default limit for the total retry duration. +const DefaultMaxElapsedTime = 15 * time.Minute + +// Operation is a function that attempts an operation and may be retried. +type Operation[T any] func() (T, error) + +// Notify is a function called on operation error with the error and backoff duration. +type Notify func(error, time.Duration) + +// retryOptions holds configuration settings for the retry mechanism. +type retryOptions struct { + BackOff BackOff // Strategy for calculating backoff periods. + Timer timer // Timer to manage retry delays. + Notify Notify // Optional function to notify on each retry error. + MaxTries uint // Maximum number of retry attempts. + MaxElapsedTime time.Duration // Maximum total time for all retries. +} + +type RetryOption func(*retryOptions) + +// WithBackOff configures a custom backoff strategy. +func WithBackOff(b BackOff) RetryOption { + return func(args *retryOptions) { + args.BackOff = b + } +} + +// withTimer sets a custom timer for managing delays between retries. +func withTimer(t timer) RetryOption { + return func(args *retryOptions) { + args.Timer = t + } +} + +// WithNotify sets a notification function to handle retry errors. +func WithNotify(n Notify) RetryOption { + return func(args *retryOptions) { + args.Notify = n + } +} + +// WithMaxTries limits the number of all attempts. +func WithMaxTries(n uint) RetryOption { + return func(args *retryOptions) { + args.MaxTries = n + } +} + +// WithMaxElapsedTime limits the total duration for retry attempts. +func WithMaxElapsedTime(d time.Duration) RetryOption { + return func(args *retryOptions) { + args.MaxElapsedTime = d + } +} + +// Retry attempts the operation until success, a permanent error, or backoff completion. +// It ensures the operation is executed at least once. +// +// Returns the operation result or error if retries are exhausted or context is cancelled. +func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOption) (T, error) { + // Initialize default retry options. + args := &retryOptions{ + BackOff: NewExponentialBackOff(), + Timer: &defaultTimer{}, + MaxElapsedTime: DefaultMaxElapsedTime, + } + + // Apply user-provided options to the default settings. + for _, opt := range opts { + opt(args) + } + + defer args.Timer.Stop() + + startedAt := time.Now() + args.BackOff.Reset() + for numTries := uint(1); ; numTries++ { + // Execute the operation. + res, err := operation() + if err == nil { + return res, nil + } + + // Stop retrying if maximum tries exceeded. + if args.MaxTries > 0 && numTries >= args.MaxTries { + return res, err + } + + // Handle permanent errors without retrying. + var permanent *PermanentError + if errors.As(err, &permanent) { + return res, permanent.Unwrap() + } + + // Stop retrying if context is cancelled. + if cerr := context.Cause(ctx); cerr != nil { + return res, cerr + } + + // Calculate next backoff duration. + next := args.BackOff.NextBackOff() + if next == Stop { + return res, err + } + + // Reset backoff if RetryAfterError is encountered. + var retryAfter *RetryAfterError + if errors.As(err, &retryAfter) { + next = retryAfter.Duration + args.BackOff.Reset() + } + + // Stop retrying if maximum elapsed time exceeded. + if args.MaxElapsedTime > 0 && time.Since(startedAt)+next > args.MaxElapsedTime { + return res, err + } + + // Notify on error if a notifier function is provided. + if args.Notify != nil { + args.Notify(err, next) + } + + // Wait for the next backoff period or context cancellation. + args.Timer.Start(next) + select { + case <-args.Timer.C(): + case <-ctx.Done(): + return res, context.Cause(ctx) + } + } +} diff --git a/vendor/github.com/cenkalti/backoff/v4/ticker.go b/vendor/github.com/cenkalti/backoff/v5/ticker.go similarity index 80% rename from vendor/github.com/cenkalti/backoff/v4/ticker.go rename to vendor/github.com/cenkalti/backoff/v5/ticker.go index df9d68bce527..f0d4b2ae7213 100644 --- a/vendor/github.com/cenkalti/backoff/v4/ticker.go +++ b/vendor/github.com/cenkalti/backoff/v5/ticker.go @@ -1,7 +1,6 @@ package backoff import ( - "context" "sync" "time" ) @@ -14,8 +13,7 @@ type Ticker struct { C <-chan time.Time c chan time.Time b BackOff - ctx context.Context - timer Timer + timer timer stop chan struct{} stopOnce sync.Once } @@ -27,22 +25,12 @@ type Ticker struct { // provided backoff policy (notably calling NextBackOff or Reset) // while the ticker is running. func NewTicker(b BackOff) *Ticker { - return NewTickerWithTimer(b, &defaultTimer{}) -} - -// NewTickerWithTimer returns a new Ticker with a custom timer. -// A default timer that uses system timer is used when nil is passed. -func NewTickerWithTimer(b BackOff, timer Timer) *Ticker { - if timer == nil { - timer = &defaultTimer{} - } c := make(chan time.Time) t := &Ticker{ C: c, c: c, b: b, - ctx: getContext(b), - timer: timer, + timer: &defaultTimer{}, stop: make(chan struct{}), } t.b.Reset() @@ -73,8 +61,6 @@ func (t *Ticker) run() { case <-t.stop: t.c = nil // Prevent future ticks from being sent to the channel. return - case <-t.ctx.Done(): - return } } } diff --git a/vendor/github.com/cenkalti/backoff/v5/timer.go b/vendor/github.com/cenkalti/backoff/v5/timer.go new file mode 100644 index 000000000000..a895309747da --- /dev/null +++ b/vendor/github.com/cenkalti/backoff/v5/timer.go @@ -0,0 +1,35 @@ +package backoff + +import "time" + +type timer interface { + Start(duration time.Duration) + Stop() + C() <-chan time.Time +} + +// defaultTimer implements Timer interface using time.Timer +type defaultTimer struct { + timer *time.Timer +} + +// C returns the timers channel which receives the current time when the timer fires. +func (t *defaultTimer) C() <-chan time.Time { + return t.timer.C +} + +// Start starts the timer to fire after the given duration +func (t *defaultTimer) Start(duration time.Duration) { + if t.timer == nil { + t.timer = time.NewTimer(duration) + } else { + t.timer.Reset(duration) + } +} + +// Stop is called when the timer is not used anymore and resources may be freed. +func (t *defaultTimer) Stop() { + if t.timer != nil { + t.timer.Stop() + } +} diff --git a/vendor/github.com/clipperhouse/uax29/v2/LICENSE b/vendor/github.com/clipperhouse/uax29/v2/LICENSE new file mode 100644 index 000000000000..6ae86a9a1903 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Matt Sherman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md b/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md new file mode 100644 index 000000000000..4d9a6d717b82 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md @@ -0,0 +1,82 @@ +An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0. + +## Quick start + +``` +go get "github.com/clipperhouse/uax29/v2/graphemes" +``` + +```go +import "github.com/clipperhouse/uax29/v2/graphemes" + +text := "Hello, 世界. Nice dog! 👍🐶" + +tokens := graphemes.FromString(text) + +for tokens.Next() { // Next() returns true until end of data + fmt.Println(tokens.Value()) // Do something with the current grapheme +} +``` + +[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes) + +_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._ + +## Conformance + +We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status: + +![Go](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg) + +## APIs + +### If you have a `string` + +```go +text := "Hello, 世界. Nice dog! 👍🐶" + +tokens := graphemes.FromString(text) + +for tokens.Next() { // Next() returns true until end of data + fmt.Println(tokens.Value()) // Do something with the current grapheme +} +``` + +### If you have an `io.Reader` + +`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods. + +```go +r := getYourReader() // from a file or network maybe +tokens := graphemes.FromReader(r) + +for tokens.Scan() { // Scan() returns true until error or EOF + fmt.Println(tokens.Text()) // Do something with the current grapheme +} + +if tokens.Err() != nil { // Check the error + log.Fatal(tokens.Err()) +} +``` + +### If you have a `[]byte` + +```go +b := []byte("Hello, 世界. Nice dog! 👍🐶") + +tokens := graphemes.FromBytes(b) + +for tokens.Next() { // Next() returns true until end of data + fmt.Println(tokens.Value()) // Do something with the current grapheme +} +``` + +### Performance + +On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations. + +### Invalid inputs + +Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”. + +Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid). diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go new file mode 100644 index 000000000000..14b4ea2c4618 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go @@ -0,0 +1,28 @@ +package graphemes + +import "github.com/clipperhouse/uax29/v2/internal/iterators" + +type Iterator[T iterators.Stringish] struct { + *iterators.Iterator[T] +} + +var ( + splitFuncString = splitFunc[string] + splitFuncBytes = splitFunc[[]byte] +) + +// FromString returns an iterator for the grapheme clusters in the input string. +// Iterate while Next() is true, and access the grapheme via Value(). +func FromString(s string) Iterator[string] { + return Iterator[string]{ + iterators.New(splitFuncString, s), + } +} + +// FromBytes returns an iterator for the grapheme clusters in the input bytes. +// Iterate while Next() is true, and access the grapheme via Value(). +func FromBytes(b []byte) Iterator[[]byte] { + return Iterator[[]byte]{ + iterators.New(splitFuncBytes, b), + } +} diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go new file mode 100644 index 000000000000..9aa0066183f0 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go @@ -0,0 +1,25 @@ +// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries +package graphemes + +import ( + "bufio" + "io" +) + +type Scanner struct { + *bufio.Scanner +} + +// FromReader returns a Scanner, to split graphemes per +// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries. +// +// It embeds a [bufio.Scanner], so you can use its methods. +// +// Iterate through graphemes by calling Scan() until false, then check Err(). +func FromReader(r io.Reader) *Scanner { + sc := bufio.NewScanner(r) + sc.Split(SplitFunc) + return &Scanner{ + Scanner: sc, + } +} diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go new file mode 100644 index 000000000000..08987f549fc0 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go @@ -0,0 +1,174 @@ +package graphemes + +import ( + "bufio" + + "github.com/clipperhouse/uax29/v2/internal/iterators" +) + +// is determines if lookup intersects propert(ies) +func (lookup property) is(properties property) bool { + return (lookup & properties) != 0 +} + +const _Ignore = _Extend + +// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner. +// +// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries. +var SplitFunc bufio.SplitFunc = splitFunc[[]byte] + +func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) { + var empty T + if len(data) == 0 { + return 0, empty, nil + } + + // These vars are stateful across loop iterations + var pos int + var lastExIgnore property = 0 // "last excluding ignored categories" + var lastLastExIgnore property = 0 // "last one before that" + var regionalIndicatorCount int + + // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property + // to the right of the ×, from which we look back or forward + + current, w := lookup(data[pos:]) + if w == 0 { + if !atEOF { + // Rune extends past current data, request more + return 0, empty, nil + } + pos = len(data) + return pos, data[:pos], nil + } + + // https://unicode.org/reports/tr29/#GB1 + // Start of text always advances + pos += w + + for { + eot := pos == len(data) // "end of text" + + if eot { + if !atEOF { + // Token extends past current data, request more + return 0, empty, nil + } + + // https://unicode.org/reports/tr29/#GB2 + break + } + + /* + We've switched the evaluation order of GB1↓ and GB2↑. It's ok: + because we've checked for len(data) at the top of this function, + sot and eot are mutually exclusive, order doesn't matter. + */ + + // Rules are usually of the form Cat1 × Cat2; "current" refers to the first property + // to the right of the ×, from which we look back or forward + + // Remember previous properties to avoid lookups/lookbacks + last := current + if !last.is(_Ignore) { + lastLastExIgnore = lastExIgnore + lastExIgnore = last + } + + current, w = lookup(data[pos:]) + if w == 0 { + if atEOF { + // Just return the bytes, we can't do anything with them + pos = len(data) + break + } + // Rune extends past current data, request more + return 0, empty, nil + } + + // Optimization: no rule can possibly apply + if current|last == 0 { // i.e. both are zero + break + } + + // https://unicode.org/reports/tr29/#GB3 + if current.is(_LF) && last.is(_CR) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB4 + // https://unicode.org/reports/tr29/#GB5 + if (current | last).is(_Control | _CR | _LF) { + break + } + + // https://unicode.org/reports/tr29/#GB6 + if current.is(_L|_V|_LV|_LVT) && last.is(_L) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB7 + if current.is(_V|_T) && last.is(_LV|_V) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB8 + if current.is(_T) && last.is(_LVT|_T) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB9 + if current.is(_Extend | _ZWJ) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB9a + if current.is(_SpacingMark) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB9b + if last.is(_Prepend) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB9c + // TODO(clipperhouse): + // It appears to be added in Unicode 15.1.0: + // https://unicode.org/versions/Unicode15.1.0/#Migration + // This package currently supports Unicode 15.0.0, so + // out of scope for now + + // https://unicode.org/reports/tr29/#GB11 + if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) { + pos += w + continue + } + + // https://unicode.org/reports/tr29/#GB12 + // https://unicode.org/reports/tr29/#GB13 + if (current & last).is(_RegionalIndicator) { + regionalIndicatorCount++ + + odd := regionalIndicatorCount%2 == 1 + if odd { + pos += w + continue + } + } + + // If we fall through all the above rules, it's a grapheme cluster break + break + } + + // Return token + return pos, data[:pos], nil +} diff --git a/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go b/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go new file mode 100644 index 000000000000..c8c6c33bcd02 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go @@ -0,0 +1,1409 @@ +package graphemes + +// generated by github.com/clipperhouse/uax29/v2 +// from https://www.unicode.org/Public/15.0.0/ucd/auxiliary/GraphemeBreakProperty.txt + +import "github.com/clipperhouse/uax29/v2/internal/iterators" + +type property uint16 + +const ( + _CR property = 1 << iota + _Control + _Extend + _ExtendedPictographic + _L + _LF + _LV + _LVT + _Prepend + _RegionalIndicator + _SpacingMark + _T + _V + _ZWJ +) + +// lookup returns the trie value for the first UTF-8 encoding in s and +// the width in bytes of this encoding. The size will be 0 if s does not +// hold enough bytes to complete the encoding. len(s) must be greater than 0. +func lookup[T iterators.Stringish](s T) (v property, sz int) { + c0 := s[0] + switch { + case c0 < 0x80: // is ASCII + return graphemesValues[c0], 1 + case c0 < 0xC2: + return 0, 1 // Illegal UTF-8: not a starter, not ASCII. + case c0 < 0xE0: // 2-byte UTF-8 + if len(s) < 2 { + return 0, 0 + } + i := graphemesIndex[c0] + c1 := s[1] + if c1 < 0x80 || 0xC0 <= c1 { + return 0, 1 // Illegal UTF-8: not a continuation byte. + } + return lookupValue(uint32(i), c1), 2 + case c0 < 0xF0: // 3-byte UTF-8 + if len(s) < 3 { + return 0, 0 + } + i := graphemesIndex[c0] + c1 := s[1] + if c1 < 0x80 || 0xC0 <= c1 { + return 0, 1 // Illegal UTF-8: not a continuation byte. + } + o := uint32(i)<<6 + uint32(c1) + i = graphemesIndex[o] + c2 := s[2] + if c2 < 0x80 || 0xC0 <= c2 { + return 0, 2 // Illegal UTF-8: not a continuation byte. + } + return lookupValue(uint32(i), c2), 3 + case c0 < 0xF8: // 4-byte UTF-8 + if len(s) < 4 { + return 0, 0 + } + i := graphemesIndex[c0] + c1 := s[1] + if c1 < 0x80 || 0xC0 <= c1 { + return 0, 1 // Illegal UTF-8: not a continuation byte. + } + o := uint32(i)<<6 + uint32(c1) + i = graphemesIndex[o] + c2 := s[2] + if c2 < 0x80 || 0xC0 <= c2 { + return 0, 2 // Illegal UTF-8: not a continuation byte. + } + o = uint32(i)<<6 + uint32(c2) + i = graphemesIndex[o] + c3 := s[3] + if c3 < 0x80 || 0xC0 <= c3 { + return 0, 3 // Illegal UTF-8: not a continuation byte. + } + return lookupValue(uint32(i), c3), 4 + } + // Illegal rune + return 0, 1 +} + +// graphemesTrie. Total size: 29120 bytes (28.44 KiB). Checksum: 80ad0c5ab9375f7. +// type graphemesTrie struct { } + +// func newGraphemesTrie(i int) *graphemesTrie { +// return &graphemesTrie{} +// } + +// lookupValue determines the type of block n and looks up the value for b. +func lookupValue(n uint32, b byte) property { + switch { + default: + return property(graphemesValues[n<<6+uint32(b)]) + } +} + +// graphemesValues: 215 blocks, 13760 entries, 27520 bytes +// The third block is the zero block. +var graphemesValues = [13760]property{ + // Block 0x0, offset 0x0 + 0x00: 0x0002, 0x01: 0x0002, 0x02: 0x0002, 0x03: 0x0002, 0x04: 0x0002, 0x05: 0x0002, + 0x06: 0x0002, 0x07: 0x0002, 0x08: 0x0002, 0x09: 0x0002, 0x0a: 0x0020, 0x0b: 0x0002, + 0x0c: 0x0002, 0x0d: 0x0001, 0x0e: 0x0002, 0x0f: 0x0002, 0x10: 0x0002, 0x11: 0x0002, + 0x12: 0x0002, 0x13: 0x0002, 0x14: 0x0002, 0x15: 0x0002, 0x16: 0x0002, 0x17: 0x0002, + 0x18: 0x0002, 0x19: 0x0002, 0x1a: 0x0002, 0x1b: 0x0002, 0x1c: 0x0002, 0x1d: 0x0002, + 0x1e: 0x0002, 0x1f: 0x0002, + // Block 0x1, offset 0x40 + 0x7f: 0x0002, + // Block 0x2, offset 0x80 + // Block 0x3, offset 0xc0 + 0xc0: 0x0002, 0xc1: 0x0002, 0xc2: 0x0002, 0xc3: 0x0002, 0xc4: 0x0002, 0xc5: 0x0002, + 0xc6: 0x0002, 0xc7: 0x0002, 0xc8: 0x0002, 0xc9: 0x0002, 0xca: 0x0002, 0xcb: 0x0002, + 0xcc: 0x0002, 0xcd: 0x0002, 0xce: 0x0002, 0xcf: 0x0002, 0xd0: 0x0002, 0xd1: 0x0002, + 0xd2: 0x0002, 0xd3: 0x0002, 0xd4: 0x0002, 0xd5: 0x0002, 0xd6: 0x0002, 0xd7: 0x0002, + 0xd8: 0x0002, 0xd9: 0x0002, 0xda: 0x0002, 0xdb: 0x0002, 0xdc: 0x0002, 0xdd: 0x0002, + 0xde: 0x0002, 0xdf: 0x0002, + 0xe9: 0x0008, + 0xed: 0x0002, 0xee: 0x0008, + // Block 0x4, offset 0x100 + 0x100: 0x0004, 0x101: 0x0004, 0x102: 0x0004, 0x103: 0x0004, 0x104: 0x0004, 0x105: 0x0004, + 0x106: 0x0004, 0x107: 0x0004, 0x108: 0x0004, 0x109: 0x0004, 0x10a: 0x0004, 0x10b: 0x0004, + 0x10c: 0x0004, 0x10d: 0x0004, 0x10e: 0x0004, 0x10f: 0x0004, 0x110: 0x0004, 0x111: 0x0004, + 0x112: 0x0004, 0x113: 0x0004, 0x114: 0x0004, 0x115: 0x0004, 0x116: 0x0004, 0x117: 0x0004, + 0x118: 0x0004, 0x119: 0x0004, 0x11a: 0x0004, 0x11b: 0x0004, 0x11c: 0x0004, 0x11d: 0x0004, + 0x11e: 0x0004, 0x11f: 0x0004, 0x120: 0x0004, 0x121: 0x0004, 0x122: 0x0004, 0x123: 0x0004, + 0x124: 0x0004, 0x125: 0x0004, 0x126: 0x0004, 0x127: 0x0004, 0x128: 0x0004, 0x129: 0x0004, + 0x12a: 0x0004, 0x12b: 0x0004, 0x12c: 0x0004, 0x12d: 0x0004, 0x12e: 0x0004, 0x12f: 0x0004, + 0x130: 0x0004, 0x131: 0x0004, 0x132: 0x0004, 0x133: 0x0004, 0x134: 0x0004, 0x135: 0x0004, + 0x136: 0x0004, 0x137: 0x0004, 0x138: 0x0004, 0x139: 0x0004, 0x13a: 0x0004, 0x13b: 0x0004, + 0x13c: 0x0004, 0x13d: 0x0004, 0x13e: 0x0004, 0x13f: 0x0004, + // Block 0x5, offset 0x140 + 0x140: 0x0004, 0x141: 0x0004, 0x142: 0x0004, 0x143: 0x0004, 0x144: 0x0004, 0x145: 0x0004, + 0x146: 0x0004, 0x147: 0x0004, 0x148: 0x0004, 0x149: 0x0004, 0x14a: 0x0004, 0x14b: 0x0004, + 0x14c: 0x0004, 0x14d: 0x0004, 0x14e: 0x0004, 0x14f: 0x0004, 0x150: 0x0004, 0x151: 0x0004, + 0x152: 0x0004, 0x153: 0x0004, 0x154: 0x0004, 0x155: 0x0004, 0x156: 0x0004, 0x157: 0x0004, + 0x158: 0x0004, 0x159: 0x0004, 0x15a: 0x0004, 0x15b: 0x0004, 0x15c: 0x0004, 0x15d: 0x0004, + 0x15e: 0x0004, 0x15f: 0x0004, 0x160: 0x0004, 0x161: 0x0004, 0x162: 0x0004, 0x163: 0x0004, + 0x164: 0x0004, 0x165: 0x0004, 0x166: 0x0004, 0x167: 0x0004, 0x168: 0x0004, 0x169: 0x0004, + 0x16a: 0x0004, 0x16b: 0x0004, 0x16c: 0x0004, 0x16d: 0x0004, 0x16e: 0x0004, 0x16f: 0x0004, + // Block 0x6, offset 0x180 + 0x183: 0x0004, 0x184: 0x0004, 0x185: 0x0004, + 0x186: 0x0004, 0x187: 0x0004, 0x188: 0x0004, 0x189: 0x0004, + // Block 0x7, offset 0x1c0 + 0x1d1: 0x0004, + 0x1d2: 0x0004, 0x1d3: 0x0004, 0x1d4: 0x0004, 0x1d5: 0x0004, 0x1d6: 0x0004, 0x1d7: 0x0004, + 0x1d8: 0x0004, 0x1d9: 0x0004, 0x1da: 0x0004, 0x1db: 0x0004, 0x1dc: 0x0004, 0x1dd: 0x0004, + 0x1de: 0x0004, 0x1df: 0x0004, 0x1e0: 0x0004, 0x1e1: 0x0004, 0x1e2: 0x0004, 0x1e3: 0x0004, + 0x1e4: 0x0004, 0x1e5: 0x0004, 0x1e6: 0x0004, 0x1e7: 0x0004, 0x1e8: 0x0004, 0x1e9: 0x0004, + 0x1ea: 0x0004, 0x1eb: 0x0004, 0x1ec: 0x0004, 0x1ed: 0x0004, 0x1ee: 0x0004, 0x1ef: 0x0004, + 0x1f0: 0x0004, 0x1f1: 0x0004, 0x1f2: 0x0004, 0x1f3: 0x0004, 0x1f4: 0x0004, 0x1f5: 0x0004, + 0x1f6: 0x0004, 0x1f7: 0x0004, 0x1f8: 0x0004, 0x1f9: 0x0004, 0x1fa: 0x0004, 0x1fb: 0x0004, + 0x1fc: 0x0004, 0x1fd: 0x0004, 0x1ff: 0x0004, + // Block 0x8, offset 0x200 + 0x201: 0x0004, 0x202: 0x0004, 0x204: 0x0004, 0x205: 0x0004, + 0x207: 0x0004, + // Block 0x9, offset 0x240 + 0x240: 0x0100, 0x241: 0x0100, 0x242: 0x0100, 0x243: 0x0100, 0x244: 0x0100, 0x245: 0x0100, + 0x250: 0x0004, 0x251: 0x0004, + 0x252: 0x0004, 0x253: 0x0004, 0x254: 0x0004, 0x255: 0x0004, 0x256: 0x0004, 0x257: 0x0004, + 0x258: 0x0004, 0x259: 0x0004, 0x25a: 0x0004, 0x25c: 0x0002, + // Block 0xa, offset 0x280 + 0x28b: 0x0004, + 0x28c: 0x0004, 0x28d: 0x0004, 0x28e: 0x0004, 0x28f: 0x0004, 0x290: 0x0004, 0x291: 0x0004, + 0x292: 0x0004, 0x293: 0x0004, 0x294: 0x0004, 0x295: 0x0004, 0x296: 0x0004, 0x297: 0x0004, + 0x298: 0x0004, 0x299: 0x0004, 0x29a: 0x0004, 0x29b: 0x0004, 0x29c: 0x0004, 0x29d: 0x0004, + 0x29e: 0x0004, 0x29f: 0x0004, + 0x2b0: 0x0004, + // Block 0xb, offset 0x2c0 + 0x2d6: 0x0004, 0x2d7: 0x0004, + 0x2d8: 0x0004, 0x2d9: 0x0004, 0x2da: 0x0004, 0x2db: 0x0004, 0x2dc: 0x0004, 0x2dd: 0x0100, + 0x2df: 0x0004, 0x2e0: 0x0004, 0x2e1: 0x0004, 0x2e2: 0x0004, 0x2e3: 0x0004, + 0x2e4: 0x0004, 0x2e7: 0x0004, 0x2e8: 0x0004, + 0x2ea: 0x0004, 0x2eb: 0x0004, 0x2ec: 0x0004, 0x2ed: 0x0004, + // Block 0xc, offset 0x300 + 0x30f: 0x0100, 0x311: 0x0004, + 0x330: 0x0004, 0x331: 0x0004, 0x332: 0x0004, 0x333: 0x0004, 0x334: 0x0004, 0x335: 0x0004, + 0x336: 0x0004, 0x337: 0x0004, 0x338: 0x0004, 0x339: 0x0004, 0x33a: 0x0004, 0x33b: 0x0004, + 0x33c: 0x0004, 0x33d: 0x0004, 0x33e: 0x0004, 0x33f: 0x0004, + // Block 0xd, offset 0x340 + 0x340: 0x0004, 0x341: 0x0004, 0x342: 0x0004, 0x343: 0x0004, 0x344: 0x0004, 0x345: 0x0004, + 0x346: 0x0004, 0x347: 0x0004, 0x348: 0x0004, 0x349: 0x0004, 0x34a: 0x0004, + // Block 0xe, offset 0x380 + 0x3a6: 0x0004, 0x3a7: 0x0004, 0x3a8: 0x0004, 0x3a9: 0x0004, + 0x3aa: 0x0004, 0x3ab: 0x0004, 0x3ac: 0x0004, 0x3ad: 0x0004, 0x3ae: 0x0004, 0x3af: 0x0004, + 0x3b0: 0x0004, + // Block 0xf, offset 0x3c0 + 0x3eb: 0x0004, 0x3ec: 0x0004, 0x3ed: 0x0004, 0x3ee: 0x0004, 0x3ef: 0x0004, + 0x3f0: 0x0004, 0x3f1: 0x0004, 0x3f2: 0x0004, 0x3f3: 0x0004, + 0x3fd: 0x0004, + // Block 0x10, offset 0x400 + 0x416: 0x0004, 0x417: 0x0004, + 0x418: 0x0004, 0x419: 0x0004, 0x41b: 0x0004, 0x41c: 0x0004, 0x41d: 0x0004, + 0x41e: 0x0004, 0x41f: 0x0004, 0x420: 0x0004, 0x421: 0x0004, 0x422: 0x0004, 0x423: 0x0004, + 0x425: 0x0004, 0x426: 0x0004, 0x427: 0x0004, 0x429: 0x0004, + 0x42a: 0x0004, 0x42b: 0x0004, 0x42c: 0x0004, 0x42d: 0x0004, + // Block 0x11, offset 0x440 + 0x459: 0x0004, 0x45a: 0x0004, 0x45b: 0x0004, + // Block 0x12, offset 0x480 + 0x490: 0x0100, 0x491: 0x0100, + 0x498: 0x0004, 0x499: 0x0004, 0x49a: 0x0004, 0x49b: 0x0004, 0x49c: 0x0004, 0x49d: 0x0004, + 0x49e: 0x0004, 0x49f: 0x0004, + // Block 0x13, offset 0x4c0 + 0x4ca: 0x0004, 0x4cb: 0x0004, + 0x4cc: 0x0004, 0x4cd: 0x0004, 0x4ce: 0x0004, 0x4cf: 0x0004, 0x4d0: 0x0004, 0x4d1: 0x0004, + 0x4d2: 0x0004, 0x4d3: 0x0004, 0x4d4: 0x0004, 0x4d5: 0x0004, 0x4d6: 0x0004, 0x4d7: 0x0004, + 0x4d8: 0x0004, 0x4d9: 0x0004, 0x4da: 0x0004, 0x4db: 0x0004, 0x4dc: 0x0004, 0x4dd: 0x0004, + 0x4de: 0x0004, 0x4df: 0x0004, 0x4e0: 0x0004, 0x4e1: 0x0004, 0x4e2: 0x0100, 0x4e3: 0x0004, + 0x4e4: 0x0004, 0x4e5: 0x0004, 0x4e6: 0x0004, 0x4e7: 0x0004, 0x4e8: 0x0004, 0x4e9: 0x0004, + 0x4ea: 0x0004, 0x4eb: 0x0004, 0x4ec: 0x0004, 0x4ed: 0x0004, 0x4ee: 0x0004, 0x4ef: 0x0004, + 0x4f0: 0x0004, 0x4f1: 0x0004, 0x4f2: 0x0004, 0x4f3: 0x0004, 0x4f4: 0x0004, 0x4f5: 0x0004, + 0x4f6: 0x0004, 0x4f7: 0x0004, 0x4f8: 0x0004, 0x4f9: 0x0004, 0x4fa: 0x0004, 0x4fb: 0x0004, + 0x4fc: 0x0004, 0x4fd: 0x0004, 0x4fe: 0x0004, 0x4ff: 0x0004, + // Block 0x14, offset 0x500 + 0x500: 0x0004, 0x501: 0x0004, 0x502: 0x0004, 0x503: 0x0400, + 0x53a: 0x0004, 0x53b: 0x0400, + 0x53c: 0x0004, 0x53e: 0x0400, 0x53f: 0x0400, + // Block 0x15, offset 0x540 + 0x540: 0x0400, 0x541: 0x0004, 0x542: 0x0004, 0x543: 0x0004, 0x544: 0x0004, 0x545: 0x0004, + 0x546: 0x0004, 0x547: 0x0004, 0x548: 0x0004, 0x549: 0x0400, 0x54a: 0x0400, 0x54b: 0x0400, + 0x54c: 0x0400, 0x54d: 0x0004, 0x54e: 0x0400, 0x54f: 0x0400, 0x551: 0x0004, + 0x552: 0x0004, 0x553: 0x0004, 0x554: 0x0004, 0x555: 0x0004, 0x556: 0x0004, 0x557: 0x0004, + 0x562: 0x0004, 0x563: 0x0004, + // Block 0x16, offset 0x580 + 0x581: 0x0004, 0x582: 0x0400, 0x583: 0x0400, + 0x5bc: 0x0004, 0x5be: 0x0004, 0x5bf: 0x0400, + // Block 0x17, offset 0x5c0 + 0x5c0: 0x0400, 0x5c1: 0x0004, 0x5c2: 0x0004, 0x5c3: 0x0004, 0x5c4: 0x0004, + 0x5c7: 0x0400, 0x5c8: 0x0400, 0x5cb: 0x0400, + 0x5cc: 0x0400, 0x5cd: 0x0004, + 0x5d7: 0x0004, + 0x5e2: 0x0004, 0x5e3: 0x0004, + 0x5fe: 0x0004, + // Block 0x18, offset 0x600 + 0x601: 0x0004, 0x602: 0x0004, 0x603: 0x0400, + 0x63c: 0x0004, 0x63e: 0x0400, 0x63f: 0x0400, + // Block 0x19, offset 0x640 + 0x640: 0x0400, 0x641: 0x0004, 0x642: 0x0004, + 0x647: 0x0004, 0x648: 0x0004, 0x64b: 0x0004, + 0x64c: 0x0004, 0x64d: 0x0004, 0x651: 0x0004, + 0x670: 0x0004, 0x671: 0x0004, 0x675: 0x0004, + // Block 0x1a, offset 0x680 + 0x680: 0x0400, 0x681: 0x0004, 0x682: 0x0004, 0x683: 0x0004, 0x684: 0x0004, 0x685: 0x0004, + 0x687: 0x0004, 0x688: 0x0004, 0x689: 0x0400, 0x68b: 0x0400, + 0x68c: 0x0400, 0x68d: 0x0004, + 0x6a2: 0x0004, 0x6a3: 0x0004, + 0x6ba: 0x0004, 0x6bb: 0x0004, + 0x6bc: 0x0004, 0x6bd: 0x0004, 0x6be: 0x0004, 0x6bf: 0x0004, + // Block 0x1b, offset 0x6c0 + 0x6c1: 0x0004, 0x6c2: 0x0400, 0x6c3: 0x0400, + 0x6fc: 0x0004, 0x6fe: 0x0004, 0x6ff: 0x0004, + // Block 0x1c, offset 0x700 + 0x700: 0x0400, 0x701: 0x0004, 0x702: 0x0004, 0x703: 0x0004, 0x704: 0x0004, + 0x707: 0x0400, 0x708: 0x0400, 0x70b: 0x0400, + 0x70c: 0x0400, 0x70d: 0x0004, + 0x715: 0x0004, 0x716: 0x0004, 0x717: 0x0004, + 0x722: 0x0004, 0x723: 0x0004, + // Block 0x1d, offset 0x740 + 0x742: 0x0004, + 0x77e: 0x0004, 0x77f: 0x0400, + // Block 0x1e, offset 0x780 + 0x780: 0x0004, 0x781: 0x0400, 0x782: 0x0400, + 0x786: 0x0400, 0x787: 0x0400, 0x788: 0x0400, 0x78a: 0x0400, 0x78b: 0x0400, + 0x78c: 0x0400, 0x78d: 0x0004, + 0x797: 0x0004, + // Block 0x1f, offset 0x7c0 + 0x7c0: 0x0004, 0x7c1: 0x0400, 0x7c2: 0x0400, 0x7c3: 0x0400, 0x7c4: 0x0004, + 0x7fc: 0x0004, 0x7fe: 0x0004, 0x7ff: 0x0004, + // Block 0x20, offset 0x800 + 0x800: 0x0004, 0x801: 0x0400, 0x802: 0x0400, 0x803: 0x0400, 0x804: 0x0400, + 0x806: 0x0004, 0x807: 0x0004, 0x808: 0x0004, 0x80a: 0x0004, 0x80b: 0x0004, + 0x80c: 0x0004, 0x80d: 0x0004, + 0x815: 0x0004, 0x816: 0x0004, + 0x822: 0x0004, 0x823: 0x0004, + // Block 0x21, offset 0x840 + 0x841: 0x0004, 0x842: 0x0400, 0x843: 0x0400, + 0x87c: 0x0004, 0x87e: 0x0400, 0x87f: 0x0004, + // Block 0x22, offset 0x880 + 0x880: 0x0400, 0x881: 0x0400, 0x882: 0x0004, 0x883: 0x0400, 0x884: 0x0400, + 0x886: 0x0004, 0x887: 0x0400, 0x888: 0x0400, 0x88a: 0x0400, 0x88b: 0x0400, + 0x88c: 0x0004, 0x88d: 0x0004, + 0x895: 0x0004, 0x896: 0x0004, + 0x8a2: 0x0004, 0x8a3: 0x0004, + 0x8b3: 0x0400, + // Block 0x23, offset 0x8c0 + 0x8c0: 0x0004, 0x8c1: 0x0004, 0x8c2: 0x0400, 0x8c3: 0x0400, + 0x8fb: 0x0004, + 0x8fc: 0x0004, 0x8fe: 0x0004, 0x8ff: 0x0400, + // Block 0x24, offset 0x900 + 0x900: 0x0400, 0x901: 0x0004, 0x902: 0x0004, 0x903: 0x0004, 0x904: 0x0004, + 0x906: 0x0400, 0x907: 0x0400, 0x908: 0x0400, 0x90a: 0x0400, 0x90b: 0x0400, + 0x90c: 0x0400, 0x90d: 0x0004, 0x90e: 0x0100, + 0x917: 0x0004, + 0x922: 0x0004, 0x923: 0x0004, + // Block 0x25, offset 0x940 + 0x941: 0x0004, 0x942: 0x0400, 0x943: 0x0400, + // Block 0x26, offset 0x980 + 0x98a: 0x0004, + 0x98f: 0x0004, 0x990: 0x0400, 0x991: 0x0400, + 0x992: 0x0004, 0x993: 0x0004, 0x994: 0x0004, 0x996: 0x0004, + 0x998: 0x0400, 0x999: 0x0400, 0x99a: 0x0400, 0x99b: 0x0400, 0x99c: 0x0400, 0x99d: 0x0400, + 0x99e: 0x0400, 0x99f: 0x0004, + 0x9b2: 0x0400, 0x9b3: 0x0400, + // Block 0x27, offset 0x9c0 + 0x9f1: 0x0004, 0x9f3: 0x0400, 0x9f4: 0x0004, 0x9f5: 0x0004, + 0x9f6: 0x0004, 0x9f7: 0x0004, 0x9f8: 0x0004, 0x9f9: 0x0004, 0x9fa: 0x0004, + // Block 0x28, offset 0xa00 + 0xa07: 0x0004, 0xa08: 0x0004, 0xa09: 0x0004, 0xa0a: 0x0004, 0xa0b: 0x0004, + 0xa0c: 0x0004, 0xa0d: 0x0004, 0xa0e: 0x0004, + // Block 0x29, offset 0xa40 + 0xa71: 0x0004, 0xa73: 0x0400, 0xa74: 0x0004, 0xa75: 0x0004, + 0xa76: 0x0004, 0xa77: 0x0004, 0xa78: 0x0004, 0xa79: 0x0004, 0xa7a: 0x0004, 0xa7b: 0x0004, + 0xa7c: 0x0004, + // Block 0x2a, offset 0xa80 + 0xa88: 0x0004, 0xa89: 0x0004, 0xa8a: 0x0004, 0xa8b: 0x0004, + 0xa8c: 0x0004, 0xa8d: 0x0004, 0xa8e: 0x0004, + // Block 0x2b, offset 0xac0 + 0xad8: 0x0004, 0xad9: 0x0004, + 0xaf5: 0x0004, + 0xaf7: 0x0004, 0xaf9: 0x0004, + 0xafe: 0x0400, 0xaff: 0x0400, + // Block 0x2c, offset 0xb00 + 0xb31: 0x0004, 0xb32: 0x0004, 0xb33: 0x0004, 0xb34: 0x0004, 0xb35: 0x0004, + 0xb36: 0x0004, 0xb37: 0x0004, 0xb38: 0x0004, 0xb39: 0x0004, 0xb3a: 0x0004, 0xb3b: 0x0004, + 0xb3c: 0x0004, 0xb3d: 0x0004, 0xb3e: 0x0004, 0xb3f: 0x0400, + // Block 0x2d, offset 0xb40 + 0xb40: 0x0004, 0xb41: 0x0004, 0xb42: 0x0004, 0xb43: 0x0004, 0xb44: 0x0004, + 0xb46: 0x0004, 0xb47: 0x0004, + 0xb4d: 0x0004, 0xb4e: 0x0004, 0xb4f: 0x0004, 0xb50: 0x0004, 0xb51: 0x0004, + 0xb52: 0x0004, 0xb53: 0x0004, 0xb54: 0x0004, 0xb55: 0x0004, 0xb56: 0x0004, 0xb57: 0x0004, + 0xb59: 0x0004, 0xb5a: 0x0004, 0xb5b: 0x0004, 0xb5c: 0x0004, 0xb5d: 0x0004, + 0xb5e: 0x0004, 0xb5f: 0x0004, 0xb60: 0x0004, 0xb61: 0x0004, 0xb62: 0x0004, 0xb63: 0x0004, + 0xb64: 0x0004, 0xb65: 0x0004, 0xb66: 0x0004, 0xb67: 0x0004, 0xb68: 0x0004, 0xb69: 0x0004, + 0xb6a: 0x0004, 0xb6b: 0x0004, 0xb6c: 0x0004, 0xb6d: 0x0004, 0xb6e: 0x0004, 0xb6f: 0x0004, + 0xb70: 0x0004, 0xb71: 0x0004, 0xb72: 0x0004, 0xb73: 0x0004, 0xb74: 0x0004, 0xb75: 0x0004, + 0xb76: 0x0004, 0xb77: 0x0004, 0xb78: 0x0004, 0xb79: 0x0004, 0xb7a: 0x0004, 0xb7b: 0x0004, + 0xb7c: 0x0004, + // Block 0x2e, offset 0xb80 + 0xb86: 0x0004, + // Block 0x2f, offset 0xbc0 + 0xbed: 0x0004, 0xbee: 0x0004, 0xbef: 0x0004, + 0xbf0: 0x0004, 0xbf1: 0x0400, 0xbf2: 0x0004, 0xbf3: 0x0004, 0xbf4: 0x0004, 0xbf5: 0x0004, + 0xbf6: 0x0004, 0xbf7: 0x0004, 0xbf9: 0x0004, 0xbfa: 0x0004, 0xbfb: 0x0400, + 0xbfc: 0x0400, 0xbfd: 0x0004, 0xbfe: 0x0004, + // Block 0x30, offset 0xc00 + 0xc16: 0x0400, 0xc17: 0x0400, + 0xc18: 0x0004, 0xc19: 0x0004, + 0xc1e: 0x0004, 0xc1f: 0x0004, 0xc20: 0x0004, + 0xc31: 0x0004, 0xc32: 0x0004, 0xc33: 0x0004, 0xc34: 0x0004, + // Block 0x31, offset 0xc40 + 0xc42: 0x0004, 0xc44: 0x0400, 0xc45: 0x0004, + 0xc46: 0x0004, + 0xc4d: 0x0004, + 0xc5d: 0x0004, + // Block 0x32, offset 0xc80 + 0xc80: 0x0010, 0xc81: 0x0010, 0xc82: 0x0010, 0xc83: 0x0010, 0xc84: 0x0010, 0xc85: 0x0010, + 0xc86: 0x0010, 0xc87: 0x0010, 0xc88: 0x0010, 0xc89: 0x0010, 0xc8a: 0x0010, 0xc8b: 0x0010, + 0xc8c: 0x0010, 0xc8d: 0x0010, 0xc8e: 0x0010, 0xc8f: 0x0010, 0xc90: 0x0010, 0xc91: 0x0010, + 0xc92: 0x0010, 0xc93: 0x0010, 0xc94: 0x0010, 0xc95: 0x0010, 0xc96: 0x0010, 0xc97: 0x0010, + 0xc98: 0x0010, 0xc99: 0x0010, 0xc9a: 0x0010, 0xc9b: 0x0010, 0xc9c: 0x0010, 0xc9d: 0x0010, + 0xc9e: 0x0010, 0xc9f: 0x0010, 0xca0: 0x0010, 0xca1: 0x0010, 0xca2: 0x0010, 0xca3: 0x0010, + 0xca4: 0x0010, 0xca5: 0x0010, 0xca6: 0x0010, 0xca7: 0x0010, 0xca8: 0x0010, 0xca9: 0x0010, + 0xcaa: 0x0010, 0xcab: 0x0010, 0xcac: 0x0010, 0xcad: 0x0010, 0xcae: 0x0010, 0xcaf: 0x0010, + 0xcb0: 0x0010, 0xcb1: 0x0010, 0xcb2: 0x0010, 0xcb3: 0x0010, 0xcb4: 0x0010, 0xcb5: 0x0010, + 0xcb6: 0x0010, 0xcb7: 0x0010, 0xcb8: 0x0010, 0xcb9: 0x0010, 0xcba: 0x0010, 0xcbb: 0x0010, + 0xcbc: 0x0010, 0xcbd: 0x0010, 0xcbe: 0x0010, 0xcbf: 0x0010, + // Block 0x33, offset 0xcc0 + 0xcc0: 0x0010, 0xcc1: 0x0010, 0xcc2: 0x0010, 0xcc3: 0x0010, 0xcc4: 0x0010, 0xcc5: 0x0010, + 0xcc6: 0x0010, 0xcc7: 0x0010, 0xcc8: 0x0010, 0xcc9: 0x0010, 0xcca: 0x0010, 0xccb: 0x0010, + 0xccc: 0x0010, 0xccd: 0x0010, 0xcce: 0x0010, 0xccf: 0x0010, 0xcd0: 0x0010, 0xcd1: 0x0010, + 0xcd2: 0x0010, 0xcd3: 0x0010, 0xcd4: 0x0010, 0xcd5: 0x0010, 0xcd6: 0x0010, 0xcd7: 0x0010, + 0xcd8: 0x0010, 0xcd9: 0x0010, 0xcda: 0x0010, 0xcdb: 0x0010, 0xcdc: 0x0010, 0xcdd: 0x0010, + 0xcde: 0x0010, 0xcdf: 0x0010, 0xce0: 0x1000, 0xce1: 0x1000, 0xce2: 0x1000, 0xce3: 0x1000, + 0xce4: 0x1000, 0xce5: 0x1000, 0xce6: 0x1000, 0xce7: 0x1000, 0xce8: 0x1000, 0xce9: 0x1000, + 0xcea: 0x1000, 0xceb: 0x1000, 0xcec: 0x1000, 0xced: 0x1000, 0xcee: 0x1000, 0xcef: 0x1000, + 0xcf0: 0x1000, 0xcf1: 0x1000, 0xcf2: 0x1000, 0xcf3: 0x1000, 0xcf4: 0x1000, 0xcf5: 0x1000, + 0xcf6: 0x1000, 0xcf7: 0x1000, 0xcf8: 0x1000, 0xcf9: 0x1000, 0xcfa: 0x1000, 0xcfb: 0x1000, + 0xcfc: 0x1000, 0xcfd: 0x1000, 0xcfe: 0x1000, 0xcff: 0x1000, + // Block 0x34, offset 0xd00 + 0xd00: 0x1000, 0xd01: 0x1000, 0xd02: 0x1000, 0xd03: 0x1000, 0xd04: 0x1000, 0xd05: 0x1000, + 0xd06: 0x1000, 0xd07: 0x1000, 0xd08: 0x1000, 0xd09: 0x1000, 0xd0a: 0x1000, 0xd0b: 0x1000, + 0xd0c: 0x1000, 0xd0d: 0x1000, 0xd0e: 0x1000, 0xd0f: 0x1000, 0xd10: 0x1000, 0xd11: 0x1000, + 0xd12: 0x1000, 0xd13: 0x1000, 0xd14: 0x1000, 0xd15: 0x1000, 0xd16: 0x1000, 0xd17: 0x1000, + 0xd18: 0x1000, 0xd19: 0x1000, 0xd1a: 0x1000, 0xd1b: 0x1000, 0xd1c: 0x1000, 0xd1d: 0x1000, + 0xd1e: 0x1000, 0xd1f: 0x1000, 0xd20: 0x1000, 0xd21: 0x1000, 0xd22: 0x1000, 0xd23: 0x1000, + 0xd24: 0x1000, 0xd25: 0x1000, 0xd26: 0x1000, 0xd27: 0x1000, 0xd28: 0x0800, 0xd29: 0x0800, + 0xd2a: 0x0800, 0xd2b: 0x0800, 0xd2c: 0x0800, 0xd2d: 0x0800, 0xd2e: 0x0800, 0xd2f: 0x0800, + 0xd30: 0x0800, 0xd31: 0x0800, 0xd32: 0x0800, 0xd33: 0x0800, 0xd34: 0x0800, 0xd35: 0x0800, + 0xd36: 0x0800, 0xd37: 0x0800, 0xd38: 0x0800, 0xd39: 0x0800, 0xd3a: 0x0800, 0xd3b: 0x0800, + 0xd3c: 0x0800, 0xd3d: 0x0800, 0xd3e: 0x0800, 0xd3f: 0x0800, + // Block 0x35, offset 0xd40 + 0xd40: 0x0800, 0xd41: 0x0800, 0xd42: 0x0800, 0xd43: 0x0800, 0xd44: 0x0800, 0xd45: 0x0800, + 0xd46: 0x0800, 0xd47: 0x0800, 0xd48: 0x0800, 0xd49: 0x0800, 0xd4a: 0x0800, 0xd4b: 0x0800, + 0xd4c: 0x0800, 0xd4d: 0x0800, 0xd4e: 0x0800, 0xd4f: 0x0800, 0xd50: 0x0800, 0xd51: 0x0800, + 0xd52: 0x0800, 0xd53: 0x0800, 0xd54: 0x0800, 0xd55: 0x0800, 0xd56: 0x0800, 0xd57: 0x0800, + 0xd58: 0x0800, 0xd59: 0x0800, 0xd5a: 0x0800, 0xd5b: 0x0800, 0xd5c: 0x0800, 0xd5d: 0x0800, + 0xd5e: 0x0800, 0xd5f: 0x0800, 0xd60: 0x0800, 0xd61: 0x0800, 0xd62: 0x0800, 0xd63: 0x0800, + 0xd64: 0x0800, 0xd65: 0x0800, 0xd66: 0x0800, 0xd67: 0x0800, 0xd68: 0x0800, 0xd69: 0x0800, + 0xd6a: 0x0800, 0xd6b: 0x0800, 0xd6c: 0x0800, 0xd6d: 0x0800, 0xd6e: 0x0800, 0xd6f: 0x0800, + 0xd70: 0x0800, 0xd71: 0x0800, 0xd72: 0x0800, 0xd73: 0x0800, 0xd74: 0x0800, 0xd75: 0x0800, + 0xd76: 0x0800, 0xd77: 0x0800, 0xd78: 0x0800, 0xd79: 0x0800, 0xd7a: 0x0800, 0xd7b: 0x0800, + 0xd7c: 0x0800, 0xd7d: 0x0800, 0xd7e: 0x0800, 0xd7f: 0x0800, + // Block 0x36, offset 0xd80 + 0xd9d: 0x0004, + 0xd9e: 0x0004, 0xd9f: 0x0004, + // Block 0x37, offset 0xdc0 + 0xdd2: 0x0004, 0xdd3: 0x0004, 0xdd4: 0x0004, 0xdd5: 0x0400, + 0xdf2: 0x0004, 0xdf3: 0x0004, 0xdf4: 0x0400, + // Block 0x38, offset 0xe00 + 0xe12: 0x0004, 0xe13: 0x0004, + 0xe32: 0x0004, 0xe33: 0x0004, + // Block 0x39, offset 0xe40 + 0xe74: 0x0004, 0xe75: 0x0004, + 0xe76: 0x0400, 0xe77: 0x0004, 0xe78: 0x0004, 0xe79: 0x0004, 0xe7a: 0x0004, 0xe7b: 0x0004, + 0xe7c: 0x0004, 0xe7d: 0x0004, 0xe7e: 0x0400, 0xe7f: 0x0400, + // Block 0x3a, offset 0xe80 + 0xe80: 0x0400, 0xe81: 0x0400, 0xe82: 0x0400, 0xe83: 0x0400, 0xe84: 0x0400, 0xe85: 0x0400, + 0xe86: 0x0004, 0xe87: 0x0400, 0xe88: 0x0400, 0xe89: 0x0004, 0xe8a: 0x0004, 0xe8b: 0x0004, + 0xe8c: 0x0004, 0xe8d: 0x0004, 0xe8e: 0x0004, 0xe8f: 0x0004, 0xe90: 0x0004, 0xe91: 0x0004, + 0xe92: 0x0004, 0xe93: 0x0004, + 0xe9d: 0x0004, + // Block 0x3b, offset 0xec0 + 0xecb: 0x0004, + 0xecc: 0x0004, 0xecd: 0x0004, 0xece: 0x0002, 0xecf: 0x0004, + // Block 0x3c, offset 0xf00 + 0xf05: 0x0004, + 0xf06: 0x0004, + 0xf29: 0x0004, + // Block 0x3d, offset 0xf40 + 0xf60: 0x0004, 0xf61: 0x0004, 0xf62: 0x0004, 0xf63: 0x0400, + 0xf64: 0x0400, 0xf65: 0x0400, 0xf66: 0x0400, 0xf67: 0x0004, 0xf68: 0x0004, 0xf69: 0x0400, + 0xf6a: 0x0400, 0xf6b: 0x0400, + 0xf70: 0x0400, 0xf71: 0x0400, 0xf72: 0x0004, 0xf73: 0x0400, 0xf74: 0x0400, 0xf75: 0x0400, + 0xf76: 0x0400, 0xf77: 0x0400, 0xf78: 0x0400, 0xf79: 0x0004, 0xf7a: 0x0004, 0xf7b: 0x0004, + // Block 0x3e, offset 0xf80 + 0xf97: 0x0004, + 0xf98: 0x0004, 0xf99: 0x0400, 0xf9a: 0x0400, 0xf9b: 0x0004, + // Block 0x3f, offset 0xfc0 + 0xfd5: 0x0400, 0xfd6: 0x0004, 0xfd7: 0x0400, + 0xfd8: 0x0004, 0xfd9: 0x0004, 0xfda: 0x0004, 0xfdb: 0x0004, 0xfdc: 0x0004, 0xfdd: 0x0004, + 0xfde: 0x0004, 0xfe0: 0x0004, 0xfe2: 0x0004, + 0xfe5: 0x0004, 0xfe6: 0x0004, 0xfe7: 0x0004, 0xfe8: 0x0004, 0xfe9: 0x0004, + 0xfea: 0x0004, 0xfeb: 0x0004, 0xfec: 0x0004, 0xfed: 0x0400, 0xfee: 0x0400, 0xfef: 0x0400, + 0xff0: 0x0400, 0xff1: 0x0400, 0xff2: 0x0400, 0xff3: 0x0004, 0xff4: 0x0004, 0xff5: 0x0004, + 0xff6: 0x0004, 0xff7: 0x0004, 0xff8: 0x0004, 0xff9: 0x0004, 0xffa: 0x0004, 0xffb: 0x0004, + 0xffc: 0x0004, 0xfff: 0x0004, + // Block 0x40, offset 0x1000 + 0x1030: 0x0004, 0x1031: 0x0004, 0x1032: 0x0004, 0x1033: 0x0004, 0x1034: 0x0004, 0x1035: 0x0004, + 0x1036: 0x0004, 0x1037: 0x0004, 0x1038: 0x0004, 0x1039: 0x0004, 0x103a: 0x0004, 0x103b: 0x0004, + 0x103c: 0x0004, 0x103d: 0x0004, 0x103e: 0x0004, 0x103f: 0x0004, + // Block 0x41, offset 0x1040 + 0x1040: 0x0004, 0x1041: 0x0004, 0x1042: 0x0004, 0x1043: 0x0004, 0x1044: 0x0004, 0x1045: 0x0004, + 0x1046: 0x0004, 0x1047: 0x0004, 0x1048: 0x0004, 0x1049: 0x0004, 0x104a: 0x0004, 0x104b: 0x0004, + 0x104c: 0x0004, 0x104d: 0x0004, 0x104e: 0x0004, + // Block 0x42, offset 0x1080 + 0x1080: 0x0004, 0x1081: 0x0004, 0x1082: 0x0004, 0x1083: 0x0004, 0x1084: 0x0400, + 0x10b4: 0x0004, 0x10b5: 0x0004, + 0x10b6: 0x0004, 0x10b7: 0x0004, 0x10b8: 0x0004, 0x10b9: 0x0004, 0x10ba: 0x0004, 0x10bb: 0x0400, + 0x10bc: 0x0004, 0x10bd: 0x0400, 0x10be: 0x0400, 0x10bf: 0x0400, + // Block 0x43, offset 0x10c0 + 0x10c0: 0x0400, 0x10c1: 0x0400, 0x10c2: 0x0004, 0x10c3: 0x0400, 0x10c4: 0x0400, + 0x10eb: 0x0004, 0x10ec: 0x0004, 0x10ed: 0x0004, 0x10ee: 0x0004, 0x10ef: 0x0004, + 0x10f0: 0x0004, 0x10f1: 0x0004, 0x10f2: 0x0004, 0x10f3: 0x0004, + // Block 0x44, offset 0x1100 + 0x1100: 0x0004, 0x1101: 0x0004, 0x1102: 0x0400, + 0x1121: 0x0400, 0x1122: 0x0004, 0x1123: 0x0004, + 0x1124: 0x0004, 0x1125: 0x0004, 0x1126: 0x0400, 0x1127: 0x0400, 0x1128: 0x0004, 0x1129: 0x0004, + 0x112a: 0x0400, 0x112b: 0x0004, 0x112c: 0x0004, 0x112d: 0x0004, + // Block 0x45, offset 0x1140 + 0x1166: 0x0004, 0x1167: 0x0400, 0x1168: 0x0004, 0x1169: 0x0004, + 0x116a: 0x0400, 0x116b: 0x0400, 0x116c: 0x0400, 0x116d: 0x0004, 0x116e: 0x0400, 0x116f: 0x0004, + 0x1170: 0x0004, 0x1171: 0x0004, 0x1172: 0x0400, 0x1173: 0x0400, + // Block 0x46, offset 0x1180 + 0x11a4: 0x0400, 0x11a5: 0x0400, 0x11a6: 0x0400, 0x11a7: 0x0400, 0x11a8: 0x0400, 0x11a9: 0x0400, + 0x11aa: 0x0400, 0x11ab: 0x0400, 0x11ac: 0x0004, 0x11ad: 0x0004, 0x11ae: 0x0004, 0x11af: 0x0004, + 0x11b0: 0x0004, 0x11b1: 0x0004, 0x11b2: 0x0004, 0x11b3: 0x0004, 0x11b4: 0x0400, 0x11b5: 0x0400, + 0x11b6: 0x0004, 0x11b7: 0x0004, + // Block 0x47, offset 0x11c0 + 0x11d0: 0x0004, 0x11d1: 0x0004, + 0x11d2: 0x0004, 0x11d4: 0x0004, 0x11d5: 0x0004, 0x11d6: 0x0004, 0x11d7: 0x0004, + 0x11d8: 0x0004, 0x11d9: 0x0004, 0x11da: 0x0004, 0x11db: 0x0004, 0x11dc: 0x0004, 0x11dd: 0x0004, + 0x11de: 0x0004, 0x11df: 0x0004, 0x11e0: 0x0004, 0x11e1: 0x0400, 0x11e2: 0x0004, 0x11e3: 0x0004, + 0x11e4: 0x0004, 0x11e5: 0x0004, 0x11e6: 0x0004, 0x11e7: 0x0004, 0x11e8: 0x0004, + 0x11ed: 0x0004, + 0x11f4: 0x0004, + 0x11f7: 0x0400, 0x11f8: 0x0004, 0x11f9: 0x0004, + // Block 0x48, offset 0x1200 + 0x120b: 0x0002, + 0x120c: 0x0004, 0x120d: 0x2000, 0x120e: 0x0002, 0x120f: 0x0002, + 0x1228: 0x0002, 0x1229: 0x0002, + 0x122a: 0x0002, 0x122b: 0x0002, 0x122c: 0x0002, 0x122d: 0x0002, 0x122e: 0x0002, + 0x123c: 0x0008, + // Block 0x49, offset 0x1240 + 0x1249: 0x0008, + 0x1260: 0x0002, 0x1261: 0x0002, 0x1262: 0x0002, 0x1263: 0x0002, + 0x1264: 0x0002, 0x1265: 0x0002, 0x1266: 0x0002, 0x1267: 0x0002, 0x1268: 0x0002, 0x1269: 0x0002, + 0x126a: 0x0002, 0x126b: 0x0002, 0x126c: 0x0002, 0x126d: 0x0002, 0x126e: 0x0002, 0x126f: 0x0002, + // Block 0x4a, offset 0x1280 + 0x1290: 0x0004, 0x1291: 0x0004, + 0x1292: 0x0004, 0x1293: 0x0004, 0x1294: 0x0004, 0x1295: 0x0004, 0x1296: 0x0004, 0x1297: 0x0004, + 0x1298: 0x0004, 0x1299: 0x0004, 0x129a: 0x0004, 0x129b: 0x0004, 0x129c: 0x0004, 0x129d: 0x0004, + 0x129e: 0x0004, 0x129f: 0x0004, 0x12a0: 0x0004, 0x12a1: 0x0004, 0x12a2: 0x0004, 0x12a3: 0x0004, + 0x12a4: 0x0004, 0x12a5: 0x0004, 0x12a6: 0x0004, 0x12a7: 0x0004, 0x12a8: 0x0004, 0x12a9: 0x0004, + 0x12aa: 0x0004, 0x12ab: 0x0004, 0x12ac: 0x0004, 0x12ad: 0x0004, 0x12ae: 0x0004, 0x12af: 0x0004, + 0x12b0: 0x0004, + // Block 0x4b, offset 0x12c0 + 0x12e2: 0x0008, + 0x12f9: 0x0008, + // Block 0x4c, offset 0x1300 + 0x1314: 0x0008, 0x1315: 0x0008, 0x1316: 0x0008, 0x1317: 0x0008, + 0x1318: 0x0008, 0x1319: 0x0008, + 0x1329: 0x0008, + 0x132a: 0x0008, + // Block 0x4d, offset 0x1340 + 0x135a: 0x0008, 0x135b: 0x0008, + 0x1368: 0x0008, + // Block 0x4e, offset 0x1380 + 0x1388: 0x0008, + // Block 0x4f, offset 0x13c0 + 0x13cf: 0x0008, + 0x13e9: 0x0008, + 0x13ea: 0x0008, 0x13eb: 0x0008, 0x13ec: 0x0008, 0x13ed: 0x0008, 0x13ee: 0x0008, 0x13ef: 0x0008, + 0x13f0: 0x0008, 0x13f1: 0x0008, 0x13f2: 0x0008, 0x13f3: 0x0008, + 0x13f8: 0x0008, 0x13f9: 0x0008, 0x13fa: 0x0008, + // Block 0x50, offset 0x1400 + 0x1402: 0x0008, + // Block 0x51, offset 0x1440 + 0x146a: 0x0008, 0x146b: 0x0008, + 0x1476: 0x0008, + // Block 0x52, offset 0x1480 + 0x1480: 0x0008, + 0x14bb: 0x0008, + 0x14bc: 0x0008, 0x14bd: 0x0008, 0x14be: 0x0008, + // Block 0x53, offset 0x14c0 + 0x14c0: 0x0008, 0x14c1: 0x0008, 0x14c2: 0x0008, 0x14c3: 0x0008, 0x14c4: 0x0008, 0x14c5: 0x0008, + 0x14c7: 0x0008, 0x14c8: 0x0008, 0x14c9: 0x0008, 0x14ca: 0x0008, 0x14cb: 0x0008, + 0x14cc: 0x0008, 0x14cd: 0x0008, 0x14ce: 0x0008, 0x14cf: 0x0008, 0x14d0: 0x0008, 0x14d1: 0x0008, + 0x14d2: 0x0008, 0x14d4: 0x0008, 0x14d5: 0x0008, 0x14d6: 0x0008, 0x14d7: 0x0008, + 0x14d8: 0x0008, 0x14d9: 0x0008, 0x14da: 0x0008, 0x14db: 0x0008, 0x14dc: 0x0008, 0x14dd: 0x0008, + 0x14de: 0x0008, 0x14df: 0x0008, 0x14e0: 0x0008, 0x14e1: 0x0008, 0x14e2: 0x0008, 0x14e3: 0x0008, + 0x14e4: 0x0008, 0x14e5: 0x0008, 0x14e6: 0x0008, 0x14e7: 0x0008, 0x14e8: 0x0008, 0x14e9: 0x0008, + 0x14ea: 0x0008, 0x14eb: 0x0008, 0x14ec: 0x0008, 0x14ed: 0x0008, 0x14ee: 0x0008, 0x14ef: 0x0008, + 0x14f0: 0x0008, 0x14f1: 0x0008, 0x14f2: 0x0008, 0x14f3: 0x0008, 0x14f4: 0x0008, 0x14f5: 0x0008, + 0x14f6: 0x0008, 0x14f7: 0x0008, 0x14f8: 0x0008, 0x14f9: 0x0008, 0x14fa: 0x0008, 0x14fb: 0x0008, + 0x14fc: 0x0008, 0x14fd: 0x0008, 0x14fe: 0x0008, 0x14ff: 0x0008, + // Block 0x54, offset 0x1500 + 0x1500: 0x0008, 0x1501: 0x0008, 0x1502: 0x0008, 0x1503: 0x0008, 0x1504: 0x0008, 0x1505: 0x0008, + 0x1506: 0x0008, 0x1507: 0x0008, 0x1508: 0x0008, 0x1509: 0x0008, 0x150a: 0x0008, 0x150b: 0x0008, + 0x150c: 0x0008, 0x150d: 0x0008, 0x150e: 0x0008, 0x150f: 0x0008, 0x1510: 0x0008, 0x1511: 0x0008, + 0x1512: 0x0008, 0x1513: 0x0008, 0x1514: 0x0008, 0x1515: 0x0008, 0x1516: 0x0008, 0x1517: 0x0008, + 0x1518: 0x0008, 0x1519: 0x0008, 0x151a: 0x0008, 0x151b: 0x0008, 0x151c: 0x0008, 0x151d: 0x0008, + 0x151e: 0x0008, 0x151f: 0x0008, 0x1520: 0x0008, 0x1521: 0x0008, 0x1522: 0x0008, 0x1523: 0x0008, + 0x1524: 0x0008, 0x1525: 0x0008, 0x1526: 0x0008, 0x1527: 0x0008, 0x1528: 0x0008, 0x1529: 0x0008, + 0x152a: 0x0008, 0x152b: 0x0008, 0x152c: 0x0008, 0x152d: 0x0008, 0x152e: 0x0008, 0x152f: 0x0008, + 0x1530: 0x0008, 0x1531: 0x0008, 0x1532: 0x0008, 0x1533: 0x0008, 0x1534: 0x0008, 0x1535: 0x0008, + 0x1536: 0x0008, 0x1537: 0x0008, 0x1538: 0x0008, 0x1539: 0x0008, 0x153a: 0x0008, 0x153b: 0x0008, + 0x153c: 0x0008, 0x153d: 0x0008, 0x153e: 0x0008, 0x153f: 0x0008, + // Block 0x55, offset 0x1540 + 0x1540: 0x0008, 0x1541: 0x0008, 0x1542: 0x0008, 0x1543: 0x0008, 0x1544: 0x0008, 0x1545: 0x0008, + 0x1550: 0x0008, 0x1551: 0x0008, + 0x1552: 0x0008, 0x1553: 0x0008, 0x1554: 0x0008, 0x1555: 0x0008, 0x1556: 0x0008, 0x1557: 0x0008, + 0x1558: 0x0008, 0x1559: 0x0008, 0x155a: 0x0008, 0x155b: 0x0008, 0x155c: 0x0008, 0x155d: 0x0008, + 0x155e: 0x0008, 0x155f: 0x0008, 0x1560: 0x0008, 0x1561: 0x0008, 0x1562: 0x0008, 0x1563: 0x0008, + 0x1564: 0x0008, 0x1565: 0x0008, 0x1566: 0x0008, 0x1567: 0x0008, 0x1568: 0x0008, 0x1569: 0x0008, + 0x156a: 0x0008, 0x156b: 0x0008, 0x156c: 0x0008, 0x156d: 0x0008, 0x156e: 0x0008, 0x156f: 0x0008, + 0x1570: 0x0008, 0x1571: 0x0008, 0x1572: 0x0008, 0x1573: 0x0008, 0x1574: 0x0008, 0x1575: 0x0008, + 0x1576: 0x0008, 0x1577: 0x0008, 0x1578: 0x0008, 0x1579: 0x0008, 0x157a: 0x0008, 0x157b: 0x0008, + 0x157c: 0x0008, 0x157d: 0x0008, 0x157e: 0x0008, 0x157f: 0x0008, + // Block 0x56, offset 0x1580 + 0x1580: 0x0008, 0x1581: 0x0008, 0x1582: 0x0008, 0x1583: 0x0008, 0x1584: 0x0008, 0x1585: 0x0008, + 0x1588: 0x0008, 0x1589: 0x0008, 0x158a: 0x0008, 0x158b: 0x0008, + 0x158c: 0x0008, 0x158d: 0x0008, 0x158e: 0x0008, 0x158f: 0x0008, 0x1590: 0x0008, 0x1591: 0x0008, + 0x1592: 0x0008, 0x1594: 0x0008, 0x1596: 0x0008, + 0x159d: 0x0008, + 0x15a1: 0x0008, + 0x15a8: 0x0008, + 0x15b3: 0x0008, 0x15b4: 0x0008, + // Block 0x57, offset 0x15c0 + 0x15c4: 0x0008, + 0x15c7: 0x0008, + 0x15cc: 0x0008, 0x15ce: 0x0008, + 0x15d3: 0x0008, 0x15d4: 0x0008, 0x15d5: 0x0008, 0x15d7: 0x0008, + 0x15e3: 0x0008, + 0x15e4: 0x0008, 0x15e5: 0x0008, 0x15e6: 0x0008, 0x15e7: 0x0008, + // Block 0x58, offset 0x1600 + 0x1615: 0x0008, 0x1616: 0x0008, 0x1617: 0x0008, + 0x1621: 0x0008, + 0x1630: 0x0008, + 0x163f: 0x0008, + // Block 0x59, offset 0x1640 + 0x1674: 0x0008, 0x1675: 0x0008, + // Block 0x5a, offset 0x1680 + 0x1685: 0x0008, + 0x1686: 0x0008, 0x1687: 0x0008, + 0x169b: 0x0008, 0x169c: 0x0008, + // Block 0x5b, offset 0x16c0 + 0x16d0: 0x0008, + 0x16d5: 0x0008, + // Block 0x5c, offset 0x1700 + 0x172f: 0x0004, + 0x1730: 0x0004, 0x1731: 0x0004, + // Block 0x5d, offset 0x1740 + 0x177f: 0x0004, + // Block 0x5e, offset 0x1780 + 0x17a0: 0x0004, 0x17a1: 0x0004, 0x17a2: 0x0004, 0x17a3: 0x0004, + 0x17a4: 0x0004, 0x17a5: 0x0004, 0x17a6: 0x0004, 0x17a7: 0x0004, 0x17a8: 0x0004, 0x17a9: 0x0004, + 0x17aa: 0x0004, 0x17ab: 0x0004, 0x17ac: 0x0004, 0x17ad: 0x0004, 0x17ae: 0x0004, 0x17af: 0x0004, + 0x17b0: 0x0004, 0x17b1: 0x0004, 0x17b2: 0x0004, 0x17b3: 0x0004, 0x17b4: 0x0004, 0x17b5: 0x0004, + 0x17b6: 0x0004, 0x17b7: 0x0004, 0x17b8: 0x0004, 0x17b9: 0x0004, 0x17ba: 0x0004, 0x17bb: 0x0004, + 0x17bc: 0x0004, 0x17bd: 0x0004, 0x17be: 0x0004, 0x17bf: 0x0004, + // Block 0x5f, offset 0x17c0 + 0x17ea: 0x0004, 0x17eb: 0x0004, 0x17ec: 0x0004, 0x17ed: 0x0004, 0x17ee: 0x0004, 0x17ef: 0x0004, + 0x17f0: 0x0008, + 0x17fd: 0x0008, + // Block 0x60, offset 0x1800 + 0x1819: 0x0004, 0x181a: 0x0004, + // Block 0x61, offset 0x1840 + 0x1857: 0x0008, + 0x1859: 0x0008, + // Block 0x62, offset 0x1880 + 0x18af: 0x0004, + 0x18b0: 0x0004, 0x18b1: 0x0004, 0x18b2: 0x0004, 0x18b4: 0x0004, 0x18b5: 0x0004, + 0x18b6: 0x0004, 0x18b7: 0x0004, 0x18b8: 0x0004, 0x18b9: 0x0004, 0x18ba: 0x0004, 0x18bb: 0x0004, + 0x18bc: 0x0004, 0x18bd: 0x0004, + // Block 0x63, offset 0x18c0 + 0x18de: 0x0004, 0x18df: 0x0004, + // Block 0x64, offset 0x1900 + 0x1930: 0x0004, 0x1931: 0x0004, + // Block 0x65, offset 0x1940 + 0x1942: 0x0004, + 0x1946: 0x0004, 0x194b: 0x0004, + 0x1963: 0x0400, + 0x1964: 0x0400, 0x1965: 0x0004, 0x1966: 0x0004, 0x1967: 0x0400, + 0x196c: 0x0004, + // Block 0x66, offset 0x1980 + 0x1980: 0x0400, 0x1981: 0x0400, + 0x19b4: 0x0400, 0x19b5: 0x0400, + 0x19b6: 0x0400, 0x19b7: 0x0400, 0x19b8: 0x0400, 0x19b9: 0x0400, 0x19ba: 0x0400, 0x19bb: 0x0400, + 0x19bc: 0x0400, 0x19bd: 0x0400, 0x19be: 0x0400, 0x19bf: 0x0400, + // Block 0x67, offset 0x19c0 + 0x19c0: 0x0400, 0x19c1: 0x0400, 0x19c2: 0x0400, 0x19c3: 0x0400, 0x19c4: 0x0004, 0x19c5: 0x0004, + 0x19e0: 0x0004, 0x19e1: 0x0004, 0x19e2: 0x0004, 0x19e3: 0x0004, + 0x19e4: 0x0004, 0x19e5: 0x0004, 0x19e6: 0x0004, 0x19e7: 0x0004, 0x19e8: 0x0004, 0x19e9: 0x0004, + 0x19ea: 0x0004, 0x19eb: 0x0004, 0x19ec: 0x0004, 0x19ed: 0x0004, 0x19ee: 0x0004, 0x19ef: 0x0004, + 0x19f0: 0x0004, 0x19f1: 0x0004, + 0x19ff: 0x0004, + // Block 0x68, offset 0x1a00 + 0x1a26: 0x0004, 0x1a27: 0x0004, 0x1a28: 0x0004, 0x1a29: 0x0004, + 0x1a2a: 0x0004, 0x1a2b: 0x0004, 0x1a2c: 0x0004, 0x1a2d: 0x0004, + // Block 0x69, offset 0x1a40 + 0x1a47: 0x0004, 0x1a48: 0x0004, 0x1a49: 0x0004, 0x1a4a: 0x0004, 0x1a4b: 0x0004, + 0x1a4c: 0x0004, 0x1a4d: 0x0004, 0x1a4e: 0x0004, 0x1a4f: 0x0004, 0x1a50: 0x0004, 0x1a51: 0x0004, + 0x1a52: 0x0400, 0x1a53: 0x0400, + 0x1a60: 0x0010, 0x1a61: 0x0010, 0x1a62: 0x0010, 0x1a63: 0x0010, + 0x1a64: 0x0010, 0x1a65: 0x0010, 0x1a66: 0x0010, 0x1a67: 0x0010, 0x1a68: 0x0010, 0x1a69: 0x0010, + 0x1a6a: 0x0010, 0x1a6b: 0x0010, 0x1a6c: 0x0010, 0x1a6d: 0x0010, 0x1a6e: 0x0010, 0x1a6f: 0x0010, + 0x1a70: 0x0010, 0x1a71: 0x0010, 0x1a72: 0x0010, 0x1a73: 0x0010, 0x1a74: 0x0010, 0x1a75: 0x0010, + 0x1a76: 0x0010, 0x1a77: 0x0010, 0x1a78: 0x0010, 0x1a79: 0x0010, 0x1a7a: 0x0010, 0x1a7b: 0x0010, + 0x1a7c: 0x0010, + // Block 0x6a, offset 0x1a80 + 0x1a80: 0x0004, 0x1a81: 0x0004, 0x1a82: 0x0004, 0x1a83: 0x0400, + 0x1ab3: 0x0004, 0x1ab4: 0x0400, 0x1ab5: 0x0400, + 0x1ab6: 0x0004, 0x1ab7: 0x0004, 0x1ab8: 0x0004, 0x1ab9: 0x0004, 0x1aba: 0x0400, 0x1abb: 0x0400, + 0x1abc: 0x0004, 0x1abd: 0x0004, 0x1abe: 0x0400, 0x1abf: 0x0400, + // Block 0x6b, offset 0x1ac0 + 0x1ac0: 0x0400, + 0x1ae5: 0x0004, + // Block 0x6c, offset 0x1b00 + 0x1b29: 0x0004, + 0x1b2a: 0x0004, 0x1b2b: 0x0004, 0x1b2c: 0x0004, 0x1b2d: 0x0004, 0x1b2e: 0x0004, 0x1b2f: 0x0400, + 0x1b30: 0x0400, 0x1b31: 0x0004, 0x1b32: 0x0004, 0x1b33: 0x0400, 0x1b34: 0x0400, 0x1b35: 0x0004, + 0x1b36: 0x0004, + // Block 0x6d, offset 0x1b40 + 0x1b43: 0x0004, + 0x1b4c: 0x0004, 0x1b4d: 0x0400, + 0x1b7c: 0x0004, + // Block 0x6e, offset 0x1b80 + 0x1bb0: 0x0004, 0x1bb2: 0x0004, 0x1bb3: 0x0004, 0x1bb4: 0x0004, + 0x1bb7: 0x0004, 0x1bb8: 0x0004, + 0x1bbe: 0x0004, 0x1bbf: 0x0004, + // Block 0x6f, offset 0x1bc0 + 0x1bc1: 0x0004, + 0x1beb: 0x0400, 0x1bec: 0x0004, 0x1bed: 0x0004, 0x1bee: 0x0400, 0x1bef: 0x0400, + 0x1bf5: 0x0400, + 0x1bf6: 0x0004, + // Block 0x70, offset 0x1c00 + 0x1c23: 0x0400, + 0x1c24: 0x0400, 0x1c25: 0x0004, 0x1c26: 0x0400, 0x1c27: 0x0400, 0x1c28: 0x0004, 0x1c29: 0x0400, + 0x1c2a: 0x0400, 0x1c2c: 0x0400, 0x1c2d: 0x0004, + // Block 0x71, offset 0x1c40 + 0x1c40: 0x0040, 0x1c41: 0x0080, 0x1c42: 0x0080, 0x1c43: 0x0080, 0x1c44: 0x0080, 0x1c45: 0x0080, + 0x1c46: 0x0080, 0x1c47: 0x0080, 0x1c48: 0x0080, 0x1c49: 0x0080, 0x1c4a: 0x0080, 0x1c4b: 0x0080, + 0x1c4c: 0x0080, 0x1c4d: 0x0080, 0x1c4e: 0x0080, 0x1c4f: 0x0080, 0x1c50: 0x0080, 0x1c51: 0x0080, + 0x1c52: 0x0080, 0x1c53: 0x0080, 0x1c54: 0x0080, 0x1c55: 0x0080, 0x1c56: 0x0080, 0x1c57: 0x0080, + 0x1c58: 0x0080, 0x1c59: 0x0080, 0x1c5a: 0x0080, 0x1c5b: 0x0080, 0x1c5c: 0x0040, 0x1c5d: 0x0080, + 0x1c5e: 0x0080, 0x1c5f: 0x0080, 0x1c60: 0x0080, 0x1c61: 0x0080, 0x1c62: 0x0080, 0x1c63: 0x0080, + 0x1c64: 0x0080, 0x1c65: 0x0080, 0x1c66: 0x0080, 0x1c67: 0x0080, 0x1c68: 0x0080, 0x1c69: 0x0080, + 0x1c6a: 0x0080, 0x1c6b: 0x0080, 0x1c6c: 0x0080, 0x1c6d: 0x0080, 0x1c6e: 0x0080, 0x1c6f: 0x0080, + 0x1c70: 0x0080, 0x1c71: 0x0080, 0x1c72: 0x0080, 0x1c73: 0x0080, 0x1c74: 0x0080, 0x1c75: 0x0080, + 0x1c76: 0x0080, 0x1c77: 0x0080, 0x1c78: 0x0040, 0x1c79: 0x0080, 0x1c7a: 0x0080, 0x1c7b: 0x0080, + 0x1c7c: 0x0080, 0x1c7d: 0x0080, 0x1c7e: 0x0080, 0x1c7f: 0x0080, + // Block 0x72, offset 0x1c80 + 0x1c80: 0x0080, 0x1c81: 0x0080, 0x1c82: 0x0080, 0x1c83: 0x0080, 0x1c84: 0x0080, 0x1c85: 0x0080, + 0x1c86: 0x0080, 0x1c87: 0x0080, 0x1c88: 0x0080, 0x1c89: 0x0080, 0x1c8a: 0x0080, 0x1c8b: 0x0080, + 0x1c8c: 0x0080, 0x1c8d: 0x0080, 0x1c8e: 0x0080, 0x1c8f: 0x0080, 0x1c90: 0x0080, 0x1c91: 0x0080, + 0x1c92: 0x0080, 0x1c93: 0x0080, 0x1c94: 0x0040, 0x1c95: 0x0080, 0x1c96: 0x0080, 0x1c97: 0x0080, + 0x1c98: 0x0080, 0x1c99: 0x0080, 0x1c9a: 0x0080, 0x1c9b: 0x0080, 0x1c9c: 0x0080, 0x1c9d: 0x0080, + 0x1c9e: 0x0080, 0x1c9f: 0x0080, 0x1ca0: 0x0080, 0x1ca1: 0x0080, 0x1ca2: 0x0080, 0x1ca3: 0x0080, + 0x1ca4: 0x0080, 0x1ca5: 0x0080, 0x1ca6: 0x0080, 0x1ca7: 0x0080, 0x1ca8: 0x0080, 0x1ca9: 0x0080, + 0x1caa: 0x0080, 0x1cab: 0x0080, 0x1cac: 0x0080, 0x1cad: 0x0080, 0x1cae: 0x0080, 0x1caf: 0x0080, + 0x1cb0: 0x0040, 0x1cb1: 0x0080, 0x1cb2: 0x0080, 0x1cb3: 0x0080, 0x1cb4: 0x0080, 0x1cb5: 0x0080, + 0x1cb6: 0x0080, 0x1cb7: 0x0080, 0x1cb8: 0x0080, 0x1cb9: 0x0080, 0x1cba: 0x0080, 0x1cbb: 0x0080, + 0x1cbc: 0x0080, 0x1cbd: 0x0080, 0x1cbe: 0x0080, 0x1cbf: 0x0080, + // Block 0x73, offset 0x1cc0 + 0x1cc0: 0x0080, 0x1cc1: 0x0080, 0x1cc2: 0x0080, 0x1cc3: 0x0080, 0x1cc4: 0x0080, 0x1cc5: 0x0080, + 0x1cc6: 0x0080, 0x1cc7: 0x0080, 0x1cc8: 0x0080, 0x1cc9: 0x0080, 0x1cca: 0x0080, 0x1ccb: 0x0080, + 0x1ccc: 0x0040, 0x1ccd: 0x0080, 0x1cce: 0x0080, 0x1ccf: 0x0080, 0x1cd0: 0x0080, 0x1cd1: 0x0080, + 0x1cd2: 0x0080, 0x1cd3: 0x0080, 0x1cd4: 0x0080, 0x1cd5: 0x0080, 0x1cd6: 0x0080, 0x1cd7: 0x0080, + 0x1cd8: 0x0080, 0x1cd9: 0x0080, 0x1cda: 0x0080, 0x1cdb: 0x0080, 0x1cdc: 0x0080, 0x1cdd: 0x0080, + 0x1cde: 0x0080, 0x1cdf: 0x0080, 0x1ce0: 0x0080, 0x1ce1: 0x0080, 0x1ce2: 0x0080, 0x1ce3: 0x0080, + 0x1ce4: 0x0080, 0x1ce5: 0x0080, 0x1ce6: 0x0080, 0x1ce7: 0x0080, 0x1ce8: 0x0040, 0x1ce9: 0x0080, + 0x1cea: 0x0080, 0x1ceb: 0x0080, 0x1cec: 0x0080, 0x1ced: 0x0080, 0x1cee: 0x0080, 0x1cef: 0x0080, + 0x1cf0: 0x0080, 0x1cf1: 0x0080, 0x1cf2: 0x0080, 0x1cf3: 0x0080, 0x1cf4: 0x0080, 0x1cf5: 0x0080, + 0x1cf6: 0x0080, 0x1cf7: 0x0080, 0x1cf8: 0x0080, 0x1cf9: 0x0080, 0x1cfa: 0x0080, 0x1cfb: 0x0080, + 0x1cfc: 0x0080, 0x1cfd: 0x0080, 0x1cfe: 0x0080, 0x1cff: 0x0080, + // Block 0x74, offset 0x1d00 + 0x1d00: 0x0080, 0x1d01: 0x0080, 0x1d02: 0x0080, 0x1d03: 0x0080, 0x1d04: 0x0040, 0x1d05: 0x0080, + 0x1d06: 0x0080, 0x1d07: 0x0080, 0x1d08: 0x0080, 0x1d09: 0x0080, 0x1d0a: 0x0080, 0x1d0b: 0x0080, + 0x1d0c: 0x0080, 0x1d0d: 0x0080, 0x1d0e: 0x0080, 0x1d0f: 0x0080, 0x1d10: 0x0080, 0x1d11: 0x0080, + 0x1d12: 0x0080, 0x1d13: 0x0080, 0x1d14: 0x0080, 0x1d15: 0x0080, 0x1d16: 0x0080, 0x1d17: 0x0080, + 0x1d18: 0x0080, 0x1d19: 0x0080, 0x1d1a: 0x0080, 0x1d1b: 0x0080, 0x1d1c: 0x0080, 0x1d1d: 0x0080, + 0x1d1e: 0x0080, 0x1d1f: 0x0080, 0x1d20: 0x0040, 0x1d21: 0x0080, 0x1d22: 0x0080, 0x1d23: 0x0080, + 0x1d24: 0x0080, 0x1d25: 0x0080, 0x1d26: 0x0080, 0x1d27: 0x0080, 0x1d28: 0x0080, 0x1d29: 0x0080, + 0x1d2a: 0x0080, 0x1d2b: 0x0080, 0x1d2c: 0x0080, 0x1d2d: 0x0080, 0x1d2e: 0x0080, 0x1d2f: 0x0080, + 0x1d30: 0x0080, 0x1d31: 0x0080, 0x1d32: 0x0080, 0x1d33: 0x0080, 0x1d34: 0x0080, 0x1d35: 0x0080, + 0x1d36: 0x0080, 0x1d37: 0x0080, 0x1d38: 0x0080, 0x1d39: 0x0080, 0x1d3a: 0x0080, 0x1d3b: 0x0080, + 0x1d3c: 0x0040, 0x1d3d: 0x0080, 0x1d3e: 0x0080, 0x1d3f: 0x0080, + // Block 0x75, offset 0x1d40 + 0x1d40: 0x0080, 0x1d41: 0x0080, 0x1d42: 0x0080, 0x1d43: 0x0080, 0x1d44: 0x0080, 0x1d45: 0x0080, + 0x1d46: 0x0080, 0x1d47: 0x0080, 0x1d48: 0x0080, 0x1d49: 0x0080, 0x1d4a: 0x0080, 0x1d4b: 0x0080, + 0x1d4c: 0x0080, 0x1d4d: 0x0080, 0x1d4e: 0x0080, 0x1d4f: 0x0080, 0x1d50: 0x0080, 0x1d51: 0x0080, + 0x1d52: 0x0080, 0x1d53: 0x0080, 0x1d54: 0x0080, 0x1d55: 0x0080, 0x1d56: 0x0080, 0x1d57: 0x0080, + 0x1d58: 0x0040, 0x1d59: 0x0080, 0x1d5a: 0x0080, 0x1d5b: 0x0080, 0x1d5c: 0x0080, 0x1d5d: 0x0080, + 0x1d5e: 0x0080, 0x1d5f: 0x0080, 0x1d60: 0x0080, 0x1d61: 0x0080, 0x1d62: 0x0080, 0x1d63: 0x0080, + 0x1d64: 0x0080, 0x1d65: 0x0080, 0x1d66: 0x0080, 0x1d67: 0x0080, 0x1d68: 0x0080, 0x1d69: 0x0080, + 0x1d6a: 0x0080, 0x1d6b: 0x0080, 0x1d6c: 0x0080, 0x1d6d: 0x0080, 0x1d6e: 0x0080, 0x1d6f: 0x0080, + 0x1d70: 0x0080, 0x1d71: 0x0080, 0x1d72: 0x0080, 0x1d73: 0x0080, 0x1d74: 0x0040, 0x1d75: 0x0080, + 0x1d76: 0x0080, 0x1d77: 0x0080, 0x1d78: 0x0080, 0x1d79: 0x0080, 0x1d7a: 0x0080, 0x1d7b: 0x0080, + 0x1d7c: 0x0080, 0x1d7d: 0x0080, 0x1d7e: 0x0080, 0x1d7f: 0x0080, + // Block 0x76, offset 0x1d80 + 0x1d80: 0x0080, 0x1d81: 0x0080, 0x1d82: 0x0080, 0x1d83: 0x0080, 0x1d84: 0x0080, 0x1d85: 0x0080, + 0x1d86: 0x0080, 0x1d87: 0x0080, 0x1d88: 0x0080, 0x1d89: 0x0080, 0x1d8a: 0x0080, 0x1d8b: 0x0080, + 0x1d8c: 0x0080, 0x1d8d: 0x0080, 0x1d8e: 0x0080, 0x1d8f: 0x0080, 0x1d90: 0x0040, 0x1d91: 0x0080, + 0x1d92: 0x0080, 0x1d93: 0x0080, 0x1d94: 0x0080, 0x1d95: 0x0080, 0x1d96: 0x0080, 0x1d97: 0x0080, + 0x1d98: 0x0080, 0x1d99: 0x0080, 0x1d9a: 0x0080, 0x1d9b: 0x0080, 0x1d9c: 0x0080, 0x1d9d: 0x0080, + 0x1d9e: 0x0080, 0x1d9f: 0x0080, 0x1da0: 0x0080, 0x1da1: 0x0080, 0x1da2: 0x0080, 0x1da3: 0x0080, + 0x1da4: 0x0080, 0x1da5: 0x0080, 0x1da6: 0x0080, 0x1da7: 0x0080, 0x1da8: 0x0080, 0x1da9: 0x0080, + 0x1daa: 0x0080, 0x1dab: 0x0080, 0x1dac: 0x0040, 0x1dad: 0x0080, 0x1dae: 0x0080, 0x1daf: 0x0080, + 0x1db0: 0x0080, 0x1db1: 0x0080, 0x1db2: 0x0080, 0x1db3: 0x0080, 0x1db4: 0x0080, 0x1db5: 0x0080, + 0x1db6: 0x0080, 0x1db7: 0x0080, 0x1db8: 0x0080, 0x1db9: 0x0080, 0x1dba: 0x0080, 0x1dbb: 0x0080, + 0x1dbc: 0x0080, 0x1dbd: 0x0080, 0x1dbe: 0x0080, 0x1dbf: 0x0080, + // Block 0x77, offset 0x1dc0 + 0x1dc0: 0x0080, 0x1dc1: 0x0080, 0x1dc2: 0x0080, 0x1dc3: 0x0080, 0x1dc4: 0x0080, 0x1dc5: 0x0080, + 0x1dc6: 0x0080, 0x1dc7: 0x0080, 0x1dc8: 0x0040, 0x1dc9: 0x0080, 0x1dca: 0x0080, 0x1dcb: 0x0080, + 0x1dcc: 0x0080, 0x1dcd: 0x0080, 0x1dce: 0x0080, 0x1dcf: 0x0080, 0x1dd0: 0x0080, 0x1dd1: 0x0080, + 0x1dd2: 0x0080, 0x1dd3: 0x0080, 0x1dd4: 0x0080, 0x1dd5: 0x0080, 0x1dd6: 0x0080, 0x1dd7: 0x0080, + 0x1dd8: 0x0080, 0x1dd9: 0x0080, 0x1dda: 0x0080, 0x1ddb: 0x0080, 0x1ddc: 0x0080, 0x1ddd: 0x0080, + 0x1dde: 0x0080, 0x1ddf: 0x0080, 0x1de0: 0x0080, 0x1de1: 0x0080, 0x1de2: 0x0080, 0x1de3: 0x0080, + 0x1de4: 0x0040, 0x1de5: 0x0080, 0x1de6: 0x0080, 0x1de7: 0x0080, 0x1de8: 0x0080, 0x1de9: 0x0080, + 0x1dea: 0x0080, 0x1deb: 0x0080, 0x1dec: 0x0080, 0x1ded: 0x0080, 0x1dee: 0x0080, 0x1def: 0x0080, + 0x1df0: 0x0080, 0x1df1: 0x0080, 0x1df2: 0x0080, 0x1df3: 0x0080, 0x1df4: 0x0080, 0x1df5: 0x0080, + 0x1df6: 0x0080, 0x1df7: 0x0080, 0x1df8: 0x0080, 0x1df9: 0x0080, 0x1dfa: 0x0080, 0x1dfb: 0x0080, + 0x1dfc: 0x0080, 0x1dfd: 0x0080, 0x1dfe: 0x0080, 0x1dff: 0x0080, + // Block 0x78, offset 0x1e00 + 0x1e00: 0x0080, 0x1e01: 0x0080, 0x1e02: 0x0080, 0x1e03: 0x0080, 0x1e04: 0x0080, 0x1e05: 0x0080, + 0x1e06: 0x0080, 0x1e07: 0x0080, 0x1e08: 0x0040, 0x1e09: 0x0080, 0x1e0a: 0x0080, 0x1e0b: 0x0080, + 0x1e0c: 0x0080, 0x1e0d: 0x0080, 0x1e0e: 0x0080, 0x1e0f: 0x0080, 0x1e10: 0x0080, 0x1e11: 0x0080, + 0x1e12: 0x0080, 0x1e13: 0x0080, 0x1e14: 0x0080, 0x1e15: 0x0080, 0x1e16: 0x0080, 0x1e17: 0x0080, + 0x1e18: 0x0080, 0x1e19: 0x0080, 0x1e1a: 0x0080, 0x1e1b: 0x0080, 0x1e1c: 0x0080, 0x1e1d: 0x0080, + 0x1e1e: 0x0080, 0x1e1f: 0x0080, 0x1e20: 0x0080, 0x1e21: 0x0080, 0x1e22: 0x0080, 0x1e23: 0x0080, + 0x1e30: 0x1000, 0x1e31: 0x1000, 0x1e32: 0x1000, 0x1e33: 0x1000, 0x1e34: 0x1000, 0x1e35: 0x1000, + 0x1e36: 0x1000, 0x1e37: 0x1000, 0x1e38: 0x1000, 0x1e39: 0x1000, 0x1e3a: 0x1000, 0x1e3b: 0x1000, + 0x1e3c: 0x1000, 0x1e3d: 0x1000, 0x1e3e: 0x1000, 0x1e3f: 0x1000, + // Block 0x79, offset 0x1e40 + 0x1e40: 0x1000, 0x1e41: 0x1000, 0x1e42: 0x1000, 0x1e43: 0x1000, 0x1e44: 0x1000, 0x1e45: 0x1000, + 0x1e46: 0x1000, 0x1e4b: 0x0800, + 0x1e4c: 0x0800, 0x1e4d: 0x0800, 0x1e4e: 0x0800, 0x1e4f: 0x0800, 0x1e50: 0x0800, 0x1e51: 0x0800, + 0x1e52: 0x0800, 0x1e53: 0x0800, 0x1e54: 0x0800, 0x1e55: 0x0800, 0x1e56: 0x0800, 0x1e57: 0x0800, + 0x1e58: 0x0800, 0x1e59: 0x0800, 0x1e5a: 0x0800, 0x1e5b: 0x0800, 0x1e5c: 0x0800, 0x1e5d: 0x0800, + 0x1e5e: 0x0800, 0x1e5f: 0x0800, 0x1e60: 0x0800, 0x1e61: 0x0800, 0x1e62: 0x0800, 0x1e63: 0x0800, + 0x1e64: 0x0800, 0x1e65: 0x0800, 0x1e66: 0x0800, 0x1e67: 0x0800, 0x1e68: 0x0800, 0x1e69: 0x0800, + 0x1e6a: 0x0800, 0x1e6b: 0x0800, 0x1e6c: 0x0800, 0x1e6d: 0x0800, 0x1e6e: 0x0800, 0x1e6f: 0x0800, + 0x1e70: 0x0800, 0x1e71: 0x0800, 0x1e72: 0x0800, 0x1e73: 0x0800, 0x1e74: 0x0800, 0x1e75: 0x0800, + 0x1e76: 0x0800, 0x1e77: 0x0800, 0x1e78: 0x0800, 0x1e79: 0x0800, 0x1e7a: 0x0800, 0x1e7b: 0x0800, + // Block 0x7a, offset 0x1e80 + 0x1e9e: 0x0004, + // Block 0x7b, offset 0x1ec0 + 0x1ec0: 0x0004, 0x1ec1: 0x0004, 0x1ec2: 0x0004, 0x1ec3: 0x0004, 0x1ec4: 0x0004, 0x1ec5: 0x0004, + 0x1ec6: 0x0004, 0x1ec7: 0x0004, 0x1ec8: 0x0004, 0x1ec9: 0x0004, 0x1eca: 0x0004, 0x1ecb: 0x0004, + 0x1ecc: 0x0004, 0x1ecd: 0x0004, 0x1ece: 0x0004, 0x1ecf: 0x0004, + 0x1ee0: 0x0004, 0x1ee1: 0x0004, 0x1ee2: 0x0004, 0x1ee3: 0x0004, + 0x1ee4: 0x0004, 0x1ee5: 0x0004, 0x1ee6: 0x0004, 0x1ee7: 0x0004, 0x1ee8: 0x0004, 0x1ee9: 0x0004, + 0x1eea: 0x0004, 0x1eeb: 0x0004, 0x1eec: 0x0004, 0x1eed: 0x0004, 0x1eee: 0x0004, 0x1eef: 0x0004, + // Block 0x7c, offset 0x1f00 + 0x1f3f: 0x0002, + // Block 0x7d, offset 0x1f40 + 0x1f70: 0x0002, 0x1f71: 0x0002, 0x1f72: 0x0002, 0x1f73: 0x0002, 0x1f74: 0x0002, 0x1f75: 0x0002, + 0x1f76: 0x0002, 0x1f77: 0x0002, 0x1f78: 0x0002, 0x1f79: 0x0002, 0x1f7a: 0x0002, 0x1f7b: 0x0002, + // Block 0x7e, offset 0x1f80 + 0x1fbd: 0x0004, + // Block 0x7f, offset 0x1fc0 + 0x1fe0: 0x0004, + // Block 0x80, offset 0x2000 + 0x2036: 0x0004, 0x2037: 0x0004, 0x2038: 0x0004, 0x2039: 0x0004, 0x203a: 0x0004, + // Block 0x81, offset 0x2040 + 0x2041: 0x0004, 0x2042: 0x0004, 0x2043: 0x0004, 0x2045: 0x0004, + 0x2046: 0x0004, + 0x204c: 0x0004, 0x204d: 0x0004, 0x204e: 0x0004, 0x204f: 0x0004, + 0x2078: 0x0004, 0x2079: 0x0004, 0x207a: 0x0004, + 0x207f: 0x0004, + // Block 0x82, offset 0x2080 + 0x20a5: 0x0004, 0x20a6: 0x0004, + // Block 0x83, offset 0x20c0 + 0x20e4: 0x0004, 0x20e5: 0x0004, 0x20e6: 0x0004, 0x20e7: 0x0004, + // Block 0x84, offset 0x2100 + 0x212b: 0x0004, 0x212c: 0x0004, + // Block 0x85, offset 0x2140 + 0x217d: 0x0004, 0x217e: 0x0004, 0x217f: 0x0004, + // Block 0x86, offset 0x2180 + 0x2186: 0x0004, 0x2187: 0x0004, 0x2188: 0x0004, 0x2189: 0x0004, 0x218a: 0x0004, 0x218b: 0x0004, + 0x218c: 0x0004, 0x218d: 0x0004, 0x218e: 0x0004, 0x218f: 0x0004, 0x2190: 0x0004, + // Block 0x87, offset 0x21c0 + 0x21c2: 0x0004, 0x21c3: 0x0004, 0x21c4: 0x0004, 0x21c5: 0x0004, + // Block 0x88, offset 0x2200 + 0x2200: 0x0400, 0x2201: 0x0004, 0x2202: 0x0400, + 0x2238: 0x0004, 0x2239: 0x0004, 0x223a: 0x0004, 0x223b: 0x0004, + 0x223c: 0x0004, 0x223d: 0x0004, 0x223e: 0x0004, 0x223f: 0x0004, + // Block 0x89, offset 0x2240 + 0x2240: 0x0004, 0x2241: 0x0004, 0x2242: 0x0004, 0x2243: 0x0004, 0x2244: 0x0004, 0x2245: 0x0004, + 0x2246: 0x0004, + 0x2270: 0x0004, 0x2273: 0x0004, 0x2274: 0x0004, + 0x227f: 0x0004, + // Block 0x8a, offset 0x2280 + 0x2280: 0x0004, 0x2281: 0x0004, 0x2282: 0x0400, + 0x22b0: 0x0400, 0x22b1: 0x0400, 0x22b2: 0x0400, 0x22b3: 0x0004, 0x22b4: 0x0004, 0x22b5: 0x0004, + 0x22b6: 0x0004, 0x22b7: 0x0400, 0x22b8: 0x0400, 0x22b9: 0x0004, 0x22ba: 0x0004, + 0x22bd: 0x0100, + // Block 0x8b, offset 0x22c0 + 0x22c2: 0x0004, + 0x22cd: 0x0100, + // Block 0x8c, offset 0x2300 + 0x2300: 0x0004, 0x2301: 0x0004, 0x2302: 0x0004, + 0x2327: 0x0004, 0x2328: 0x0004, 0x2329: 0x0004, + 0x232a: 0x0004, 0x232b: 0x0004, 0x232c: 0x0400, 0x232d: 0x0004, 0x232e: 0x0004, 0x232f: 0x0004, + 0x2330: 0x0004, 0x2331: 0x0004, 0x2332: 0x0004, 0x2333: 0x0004, 0x2334: 0x0004, + // Block 0x8d, offset 0x2340 + 0x2345: 0x0400, + 0x2346: 0x0400, + 0x2373: 0x0004, + // Block 0x8e, offset 0x2380 + 0x2380: 0x0004, 0x2381: 0x0004, 0x2382: 0x0400, + 0x23b3: 0x0400, 0x23b4: 0x0400, 0x23b5: 0x0400, + 0x23b6: 0x0004, 0x23b7: 0x0004, 0x23b8: 0x0004, 0x23b9: 0x0004, 0x23ba: 0x0004, 0x23bb: 0x0004, + 0x23bc: 0x0004, 0x23bd: 0x0004, 0x23be: 0x0004, 0x23bf: 0x0400, + // Block 0x8f, offset 0x23c0 + 0x23c0: 0x0400, 0x23c2: 0x0100, 0x23c3: 0x0100, + 0x23c9: 0x0004, 0x23ca: 0x0004, 0x23cb: 0x0004, + 0x23cc: 0x0004, 0x23ce: 0x0400, 0x23cf: 0x0004, + // Block 0x90, offset 0x2400 + 0x242c: 0x0400, 0x242d: 0x0400, 0x242e: 0x0400, 0x242f: 0x0004, + 0x2430: 0x0004, 0x2431: 0x0004, 0x2432: 0x0400, 0x2433: 0x0400, 0x2434: 0x0004, 0x2435: 0x0400, + 0x2436: 0x0004, 0x2437: 0x0004, + 0x243e: 0x0004, + // Block 0x91, offset 0x2440 + 0x2441: 0x0004, + // Block 0x92, offset 0x2480 + 0x249f: 0x0004, 0x24a0: 0x0400, 0x24a1: 0x0400, 0x24a2: 0x0400, 0x24a3: 0x0004, + 0x24a4: 0x0004, 0x24a5: 0x0004, 0x24a6: 0x0004, 0x24a7: 0x0004, 0x24a8: 0x0004, 0x24a9: 0x0004, + 0x24aa: 0x0004, + // Block 0x93, offset 0x24c0 + 0x24c0: 0x0004, 0x24c1: 0x0400, 0x24c2: 0x0400, 0x24c3: 0x0400, 0x24c4: 0x0400, + 0x24c7: 0x0400, 0x24c8: 0x0400, 0x24cb: 0x0400, + 0x24cc: 0x0400, 0x24cd: 0x0400, + 0x24d7: 0x0004, + 0x24e2: 0x0400, 0x24e3: 0x0400, + 0x24e6: 0x0004, 0x24e7: 0x0004, 0x24e8: 0x0004, 0x24e9: 0x0004, + 0x24ea: 0x0004, 0x24eb: 0x0004, 0x24ec: 0x0004, + 0x24f0: 0x0004, 0x24f1: 0x0004, 0x24f2: 0x0004, 0x24f3: 0x0004, 0x24f4: 0x0004, + // Block 0x94, offset 0x2500 + 0x2535: 0x0400, + 0x2536: 0x0400, 0x2537: 0x0400, 0x2538: 0x0004, 0x2539: 0x0004, 0x253a: 0x0004, 0x253b: 0x0004, + 0x253c: 0x0004, 0x253d: 0x0004, 0x253e: 0x0004, 0x253f: 0x0004, + // Block 0x95, offset 0x2540 + 0x2540: 0x0400, 0x2541: 0x0400, 0x2542: 0x0004, 0x2543: 0x0004, 0x2544: 0x0004, 0x2545: 0x0400, + 0x2546: 0x0004, + 0x255e: 0x0004, + // Block 0x96, offset 0x2580 + 0x25b0: 0x0004, 0x25b1: 0x0400, 0x25b2: 0x0400, 0x25b3: 0x0004, 0x25b4: 0x0004, 0x25b5: 0x0004, + 0x25b6: 0x0004, 0x25b7: 0x0004, 0x25b8: 0x0004, 0x25b9: 0x0400, 0x25ba: 0x0004, 0x25bb: 0x0400, + 0x25bc: 0x0400, 0x25bd: 0x0004, 0x25be: 0x0400, 0x25bf: 0x0004, + // Block 0x97, offset 0x25c0 + 0x25c0: 0x0004, 0x25c1: 0x0400, 0x25c2: 0x0004, 0x25c3: 0x0004, + // Block 0x98, offset 0x2600 + 0x262f: 0x0004, + 0x2630: 0x0400, 0x2631: 0x0400, 0x2632: 0x0004, 0x2633: 0x0004, 0x2634: 0x0004, 0x2635: 0x0004, + 0x2638: 0x0400, 0x2639: 0x0400, 0x263a: 0x0400, 0x263b: 0x0400, + 0x263c: 0x0004, 0x263d: 0x0004, 0x263e: 0x0400, 0x263f: 0x0004, + // Block 0x99, offset 0x2640 + 0x2640: 0x0004, + 0x265c: 0x0004, 0x265d: 0x0004, + // Block 0x9a, offset 0x2680 + 0x26b0: 0x0400, 0x26b1: 0x0400, 0x26b2: 0x0400, 0x26b3: 0x0004, 0x26b4: 0x0004, 0x26b5: 0x0004, + 0x26b6: 0x0004, 0x26b7: 0x0004, 0x26b8: 0x0004, 0x26b9: 0x0004, 0x26ba: 0x0004, 0x26bb: 0x0400, + 0x26bc: 0x0400, 0x26bd: 0x0004, 0x26be: 0x0400, 0x26bf: 0x0004, + // Block 0x9b, offset 0x26c0 + 0x26c0: 0x0004, + // Block 0x9c, offset 0x2700 + 0x272b: 0x0004, 0x272c: 0x0400, 0x272d: 0x0004, 0x272e: 0x0400, 0x272f: 0x0400, + 0x2730: 0x0004, 0x2731: 0x0004, 0x2732: 0x0004, 0x2733: 0x0004, 0x2734: 0x0004, 0x2735: 0x0004, + 0x2736: 0x0400, 0x2737: 0x0004, + // Block 0x9d, offset 0x2740 + 0x275d: 0x0004, + 0x275e: 0x0004, 0x275f: 0x0004, 0x2762: 0x0004, 0x2763: 0x0004, + 0x2764: 0x0004, 0x2765: 0x0004, 0x2766: 0x0400, 0x2767: 0x0004, 0x2768: 0x0004, 0x2769: 0x0004, + 0x276a: 0x0004, 0x276b: 0x0004, + // Block 0x9e, offset 0x2780 + 0x27ac: 0x0400, 0x27ad: 0x0400, 0x27ae: 0x0400, 0x27af: 0x0004, + 0x27b0: 0x0004, 0x27b1: 0x0004, 0x27b2: 0x0004, 0x27b3: 0x0004, 0x27b4: 0x0004, 0x27b5: 0x0004, + 0x27b6: 0x0004, 0x27b7: 0x0004, 0x27b8: 0x0400, 0x27b9: 0x0004, 0x27ba: 0x0004, + // Block 0x9f, offset 0x27c0 + 0x27f0: 0x0004, 0x27f1: 0x0400, 0x27f2: 0x0400, 0x27f3: 0x0400, 0x27f4: 0x0400, 0x27f5: 0x0400, + 0x27f7: 0x0400, 0x27f8: 0x0400, 0x27fb: 0x0004, + 0x27fc: 0x0004, 0x27fd: 0x0400, 0x27fe: 0x0004, 0x27ff: 0x0100, + // Block 0xa0, offset 0x2800 + 0x2800: 0x0400, 0x2801: 0x0100, 0x2802: 0x0400, 0x2803: 0x0004, + // Block 0xa1, offset 0x2840 + 0x2851: 0x0400, + 0x2852: 0x0400, 0x2853: 0x0400, 0x2854: 0x0004, 0x2855: 0x0004, 0x2856: 0x0004, 0x2857: 0x0004, + 0x285a: 0x0004, 0x285b: 0x0004, 0x285c: 0x0400, 0x285d: 0x0400, + 0x285e: 0x0400, 0x285f: 0x0400, 0x2860: 0x0004, + 0x2864: 0x0400, + // Block 0xa2, offset 0x2880 + 0x2881: 0x0004, 0x2882: 0x0004, 0x2883: 0x0004, 0x2884: 0x0004, 0x2885: 0x0004, + 0x2886: 0x0004, 0x2887: 0x0004, 0x2888: 0x0004, 0x2889: 0x0004, 0x288a: 0x0004, + 0x28b3: 0x0004, 0x28b4: 0x0004, 0x28b5: 0x0004, + 0x28b6: 0x0004, 0x28b7: 0x0004, 0x28b8: 0x0004, 0x28b9: 0x0400, 0x28ba: 0x0100, 0x28bb: 0x0004, + 0x28bc: 0x0004, 0x28bd: 0x0004, 0x28be: 0x0004, + // Block 0xa3, offset 0x28c0 + 0x28c7: 0x0004, + 0x28d1: 0x0004, + 0x28d2: 0x0004, 0x28d3: 0x0004, 0x28d4: 0x0004, 0x28d5: 0x0004, 0x28d6: 0x0004, 0x28d7: 0x0400, + 0x28d8: 0x0400, 0x28d9: 0x0004, 0x28da: 0x0004, 0x28db: 0x0004, + // Block 0xa4, offset 0x2900 + 0x2904: 0x0100, 0x2905: 0x0100, + 0x2906: 0x0100, 0x2907: 0x0100, 0x2908: 0x0100, 0x2909: 0x0100, 0x290a: 0x0004, 0x290b: 0x0004, + 0x290c: 0x0004, 0x290d: 0x0004, 0x290e: 0x0004, 0x290f: 0x0004, 0x2910: 0x0004, 0x2911: 0x0004, + 0x2912: 0x0004, 0x2913: 0x0004, 0x2914: 0x0004, 0x2915: 0x0004, 0x2916: 0x0004, 0x2917: 0x0400, + 0x2918: 0x0004, 0x2919: 0x0004, + // Block 0xa5, offset 0x2940 + 0x296f: 0x0400, + 0x2970: 0x0004, 0x2971: 0x0004, 0x2972: 0x0004, 0x2973: 0x0004, 0x2974: 0x0004, 0x2975: 0x0004, + 0x2976: 0x0004, 0x2978: 0x0004, 0x2979: 0x0004, 0x297a: 0x0004, 0x297b: 0x0004, + 0x297c: 0x0004, 0x297d: 0x0004, 0x297e: 0x0400, 0x297f: 0x0004, + // Block 0xa6, offset 0x2980 + 0x2992: 0x0004, 0x2993: 0x0004, 0x2994: 0x0004, 0x2995: 0x0004, 0x2996: 0x0004, 0x2997: 0x0004, + 0x2998: 0x0004, 0x2999: 0x0004, 0x299a: 0x0004, 0x299b: 0x0004, 0x299c: 0x0004, 0x299d: 0x0004, + 0x299e: 0x0004, 0x299f: 0x0004, 0x29a0: 0x0004, 0x29a1: 0x0004, 0x29a2: 0x0004, 0x29a3: 0x0004, + 0x29a4: 0x0004, 0x29a5: 0x0004, 0x29a6: 0x0004, 0x29a7: 0x0004, 0x29a9: 0x0400, + 0x29aa: 0x0004, 0x29ab: 0x0004, 0x29ac: 0x0004, 0x29ad: 0x0004, 0x29ae: 0x0004, 0x29af: 0x0004, + 0x29b0: 0x0004, 0x29b1: 0x0400, 0x29b2: 0x0004, 0x29b3: 0x0004, 0x29b4: 0x0400, 0x29b5: 0x0004, + 0x29b6: 0x0004, + // Block 0xa7, offset 0x29c0 + 0x29f1: 0x0004, 0x29f2: 0x0004, 0x29f3: 0x0004, 0x29f4: 0x0004, 0x29f5: 0x0004, + 0x29f6: 0x0004, 0x29fa: 0x0004, + 0x29fc: 0x0004, 0x29fd: 0x0004, 0x29ff: 0x0004, + // Block 0xa8, offset 0x2a00 + 0x2a00: 0x0004, 0x2a01: 0x0004, 0x2a02: 0x0004, 0x2a03: 0x0004, 0x2a04: 0x0004, 0x2a05: 0x0004, + 0x2a06: 0x0100, 0x2a07: 0x0004, + // Block 0xa9, offset 0x2a40 + 0x2a4a: 0x0400, 0x2a4b: 0x0400, + 0x2a4c: 0x0400, 0x2a4d: 0x0400, 0x2a4e: 0x0400, 0x2a50: 0x0004, 0x2a51: 0x0004, + 0x2a53: 0x0400, 0x2a54: 0x0400, 0x2a55: 0x0004, 0x2a56: 0x0400, 0x2a57: 0x0004, + // Block 0xaa, offset 0x2a80 + 0x2ab3: 0x0004, 0x2ab4: 0x0004, 0x2ab5: 0x0400, + 0x2ab6: 0x0400, + // Block 0xab, offset 0x2ac0 + 0x2ac0: 0x0004, 0x2ac1: 0x0004, 0x2ac2: 0x0100, 0x2ac3: 0x0400, + 0x2af4: 0x0400, 0x2af5: 0x0400, + 0x2af6: 0x0004, 0x2af7: 0x0004, 0x2af8: 0x0004, 0x2af9: 0x0004, 0x2afa: 0x0004, + 0x2afe: 0x0400, 0x2aff: 0x0400, + // Block 0xac, offset 0x2b00 + 0x2b00: 0x0004, 0x2b01: 0x0400, 0x2b02: 0x0004, + // Block 0xad, offset 0x2b40 + 0x2b70: 0x0002, 0x2b71: 0x0002, 0x2b72: 0x0002, 0x2b73: 0x0002, 0x2b74: 0x0002, 0x2b75: 0x0002, + 0x2b76: 0x0002, 0x2b77: 0x0002, 0x2b78: 0x0002, 0x2b79: 0x0002, 0x2b7a: 0x0002, 0x2b7b: 0x0002, + 0x2b7c: 0x0002, 0x2b7d: 0x0002, 0x2b7e: 0x0002, 0x2b7f: 0x0002, + // Block 0xae, offset 0x2b80 + 0x2b80: 0x0004, + 0x2b87: 0x0004, 0x2b88: 0x0004, 0x2b89: 0x0004, 0x2b8a: 0x0004, 0x2b8b: 0x0004, + 0x2b8c: 0x0004, 0x2b8d: 0x0004, 0x2b8e: 0x0004, 0x2b8f: 0x0004, 0x2b90: 0x0004, 0x2b91: 0x0004, + 0x2b92: 0x0004, 0x2b93: 0x0004, 0x2b94: 0x0004, 0x2b95: 0x0004, + // Block 0xaf, offset 0x2bc0 + 0x2bf0: 0x0004, 0x2bf1: 0x0004, 0x2bf2: 0x0004, 0x2bf3: 0x0004, 0x2bf4: 0x0004, + // Block 0xb0, offset 0x2c00 + 0x2c30: 0x0004, 0x2c31: 0x0004, 0x2c32: 0x0004, 0x2c33: 0x0004, 0x2c34: 0x0004, 0x2c35: 0x0004, + 0x2c36: 0x0004, + // Block 0xb1, offset 0x2c40 + 0x2c4f: 0x0004, 0x2c51: 0x0400, + 0x2c52: 0x0400, 0x2c53: 0x0400, 0x2c54: 0x0400, 0x2c55: 0x0400, 0x2c56: 0x0400, 0x2c57: 0x0400, + 0x2c58: 0x0400, 0x2c59: 0x0400, 0x2c5a: 0x0400, 0x2c5b: 0x0400, 0x2c5c: 0x0400, 0x2c5d: 0x0400, + 0x2c5e: 0x0400, 0x2c5f: 0x0400, 0x2c60: 0x0400, 0x2c61: 0x0400, 0x2c62: 0x0400, 0x2c63: 0x0400, + 0x2c64: 0x0400, 0x2c65: 0x0400, 0x2c66: 0x0400, 0x2c67: 0x0400, 0x2c68: 0x0400, 0x2c69: 0x0400, + 0x2c6a: 0x0400, 0x2c6b: 0x0400, 0x2c6c: 0x0400, 0x2c6d: 0x0400, 0x2c6e: 0x0400, 0x2c6f: 0x0400, + 0x2c70: 0x0400, 0x2c71: 0x0400, 0x2c72: 0x0400, 0x2c73: 0x0400, 0x2c74: 0x0400, 0x2c75: 0x0400, + 0x2c76: 0x0400, 0x2c77: 0x0400, 0x2c78: 0x0400, 0x2c79: 0x0400, 0x2c7a: 0x0400, 0x2c7b: 0x0400, + 0x2c7c: 0x0400, 0x2c7d: 0x0400, 0x2c7e: 0x0400, 0x2c7f: 0x0400, + // Block 0xb2, offset 0x2c80 + 0x2c80: 0x0400, 0x2c81: 0x0400, 0x2c82: 0x0400, 0x2c83: 0x0400, 0x2c84: 0x0400, 0x2c85: 0x0400, + 0x2c86: 0x0400, 0x2c87: 0x0400, + 0x2c8f: 0x0004, 0x2c90: 0x0004, 0x2c91: 0x0004, + 0x2c92: 0x0004, + // Block 0xb3, offset 0x2cc0 + 0x2ce4: 0x0004, + 0x2cf0: 0x0400, 0x2cf1: 0x0400, + // Block 0xb4, offset 0x2d00 + 0x2d1d: 0x0004, + 0x2d1e: 0x0004, 0x2d20: 0x0002, 0x2d21: 0x0002, 0x2d22: 0x0002, 0x2d23: 0x0002, + // Block 0xb5, offset 0x2d40 + 0x2d40: 0x0004, 0x2d41: 0x0004, 0x2d42: 0x0004, 0x2d43: 0x0004, 0x2d44: 0x0004, 0x2d45: 0x0004, + 0x2d46: 0x0004, 0x2d47: 0x0004, 0x2d48: 0x0004, 0x2d49: 0x0004, 0x2d4a: 0x0004, 0x2d4b: 0x0004, + 0x2d4c: 0x0004, 0x2d4d: 0x0004, 0x2d4e: 0x0004, 0x2d4f: 0x0004, 0x2d50: 0x0004, 0x2d51: 0x0004, + 0x2d52: 0x0004, 0x2d53: 0x0004, 0x2d54: 0x0004, 0x2d55: 0x0004, 0x2d56: 0x0004, 0x2d57: 0x0004, + 0x2d58: 0x0004, 0x2d59: 0x0004, 0x2d5a: 0x0004, 0x2d5b: 0x0004, 0x2d5c: 0x0004, 0x2d5d: 0x0004, + 0x2d5e: 0x0004, 0x2d5f: 0x0004, 0x2d60: 0x0004, 0x2d61: 0x0004, 0x2d62: 0x0004, 0x2d63: 0x0004, + 0x2d64: 0x0004, 0x2d65: 0x0004, 0x2d66: 0x0004, 0x2d67: 0x0004, 0x2d68: 0x0004, 0x2d69: 0x0004, + 0x2d6a: 0x0004, 0x2d6b: 0x0004, 0x2d6c: 0x0004, 0x2d6d: 0x0004, + 0x2d70: 0x0004, 0x2d71: 0x0004, 0x2d72: 0x0004, 0x2d73: 0x0004, 0x2d74: 0x0004, 0x2d75: 0x0004, + 0x2d76: 0x0004, 0x2d77: 0x0004, 0x2d78: 0x0004, 0x2d79: 0x0004, 0x2d7a: 0x0004, 0x2d7b: 0x0004, + 0x2d7c: 0x0004, 0x2d7d: 0x0004, 0x2d7e: 0x0004, 0x2d7f: 0x0004, + // Block 0xb6, offset 0x2d80 + 0x2d80: 0x0004, 0x2d81: 0x0004, 0x2d82: 0x0004, 0x2d83: 0x0004, 0x2d84: 0x0004, 0x2d85: 0x0004, + 0x2d86: 0x0004, + // Block 0xb7, offset 0x2dc0 + 0x2de5: 0x0004, 0x2de6: 0x0400, 0x2de7: 0x0004, 0x2de8: 0x0004, 0x2de9: 0x0004, + 0x2ded: 0x0400, 0x2dee: 0x0004, 0x2def: 0x0004, + 0x2df0: 0x0004, 0x2df1: 0x0004, 0x2df2: 0x0004, 0x2df3: 0x0002, 0x2df4: 0x0002, 0x2df5: 0x0002, + 0x2df6: 0x0002, 0x2df7: 0x0002, 0x2df8: 0x0002, 0x2df9: 0x0002, 0x2dfa: 0x0002, 0x2dfb: 0x0004, + 0x2dfc: 0x0004, 0x2dfd: 0x0004, 0x2dfe: 0x0004, 0x2dff: 0x0004, + // Block 0xb8, offset 0x2e00 + 0x2e00: 0x0004, 0x2e01: 0x0004, 0x2e02: 0x0004, 0x2e05: 0x0004, + 0x2e06: 0x0004, 0x2e07: 0x0004, 0x2e08: 0x0004, 0x2e09: 0x0004, 0x2e0a: 0x0004, 0x2e0b: 0x0004, + 0x2e2a: 0x0004, 0x2e2b: 0x0004, 0x2e2c: 0x0004, 0x2e2d: 0x0004, + // Block 0xb9, offset 0x2e40 + 0x2e42: 0x0004, 0x2e43: 0x0004, 0x2e44: 0x0004, + // Block 0xba, offset 0x2e80 + 0x2e80: 0x0004, 0x2e81: 0x0004, 0x2e82: 0x0004, 0x2e83: 0x0004, 0x2e84: 0x0004, 0x2e85: 0x0004, + 0x2e86: 0x0004, 0x2e87: 0x0004, 0x2e88: 0x0004, 0x2e89: 0x0004, 0x2e8a: 0x0004, 0x2e8b: 0x0004, + 0x2e8c: 0x0004, 0x2e8d: 0x0004, 0x2e8e: 0x0004, 0x2e8f: 0x0004, 0x2e90: 0x0004, 0x2e91: 0x0004, + 0x2e92: 0x0004, 0x2e93: 0x0004, 0x2e94: 0x0004, 0x2e95: 0x0004, 0x2e96: 0x0004, 0x2e97: 0x0004, + 0x2e98: 0x0004, 0x2e99: 0x0004, 0x2e9a: 0x0004, 0x2e9b: 0x0004, 0x2e9c: 0x0004, 0x2e9d: 0x0004, + 0x2e9e: 0x0004, 0x2e9f: 0x0004, 0x2ea0: 0x0004, 0x2ea1: 0x0004, 0x2ea2: 0x0004, 0x2ea3: 0x0004, + 0x2ea4: 0x0004, 0x2ea5: 0x0004, 0x2ea6: 0x0004, 0x2ea7: 0x0004, 0x2ea8: 0x0004, 0x2ea9: 0x0004, + 0x2eaa: 0x0004, 0x2eab: 0x0004, 0x2eac: 0x0004, 0x2ead: 0x0004, 0x2eae: 0x0004, 0x2eaf: 0x0004, + 0x2eb0: 0x0004, 0x2eb1: 0x0004, 0x2eb2: 0x0004, 0x2eb3: 0x0004, 0x2eb4: 0x0004, 0x2eb5: 0x0004, + 0x2eb6: 0x0004, 0x2ebb: 0x0004, + 0x2ebc: 0x0004, 0x2ebd: 0x0004, 0x2ebe: 0x0004, 0x2ebf: 0x0004, + // Block 0xbb, offset 0x2ec0 + 0x2ec0: 0x0004, 0x2ec1: 0x0004, 0x2ec2: 0x0004, 0x2ec3: 0x0004, 0x2ec4: 0x0004, 0x2ec5: 0x0004, + 0x2ec6: 0x0004, 0x2ec7: 0x0004, 0x2ec8: 0x0004, 0x2ec9: 0x0004, 0x2eca: 0x0004, 0x2ecb: 0x0004, + 0x2ecc: 0x0004, 0x2ecd: 0x0004, 0x2ece: 0x0004, 0x2ecf: 0x0004, 0x2ed0: 0x0004, 0x2ed1: 0x0004, + 0x2ed2: 0x0004, 0x2ed3: 0x0004, 0x2ed4: 0x0004, 0x2ed5: 0x0004, 0x2ed6: 0x0004, 0x2ed7: 0x0004, + 0x2ed8: 0x0004, 0x2ed9: 0x0004, 0x2eda: 0x0004, 0x2edb: 0x0004, 0x2edc: 0x0004, 0x2edd: 0x0004, + 0x2ede: 0x0004, 0x2edf: 0x0004, 0x2ee0: 0x0004, 0x2ee1: 0x0004, 0x2ee2: 0x0004, 0x2ee3: 0x0004, + 0x2ee4: 0x0004, 0x2ee5: 0x0004, 0x2ee6: 0x0004, 0x2ee7: 0x0004, 0x2ee8: 0x0004, 0x2ee9: 0x0004, + 0x2eea: 0x0004, 0x2eeb: 0x0004, 0x2eec: 0x0004, + 0x2ef5: 0x0004, + // Block 0xbc, offset 0x2f00 + 0x2f04: 0x0004, + 0x2f1b: 0x0004, 0x2f1c: 0x0004, 0x2f1d: 0x0004, + 0x2f1e: 0x0004, 0x2f1f: 0x0004, 0x2f21: 0x0004, 0x2f22: 0x0004, 0x2f23: 0x0004, + 0x2f24: 0x0004, 0x2f25: 0x0004, 0x2f26: 0x0004, 0x2f27: 0x0004, 0x2f28: 0x0004, 0x2f29: 0x0004, + 0x2f2a: 0x0004, 0x2f2b: 0x0004, 0x2f2c: 0x0004, 0x2f2d: 0x0004, 0x2f2e: 0x0004, 0x2f2f: 0x0004, + // Block 0xbd, offset 0x2f40 + 0x2f40: 0x0004, 0x2f41: 0x0004, 0x2f42: 0x0004, 0x2f43: 0x0004, 0x2f44: 0x0004, 0x2f45: 0x0004, + 0x2f46: 0x0004, 0x2f48: 0x0004, 0x2f49: 0x0004, 0x2f4a: 0x0004, 0x2f4b: 0x0004, + 0x2f4c: 0x0004, 0x2f4d: 0x0004, 0x2f4e: 0x0004, 0x2f4f: 0x0004, 0x2f50: 0x0004, 0x2f51: 0x0004, + 0x2f52: 0x0004, 0x2f53: 0x0004, 0x2f54: 0x0004, 0x2f55: 0x0004, 0x2f56: 0x0004, 0x2f57: 0x0004, + 0x2f58: 0x0004, 0x2f5b: 0x0004, 0x2f5c: 0x0004, 0x2f5d: 0x0004, + 0x2f5e: 0x0004, 0x2f5f: 0x0004, 0x2f60: 0x0004, 0x2f61: 0x0004, 0x2f63: 0x0004, + 0x2f64: 0x0004, 0x2f66: 0x0004, 0x2f67: 0x0004, 0x2f68: 0x0004, 0x2f69: 0x0004, + 0x2f6a: 0x0004, + // Block 0xbe, offset 0x2f80 + 0x2f8f: 0x0004, + // Block 0xbf, offset 0x2fc0 + 0x2fee: 0x0004, + // Block 0xc0, offset 0x3000 + 0x302c: 0x0004, 0x302d: 0x0004, 0x302e: 0x0004, 0x302f: 0x0004, + // Block 0xc1, offset 0x3040 + 0x3050: 0x0004, 0x3051: 0x0004, + 0x3052: 0x0004, 0x3053: 0x0004, 0x3054: 0x0004, 0x3055: 0x0004, 0x3056: 0x0004, + // Block 0xc2, offset 0x3080 + 0x3084: 0x0004, 0x3085: 0x0004, + 0x3086: 0x0004, 0x3087: 0x0004, 0x3088: 0x0004, 0x3089: 0x0004, 0x308a: 0x0004, + // Block 0xc3, offset 0x30c0 + 0x30cd: 0x0008, 0x30ce: 0x0008, 0x30cf: 0x0008, + 0x30ef: 0x0008, + // Block 0xc4, offset 0x3100 + 0x312c: 0x0008, 0x312d: 0x0008, 0x312e: 0x0008, 0x312f: 0x0008, + 0x3130: 0x0008, 0x3131: 0x0008, + 0x313e: 0x0008, 0x313f: 0x0008, + // Block 0xc5, offset 0x3140 + 0x314e: 0x0008, 0x3151: 0x0008, + 0x3152: 0x0008, 0x3153: 0x0008, 0x3154: 0x0008, 0x3155: 0x0008, 0x3156: 0x0008, 0x3157: 0x0008, + 0x3158: 0x0008, 0x3159: 0x0008, 0x315a: 0x0008, + 0x316d: 0x0008, 0x316e: 0x0008, 0x316f: 0x0008, + 0x3170: 0x0008, 0x3171: 0x0008, 0x3172: 0x0008, 0x3173: 0x0008, 0x3174: 0x0008, 0x3175: 0x0008, + 0x3176: 0x0008, 0x3177: 0x0008, 0x3178: 0x0008, 0x3179: 0x0008, 0x317a: 0x0008, 0x317b: 0x0008, + 0x317c: 0x0008, 0x317d: 0x0008, 0x317e: 0x0008, 0x317f: 0x0008, + // Block 0xc6, offset 0x3180 + 0x3180: 0x0008, 0x3181: 0x0008, 0x3182: 0x0008, 0x3183: 0x0008, 0x3184: 0x0008, 0x3185: 0x0008, + 0x3186: 0x0008, 0x3187: 0x0008, 0x3188: 0x0008, 0x3189: 0x0008, 0x318a: 0x0008, 0x318b: 0x0008, + 0x318c: 0x0008, 0x318d: 0x0008, 0x318e: 0x0008, 0x318f: 0x0008, 0x3190: 0x0008, 0x3191: 0x0008, + 0x3192: 0x0008, 0x3193: 0x0008, 0x3194: 0x0008, 0x3195: 0x0008, 0x3196: 0x0008, 0x3197: 0x0008, + 0x3198: 0x0008, 0x3199: 0x0008, 0x319a: 0x0008, 0x319b: 0x0008, 0x319c: 0x0008, 0x319d: 0x0008, + 0x319e: 0x0008, 0x319f: 0x0008, 0x31a0: 0x0008, 0x31a1: 0x0008, 0x31a2: 0x0008, 0x31a3: 0x0008, + 0x31a4: 0x0008, 0x31a5: 0x0008, 0x31a6: 0x0200, 0x31a7: 0x0200, 0x31a8: 0x0200, 0x31a9: 0x0200, + 0x31aa: 0x0200, 0x31ab: 0x0200, 0x31ac: 0x0200, 0x31ad: 0x0200, 0x31ae: 0x0200, 0x31af: 0x0200, + 0x31b0: 0x0200, 0x31b1: 0x0200, 0x31b2: 0x0200, 0x31b3: 0x0200, 0x31b4: 0x0200, 0x31b5: 0x0200, + 0x31b6: 0x0200, 0x31b7: 0x0200, 0x31b8: 0x0200, 0x31b9: 0x0200, 0x31ba: 0x0200, 0x31bb: 0x0200, + 0x31bc: 0x0200, 0x31bd: 0x0200, 0x31be: 0x0200, 0x31bf: 0x0200, + // Block 0xc7, offset 0x31c0 + 0x31c1: 0x0008, 0x31c2: 0x0008, 0x31c3: 0x0008, 0x31c4: 0x0008, 0x31c5: 0x0008, + 0x31c6: 0x0008, 0x31c7: 0x0008, 0x31c8: 0x0008, 0x31c9: 0x0008, 0x31ca: 0x0008, 0x31cb: 0x0008, + 0x31cc: 0x0008, 0x31cd: 0x0008, 0x31ce: 0x0008, 0x31cf: 0x0008, + 0x31da: 0x0008, + 0x31ef: 0x0008, + 0x31f2: 0x0008, 0x31f3: 0x0008, 0x31f4: 0x0008, 0x31f5: 0x0008, + 0x31f6: 0x0008, 0x31f7: 0x0008, 0x31f8: 0x0008, 0x31f9: 0x0008, 0x31fa: 0x0008, + 0x31fc: 0x0008, 0x31fd: 0x0008, 0x31fe: 0x0008, 0x31ff: 0x0008, + // Block 0xc8, offset 0x3200 + 0x3209: 0x0008, 0x320a: 0x0008, 0x320b: 0x0008, + 0x320c: 0x0008, 0x320d: 0x0008, 0x320e: 0x0008, 0x320f: 0x0008, 0x3210: 0x0008, 0x3211: 0x0008, + 0x3212: 0x0008, 0x3213: 0x0008, 0x3214: 0x0008, 0x3215: 0x0008, 0x3216: 0x0008, 0x3217: 0x0008, + 0x3218: 0x0008, 0x3219: 0x0008, 0x321a: 0x0008, 0x321b: 0x0008, 0x321c: 0x0008, 0x321d: 0x0008, + 0x321e: 0x0008, 0x321f: 0x0008, 0x3220: 0x0008, 0x3221: 0x0008, 0x3222: 0x0008, 0x3223: 0x0008, + 0x3224: 0x0008, 0x3225: 0x0008, 0x3226: 0x0008, 0x3227: 0x0008, 0x3228: 0x0008, 0x3229: 0x0008, + 0x322a: 0x0008, 0x322b: 0x0008, 0x322c: 0x0008, 0x322d: 0x0008, 0x322e: 0x0008, 0x322f: 0x0008, + 0x3230: 0x0008, 0x3231: 0x0008, 0x3232: 0x0008, 0x3233: 0x0008, 0x3234: 0x0008, 0x3235: 0x0008, + 0x3236: 0x0008, 0x3237: 0x0008, 0x3238: 0x0008, 0x3239: 0x0008, 0x323a: 0x0008, 0x323b: 0x0008, + 0x323c: 0x0008, 0x323d: 0x0008, 0x323e: 0x0008, 0x323f: 0x0008, + // Block 0xc9, offset 0x3240 + 0x3240: 0x0008, 0x3241: 0x0008, 0x3242: 0x0008, 0x3243: 0x0008, 0x3244: 0x0008, 0x3245: 0x0008, + 0x3246: 0x0008, 0x3247: 0x0008, 0x3248: 0x0008, 0x3249: 0x0008, 0x324a: 0x0008, 0x324b: 0x0008, + 0x324c: 0x0008, 0x324d: 0x0008, 0x324e: 0x0008, 0x324f: 0x0008, 0x3250: 0x0008, 0x3251: 0x0008, + 0x3252: 0x0008, 0x3253: 0x0008, 0x3254: 0x0008, 0x3255: 0x0008, 0x3256: 0x0008, 0x3257: 0x0008, + 0x3258: 0x0008, 0x3259: 0x0008, 0x325a: 0x0008, 0x325b: 0x0008, 0x325c: 0x0008, 0x325d: 0x0008, + 0x325e: 0x0008, 0x325f: 0x0008, 0x3260: 0x0008, 0x3261: 0x0008, 0x3262: 0x0008, 0x3263: 0x0008, + 0x3264: 0x0008, 0x3265: 0x0008, 0x3266: 0x0008, 0x3267: 0x0008, 0x3268: 0x0008, 0x3269: 0x0008, + 0x326a: 0x0008, 0x326b: 0x0008, 0x326c: 0x0008, 0x326d: 0x0008, 0x326e: 0x0008, 0x326f: 0x0008, + 0x3270: 0x0008, 0x3271: 0x0008, 0x3272: 0x0008, 0x3273: 0x0008, 0x3274: 0x0008, 0x3275: 0x0008, + 0x3276: 0x0008, 0x3277: 0x0008, 0x3278: 0x0008, 0x3279: 0x0008, 0x327a: 0x0008, 0x327b: 0x0004, + 0x327c: 0x0004, 0x327d: 0x0004, 0x327e: 0x0004, 0x327f: 0x0004, + // Block 0xca, offset 0x3280 + 0x3280: 0x0008, 0x3281: 0x0008, 0x3282: 0x0008, 0x3283: 0x0008, 0x3284: 0x0008, 0x3285: 0x0008, + 0x3286: 0x0008, 0x3287: 0x0008, 0x3288: 0x0008, 0x3289: 0x0008, 0x328a: 0x0008, 0x328b: 0x0008, + 0x328c: 0x0008, 0x328d: 0x0008, 0x328e: 0x0008, 0x328f: 0x0008, 0x3290: 0x0008, 0x3291: 0x0008, + 0x3292: 0x0008, 0x3293: 0x0008, 0x3294: 0x0008, 0x3295: 0x0008, 0x3296: 0x0008, 0x3297: 0x0008, + 0x3298: 0x0008, 0x3299: 0x0008, 0x329a: 0x0008, 0x329b: 0x0008, 0x329c: 0x0008, 0x329d: 0x0008, + 0x329e: 0x0008, 0x329f: 0x0008, 0x32a0: 0x0008, 0x32a1: 0x0008, 0x32a2: 0x0008, 0x32a3: 0x0008, + 0x32a4: 0x0008, 0x32a5: 0x0008, 0x32a6: 0x0008, 0x32a7: 0x0008, 0x32a8: 0x0008, 0x32a9: 0x0008, + 0x32aa: 0x0008, 0x32ab: 0x0008, 0x32ac: 0x0008, 0x32ad: 0x0008, 0x32ae: 0x0008, 0x32af: 0x0008, + 0x32b0: 0x0008, 0x32b1: 0x0008, 0x32b2: 0x0008, 0x32b3: 0x0008, 0x32b4: 0x0008, 0x32b5: 0x0008, + 0x32b6: 0x0008, 0x32b7: 0x0008, 0x32b8: 0x0008, 0x32b9: 0x0008, 0x32ba: 0x0008, 0x32bb: 0x0008, + 0x32bc: 0x0008, 0x32bd: 0x0008, + // Block 0xcb, offset 0x32c0 + 0x32c6: 0x0008, 0x32c7: 0x0008, 0x32c8: 0x0008, 0x32c9: 0x0008, 0x32ca: 0x0008, 0x32cb: 0x0008, + 0x32cc: 0x0008, 0x32cd: 0x0008, 0x32ce: 0x0008, 0x32cf: 0x0008, 0x32d0: 0x0008, 0x32d1: 0x0008, + 0x32d2: 0x0008, 0x32d3: 0x0008, 0x32d4: 0x0008, 0x32d5: 0x0008, 0x32d6: 0x0008, 0x32d7: 0x0008, + 0x32d8: 0x0008, 0x32d9: 0x0008, 0x32da: 0x0008, 0x32db: 0x0008, 0x32dc: 0x0008, 0x32dd: 0x0008, + 0x32de: 0x0008, 0x32df: 0x0008, 0x32e0: 0x0008, 0x32e1: 0x0008, 0x32e2: 0x0008, 0x32e3: 0x0008, + 0x32e4: 0x0008, 0x32e5: 0x0008, 0x32e6: 0x0008, 0x32e7: 0x0008, 0x32e8: 0x0008, 0x32e9: 0x0008, + 0x32ea: 0x0008, 0x32eb: 0x0008, 0x32ec: 0x0008, 0x32ed: 0x0008, 0x32ee: 0x0008, 0x32ef: 0x0008, + 0x32f0: 0x0008, 0x32f1: 0x0008, 0x32f2: 0x0008, 0x32f3: 0x0008, 0x32f4: 0x0008, 0x32f5: 0x0008, + 0x32f6: 0x0008, 0x32f7: 0x0008, 0x32f8: 0x0008, 0x32f9: 0x0008, 0x32fa: 0x0008, 0x32fb: 0x0008, + 0x32fc: 0x0008, 0x32fd: 0x0008, 0x32fe: 0x0008, 0x32ff: 0x0008, + // Block 0xcc, offset 0x3300 + 0x3300: 0x0008, 0x3301: 0x0008, 0x3302: 0x0008, 0x3303: 0x0008, 0x3304: 0x0008, 0x3305: 0x0008, + 0x3306: 0x0008, 0x3307: 0x0008, 0x3308: 0x0008, 0x3309: 0x0008, 0x330a: 0x0008, 0x330b: 0x0008, + 0x330c: 0x0008, 0x330d: 0x0008, 0x330e: 0x0008, 0x330f: 0x0008, + // Block 0xcd, offset 0x3340 + 0x3374: 0x0008, 0x3375: 0x0008, + 0x3376: 0x0008, 0x3377: 0x0008, 0x3378: 0x0008, 0x3379: 0x0008, 0x337a: 0x0008, 0x337b: 0x0008, + 0x337c: 0x0008, 0x337d: 0x0008, 0x337e: 0x0008, 0x337f: 0x0008, + // Block 0xce, offset 0x3380 + 0x3395: 0x0008, 0x3396: 0x0008, 0x3397: 0x0008, + 0x3398: 0x0008, 0x3399: 0x0008, 0x339a: 0x0008, 0x339b: 0x0008, 0x339c: 0x0008, 0x339d: 0x0008, + 0x339e: 0x0008, 0x339f: 0x0008, 0x33a0: 0x0008, 0x33a1: 0x0008, 0x33a2: 0x0008, 0x33a3: 0x0008, + 0x33a4: 0x0008, 0x33a5: 0x0008, 0x33a6: 0x0008, 0x33a7: 0x0008, 0x33a8: 0x0008, 0x33a9: 0x0008, + 0x33aa: 0x0008, 0x33ab: 0x0008, 0x33ac: 0x0008, 0x33ad: 0x0008, 0x33ae: 0x0008, 0x33af: 0x0008, + 0x33b0: 0x0008, 0x33b1: 0x0008, 0x33b2: 0x0008, 0x33b3: 0x0008, 0x33b4: 0x0008, 0x33b5: 0x0008, + 0x33b6: 0x0008, 0x33b7: 0x0008, 0x33b8: 0x0008, 0x33b9: 0x0008, 0x33ba: 0x0008, 0x33bb: 0x0008, + 0x33bc: 0x0008, 0x33bd: 0x0008, 0x33be: 0x0008, 0x33bf: 0x0008, + // Block 0xcf, offset 0x33c0 + 0x33cc: 0x0008, 0x33cd: 0x0008, 0x33ce: 0x0008, 0x33cf: 0x0008, + // Block 0xd0, offset 0x3400 + 0x3408: 0x0008, 0x3409: 0x0008, 0x340a: 0x0008, 0x340b: 0x0008, + 0x340c: 0x0008, 0x340d: 0x0008, 0x340e: 0x0008, 0x340f: 0x0008, + 0x341a: 0x0008, 0x341b: 0x0008, 0x341c: 0x0008, 0x341d: 0x0008, + 0x341e: 0x0008, 0x341f: 0x0008, + // Block 0xd1, offset 0x3440 + 0x3448: 0x0008, 0x3449: 0x0008, 0x344a: 0x0008, 0x344b: 0x0008, + 0x344c: 0x0008, 0x344d: 0x0008, 0x344e: 0x0008, 0x344f: 0x0008, + 0x346e: 0x0008, 0x346f: 0x0008, + 0x3470: 0x0008, 0x3471: 0x0008, 0x3472: 0x0008, 0x3473: 0x0008, 0x3474: 0x0008, 0x3475: 0x0008, + 0x3476: 0x0008, 0x3477: 0x0008, 0x3478: 0x0008, 0x3479: 0x0008, 0x347a: 0x0008, 0x347b: 0x0008, + 0x347c: 0x0008, 0x347d: 0x0008, 0x347e: 0x0008, 0x347f: 0x0008, + // Block 0xd2, offset 0x3480 + 0x348c: 0x0008, 0x348d: 0x0008, 0x348e: 0x0008, 0x348f: 0x0008, 0x3490: 0x0008, 0x3491: 0x0008, + 0x3492: 0x0008, 0x3493: 0x0008, 0x3494: 0x0008, 0x3495: 0x0008, 0x3496: 0x0008, 0x3497: 0x0008, + 0x3498: 0x0008, 0x3499: 0x0008, 0x349a: 0x0008, 0x349b: 0x0008, 0x349c: 0x0008, 0x349d: 0x0008, + 0x349e: 0x0008, 0x349f: 0x0008, 0x34a0: 0x0008, 0x34a1: 0x0008, 0x34a2: 0x0008, 0x34a3: 0x0008, + 0x34a4: 0x0008, 0x34a5: 0x0008, 0x34a6: 0x0008, 0x34a7: 0x0008, 0x34a8: 0x0008, 0x34a9: 0x0008, + 0x34aa: 0x0008, 0x34ab: 0x0008, 0x34ac: 0x0008, 0x34ad: 0x0008, 0x34ae: 0x0008, 0x34af: 0x0008, + 0x34b0: 0x0008, 0x34b1: 0x0008, 0x34b2: 0x0008, 0x34b3: 0x0008, 0x34b4: 0x0008, 0x34b5: 0x0008, + 0x34b6: 0x0008, 0x34b7: 0x0008, 0x34b8: 0x0008, 0x34b9: 0x0008, 0x34ba: 0x0008, + 0x34bc: 0x0008, 0x34bd: 0x0008, 0x34be: 0x0008, 0x34bf: 0x0008, + // Block 0xd3, offset 0x34c0 + 0x34c0: 0x0008, 0x34c1: 0x0008, 0x34c2: 0x0008, 0x34c3: 0x0008, 0x34c4: 0x0008, 0x34c5: 0x0008, + 0x34c7: 0x0008, 0x34c8: 0x0008, 0x34c9: 0x0008, 0x34ca: 0x0008, 0x34cb: 0x0008, + 0x34cc: 0x0008, 0x34cd: 0x0008, 0x34ce: 0x0008, 0x34cf: 0x0008, 0x34d0: 0x0008, 0x34d1: 0x0008, + 0x34d2: 0x0008, 0x34d3: 0x0008, 0x34d4: 0x0008, 0x34d5: 0x0008, 0x34d6: 0x0008, 0x34d7: 0x0008, + 0x34d8: 0x0008, 0x34d9: 0x0008, 0x34da: 0x0008, 0x34db: 0x0008, 0x34dc: 0x0008, 0x34dd: 0x0008, + 0x34de: 0x0008, 0x34df: 0x0008, 0x34e0: 0x0008, 0x34e1: 0x0008, 0x34e2: 0x0008, 0x34e3: 0x0008, + 0x34e4: 0x0008, 0x34e5: 0x0008, 0x34e6: 0x0008, 0x34e7: 0x0008, 0x34e8: 0x0008, 0x34e9: 0x0008, + 0x34ea: 0x0008, 0x34eb: 0x0008, 0x34ec: 0x0008, 0x34ed: 0x0008, 0x34ee: 0x0008, 0x34ef: 0x0008, + 0x34f0: 0x0008, 0x34f1: 0x0008, 0x34f2: 0x0008, 0x34f3: 0x0008, 0x34f4: 0x0008, 0x34f5: 0x0008, + 0x34f6: 0x0008, 0x34f7: 0x0008, 0x34f8: 0x0008, 0x34f9: 0x0008, 0x34fa: 0x0008, 0x34fb: 0x0008, + 0x34fc: 0x0008, 0x34fd: 0x0008, 0x34fe: 0x0008, 0x34ff: 0x0008, + // Block 0xd4, offset 0x3500 + 0x3500: 0x0002, 0x3501: 0x0002, 0x3502: 0x0002, 0x3503: 0x0002, 0x3504: 0x0002, 0x3505: 0x0002, + 0x3506: 0x0002, 0x3507: 0x0002, 0x3508: 0x0002, 0x3509: 0x0002, 0x350a: 0x0002, 0x350b: 0x0002, + 0x350c: 0x0002, 0x350d: 0x0002, 0x350e: 0x0002, 0x350f: 0x0002, 0x3510: 0x0002, 0x3511: 0x0002, + 0x3512: 0x0002, 0x3513: 0x0002, 0x3514: 0x0002, 0x3515: 0x0002, 0x3516: 0x0002, 0x3517: 0x0002, + 0x3518: 0x0002, 0x3519: 0x0002, 0x351a: 0x0002, 0x351b: 0x0002, 0x351c: 0x0002, 0x351d: 0x0002, + 0x351e: 0x0002, 0x351f: 0x0002, 0x3520: 0x0004, 0x3521: 0x0004, 0x3522: 0x0004, 0x3523: 0x0004, + 0x3524: 0x0004, 0x3525: 0x0004, 0x3526: 0x0004, 0x3527: 0x0004, 0x3528: 0x0004, 0x3529: 0x0004, + 0x352a: 0x0004, 0x352b: 0x0004, 0x352c: 0x0004, 0x352d: 0x0004, 0x352e: 0x0004, 0x352f: 0x0004, + 0x3530: 0x0004, 0x3531: 0x0004, 0x3532: 0x0004, 0x3533: 0x0004, 0x3534: 0x0004, 0x3535: 0x0004, + 0x3536: 0x0004, 0x3537: 0x0004, 0x3538: 0x0004, 0x3539: 0x0004, 0x353a: 0x0004, 0x353b: 0x0004, + 0x353c: 0x0004, 0x353d: 0x0004, 0x353e: 0x0004, 0x353f: 0x0004, + // Block 0xd5, offset 0x3540 + 0x3540: 0x0002, 0x3541: 0x0002, 0x3542: 0x0002, 0x3543: 0x0002, 0x3544: 0x0002, 0x3545: 0x0002, + 0x3546: 0x0002, 0x3547: 0x0002, 0x3548: 0x0002, 0x3549: 0x0002, 0x354a: 0x0002, 0x354b: 0x0002, + 0x354c: 0x0002, 0x354d: 0x0002, 0x354e: 0x0002, 0x354f: 0x0002, 0x3550: 0x0002, 0x3551: 0x0002, + 0x3552: 0x0002, 0x3553: 0x0002, 0x3554: 0x0002, 0x3555: 0x0002, 0x3556: 0x0002, 0x3557: 0x0002, + 0x3558: 0x0002, 0x3559: 0x0002, 0x355a: 0x0002, 0x355b: 0x0002, 0x355c: 0x0002, 0x355d: 0x0002, + 0x355e: 0x0002, 0x355f: 0x0002, 0x3560: 0x0002, 0x3561: 0x0002, 0x3562: 0x0002, 0x3563: 0x0002, + 0x3564: 0x0002, 0x3565: 0x0002, 0x3566: 0x0002, 0x3567: 0x0002, 0x3568: 0x0002, 0x3569: 0x0002, + 0x356a: 0x0002, 0x356b: 0x0002, 0x356c: 0x0002, 0x356d: 0x0002, 0x356e: 0x0002, 0x356f: 0x0002, + 0x3570: 0x0002, 0x3571: 0x0002, 0x3572: 0x0002, 0x3573: 0x0002, 0x3574: 0x0002, 0x3575: 0x0002, + 0x3576: 0x0002, 0x3577: 0x0002, 0x3578: 0x0002, 0x3579: 0x0002, 0x357a: 0x0002, 0x357b: 0x0002, + 0x357c: 0x0002, 0x357d: 0x0002, 0x357e: 0x0002, 0x357f: 0x0002, + // Block 0xd6, offset 0x3580 + 0x3580: 0x0004, 0x3581: 0x0004, 0x3582: 0x0004, 0x3583: 0x0004, 0x3584: 0x0004, 0x3585: 0x0004, + 0x3586: 0x0004, 0x3587: 0x0004, 0x3588: 0x0004, 0x3589: 0x0004, 0x358a: 0x0004, 0x358b: 0x0004, + 0x358c: 0x0004, 0x358d: 0x0004, 0x358e: 0x0004, 0x358f: 0x0004, 0x3590: 0x0004, 0x3591: 0x0004, + 0x3592: 0x0004, 0x3593: 0x0004, 0x3594: 0x0004, 0x3595: 0x0004, 0x3596: 0x0004, 0x3597: 0x0004, + 0x3598: 0x0004, 0x3599: 0x0004, 0x359a: 0x0004, 0x359b: 0x0004, 0x359c: 0x0004, 0x359d: 0x0004, + 0x359e: 0x0004, 0x359f: 0x0004, 0x35a0: 0x0004, 0x35a1: 0x0004, 0x35a2: 0x0004, 0x35a3: 0x0004, + 0x35a4: 0x0004, 0x35a5: 0x0004, 0x35a6: 0x0004, 0x35a7: 0x0004, 0x35a8: 0x0004, 0x35a9: 0x0004, + 0x35aa: 0x0004, 0x35ab: 0x0004, 0x35ac: 0x0004, 0x35ad: 0x0004, 0x35ae: 0x0004, 0x35af: 0x0004, + 0x35b0: 0x0002, 0x35b1: 0x0002, 0x35b2: 0x0002, 0x35b3: 0x0002, 0x35b4: 0x0002, 0x35b5: 0x0002, + 0x35b6: 0x0002, 0x35b7: 0x0002, 0x35b8: 0x0002, 0x35b9: 0x0002, 0x35ba: 0x0002, 0x35bb: 0x0002, + 0x35bc: 0x0002, 0x35bd: 0x0002, 0x35be: 0x0002, 0x35bf: 0x0002, +} + +// graphemesIndex: 25 blocks, 1600 entries, 1600 bytes +// Block 0 is the zero block. +var graphemesIndex = [1600]property{ + // Block 0x0, offset 0x0 + // Block 0x1, offset 0x40 + // Block 0x2, offset 0x80 + // Block 0x3, offset 0xc0 + 0xc2: 0x01, + 0xcc: 0x02, 0xcd: 0x03, + 0xd2: 0x04, 0xd6: 0x05, 0xd7: 0x06, + 0xd8: 0x07, 0xd9: 0x08, 0xdb: 0x09, 0xdc: 0x0a, 0xdd: 0x0b, 0xde: 0x0c, 0xdf: 0x0d, + 0xe0: 0x02, 0xe1: 0x03, 0xe2: 0x04, 0xe3: 0x05, + 0xea: 0x06, 0xeb: 0x07, 0xec: 0x08, 0xed: 0x09, 0xef: 0x0a, + 0xf0: 0x14, 0xf3: 0x16, + // Block 0x4, offset 0x100 + 0x120: 0x0e, 0x121: 0x0f, 0x122: 0x10, 0x123: 0x11, 0x124: 0x12, 0x125: 0x13, 0x126: 0x14, 0x127: 0x15, + 0x128: 0x16, 0x129: 0x17, 0x12a: 0x16, 0x12b: 0x18, 0x12c: 0x19, 0x12d: 0x1a, 0x12e: 0x1b, 0x12f: 0x1c, + 0x130: 0x1d, 0x131: 0x1e, 0x132: 0x1f, 0x133: 0x20, 0x134: 0x21, 0x135: 0x22, 0x136: 0x23, 0x137: 0x24, + 0x138: 0x25, 0x139: 0x26, 0x13a: 0x27, 0x13b: 0x28, 0x13c: 0x29, 0x13d: 0x2a, 0x13e: 0x2b, 0x13f: 0x2c, + // Block 0x5, offset 0x140 + 0x140: 0x2d, 0x141: 0x2e, 0x142: 0x2f, 0x144: 0x30, 0x145: 0x31, 0x146: 0x32, 0x147: 0x33, + 0x14d: 0x34, + 0x15c: 0x35, 0x15d: 0x36, 0x15e: 0x37, 0x15f: 0x38, + 0x160: 0x39, 0x162: 0x3a, 0x164: 0x3b, + 0x168: 0x3c, 0x169: 0x3d, 0x16a: 0x3e, 0x16b: 0x3f, 0x16c: 0x40, 0x16d: 0x41, 0x16e: 0x42, 0x16f: 0x43, + 0x170: 0x44, 0x173: 0x45, 0x177: 0x02, + // Block 0x6, offset 0x180 + 0x180: 0x46, 0x181: 0x47, 0x183: 0x48, 0x184: 0x49, 0x186: 0x4a, + 0x18c: 0x4b, 0x18e: 0x4c, 0x18f: 0x4d, + 0x193: 0x4e, 0x196: 0x4f, 0x197: 0x50, + 0x198: 0x51, 0x199: 0x52, 0x19a: 0x53, 0x19b: 0x52, 0x19c: 0x54, 0x19d: 0x55, 0x19e: 0x56, + 0x1a4: 0x57, + 0x1ac: 0x58, 0x1ad: 0x59, + 0x1b3: 0x5a, 0x1b5: 0x5b, 0x1b7: 0x5c, + // Block 0x7, offset 0x1c0 + 0x1c0: 0x5d, 0x1c2: 0x5e, + 0x1ca: 0x5f, + // Block 0x8, offset 0x200 + 0x219: 0x60, 0x21a: 0x61, 0x21b: 0x62, + 0x220: 0x63, 0x222: 0x64, 0x223: 0x65, 0x224: 0x66, 0x225: 0x67, 0x226: 0x68, 0x227: 0x69, + 0x228: 0x6a, 0x229: 0x6b, 0x22a: 0x6c, 0x22b: 0x6d, 0x22f: 0x6e, + 0x230: 0x6f, 0x231: 0x70, 0x232: 0x71, 0x233: 0x72, 0x234: 0x73, 0x235: 0x74, 0x236: 0x75, 0x237: 0x6f, + 0x238: 0x70, 0x239: 0x71, 0x23a: 0x72, 0x23b: 0x73, 0x23c: 0x74, 0x23d: 0x75, 0x23e: 0x6f, 0x23f: 0x70, + // Block 0x9, offset 0x240 + 0x240: 0x71, 0x241: 0x72, 0x242: 0x73, 0x243: 0x74, 0x244: 0x75, 0x245: 0x6f, 0x246: 0x70, 0x247: 0x71, + 0x248: 0x72, 0x249: 0x73, 0x24a: 0x74, 0x24b: 0x75, 0x24c: 0x6f, 0x24d: 0x70, 0x24e: 0x71, 0x24f: 0x72, + 0x250: 0x73, 0x251: 0x74, 0x252: 0x75, 0x253: 0x6f, 0x254: 0x70, 0x255: 0x71, 0x256: 0x72, 0x257: 0x73, + 0x258: 0x74, 0x259: 0x75, 0x25a: 0x6f, 0x25b: 0x70, 0x25c: 0x71, 0x25d: 0x72, 0x25e: 0x73, 0x25f: 0x74, + 0x260: 0x75, 0x261: 0x6f, 0x262: 0x70, 0x263: 0x71, 0x264: 0x72, 0x265: 0x73, 0x266: 0x74, 0x267: 0x75, + 0x268: 0x6f, 0x269: 0x70, 0x26a: 0x71, 0x26b: 0x72, 0x26c: 0x73, 0x26d: 0x74, 0x26e: 0x75, 0x26f: 0x6f, + 0x270: 0x70, 0x271: 0x71, 0x272: 0x72, 0x273: 0x73, 0x274: 0x74, 0x275: 0x75, 0x276: 0x6f, 0x277: 0x70, + 0x278: 0x71, 0x279: 0x72, 0x27a: 0x73, 0x27b: 0x74, 0x27c: 0x75, 0x27d: 0x6f, 0x27e: 0x70, 0x27f: 0x71, + // Block 0xa, offset 0x280 + 0x280: 0x72, 0x281: 0x73, 0x282: 0x74, 0x283: 0x75, 0x284: 0x6f, 0x285: 0x70, 0x286: 0x71, 0x287: 0x72, + 0x288: 0x73, 0x289: 0x74, 0x28a: 0x75, 0x28b: 0x6f, 0x28c: 0x70, 0x28d: 0x71, 0x28e: 0x72, 0x28f: 0x73, + 0x290: 0x74, 0x291: 0x75, 0x292: 0x6f, 0x293: 0x70, 0x294: 0x71, 0x295: 0x72, 0x296: 0x73, 0x297: 0x74, + 0x298: 0x75, 0x299: 0x6f, 0x29a: 0x70, 0x29b: 0x71, 0x29c: 0x72, 0x29d: 0x73, 0x29e: 0x74, 0x29f: 0x75, + 0x2a0: 0x6f, 0x2a1: 0x70, 0x2a2: 0x71, 0x2a3: 0x72, 0x2a4: 0x73, 0x2a5: 0x74, 0x2a6: 0x75, 0x2a7: 0x6f, + 0x2a8: 0x70, 0x2a9: 0x71, 0x2aa: 0x72, 0x2ab: 0x73, 0x2ac: 0x74, 0x2ad: 0x75, 0x2ae: 0x6f, 0x2af: 0x70, + 0x2b0: 0x71, 0x2b1: 0x72, 0x2b2: 0x73, 0x2b3: 0x74, 0x2b4: 0x75, 0x2b5: 0x6f, 0x2b6: 0x70, 0x2b7: 0x71, + 0x2b8: 0x72, 0x2b9: 0x73, 0x2ba: 0x74, 0x2bb: 0x75, 0x2bc: 0x6f, 0x2bd: 0x70, 0x2be: 0x71, 0x2bf: 0x72, + // Block 0xb, offset 0x2c0 + 0x2c0: 0x73, 0x2c1: 0x74, 0x2c2: 0x75, 0x2c3: 0x6f, 0x2c4: 0x70, 0x2c5: 0x71, 0x2c6: 0x72, 0x2c7: 0x73, + 0x2c8: 0x74, 0x2c9: 0x75, 0x2ca: 0x6f, 0x2cb: 0x70, 0x2cc: 0x71, 0x2cd: 0x72, 0x2ce: 0x73, 0x2cf: 0x74, + 0x2d0: 0x75, 0x2d1: 0x6f, 0x2d2: 0x70, 0x2d3: 0x71, 0x2d4: 0x72, 0x2d5: 0x73, 0x2d6: 0x74, 0x2d7: 0x75, + 0x2d8: 0x6f, 0x2d9: 0x70, 0x2da: 0x71, 0x2db: 0x72, 0x2dc: 0x73, 0x2dd: 0x74, 0x2de: 0x76, 0x2df: 0x77, + // Block 0xc, offset 0x300 + 0x32c: 0x78, + 0x338: 0x79, 0x33b: 0x7a, 0x33e: 0x61, 0x33f: 0x7b, + // Block 0xd, offset 0x340 + 0x347: 0x7c, + 0x34b: 0x7d, 0x34d: 0x7e, + 0x368: 0x7f, 0x36b: 0x80, + 0x374: 0x81, + 0x37a: 0x82, 0x37b: 0x83, 0x37d: 0x84, 0x37e: 0x85, + // Block 0xe, offset 0x380 + 0x380: 0x86, 0x381: 0x87, 0x382: 0x88, 0x383: 0x89, 0x384: 0x8a, 0x385: 0x8b, 0x386: 0x8c, 0x387: 0x8d, + 0x388: 0x8e, 0x389: 0x8f, 0x38b: 0x90, 0x38c: 0x21, 0x38d: 0x91, + 0x390: 0x92, 0x391: 0x93, 0x392: 0x94, 0x393: 0x95, 0x396: 0x96, 0x397: 0x97, + 0x398: 0x98, 0x399: 0x99, 0x39a: 0x9a, 0x39c: 0x9b, + 0x3a0: 0x9c, 0x3a4: 0x9d, 0x3a5: 0x9e, 0x3a7: 0x9f, + 0x3a8: 0xa0, 0x3a9: 0xa1, 0x3aa: 0xa2, + 0x3b0: 0xa3, 0x3b2: 0xa4, 0x3b4: 0xa5, 0x3b5: 0xa6, 0x3b6: 0xa7, + 0x3bb: 0xa8, 0x3bc: 0xa9, 0x3bd: 0xaa, + // Block 0xf, offset 0x3c0 + 0x3d0: 0xab, 0x3d1: 0xac, + // Block 0x10, offset 0x400 + 0x42b: 0xad, 0x42c: 0xae, + 0x43d: 0xaf, 0x43e: 0xb0, 0x43f: 0xb1, + // Block 0x11, offset 0x440 + 0x472: 0xb2, + // Block 0x12, offset 0x480 + 0x4bc: 0xb3, 0x4bd: 0xb4, + // Block 0x13, offset 0x4c0 + 0x4c5: 0xb5, 0x4c6: 0xb6, + 0x4c9: 0xb7, + 0x4e8: 0xb8, 0x4e9: 0xb9, 0x4ea: 0xba, + // Block 0x14, offset 0x500 + 0x500: 0xbb, 0x502: 0xbc, 0x504: 0xae, + 0x50a: 0xbd, 0x50b: 0xbe, + 0x513: 0xbe, + 0x523: 0xbf, 0x525: 0xc0, + // Block 0x15, offset 0x540 + 0x540: 0x52, 0x541: 0x52, 0x542: 0x52, 0x543: 0x52, 0x544: 0xc1, 0x545: 0xc2, 0x546: 0xc3, 0x547: 0xc4, + 0x548: 0xc5, 0x549: 0xc6, 0x54a: 0x52, 0x54b: 0x52, 0x54c: 0x52, 0x54d: 0x52, 0x54e: 0x52, 0x54f: 0xc7, + 0x550: 0x52, 0x551: 0x52, 0x552: 0x52, 0x553: 0x52, 0x554: 0xc8, 0x555: 0xc9, 0x556: 0x52, 0x557: 0x52, + 0x558: 0x52, 0x559: 0xca, 0x55a: 0x52, 0x55b: 0x52, 0x55d: 0xcb, 0x55f: 0xcc, + 0x560: 0xcd, 0x561: 0xce, 0x562: 0xcf, 0x563: 0x52, 0x564: 0xd0, 0x565: 0xd1, 0x566: 0x52, 0x567: 0x52, + 0x568: 0x52, 0x569: 0x52, 0x56a: 0x52, 0x56b: 0x52, + 0x570: 0x52, 0x571: 0x52, 0x572: 0x52, 0x573: 0x52, 0x574: 0x52, 0x575: 0x52, 0x576: 0x52, 0x577: 0x52, + 0x578: 0x52, 0x579: 0x52, 0x57a: 0x52, 0x57b: 0x52, 0x57c: 0x52, 0x57d: 0x52, 0x57e: 0x52, 0x57f: 0xc8, + // Block 0x16, offset 0x580 + 0x590: 0x0b, 0x591: 0x0c, 0x593: 0x0d, 0x596: 0x0e, + 0x59b: 0x0f, 0x59c: 0x10, 0x59d: 0x11, 0x59e: 0x12, 0x59f: 0x13, + // Block 0x17, offset 0x5c0 + 0x5c0: 0xd2, 0x5c1: 0x02, 0x5c2: 0xd3, 0x5c3: 0xd3, 0x5c4: 0x02, 0x5c5: 0x02, 0x5c6: 0x02, 0x5c7: 0xd4, + 0x5c8: 0xd3, 0x5c9: 0xd3, 0x5ca: 0xd3, 0x5cb: 0xd3, 0x5cc: 0xd3, 0x5cd: 0xd3, 0x5ce: 0xd3, 0x5cf: 0xd3, + 0x5d0: 0xd3, 0x5d1: 0xd3, 0x5d2: 0xd3, 0x5d3: 0xd3, 0x5d4: 0xd3, 0x5d5: 0xd3, 0x5d6: 0xd3, 0x5d7: 0xd3, + 0x5d8: 0xd3, 0x5d9: 0xd3, 0x5da: 0xd3, 0x5db: 0xd3, 0x5dc: 0xd3, 0x5dd: 0xd3, 0x5de: 0xd3, 0x5df: 0xd3, + 0x5e0: 0xd3, 0x5e1: 0xd3, 0x5e2: 0xd3, 0x5e3: 0xd3, 0x5e4: 0xd3, 0x5e5: 0xd3, 0x5e6: 0xd3, 0x5e7: 0xd3, + 0x5e8: 0xd3, 0x5e9: 0xd3, 0x5ea: 0xd3, 0x5eb: 0xd3, 0x5ec: 0xd3, 0x5ed: 0xd3, 0x5ee: 0xd3, 0x5ef: 0xd3, + 0x5f0: 0xd3, 0x5f1: 0xd3, 0x5f2: 0xd3, 0x5f3: 0xd3, 0x5f4: 0xd3, 0x5f5: 0xd3, 0x5f6: 0xd3, 0x5f7: 0xd3, + 0x5f8: 0xd3, 0x5f9: 0xd3, 0x5fa: 0xd3, 0x5fb: 0xd3, 0x5fc: 0xd3, 0x5fd: 0xd3, 0x5fe: 0xd3, 0x5ff: 0xd3, + // Block 0x18, offset 0x600 + 0x620: 0x15, +} diff --git a/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go b/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go new file mode 100644 index 000000000000..17e9b550b555 --- /dev/null +++ b/vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go @@ -0,0 +1,85 @@ +package iterators + +type Stringish interface { + []byte | string +} + +type SplitFunc[T Stringish] func(T, bool) (int, T, error) + +// Iterator is a generic iterator for words that are either []byte or string. +// Iterate while Next() is true, and access the word via Value(). +type Iterator[T Stringish] struct { + split SplitFunc[T] + data T + start int + pos int +} + +// New creates a new Iterator for the given data and SplitFunc. +func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] { + return &Iterator[T]{ + split: split, + data: data, + } +} + +// SetText sets the text for the iterator to operate on, and resets all state. +func (iter *Iterator[T]) SetText(data T) { + iter.data = data + iter.start = 0 + iter.pos = 0 +} + +// Split sets the SplitFunc for the Iterator. +func (iter *Iterator[T]) Split(split SplitFunc[T]) { + iter.split = split +} + +// Next advances the iterator to the next token. It returns false when there +// are no remaining tokens or an error occurred. +func (iter *Iterator[T]) Next() bool { + if iter.pos == len(iter.data) { + return false + } + if iter.pos > len(iter.data) { + panic("SplitFunc advanced beyond the end of the data") + } + + iter.start = iter.pos + + advance, _, err := iter.split(iter.data[iter.pos:], true) + if err != nil { + panic(err) + } + if advance <= 0 { + panic("SplitFunc returned a zero or negative advance") + } + + iter.pos += advance + if iter.pos > len(iter.data) { + panic("SplitFunc advanced beyond the end of the data") + } + + return true +} + +// Value returns the current token. +func (iter *Iterator[T]) Value() T { + return iter.data[iter.start:iter.pos] +} + +// Start returns the byte position of the current token in the original data. +func (iter *Iterator[T]) Start() int { + return iter.start +} + +// End returns the byte position after the current token in the original data. +func (iter *Iterator[T]) End() int { + return iter.pos +} + +// Reset resets the iterator to the beginning of the data. +func (iter *Iterator[T]) Reset() { + iter.start = 0 + iter.pos = 0 +} diff --git a/vendor/github.com/containerd/platforms/defaults_windows.go b/vendor/github.com/containerd/platforms/defaults_windows.go index 0165adea7e41..64e284667480 100644 --- a/vendor/github.com/containerd/platforms/defaults_windows.go +++ b/vendor/github.com/containerd/platforms/defaults_windows.go @@ -38,5 +38,5 @@ func DefaultSpec() specs.Platform { // Default returns the current platform's default platform specification. func Default() MatchComparer { - return Only(DefaultSpec()) + return &windowsMatchComparer{Matcher: NewMatcher(DefaultSpec())} } diff --git a/vendor/github.com/containerd/platforms/platform_windows_compat.go b/vendor/github.com/containerd/platforms/platform_windows_compat.go index 7f3d9966bcf0..f31ebe0c9e2e 100644 --- a/vendor/github.com/containerd/platforms/platform_windows_compat.go +++ b/vendor/github.com/containerd/platforms/platform_windows_compat.go @@ -42,18 +42,30 @@ const ( // rs5 (version 1809, codename "Redstone 5") corresponds to Windows Server // 2019 (ltsc2019), and Windows 10 (October 2018 Update). rs5 = 17763 + // ltsc2019 (Windows Server 2019) is an alias for [RS5]. + ltsc2019 = rs5 // v21H2Server corresponds to Windows Server 2022 (ltsc2022). v21H2Server = 20348 + // ltsc2022 (Windows Server 2022) is an alias for [v21H2Server] + ltsc2022 = v21H2Server // v22H2Win11 corresponds to Windows 11 (2022 Update). v22H2Win11 = 22621 + + // v23H2 is the 23H2 release in the Windows Server annual channel. + v23H2 = 25398 + + // Windows Server 2025 build 26100 + v25H1Server = 26100 + ltsc2025 = v25H1Server ) // List of stable ABI compliant ltsc releases // Note: List must be sorted in ascending order var compatLTSCReleases = []uint16{ - v21H2Server, + ltsc2022, + ltsc2025, } // CheckHostAndContainerCompat checks if given host and container @@ -70,18 +82,27 @@ func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool { } // If host is < WS 2022, exact version match is required - if host.Build < v21H2Server { + if host.Build < ltsc2022 { return host.Build == ctr.Build } - var supportedLtscRelease uint16 + // Find the latest LTSC version that is earlier than the host version. + // This is the earliest version of container that the host can run. + // + // If the host version is an LTSC, then it supports compatibility with + // everything from the previous LTSC up to itself, so we want supportedLTSCRelease + // to be the previous entry. + // + // If no match is found, then we know that the host is LTSC2022 exactly, + // since we already checked that it's not less than LTSC2022. + var supportedLTSCRelease uint16 = ltsc2022 for i := len(compatLTSCReleases) - 1; i >= 0; i-- { - if host.Build >= compatLTSCReleases[i] { - supportedLtscRelease = compatLTSCReleases[i] + if host.Build > compatLTSCReleases[i] { + supportedLTSCRelease = compatLTSCReleases[i] break } } - return ctr.Build >= supportedLtscRelease && ctr.Build <= host.Build + return supportedLTSCRelease <= ctr.Build && ctr.Build <= host.Build } func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion { @@ -114,18 +135,6 @@ func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion { } } -func winRevision(v string) int { - parts := strings.Split(v, ".") - if len(parts) < 4 { - return 0 - } - r, err := strconv.Atoi(parts[3]) - if err != nil { - return 0 - } - return r -} - type windowsVersionMatcher struct { windowsOSVersion } @@ -149,8 +158,7 @@ type windowsMatchComparer struct { func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool { m1, m2 := c.Match(p1), c.Match(p2) if m1 && m2 { - r1, r2 := winRevision(p1.OSVersion), winRevision(p2.OSVersion) - return r1 > r2 + return p1.OSVersion > p2.OSVersion } return m1 && !m2 } diff --git a/vendor/github.com/docker/cli-docs-tool/Dockerfile b/vendor/github.com/docker/cli-docs-tool/Dockerfile index 1c36a9e03ae5..3445331c8a0a 100644 --- a/vendor/github.com/docker/cli-docs-tool/Dockerfile +++ b/vendor/github.com/docker/cli-docs-tool/Dockerfile @@ -15,9 +15,9 @@ # limitations under the License. ARG GO_VERSION="1.24" -ARG XX_VERSION="1.6.1" -ARG GOLANGCI_LINT_VERSION="v2.1.5" -ARG ADDLICENSE_VERSION="v1.1.1" +ARG XX_VERSION="1.9.0" +ARG GOLANGCI_LINT_VERSION="v2.7.1" +ARG ADDLICENSE_VERSION="v1.2.0" ARG LICENSE_ARGS="-c cli-docs-tool -l apache" ARG LICENSE_FILES=".*\(Dockerfile\|\.go\|\.hcl\|\.sh\)" diff --git a/vendor/github.com/docker/cli-docs-tool/clidocstool_yaml.go b/vendor/github.com/docker/cli-docs-tool/clidocstool_yaml.go index cee53e5ac4a6..e55e5ad385be 100644 --- a/vendor/github.com/docker/cli-docs-tool/clidocstool_yaml.go +++ b/vendor/github.com/docker/cli-docs-tool/clidocstool_yaml.go @@ -26,7 +26,7 @@ import ( "github.com/docker/cli-docs-tool/annotation" "github.com/spf13/cobra" "github.com/spf13/pflag" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" ) type cmdOption struct { diff --git a/vendor/github.com/docker/distribution/uuid/uuid.go b/vendor/github.com/docker/distribution/uuid/uuid.go deleted file mode 100644 index d433ccaf512d..000000000000 --- a/vendor/github.com/docker/distribution/uuid/uuid.go +++ /dev/null @@ -1,126 +0,0 @@ -// Package uuid provides simple UUID generation. Only version 4 style UUIDs -// can be generated. -// -// Please see http://tools.ietf.org/html/rfc4122 for details on UUIDs. -package uuid - -import ( - "crypto/rand" - "fmt" - "io" - "os" - "syscall" - "time" -) - -const ( - // Bits is the number of bits in a UUID - Bits = 128 - - // Size is the number of bytes in a UUID - Size = Bits / 8 - - format = "%08x-%04x-%04x-%04x-%012x" -) - -var ( - // ErrUUIDInvalid indicates a parsed string is not a valid uuid. - ErrUUIDInvalid = fmt.Errorf("invalid uuid") - - // Loggerf can be used to override the default logging destination. Such - // log messages in this library should be logged at warning or higher. - Loggerf = func(format string, args ...interface{}) {} -) - -// UUID represents a UUID value. UUIDs can be compared and set to other values -// and accessed by byte. -type UUID [Size]byte - -// Generate creates a new, version 4 uuid. -func Generate() (u UUID) { - const ( - // ensures we backoff for less than 450ms total. Use the following to - // select new value, in units of 10ms: - // n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2 - maxretries = 9 - backoff = time.Millisecond * 10 - ) - - var ( - totalBackoff time.Duration - count int - retries int - ) - - for { - // This should never block but the read may fail. Because of this, - // we just try to read the random number generator until we get - // something. This is a very rare condition but may happen. - b := time.Duration(retries) * backoff - time.Sleep(b) - totalBackoff += b - - n, err := io.ReadFull(rand.Reader, u[count:]) - if err != nil { - if retryOnError(err) && retries < maxretries { - count += n - retries++ - Loggerf("error generating version 4 uuid, retrying: %v", err) - continue - } - - // Any other errors represent a system problem. What did someone - // do to /dev/urandom? - panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err)) - } - - break - } - - u[6] = (u[6] & 0x0f) | 0x40 // set version byte - u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b} - - return u -} - -// Parse attempts to extract a uuid from the string or returns an error. -func Parse(s string) (u UUID, err error) { - if len(s) != 36 { - return UUID{}, ErrUUIDInvalid - } - - // create stack addresses for each section of the uuid. - p := make([][]byte, 5) - - if _, err := fmt.Sscanf(s, format, &p[0], &p[1], &p[2], &p[3], &p[4]); err != nil { - return u, err - } - - copy(u[0:4], p[0]) - copy(u[4:6], p[1]) - copy(u[6:8], p[2]) - copy(u[8:10], p[3]) - copy(u[10:16], p[4]) - - return -} - -func (u UUID) String() string { - return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:]) -} - -// retryOnError tries to detect whether or not retrying would be fruitful. -func retryOnError(err error) bool { - switch err := err.(type) { - case *os.PathError: - return retryOnError(err.Err) // unpack the target error - case syscall.Errno: - if err == syscall.EPERM { - // EPERM represents an entropy pool exhaustion, a condition under - // which we backoff and retry. - return true - } - } - - return false -} diff --git a/vendor/github.com/docker/docker/AUTHORS b/vendor/github.com/docker/docker/AUTHORS deleted file mode 100644 index c7c649471c2d..000000000000 --- a/vendor/github.com/docker/docker/AUTHORS +++ /dev/null @@ -1,2496 +0,0 @@ -# File @generated by hack/generate-authors.sh. DO NOT EDIT. -# This file lists all contributors to the repository. -# See hack/generate-authors.sh to make modifications. - -17neverends -7sunarni <710720732@qq.com> -Aanand Prasad -Aarni Koskela -Aaron Davidson -Aaron Feng -Aaron Hnatiw -Aaron Huslage -Aaron L. Xu -Aaron Lehmann -Aaron Welch -Aaron Yoshitake -Abdur Rehman -Abel Muiño -Abhijeet Kasurde -Abhinandan Prativadi -Abhinav Ajgaonkar -Abhishek Chanda -Abhishek Sharma -Abin Shahab -Abirdcfly -Ada Mancini -Adam Avilla -Adam Dobrawy -Adam Eijdenberg -Adam Kunk -Adam Lamers -Adam Miller -Adam Mills -Adam Pointer -Adam Simon -Adam Singer -Adam Thornton -Adam Walz -Adam Williams -AdamKorcz -Addam Hardy -Aditi Rajagopal -Aditya -Adnan Khan -Adolfo Ochagavía -Adria Casas -Adrian Moisey -Adrian Mouat -Adrian Oprea -Adrien Folie -Adrien Gallouët -Ahmed Kamal -Ahmet Alp Balkan -Aidan Feldman -Aidan Hobson Sayers -AJ Bowen -Ajey Charantimath -ajneu -Akash Gupta -Akhil Mohan -Akihiro Matsushima -Akihiro Suda -Akim Demaille -Akira Koyasu -Akshay Karle -Akshay Moghe -Al Tobey -alambike -Alan Hoyle -Alan Scherger -Alan Thompson -Alano Terblanche -Albert Callarisa -Albert Zhang -Albin Kerouanton -Alec Benson -Alejandro González Hevia -Aleksa Sarai -Aleksandr Chebotov -Aleksandrs Fadins -Alena Prokharchyk -Alessandro Boch -Alessio Biancalana -Alex Chan -Alex Chen -Alex Coventry -Alex Crawford -Alex Ellis -Alex Gaynor -Alex Goodman -Alex Nordlund -Alex Olshansky -Alex Samorukov -Alex Stockinger -Alex Warhawk -Alexander Artemenko -Alexander Boyd -Alexander Larsson -Alexander Midlash -Alexander Morozov -Alexander Polakov -Alexander Shopov -Alexandre Beslic -Alexandre Garnier -Alexandre González -Alexandre Jomin -Alexandru Sfirlogea -Alexei Margasov -Alexey Guskov -Alexey Kotlyarov -Alexey Shamrin -Alexis Ries -Alexis Thomas -Alfred Landrum -Ali Dehghani -Alicia Lauerman -Alihan Demir -Allen Madsen -Allen Sun -almoehi -Alvaro Saurin -Alvin Deng -Alvin Richards -amangoel -Amen Belayneh -Ameya Gawde -Amir Goldstein -AmirBuddy -Amit Bakshi -Amit Krishnan -Amit Shukla -Amr Gawish -Amy Lindburg -Anand Patil -AnandkumarPatel -Anatoly Borodin -Anca Iordache -Anchal Agrawal -Anda Xu -Anders Janmyr -Andre Dublin <81dublin@gmail.com> -Andre Granovsky -Andrea Denisse Gómez -Andrea Luzzardi -Andrea Turli -Andreas Elvers -Andreas Köhler -Andreas Savvides -Andreas Tiefenthaler -Andrei Gherzan -Andrei Ushakov -Andrei Vagin -Andrew Baxter <423qpsxzhh8k3h@s.rendaw.me> -Andrew C. Bodine -Andrew Clay Shafer -Andrew Duckworth -Andrew France -Andrew Gerrand -Andrew Guenther -Andrew He -Andrew Hsu -Andrew Kim -Andrew Kuklewicz -Andrew Macgregor -Andrew Macpherson -Andrew Martin -Andrew McDonnell -Andrew Munsell -Andrew Pennebaker -Andrew Po -Andrew Weiss -Andrew Williams -Andrews Medina -Andrey Kolomentsev -Andrey Petrov -Andrey Stolbovsky -André Martins -Andrés Maldonado -Andy Chambers -andy diller -Andy Goldstein -Andy Kipp -Andy Lindeman -Andy Rothfusz -Andy Smith -Andy Wilson -Andy Zhang -Aneesh Kulkarni -Anes Hasicic -Angel Velazquez -Anil Belur -Anil Madhavapeddy -Anirudh Aithal -Ankit Jain -Ankush Agarwal -Anonmily -Anran Qiao -Anshul Pundir -Anthon van der Neut -Anthony Baire -Anthony Bishopric -Anthony Dahanne -Anthony Sottile -Anton Löfgren -Anton Nikitin -Anton Polonskiy -Anton Tiurin -Antonio Aguilar -Antonio Murdaca -Antonis Kalipetis -Antony Messerli -Anuj Bahuguna -Anuj Varma -Anusha Ragunathan -Anyu Wang -apocas -Arash Deshmeh -arcosx -ArikaChen -Arko Dasgupta -Arnaud Lefebvre -Arnaud Porterie -Arnaud Rebillout -Artem Khramov -Arthur Barr -Arthur Gautier -Artur Meyster -Arun Gupta -Asad Saeeduddin -Asbjørn Enge -Ashly Mathew -Austin Vazquez -averagehuman -Avi Das -Avi Kivity -Avi Miller -Avi Vaid -Azat Khuyiyakhmetov -Bao Yonglei -Bardia Keyoumarsi -Barnaby Gray -Barry Allard -Bartłomiej Piotrowski -Bastiaan Bakker -Bastien Pascard -bdevloed -Bearice Ren -Ben Bonnefoy -Ben Firshman -Ben Golub -Ben Gould -Ben Hall -Ben Langfeld -Ben Lovy -Ben Sargent -Ben Severson -Ben Toews -Ben Wiklund -Benjamin Atkin -Benjamin Baker -Benjamin Boudreau -Benjamin Böhmke -Benjamin Wang -Benjamin Yolken -Benny Ng -Benoit Chesneau -Bernerd Schaefer -Bernhard M. Wiedemann -Bert Goethals -Bertrand Roussel -Bevisy Zhang -Bharath Thiruveedula -Bhiraj Butala -Bhumika Bayani -Bilal Amarni -Bill Wang -Billy Ridgway -Bily Zhang -Bin Liu -Bingshen Wang -Bjorn Neergaard -Blake Geno -Boaz Shuster -bobby abbott -Bojun Zhu -Boqin Qin -Boris Pruessmann -Boshi Lian -Bouke Haarsma -Boyd Hemphill -boynux -Bradley Cicenas -Bradley Wright -Brandon Liu -Brandon Philips -Brandon Rhodes -Brendan Dixon -Brendon Smith -Brennan Kinney <5098581+polarathene@users.noreply.github.com> -Brent Salisbury -Brett Higgins -Brett Kochendorfer -Brett Milford -Brett Randall -Brian (bex) Exelbierd -Brian Bland -Brian DeHamer -Brian Dorsey -Brian Flad -Brian Goff -Brian McCallister -Brian Olsen -Brian Schwind -Brian Shumate -Brian Torres-Gil -Brian Trump -Brice Jaglin -Briehan Lombaard -Brielle Broder -Bruno Bigras -Bruno Binet -Bruno Gazzera -Bruno Renié -Bruno Tavares -Bryan Bess -Bryan Boreham -Bryan Matsuo -Bryan Murphy -Burke Libbey -Byung Kang -Caleb Spare -Calen Pennington -Calvin Liu -Cameron Boehmer -Cameron Sparr -Cameron Spear -Campbell Allen -Candid Dauth -Cao Weiwei -Carl Henrik Lunde -Carl Loa Odin -Carl X. Su -Carlo Mion -Carlos Alexandro Becker -Carlos de Paula -Carlos Sanchez -Carol Fager-Higgins -Cary -Casey Bisson -Catalin Pirvu -Ce Gao -Cedric Davies -Cesar Talledo -Cezar Sa Espinola -Chad Swenson -Chance Zibolski -Chander Govindarajan -Chanhun Jeong -Chao Wang -Charity Kathure -Charles Chan -Charles Hooper -Charles Law -Charles Lindsay -Charles Merriam -Charles Sarrazin -Charles Smith -Charlie Drage -Charlie Lewis -Chase Bolt -ChaYoung You -Chee Hau Lim -Chen Chao -Chen Chuanliang -Chen Hanxiao -Chen Min -Chen Mingjie -Chen Qiu -Cheng-mean Liu -Chengfei Shang -Chengguang Xu -Chengyu Zhu -Chentianze -Chenyang Yan -chenyuzhu -Chetan Birajdar -Chewey -Chia-liang Kao -Chiranjeevi Tirunagari -chli -Cholerae Hu -Chris Alfonso -Chris Armstrong -Chris Dias -Chris Dituri -Chris Fordham -Chris Gavin -Chris Gibson -Chris Khoo -Chris Kreussling (Flatbush Gardener) -Chris McKinnel -Chris McKinnel -Chris Price -Chris Seto -Chris Snow -Chris St. Pierre -Chris Stivers -Chris Swan -Chris Telfer -Chris Wahl -Chris Weyl -Chris White -Christian Becker -Christian Berendt -Christian Brauner -Christian Böhme -Christian Muehlhaeuser -Christian Persson -Christian Rotzoll -Christian Simon -Christian Stefanescu -Christoph Ziebuhr -Christophe Mehay -Christophe Troestler -Christophe Vidal -Christopher Biscardi -Christopher Crone -Christopher Currie -Christopher Jones -Christopher Latham -Christopher Petito -Christopher Rigor -Christy Norman -Chun Chen -Ciro S. Costa -Clayton Coleman -Clint Armstrong -Clinton Kitson -clubby789 -Cody Roseborough -Coenraad Loubser -Colin Dunklau -Colin Hebert -Colin Panisset -Colin Rice -Colin Walters -Collin Guarino -Colm Hally -companycy -Conor Evans -Corbin Coleman -Corey Farrell -Cory Forsyth -Cory Snider -cressie176 -Cristian Ariza -Cristian Staretu -cristiano balducci -Cristina Yenyxe Gonzalez Garcia -Cruceru Calin-Cristian -cui fliter -CUI Wei -Cuong Manh Le -Cyprian Gracz -Cyril F -Da McGrady -Daan van Berkel -Daehyeok Mun -Dafydd Crosby -dalanlan -Damian Smyth -Damien Nadé -Damien Nozay -Damjan Georgievski -Dan Anolik -Dan Buch -Dan Cotora -Dan Feldman -Dan Griffin -Dan Hirsch -Dan Keder -Dan Levy -Dan McPherson -Dan Plamadeala -Dan Stine -Dan Williams -Dani Hodovic -Dani Louca -Daniel Antlinger -Daniel Black -Daniel Dao -Daniel Exner -Daniel Farrell -Daniel Garcia -Daniel Gasienica -Daniel Grunwell -Daniel Guns -Daniel Helfand -Daniel Hiltgen -Daniel J Walsh -Daniel Menet -Daniel Mizyrycki -Daniel Nephin -Daniel Norberg -Daniel Nordberg -Daniel P. Berrangé -Daniel Robinson -Daniel S -Daniel Sweet -Daniel Von Fange -Daniel Watkins -Daniel X Moore -Daniel YC Lin -Daniel Zhang -Daniele Rondina -Danny Berger -Danny Milosavljevic -Danny Yates -Danyal Khaliq -Darren Coxall -Darren Shepherd -Darren Stahl -Dattatraya Kumbhar -Davanum Srinivas -Dave Barboza -Dave Goodchild -Dave Henderson -Dave MacDonald -Dave Tucker -David Anderson -David Bellotti -David Calavera -David Chung -David Corking -David Cramer -David Currie -David Davis -David Dooling -David Gageot -David Gebler -David Glasser -David Karlsson <35727626+dvdksn@users.noreply.github.com> -David Lawrence -David Lechner -David M. Karr -David Mackey -David Manouchehri -David Mat -David Mcanulty -David McKay -David O'Rourke -David P Hilton -David Pelaez -David R. Jenni -David Röthlisberger -David Sheets -David Sissitka -David Trott -David Wang <00107082@163.com> -David Williamson -David Xia -David Young -Davide Ceretti -Dawn Chen -dbdd -dcylabs -Debayan De -Deborah Gertrude Digges -deed02392 -Deep Debroy -Deng Guangxing -Deni Bertovic -Denis Defreyne -Denis Gladkikh -Denis Ollier -Dennis Chen -Dennis Chen -Dennis Docter -Derek -Derek -Derek Ch -Derek McGowan -Deric Crago -Deshi Xiao -Devon Estes -Devvyn Murphy -Dharmit Shah -Dhawal Yogesh Bhanushali -Dhilip Kumars -Diego Romero -Diego Siqueira -Dieter Reuter -Dillon Dixon -Dima Stopel -Dimitri John Ledkov -Dimitris Mandalidis -Dimitris Rozakis -Dimitry Andric -Dinesh Subhraveti -Ding Fei -dingwei -Diogo Monica -DiuDiugirl -Djibril Koné -Djordje Lukic -dkumor -Dmitri Logvinenko -Dmitri Shuralyov -Dmitry Demeshchuk -Dmitry Gusev -Dmitry Kononenko -Dmitry Sharshakov -Dmitry Shyshkin -Dmitry Smirnov -Dmitry V. Krivenok -Dmitry Vorobev -Dmytro Iakovliev -docker-unir[bot] -Dolph Mathews -Dominic Tubach -Dominic Yin -Dominik Dingel -Dominik Finkbeiner -Dominik Honnef -Don Kirkby -Don Kjer -Don Spaulding -Donald Huang -Dong Chen -Donghwa Kim -Donovan Jones -Dorin Geman -Doron Podoleanu -Doug Davis -Doug MacEachern -Doug Tangren -Douglas Curtis -Dr Nic Williams -dragon788 -Dražen Lučanin -Drew Erny -Drew Hubl -Dustin Sallings -Ed Costello -Edmund Wagner -Eiichi Tsukata -Eike Herzbach -Eivin Giske Skaaren -Eivind Uggedal -Elan Ruusamäe -Elango Sivanandam -Elena Morozova -Eli Uriegas -Elias Faxö -Elias Koromilas -Elias Probst -Elijah Zupancic -eluck -Elvir Kuric -Emil Davtyan -Emil Hernvall -Emily Maier -Emily Rose -Emir Ozer -Eng Zer Jun -Enguerran -Enrico Weigelt, metux IT consult -Eohyung Lee -epeterso -er0k -Eric Barch -Eric Curtin -Eric G. Noriega -Eric Hanchrow -Eric Lee -Eric Mountain -Eric Myhre -Eric Paris -Eric Rafaloff -Eric Rosenberg -Eric Sage -Eric Soderstrom -Eric Yang -Eric-Olivier Lamey -Erica Windisch -Erich Cordoba -Erik Bray -Erik Dubbelboer -Erik Hollensbe -Erik Inge Bolsø -Erik Kristensen -Erik Sipsma -Erik Sjölund -Erik St. Martin -Erik Weathers -Erno Hopearuoho -Erwin van der Koogh -Espen Suenson -Ethan Bell -Ethan Mosbaugh -Euan Harris -Euan Kemp -Eugen Krizo -Eugene Yakubovich -Evan Allrich -Evan Carmi -Evan Hazlett -Evan Krall -Evan Lezar -Evan Phoenix -Evan Wies -Evelyn Xu -Everett Toews -Evgeniy Makhrov -Evgeny Shmarnev -Evgeny Vereshchagin -Ewa Czechowska -Eystein Måløy Stenberg -ezbercih -Ezra Silvera -Fabian Kramm -Fabian Lauer -Fabian Raetz -Fabiano Rosas -Fabio Falci -Fabio Kung -Fabio Rapposelli -Fabio Rehm -Fabrizio Regini -Fabrizio Soppelsa -Faiz Khan -falmp -Fangming Fang -Fangyuan Gao <21551127@zju.edu.cn> -fanjiyun -Fareed Dudhia -Fathi Boudra -Federico Gimenez -Felipe Oliveira -Felipe Ruhland -Felix Abecassis -Felix Geisendörfer -Felix Hupfeld -Felix Rabe -Felix Ruess -Felix Schindler -Feng Yan -Fengtu Wang -Ferenc Szabo -Fernando -Fero Volar -Feroz Salam -Ferran Rodenas -Filipe Brandenburger -Filipe Oliveira -Filipe Pina -Flavio Castelli -Flavio Crisciani -Florian -Florian Klein -Florian Maier -Florian Noeding -Florian Schmaus -Florian Weingarten -Florin Asavoaie -Florin Patan -fonglh -Foysal Iqbal -Francesc Campoy -Francesco Degrassi -Francesco Mari -Francis Chuang -Francisco Carriedo -Francisco Souza -Frank Groeneveld -Frank Herrmann -Frank Macreery -Frank Rosquin -Frank Villaro-Dixon -Frank Yang -François Scala -Fred Lifton -Frederick F. Kautz IV -Frederico F. de Oliveira -Frederik Loeffert -Frederik Nordahl Jul Sabroe -Freek Kalter -Frieder Bluemle -frobnicaty <92033765+frobnicaty@users.noreply.github.com> -Frédéric Dalleau -Fu JinLin -Félix Baylac-Jacqué -Félix Cantournet -Gabe Rosenhouse -Gabor Nagy -Gabriel Adrian Samfira -Gabriel Goller -Gabriel L. Somlo -Gabriel Linder -Gabriel Monroy -Gabriel Nicolas Avellaneda -Gabriel Tomitsuka -Gaetan de Villele -Galen Sampson -Gang Qiao -Gareth Rushgrove -Garrett Barboza -Gary Schaetz -Gaurav -Gaurav Singh -Gaël PORTAY -Genki Takiuchi -GennadySpb -Geoff Levand -Geoffrey Bachelet -Geon Kim -George Adams -George Kontridze -George Ma -George MacRorie -George Xie -Georgi Hristozov -Georgy Yakovlev -Gereon Frey -German DZ -Gert van Valkenhoef -Gerwim Feiken -Ghislain Bourgeois -Giampaolo Mancini -Gianluca Borello -Gildas Cuisinier -Giovan Isa Musthofa -gissehel -Giuseppe Mazzotta -Giuseppe Scrivano -Gleb Fotengauer-Malinovskiy -Gleb M Borisov -Glyn Normington -GoBella -Goffert van Gool -Goldwyn Rodrigues -Gopikannan Venugopalsamy -Gosuke Miyashita -Gou Rao -Govinda Fichtner -Grace Choi -Grant Millar -Grant Reaber -Graydon Hoare -Greg Fausak -Greg Pflaum -Greg Stephens -Greg Thornton -Grzegorz Jaśkiewicz -Guilhem Lettron -Guilherme Salgado -Guillaume Dufour -Guillaume J. Charmes -Gunadhya S. <6939749+gunadhya@users.noreply.github.com> -Guoqiang QI -guoxiuyan -Guri -Gurjeet Singh -Guruprasad -Gustav Sinder -gwx296173 -Günter Zöchbauer -Haichao Yang -haikuoliu -haining.cao -Hakan Özler -Hamish Hutchings -Hannes Ljungberg -Hans Kristian Flaatten -Hans Rødtang -Hao Shu Wei -Hao Zhang <21521210@zju.edu.cn> -Harald Albers -Harald Niesche -Harley Laue -Harold Cooper -Harrison Turton -Harry Zhang -Harshal Patil -Harshal Patil -He Simei -He Xiaoxi -He Xin -heartlock <21521209@zju.edu.cn> -Hector Castro -Helen Xie -Henning Sprang -Hiroshi Hatake -Hiroyuki Sasagawa -Hobofan -Hollie Teal -Hong Xu -Hongbin Lu -Hongxu Jia -Honza Pokorny -Hsing-Hui Hsu -Hsing-Yu (David) Chen -hsinko <21551195@zju.edu.cn> -Hu Keping -Hu Tao -Huajin Tong -huang-jl <1046678590@qq.com> -HuanHuan Ye -Huanzhong Zhang -Huayi Zhang -Hugo Barrera -Hugo Duncan -Hugo Marisco <0x6875676f@gmail.com> -Hui Kang -Hunter Blanks -huqun -Huu Nguyen -Hyeongkyu Lee -Hyzhou Zhy -Iago López Galeiras -Ian Bishop -Ian Bull -Ian Calvert -Ian Campbell -Ian Chen -Ian Lee -Ian Main -Ian Philpot -Ian Truslove -Iavael -Icaro Seara -Ignacio Capurro -Igor Dolzhikov -Igor Karpovich -Iliana Weller -Ilkka Laukkanen -Illia Antypenko -Illo Abdulrahim -Ilya Dmitrichenko -Ilya Gusev -Ilya Khlopotov -imalasong <2879499479@qq.com> -imre Fitos -inglesp -Ingo Gottwald -Innovimax -Isaac Dupree -Isabel Jimenez -Isaiah Grace -Isao Jonas -Iskander Sharipov -Ivan Babrou -Ivan Fraixedes -Ivan Grcic -Ivan Markin -J Bruni -J. Nunn -Jack Danger Canty -Jack Laxson -Jack Walker <90711509+j2walker@users.noreply.github.com> -Jacob Atzen -Jacob Edelman -Jacob Tomlinson -Jacob Vallejo -Jacob Wen -Jaime Cepeda -Jaivish Kothari -Jake Champlin -Jake Moshenko -Jake Sanders -Jakub Drahos -Jakub Guzik -James Allen -James Carey -James Carr -James DeFelice -James Harrison Fisher -James Kyburz -James Kyle -James Lal -James Mills -James Nesbitt -James Nugent -James Sanders -James Turnbull -James Watkins-Harvey -Jameson Hyde -Jamie Hannaford -Jamshid Afshar -Jan Breig -Jan Chren -Jan Garcia -Jan Götte -Jan Keromnes -Jan Koprowski -Jan Pazdziora -Jan Toebes -Jan-Gerd Tenberge -Jan-Jaap Driessen -Jana Radhakrishnan -Jannick Fahlbusch -Januar Wayong -Jared Biel -Jared Hocutt -Jaroslav Jindrak -Jaroslaw Zabiello -Jasmine Hegman -Jason A. Donenfeld -Jason Divock -Jason Giedymin -Jason Green -Jason Hall -Jason Heiss -Jason Livesay -Jason McVetta -Jason Plum -Jason Shepherd -Jason Smith -Jason Sommer -Jason Stangroome -Jasper Siepkes -Javier Bassi -jaxgeller -Jay -Jay Kamat -Jay Lim -Jean Rouge -Jean-Baptiste Barth -Jean-Baptiste Dalido -Jean-Christophe Berthon -Jean-Michel Rouet -Jean-Paul Calderone -Jean-Pierre Huynh -Jean-Tiare Le Bigot -Jeeva S. Chelladhurai -Jeff Anderson -Jeff Hajewski -Jeff Johnston -Jeff Lindsay -Jeff Mickey -Jeff Minard -Jeff Nickoloff -Jeff Silberman -Jeff Welch -Jeff Zvier -Jeffrey Bolle -Jeffrey Morgan -Jeffrey van Gogh -Jenny Gebske -Jeongseok Kang -Jeremy Chambers -Jeremy Grosser -Jeremy Huntwork -Jeremy Price -Jeremy Qian -Jeremy Unruh -Jeremy Yallop -Jeroen Franse -Jeroen Jacobs -Jesse Dearing -Jesse Dubay -Jessica Frazelle -Jeyanthinath Muthuram -Jezeniel Zapanta -Jhon Honce -Ji.Zhilong -Jian Liao -Jian Zeng -Jian Zhang -Jiang Jinyang -Jianyong Wu -Jie Luo -Jie Ma -Jihyun Hwang -Jilles Oldenbeuving -Jim Alateras -Jim Carroll -Jim Ehrismann -Jim Galasyn -Jim Lin -Jim Minter -Jim Perrin -Jimmy Cuadra -Jimmy Puckett -Jimmy Song -jinjiadu -Jinsoo Park -Jintao Zhang -Jiri Appl -Jiri Popelka -Jiuyue Ma -Jiří Župka -jjimbo137 <115816493+jjimbo137@users.noreply.github.com> -Joakim Roubert -Joan Grau -Joao Fernandes -Joao Trindade -Joe Beda -Joe Doliner -Joe Ferguson -Joe Gordon -Joe Shaw -Joe Van Dyk -Joel Friedly -Joel Handwell -Joel Hansson -Joel Wurtz -Joey Geiger -Joey Geiger -Joey Gibson -Joffrey F -Johan Euphrosine -Johan Rydberg -Johanan Lieberman -Johannes 'fish' Ziemke -John Costa -John Feminella -John Gardiner Myers -John Gossman -John Harris -John Howard -John Laswell -John Maguire -John Mulhausen -John OBrien III -John Starks -John Stephens -John Tims -John V. Martinez -John Warwick -John Willis -Jon Johnson -Jon Surrell -Jon Wedaman -Jonas Dohse -Jonas Geiler -Jonas Heinrich -Jonas Pfenniger -Jonathan A. Schweder -Jonathan A. Sternberg -Jonathan Boulle -Jonathan Camp -Jonathan Choy -Jonathan Dowland -Jonathan Lebon -Jonathan Lomas -Jonathan McCrohan -Jonathan Mueller -Jonathan Pares -Jonathan Rudenberg -Jonathan Stoppani -Jonh Wendell -Joni Sar -Joost Cassee -Jordan Arentsen -Jordan Jennings -Jordan Sissel -Jordi Massaguer Pla -Jorge Marin -Jorit Kleine-Möllhoff -Jose Diaz-Gonzalez -Joseph Anthony Pasquale Holsten -Joseph Hager -Joseph Kern -Joseph Rothrock -Josh -Josh Bodah -Josh Bonczkowski -Josh Chorlton -Josh Eveleth -Josh Hawn -Josh Horwitz -Josh Poimboeuf -Josh Soref -Josh Wilson -Josiah Kiehl -José Tomás Albornoz -Joyce Jang -JP -JSchltggr -Julian Taylor -Julien Barbier -Julien Bisconti -Julien Bordellier -Julien Dubois -Julien Kassar -Julien Maitrehenry -Julien Pervillé -Julien Pivotto -Julio Guerra -Julio Montes -Jun Du -Jun-Ru Chang -junxu -Jussi Nummelin -Justas Brazauskas -Justen Martin -Justin Chadwell -Justin Cormack -Justin Force -Justin Keller <85903732+jk-vb@users.noreply.github.com> -Justin Menga -Justin Plock -Justin Simonelis -Justin Terry -Justyn Temme -Jyrki Puttonen -Jérémy Leherpeur -Jérôme Petazzoni -Jörg Thalheim -K. Heller -Kai Blin -Kai Qiang Wu (Kennan) -Kaijie Chen -Kaita Nakamura -Kamil Domański -Kamjar Gerami -Kanstantsin Shautsou -Kara Alexandra -Karan Lyons -Kareem Khazem -kargakis -Karl Grzeszczak -Karol Duleba -Karthik Karanth -Karthik Nayak -Kasper Fabæch Brandt -Kate Heddleston -Katie McLaughlin -Kato Kazuyoshi -Katrina Owen -Kawsar Saiyeed -Kay Yan -kayrus -Kazuhiro Sera -Kazuyoshi Kato -Ke Li -Ke Xu -Kei Ohmura -Keith Hudgins -Keli Hu -Ken Bannister -Ken Cochrane -Ken Herner -Ken ICHIKAWA -Ken Reese -Kenfe-Mickaël Laventure -Kenjiro Nakayama -Kent Johnson -Kenta Tada -Kevin "qwazerty" Houdebert -Kevin Alvarez -Kevin Burke -Kevin Clark -Kevin Feyrer -Kevin J. Lynagh -Kevin Jing Qiu -Kevin Kern -Kevin Menard -Kevin Meredith -Kevin P. Kucharczyk -Kevin Parsons -Kevin Richardson -Kevin Shi -Kevin Wallace -Kevin Yap -Keyvan Fatehi -kies -Kim BKC Carlbacker -Kim Eik -Kimbro Staken -Kir Kolyshkin -Kiran Gangadharan -Kirill SIbirev -Kirk Easterson -knappe -Kohei Tsuruta -Koichi Shiraishi -Konrad Kleine -Konrad Ponichtera -Konstantin Gribov -Konstantin L -Konstantin Pelykh -Kostadin Plachkov -kpcyrd -Krasi Georgiev -Krasimir Georgiev -Kris-Mikael Krister -Kristian Haugene -Kristian Heljas -Kristina Zabunova -Krystian Wojcicki -Kunal Kushwaha -Kunal Tyagi -Kyle Conroy -Kyle Linden -Kyle Squizzato -Kyle Wuolle -kyu -Lachlan Coote -Lai Jiangshan -Lajos Papp -Lakshan Perera -Lalatendu Mohanty -Lance Chen -Lance Kinley -Lars Andringa -Lars Butler -Lars Kellogg-Stedman -Lars R. Damerow -Lars-Magnus Skog -Laszlo Meszaros -Laura Brehm -Laura Frank -Laurent Bernaille -Laurent Erignoux -Laurent Goderre -Laurie Voss -Leandro Motta Barros -Leandro Siqueira -Lee Calcote -Lee Chao <932819864@qq.com> -Lee, Meng-Han -Lei Gong -Lei Jitang -Leiiwang -Len Weincier -Lennie -Leo Gallucci -Leonardo Nodari -Leonardo Taccari -Leszek Kowalski -Levi Blackstone -Levi Gross -Levi Harrison -Lewis Daly -Lewis Marshall -Lewis Peckover -Li Yi -Liam Macgillavry -Liana Lo -Liang Mingqiang -Liang-Chi Hsieh -liangwei -Liao Qingwei -Lifubang -Lihua Tang -Lily Guo -limeidan -Lin Lu -LingFaKe -Linus Heckemann -Liran Tal -Liron Levin -Liu Bo -Liu Hua -liwenqi -lixiaobing10051267 -Liz Zhang -LIZAO LI -Lizzie Dixon <_@lizzie.io> -Lloyd Dewolf -Lokesh Mandvekar -longliqiang88 <394564827@qq.com> -Lorenz Leutgeb -Lorenzo Fontana -Lotus Fenn -Louis Delossantos -Louis Opter -Luboslav Pivarc -Luca Favatella -Luca Marturana -Luca Orlandi -Luca-Bogdan Grigorescu -Lucas Chan -Lucas Chi -Lucas Molas -Lucas Silvestre -Luciano Mores -Luis Henrique Mulinari -Luis Martínez de Bartolomé Izquierdo -Luiz Svoboda -Lukas Heeren -Lukas Waslowski -lukaspustina -Lukasz Zajaczkowski -Luke Marsden -Lyn -Lynda O'Leary -Lénaïc Huard -Ma Müller -Ma Shimiao -Mabin -Madhan Raj Mookkandy -Madhav Puri -Madhu Venugopal -Mageee -maggie44 <64841595+maggie44@users.noreply.github.com> -Mahesh Tiyyagura -malnick -Malte Janduda -Manfred Touron -Manfred Zabarauskas -Manjunath A Kumatagi -Mansi Nahar -Manuel Meurer -Manuel Rüger -Manuel Woelker -mapk0y -Marat Radchenko -Marc Abramowitz -Marc Kuo -Marc Tamsky -Marcel Edmund Franke -Marcelo Horacio Fortino -Marcelo Salazar -Marco Hennings -Marcus Cobden -Marcus Farkas -Marcus Linke -Marcus Martins -Marcus Ramberg -Marek Goldmann -Marian Marinov -Marianna Tessel -Mario Loriedo -Marius Gundersen -Marius Sturm -Marius Voila -Mark Allen -Mark Feit -Mark Jeromin -Mark McGranaghan -Mark McKinstry -Mark Milstein -Mark Oates -Mark Parker -Mark Vainomaa -Mark West -Markan Patel -Marko Mikulicic -Marko Tibold -Markus Fix -Markus Kortlang -Martijn Dwars -Martijn van Oosterhout -Martin Braun -Martin Dojcak -Martin Honermeyer -Martin Jirku -Martin Kelly -Martin Mosegaard Amdisen -Martin Muzatko -Martin Redmond -Maru Newby -Mary Anthony -Masahito Zembutsu -Masato Ohba -Masayuki Morita -Mason Malone -Mateusz Sulima -Mathias Monnerville -Mathieu Champlon -Mathieu Le Marec - Pasquet -Mathieu Parent -Mathieu Paturel -Matt Apperson -Matt Bachmann -Matt Bajor -Matt Bentley -Matt Haggard -Matt Hoyle -Matt McCormick -Matt Moore -Matt Morrison <3maven@gmail.com> -Matt Richardson -Matt Rickard -Matt Robenolt -Matt Schurenko -Matt Williams -Matthew Heon -Matthew Lapworth -Matthew Mayer -Matthew Mosesohn -Matthew Mueller -Matthew Riley -Matthias Klumpp -Matthias Kühnle -Matthias Rampke -Matthieu Fronton -Matthieu Hauglustaine -Matthieu MOREL -Mattias Jernberg -Mauricio Garavaglia -mauriyouth -Max Harmathy -Max Shytikov -Max Timchenko -Maxim Fedchyshyn -Maxim Ivanov -Maxim Kulkin -Maxim Treskin -Maxime Petazzoni -Maximiliano Maccanti -Maxwell -Meaglith Ma -meejah -Megan Kostick -Mehul Kar -Mei ChunTao -Mengdi Gao -Menghui Chen -Mert Yazıcıoğlu -mgniu -Micah Zoltu -Michael A. Smith -Michael Beskin -Michael Bridgen -Michael Brown -Michael Chiang -Michael Crosby -Michael Currie -Michael Friis -Michael Gorsuch -Michael Grauer -Michael Holzheu -Michael Hudson-Doyle -Michael Huettermann -Michael Irwin -Michael Kebe -Michael Kuehn -Michael Käufl -Michael Neale -Michael Nussbaum -Michael Prokop -Michael Scharf -Michael Spetsiotis -Michael Stapelberg -Michael Steinert -Michael Thies -Michael Weidmann -Michael West -Michael Zhao -Michal Fojtik -Michal Gebauer -Michal Jemala -Michal Kostrzewa -Michal Minář -Michal Rostecki -Michal Wieczorek -Michaël Pailloncy -Michał Czeraszkiewicz -Michał Gryko -Michał Kosek -Michiel de Jong -Mickaël Fortunato -Mickaël Remars -Miguel Angel Fernández -Miguel Morales -Miguel Perez -Mihai Borobocea -Mihuleacc Sergiu -Mikael Davranche -Mike Brown -Mike Bush -Mike Casas -Mike Chelen -Mike Danese -Mike Dillon -Mike Dougherty -Mike Estes -Mike Gaffney -Mike Goelzer -Mike Leone -Mike Lundy -Mike MacCana -Mike Naberezny -Mike Snitzer -Mike Sul -mikelinjie <294893458@qq.com> -Mikhail Sobolev -Miklos Szegedi -Milas Bowman -Milind Chawre -Miloslav Trmač -mingqing -Mingzhen Feng -Misty Stanley-Jones -Mitch Capper -Mizuki Urushida -mlarcher -Mohammad Banikazemi -Mohammad Nasirifar -Mohammed Aaqib Ansari -Mohd Sadiq -Mohit Soni -Moorthy RS -Morgan Bauer -Morgante Pell -Morgy93 -Morten Siebuhr -Morton Fox -Moysés Borges -mrfly -Mrunal Patel -Muayyad Alsadi -Muhammad Zohaib Aslam -Mustafa Akın -Muthukumar R -Myeongjoon Kim -Máximo Cuadros -Médi-Rémi Hashim -Nace Oroz -Nahum Shalman -Nakul Pathak -Nalin Dahyabhai -Nan Monnand Deng -Naoki Orii -Natalie Parker -Natanael Copa -Natasha Jarus -Nate Brennand -Nate Eagleson -Nate Jones -Nathan Baulch -Nathan Carlson -Nathan Herald -Nathan Hsieh -Nathan Kleyn -Nathan LeClaire -Nathan McCauley -Nathan Williams -Naveed Jamil -Neal McBurnett -Neil Horman -Neil Peterson -Nelson Chen -Neyazul Haque -Nghia Tran -Niall O'Higgins -Nicholas E. Rabenau -Nick Adcock -Nick DeCoursin -Nick Irvine -Nick Neisen -Nick Parker -Nick Payne -Nick Russo -Nick Santos -Nick Stenning -Nick Stinemates -Nick Wood -NickrenREN -Nicola Kabar -Nicolas Borboën -Nicolas De Loof -Nicolas Dudebout -Nicolas Goy -Nicolas Kaiser -Nicolas Sterchele -Nicolas V Castet -Nicolás Hock Isaza -Niel Drummond -Nigel Poulton -Nik Nyby -Nikhil Chawla -NikolaMandic -Nikolas Garofil -Nikolay Edigaryev -Nikolay Milovanov -ningmingxiao -Nirmal Mehta -Nishant Totla -NIWA Hideyuki -Noah Meyerhans -Noah Treuhaft -NobodyOnSE -noducks -Nolan Darilek -Nolan Miles -Noriki Nakamura -nponeccop -Nurahmadie -Nuutti Kotivuori -nzwsch -O.S. Tezer -objectified -Octol1ttle -Odin Ugedal -Oguz Bilgic -Oh Jinkyun -Ohad Schneider -ohmystack -Ole Reifschneider -Oliver Neal -Oliver Reason -Olivier Gambier -Olle Jonsson -Olli Janatuinen -Olly Pomeroy -Omri Shiv -Onur Filiz -Oriol Francès -Oscar Bonilla <6f6231@gmail.com> -oscar.chen <2972789494@qq.com> -Oskar Niburski -Otto Kekäläinen -Ouyang Liduo -Ovidio Mallo -Panagiotis Moustafellos -Paolo G. Giarrusso -Pascal -Pascal Bach -Pascal Borreli -Pascal Hartig -Patrick Böänziger -Patrick Devine -Patrick Haas -Patrick Hemmer -Patrick St. laurent -Patrick Stapleton -Patrik Cyvoct -Patrik Leifert -pattichen -Paul "TBBle" Hampson -Paul -paul -Paul Annesley -Paul Bellamy -Paul Bowsher -Paul Furtado -Paul Hammond -Paul Jimenez -Paul Kehrer -Paul Lietar -Paul Liljenberg -Paul Morie -Paul Nasrat -Paul Seiffert -Paul Weaver -Paulo Gomes -Paulo Ribeiro -Pavel Lobashov -Pavel Matěja -Pavel Pletenev -Pavel Pospisil -Pavel Sutyrin -Pavel Tikhomirov -Pavlos Ratis -Pavol Vargovcik -Pawel Konczalski -Paweł Gronowski -payall4u -Peeyush Gupta -Peggy Li -Pei Su -Peng Tao -Penghan Wang -Per Weijnitz -perhapszzy@sina.com -Pete Woods -Peter Bourgon -Peter Braden -Peter Bücker -Peter Choi -Peter Dave Hello -Peter Edge -Peter Ericson -Peter Esbensen -Peter Jaffe -Peter Kang -Peter Malmgren -Peter Salvatore -Peter Volpe -Peter Waller -Petr Švihlík -Petros Angelatos -Phil -Phil Estes -Phil Sphicas -Phil Spitler -Philip Alexander Etling -Philip K. Warren -Philip Monroe -Philipp Fruck -Philipp Gillé -Philipp Wahala -Philipp Weissensteiner -Phillip Alexander -phineas -pidster -Piergiuliano Bossi -Pierre -Pierre Carrier -Pierre Dal-Pra -Pierre Wacrenier -Pierre-Alain RIVIERE -pinglanlu -Piotr Bogdan -Piotr Karbowski -Porjo -Poul Kjeldager Sørensen -Pradeep Chhetri -Pradip Dhara -Pradipta Kr. Banerjee -Prasanna Gautam -Pratik Karki -Prayag Verma -Priya Wadhwa -Projjol Banerji -Przemek Hejman -Puneet Pruthi -Pure White -pysqz -Qiang Huang -Qin TianHuan -Qinglan Peng -Quan Tian -qudongfang -Quentin Brossard -Quentin Perez -Quentin Tayssier -r0n22 -Rachit Sharma -Radostin Stoyanov -Rafael Fernández López -Rafal Jeczalik -Rafe Colton -Raghavendra K T -Raghuram Devarakonda -Raja Sami -Rajat Pandit -Rajdeep Dua -Ralf Sippl -Ralle -Ralph Bean -Ramkumar Ramachandra -Ramon Brooker -Ramon van Alteren -RaviTeja Pothana -Ray Tsang -ReadmeCritic -realityone -Recursive Madman -Reficul -Regan McCooey -Remi Rampin -Remy Suen -Renato Riccieri Santos Zannon -Renaud Gaubert -Rhys Hiltner -Ri Xu -Ricardo N Feliciano -Rich Horwood -Rich Moyse -Rich Seymour -Richard Burnison -Richard Hansen -Richard Harvey -Richard Mathie -Richard Metzler -Richard Scothern -Richo Healey -Rick Bradley -Rick van de Loo -Rick Wieman -Rik Nijessen -Riku Voipio -Riley Guerin -Ritesh H Shukla -Riyaz Faizullabhoy -Rob Cowsill <42620235+rcowsill@users.noreply.github.com> -Rob Gulewich -Rob Murray -Rob Vesse -Robert Bachmann -Robert Bittle -Robert Obryk -Robert Schneider -Robert Shade -Robert Stern -Robert Sturla -Robert Terhaar -Robert Wallis -Robert Wang -Roberto G. Hashioka -Roberto Muñoz Fernández -Robin Naundorf -Robin Schneider -Robin Speekenbrink -Robin Thoni -robpc -Rodolfo Carvalho -Rodrigo Campos -Rodrigo Vaz -Roel Van Nyen -Roger Peppe -Rohit Jnagal -Rohit Kadam -Rohit Kapur -Rojin George -Roland Huß -Roland Kammerer -Roland Moriz -Roma Sokolov -Roman Dudin -Roman Mazur -Roman Strashkin -Roman Volosatovs -Roman Zabaluev -Ron Smits -Ron Williams -Rong Gao -Rong Zhang -Rongxiang Song -Rony Weng -root -root -root -root -Rory Hunter -Rory McCune -Ross Boucher -Rovanion Luckey -Roy Reznik -Royce Remer -Rozhnov Alexandr -Rudolph Gottesheim -Rui Cao -Rui JingAn -Rui Lopes -Ruilin Li -Runshen Zhu -Russ Magee -Ryan Abrams -Ryan Anderson -Ryan Aslett -Ryan Barry -Ryan Belgrave -Ryan Campbell -Ryan Detzel -Ryan Fowler -Ryan Liu -Ryan McLaughlin -Ryan O'Donnell -Ryan Seto -Ryan Shea -Ryan Simmen -Ryan Stelly -Ryan Thomas -Ryan Trauntvein -Ryan Wallner -Ryan Zhang -ryancooper7 -RyanDeng -Ryo Nakao -Ryoga Saito -Régis Behmo -Rémy Greinhofer -s. rannou -Sabin Basyal -Sachin Joshi -Sagar Hani -Sainath Grandhi -Sakeven Jiang -Salahuddin Khan -Sally O'Malley -Sam Abed -Sam Alba -Sam Bailey -Sam J Sharpe -Sam Neirinck -Sam Reis -Sam Rijs -Sam Thibault -Sam Whited -Sambuddha Basu -Sami Wagiaalla -Samuel Andaya -Samuel Dion-Girardeau -Samuel Karp -Samuel PHAN -sanchayanghosh -Sandeep Bansal -Sankar சங்கர் -Sanket Saurav -Santhosh Manohar -sapphiredev -Sargun Dhillon -Sascha Andres -Sascha Grunert -SataQiu -Satnam Singh -Satoshi Amemiya -Satoshi Tagomori -Scott Bessler -Scott Collier -Scott Johnston -Scott Moser -Scott Percival -Scott Stamp -Scott Walls -sdreyesg -Sean Christopherson -Sean Cronin -Sean Lee -Sean McIntyre -Sean OMeara -Sean P. Kane -Sean Rodman -Sebastiaan van Steenis -Sebastiaan van Stijn -Sebastian Höffner -Sebastian Radloff -Sebastian Thomschke -Sebastien Goasguen -Senthil Kumar Selvaraj -Senthil Kumaran -SeongJae Park -Seongyeol Lim -Serge Hallyn -Sergey Alekseev -Sergey Evstifeev -Sergii Kabashniuk -Sergio Lopez -Serhat Gülçiçek -Serhii Nakon -SeungUkLee -Sevki Hasirci -Shane Canon -Shane da Silva -Shaun Kaasten -Shaun Thompson -shaunol -Shawn Landden -Shawn Siefkas -shawnhe -Shayan Pooya -Shayne Wang -Shekhar Gulati -Sheng Yang -Shengbo Song -Shengjing Zhu -Shev Yan -Shih-Yuan Lee -Shihao Xia -Shijiang Wei -Shijun Qin -Shishir Mahajan -Shoubhik Bose -Shourya Sarcar -Shreenidhi Shedi -Shu-Wai Chow -shuai-z -Shukui Yang -Sian Lerk Lau -Siarhei Rasiukevich -Sidhartha Mani -sidharthamani -Silas Sewell -Silvan Jegen -Simão Reis -Simon Barendse -Simon Eskildsen -Simon Ferquel -Simon Leinen -Simon Menke -Simon Taranto -Simon Vikstrom -Sindhu S -Sjoerd Langkemper -skanehira -Smark Meng -Solganik Alexander -Solomon Hykes -Song Gao -Soshi Katsuta -Sotiris Salloumis -Soulou -Spencer Brown -Spencer Smith -Spike Curtis -Sridatta Thatipamala -Sridhar Ratnakumar -Srini Brahmaroutu -Srinivasan Srivatsan -Staf Wagemakers -Stanislav Bondarenko -Stanislav Levin -Steeve Morin -Stefan Berger -Stefan Gehrig -Stefan J. Wernli -Stefan Praszalowicz -Stefan S. -Stefan Scherer -Stefan Staudenmeyer -Stefan Weil -Steffen Butzer -Stephan Henningsen -Stephan Spindler -Stephen Benjamin -Stephen Crosby -Stephen Day -Stephen Drake -Stephen Rust -Steve Desmond -Steve Dougherty -Steve Durrheimer -Steve Francia -Steve Koch -Steven Burgess -Steven Erenst -Steven Hartland -Steven Iveson -Steven Merrill -Steven Richards -Steven Taylor -Stéphane Este-Gracias -Stig Larsson -Su Wang -Subhajit Ghosh -Sujith Haridasan -Sun Gengze <690388648@qq.com> -Sun Jianbo -Sune Keller -Sunny Gogoi -Suryakumar Sudar -Sven Dowideit -Swapnil Daingade -Sylvain Baubeau -Sylvain Bellemare -Sébastien -Sébastien HOUZÉ -Sébastien Luttringer -Sébastien Stormacq -Sören Tempel -Tabakhase -Tadej Janež -Tadeusz Dudkiewicz -Takuto Sato -tang0th -Tangi Colin -Tatsuki Sugiura -Tatsushi Inagaki -Taylan Isikdemir -Taylor Jones -tcpdumppy <847462026@qq.com> -Ted M. Young -Tehmasp Chaudhri -Tejaswini Duggaraju -Tejesh Mehta -Terry Chu -terryding77 <550147740@qq.com> -Thatcher Peskens -theadactyl -Thell 'Bo' Fowler -Thermionix -Thiago Alves Silva -Thijs Terlouw -Thomas Bikeev -Thomas Frössman -Thomas Gazagnaire -Thomas Graf -Thomas Grainger -Thomas Hansen -Thomas Ledos -Thomas Leonard -Thomas Léveil -Thomas Orozco -Thomas Riccardi -Thomas Schroeter -Thomas Sjögren -Thomas Swift -Thomas Tanaka -Thomas Texier -Ti Zhou -Tiago Seabra -Tianon Gravi -Tianyi Wang -Tibor Vass -Tiffany Jernigan -Tiffany Low -Till Claassen -Till Wegmüller -Tim -Tim Bart -Tim Bosse -Tim Dettrick -Tim Düsterhus -Tim Hockin -Tim Potter -Tim Ruffles -Tim Smith -Tim Terhorst -Tim Wagner -Tim Wang -Tim Waugh -Tim Wraight -Tim Zju <21651152@zju.edu.cn> -timchenxiaoyu <837829664@qq.com> -timfeirg -Timo Rothenpieler -Timothy Hobbs -tjwebb123 -tobe -Tobias Bieniek -Tobias Bradtke -Tobias Gesellchen -Tobias Klauser -Tobias Munk -Tobias Pfandzelter -Tobias Schmidt -Tobias Schwab -Todd Crane -Todd Lunter -Todd Whiteman -Toli Kuznets -Tom Barlow -Tom Booth -Tom Denham -Tom Fotherby -Tom Howe -Tom Hulihan -Tom Maaswinkel -Tom Parker -Tom Sweeney -Tom Wilkie -Tom X. Tobin -Tom Zhao -Tomas Janousek -Tomas Kral -Tomas Tomecek -Tomasz Kopczynski -Tomasz Lipinski -Tomasz Nurkiewicz -Tomek Mańko -Tommaso Visconti -Tomoya Tabuchi -Tomáš Hrčka -Tomáš Virtus -tonic -Tonny Xu -Tony Abboud -Tony Daws -Tony Miller -toogley -Torstein Husebø -Toshiaki Makita -Tõnis Tiigi -Trace Andreason -tracylihui <793912329@qq.com> -Trapier Marshall -Travis Cline -Travis Thieman -Trent Ogren -Trevor -Trevor Pounds -Trevor Sullivan -Trishna Guha -Tristan Carel -Troy Denton -Tudor Brindus -Ty Alexander -Tycho Andersen -Tyler Brock -Tyler Brown -Tzu-Jung Lee -uhayate -Ulysse Carion -Umesh Yadav -Utz Bacher -vagrant -Vaidas Jablonskis -Valentin Kulesh -vanderliang -Velko Ivanov -Veres Lajos -Victor Algaze -Victor Coisne -Victor Costan -Victor I. Wood -Victor Lyuboslavsky -Victor Marmol -Victor Palma -Victor Toni -Victor Vieux -Victoria Bialas -Vijaya Kumar K -Vikas Choudhary -Vikram bir Singh -Viktor Stanchev -Viktor Vojnovski -VinayRaghavanKS -Vincent Batts -Vincent Bernat -Vincent Boulineau -Vincent Demeester -Vincent Giersch -Vincent Mayers -Vincent Woo -Vinod Kulkarni -Vishal Doshi -Vishnu Kannan -Vitaly Ostrosablin -Vitor Anjos -Vitor Monteiro -Vivek Agarwal -Vivek Dasgupta -Vivek Goyal -Vladimir Bulyga -Vladimir Kirillov -Vladimir Pouzanov -Vladimir Rutsky -Vladimir Varankin -VladimirAus -Vladislav Kolesnikov -Vlastimil Zeman -Vojtech Vitek (V-Teq) -voloder <110066198+voloder@users.noreply.github.com> -Walter Leibbrandt -Walter Stanish -Wang Chao -Wang Guoliang -Wang Jie -Wang Long -Wang Ping -Wang Xing -Wang Yuexiao -Wang Yumu <37442693@qq.com> -wanghuaiqing -Ward Vandewege -WarheadsSE -Wassim Dhif -Wataru Ishida -Wayne Chang -Wayne Song -weebney -Weerasak Chongnguluam -Wei Fu -Wei Wu -Wei-Ting Kuo -weipeng -weiyan -Weiyang Zhu -Wen Cheng Ma -Wendel Fleming -Wenjun Tang -Wenkai Yin -wenlxie -Wenxuan Zhao -Wenyu You <21551128@zju.edu.cn> -Wenzhi Liang -Wes Morgan -Wesley Pettit -Wewang Xiaorenfine -Wiktor Kwapisiewicz -Will Dietz -Will Rouesnel -Will Weaver -willhf -William Delanoue -William Henry -William Hubbs -William Martin -William Riancho -William Thurston -Wilson Júnior -Wing-Kam Wong -WiseTrem -Wolfgang Nagele -Wolfgang Powisch -Wonjun Kim -WuLonghui -xamyzhao -Xia Wu -Xian Chaobo -Xianglin Gao -Xianjie -Xianlu Bird -Xiao YongBiao -Xiao Zhang -XiaoBing Jiang -Xiaodong Liu -Xiaodong Zhang -Xiaohua Ding -Xiaoxi He -Xiaoxu Chen -Xiaoyu Zhang -xichengliudui <1693291525@qq.com> -xiekeyang -Ximo Guanter Gonzálbez -xin.li -Xinbo Weng -Xinfeng Liu -Xinzi Zhou -Xiuming Chen -Xuecong Liao -xuzhaokui -Yadnyawalkya Tale -Yahya -yalpul -YAMADA Tsuyoshi -Yamasaki Masahide -Yamazaki Masashi -Yan Feng -Yan Zhu -Yang Bai -Yang Li -Yang Pengfei -yangchenliang -Yann Autissier -Yanqiang Miao -Yao Zaiyong -Yash Murty -Yassine Tijani -Yasunori Mahata -Yazhong Liu -Yestin Sun -Yi EungJun -Yibai Zhang -Yihang Ho -Ying Li -Yohei Ueda -Yong Tang -Yongxin Li -Yongzhi Pan -Yosef Fertel -You-Sheng Yang (楊有勝) -youcai -Youcef YEKHLEF -Youfu Zhang -YR Chen -Yu Changchun -Yu Chengxia -Yu Peng -Yu-Ju Hong -Yuan Sun -Yuanhong Peng -Yue Zhang -Yufei Xiong -Yuhao Fang -Yuichiro Kaneko -YujiOshima -Yunxiang Huang -Yurii Rashkovskii -Yusuf Tarık Günaydın -Yves Blusseau <90z7oey02@sneakemail.com> -Yves Junqueira -Zac Dover -Zach Borboa -Zach Gershman -Zachary Jaffee -Zain Memon -Zaiste! -Zane DeGraffenried -Zefan Li -Zen Lin(Zhinan Lin) -Zhang Kun -Zhang Wei -Zhang Wentao -zhangguanzhang -ZhangHang -zhangxianwei -Zhenan Ye <21551168@zju.edu.cn> -zhenghenghuo -Zhenhai Gao -Zhenkun Bi -ZhiPeng Lu -zhipengzuo -Zhou Hao -Zhoulin Xie -Zhu Guihua -Zhu Kunjia -Zhuoyun Wei -Ziheng Liu -Zilin Du -zimbatm -Ziming Dong -ZJUshuaizhou <21551191@zju.edu.cn> -zmarouf -Zoltan Tombol -Zou Yu -zqh -Zuhayr Elahi -Zunayed Ali -Álvaro Lázaro -Átila Camurça Alves -吴小白 <296015668@qq.com> -尹吉峰 -屈骏 -徐俊杰 -慕陶 -搏通 -黄艳红00139573 -정재영 diff --git a/vendor/github.com/docker/docker/NOTICE b/vendor/github.com/docker/docker/NOTICE deleted file mode 100644 index 58b19b6d15b9..000000000000 --- a/vendor/github.com/docker/docker/NOTICE +++ /dev/null @@ -1,19 +0,0 @@ -Docker -Copyright 2012-2017 Docker, Inc. - -This product includes software developed at Docker, Inc. (https://www.docker.com). - -This product contains software (https://github.com/creack/pty) developed -by Keith Rarick, licensed under the MIT License. - -The following is courtesy of our legal counsel: - - -Use and transfer of Docker may be subject to certain restrictions by the -United States and other governments. -It is your responsibility to ensure that your use and/or transfer does not -violate applicable laws. - -For more information, please see https://www.bis.doc.gov - -See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. diff --git a/vendor/github.com/docker/docker/api/README.md b/vendor/github.com/docker/docker/api/README.md deleted file mode 100644 index 381f19881fad..000000000000 --- a/vendor/github.com/docker/docker/api/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Working on the Engine API - -The Engine API is an HTTP API used by the command-line client to communicate with the daemon. It can also be used by third-party software to control the daemon. - -It consists of various components in this repository: - -- `api/swagger.yaml` A Swagger definition of the API. -- `api/types/` Types shared by both the client and server, representing various objects, options, responses, etc. Most are written manually, but some are automatically generated from the Swagger definition. See [#27919](https://github.com/docker/docker/issues/27919) for progress on this. -- `cli/` The command-line client. -- `client/` The Go client used by the command-line client. It can also be used by third-party Go programs. -- `daemon/` The daemon, which serves the API. - -## Swagger definition - -The API is defined by the [Swagger](http://swagger.io/specification/) definition in `api/swagger.yaml`. This definition can be used to: - -1. Automatically generate documentation. -2. Automatically generate the Go server and client. (A work-in-progress.) -3. Provide a machine readable version of the API for introspecting what it can do, automatically generating clients for other languages, etc. - -## Updating the API documentation - -The API documentation is generated entirely from `api/swagger.yaml`. If you make updates to the API, edit this file to represent the change in the documentation. - -The file is split into two main sections: - -- `definitions`, which defines re-usable objects used in requests and responses -- `paths`, which defines the API endpoints (and some inline objects which don't need to be reusable) - -To make an edit, first look for the endpoint you want to edit under `paths`, then make the required edits. Endpoints may reference reusable objects with `$ref`, which can be found in the `definitions` section. - -There is hopefully enough example material in the file for you to copy a similar pattern from elsewhere in the file (e.g. adding new fields or endpoints), but for the full reference, see the [Swagger specification](https://github.com/docker/docker/issues/27919). - -`swagger.yaml` is validated by `hack/validate/swagger` to ensure it is a valid Swagger definition. This is useful when making edits to ensure you are doing the right thing. - -## Viewing the API documentation - -When you make edits to `swagger.yaml`, you may want to check the generated API documentation to ensure it renders correctly. - -Run `make swagger-docs` and a preview will be running at `http://localhost:9000`. Some of the styling may be incorrect, but you'll be able to ensure that it is generating the correct documentation. - -The production documentation is generated by vendoring `swagger.yaml` into [docker/docker.github.io](https://github.com/docker/docker.github.io). diff --git a/vendor/github.com/docker/docker/api/common.go b/vendor/github.com/docker/docker/api/common.go deleted file mode 100644 index 702d3dcae4f9..000000000000 --- a/vendor/github.com/docker/docker/api/common.go +++ /dev/null @@ -1,20 +0,0 @@ -package api - -// Common constants for daemon and client. -const ( - // DefaultVersion of the current REST API. - DefaultVersion = "1.51" - - // MinSupportedAPIVersion is the minimum API version that can be supported - // by the API server, specified as "major.minor". Note that the daemon - // may be configured with a different minimum API version, as returned - // in [github.com/docker/docker/api/types.Version.MinAPIVersion]. - // - // API requests for API versions lower than the configured version produce - // an error. - MinSupportedAPIVersion = "1.24" - - // NoBaseImageSpecifier is the symbol used by the FROM - // command to specify that no base image is to be used. - NoBaseImageSpecifier = "scratch" -) diff --git a/vendor/github.com/docker/docker/api/swagger-gen.yaml b/vendor/github.com/docker/docker/api/swagger-gen.yaml deleted file mode 100644 index f07a02737f73..000000000000 --- a/vendor/github.com/docker/docker/api/swagger-gen.yaml +++ /dev/null @@ -1,12 +0,0 @@ - -layout: - models: - - name: definition - source: asset:model - target: "{{ joinFilePath .Target .ModelPackage }}" - file_name: "{{ (snakize (pascalize .Name)) }}.go" - operations: - - name: handler - source: asset:serverOperation - target: "{{ joinFilePath .Target .APIPackage .Package }}" - file_name: "{{ (snakize (pascalize .Name)) }}.go" diff --git a/vendor/github.com/docker/docker/api/swagger.yaml b/vendor/github.com/docker/docker/api/swagger.yaml deleted file mode 100644 index 8d6a8f9356a4..000000000000 --- a/vendor/github.com/docker/docker/api/swagger.yaml +++ /dev/null @@ -1,13430 +0,0 @@ -# A Swagger 2.0 (a.k.a. OpenAPI) definition of the Engine API. -# -# This is used for generating API documentation and the types used by the -# client/server. See api/README.md for more information. -# -# Some style notes: -# - This file is used by ReDoc, which allows GitHub Flavored Markdown in -# descriptions. -# - There is no maximum line length, for ease of editing and pretty diffs. -# - operationIds are in the format "NounVerb", with a singular noun. - -swagger: "2.0" -schemes: - - "http" - - "https" -produces: - - "application/json" - - "text/plain" -consumes: - - "application/json" - - "text/plain" -basePath: "/v1.51" -info: - title: "Docker Engine API" - version: "1.51" - x-logo: - url: "https://docs.docker.com/assets/images/logo-docker-main.png" - description: | - The Engine API is an HTTP API served by Docker Engine. It is the API the - Docker client uses to communicate with the Engine, so everything the Docker - client can do can be done with the API. - - Most of the client's commands map directly to API endpoints (e.g. `docker ps` - is `GET /containers/json`). The notable exception is running containers, - which consists of several API calls. - - # Errors - - The API uses standard HTTP status codes to indicate the success or failure - of the API call. The body of the response will be JSON in the following - format: - - ``` - { - "message": "page not found" - } - ``` - - # Versioning - - The API is usually changed in each release, so API calls are versioned to - ensure that clients don't break. To lock to a specific version of the API, - you prefix the URL with its version, for example, call `/v1.30/info` to use - the v1.30 version of the `/info` endpoint. If the API version specified in - the URL is not supported by the daemon, a HTTP `400 Bad Request` error message - is returned. - - If you omit the version-prefix, the current version of the API (v1.50) is used. - For example, calling `/info` is the same as calling `/v1.51/info`. Using the - API without a version-prefix is deprecated and will be removed in a future release. - - Engine releases in the near future should support this version of the API, - so your client will continue to work even if it is talking to a newer Engine. - - The API uses an open schema model, which means the server may add extra properties - to responses. Likewise, the server will ignore any extra query parameters and - request body properties. When you write clients, you need to ignore additional - properties in responses to ensure they do not break when talking to newer - daemons. - - - # Authentication - - Authentication for registries is handled client side. The client has to send - authentication details to various endpoints that need to communicate with - registries, such as `POST /images/(name)/push`. These are sent as - `X-Registry-Auth` header as a [base64url encoded](https://tools.ietf.org/html/rfc4648#section-5) - (JSON) string with the following structure: - - ``` - { - "username": "string", - "password": "string", - "email": "string", - "serveraddress": "string" - } - ``` - - The `serveraddress` is a domain/IP without a protocol. Throughout this - structure, double quotes are required. - - If you have already got an identity token from the [`/auth` endpoint](#operation/SystemAuth), - you can just pass this instead of credentials: - - ``` - { - "identitytoken": "9cbaf023786cd7..." - } - ``` - -# The tags on paths define the menu sections in the ReDoc documentation, so -# the usage of tags must make sense for that: -# - They should be singular, not plural. -# - There should not be too many tags, or the menu becomes unwieldy. For -# example, it is preferable to add a path to the "System" tag instead of -# creating a tag with a single path in it. -# - The order of tags in this list defines the order in the menu. -tags: - # Primary objects - - name: "Container" - x-displayName: "Containers" - description: | - Create and manage containers. - - name: "Image" - x-displayName: "Images" - - name: "Network" - x-displayName: "Networks" - description: | - Networks are user-defined networks that containers can be attached to. - See the [networking documentation](https://docs.docker.com/network/) - for more information. - - name: "Volume" - x-displayName: "Volumes" - description: | - Create and manage persistent storage that can be attached to containers. - - name: "Exec" - x-displayName: "Exec" - description: | - Run new commands inside running containers. Refer to the - [command-line reference](https://docs.docker.com/engine/reference/commandline/exec/) - for more information. - - To exec a command in a container, you first need to create an exec instance, - then start it. These two API endpoints are wrapped up in a single command-line - command, `docker exec`. - - # Swarm things - - name: "Swarm" - x-displayName: "Swarm" - description: | - Engines can be clustered together in a swarm. Refer to the - [swarm mode documentation](https://docs.docker.com/engine/swarm/) - for more information. - - name: "Node" - x-displayName: "Nodes" - description: | - Nodes are instances of the Engine participating in a swarm. Swarm mode - must be enabled for these endpoints to work. - - name: "Service" - x-displayName: "Services" - description: | - Services are the definitions of tasks to run on a swarm. Swarm mode must - be enabled for these endpoints to work. - - name: "Task" - x-displayName: "Tasks" - description: | - A task is a container running on a swarm. It is the atomic scheduling unit - of swarm. Swarm mode must be enabled for these endpoints to work. - - name: "Secret" - x-displayName: "Secrets" - description: | - Secrets are sensitive data that can be used by services. Swarm mode must - be enabled for these endpoints to work. - - name: "Config" - x-displayName: "Configs" - description: | - Configs are application configurations that can be used by services. Swarm - mode must be enabled for these endpoints to work. - # System things - - name: "Plugin" - x-displayName: "Plugins" - - name: "System" - x-displayName: "System" - -definitions: - Port: - type: "object" - description: "An open port on a container" - required: [PrivatePort, Type] - properties: - IP: - type: "string" - format: "ip-address" - description: "Host IP address that the container's port is mapped to" - PrivatePort: - type: "integer" - format: "uint16" - x-nullable: false - description: "Port on the container" - PublicPort: - type: "integer" - format: "uint16" - description: "Port exposed on the host" - Type: - type: "string" - x-nullable: false - enum: ["tcp", "udp", "sctp"] - example: - PrivatePort: 8080 - PublicPort: 80 - Type: "tcp" - - MountPoint: - type: "object" - description: | - MountPoint represents a mount point configuration inside the container. - This is used for reporting the mountpoints in use by a container. - properties: - Type: - description: | - The mount type: - - - `bind` a mount of a file or directory from the host into the container. - - `volume` a docker volume with the given `Name`. - - `image` a docker image - - `tmpfs` a `tmpfs`. - - `npipe` a named pipe from the host into the container. - - `cluster` a Swarm cluster volume - type: "string" - enum: - - "bind" - - "volume" - - "image" - - "tmpfs" - - "npipe" - - "cluster" - example: "volume" - Name: - description: | - Name is the name reference to the underlying data defined by `Source` - e.g., the volume name. - type: "string" - example: "myvolume" - Source: - description: | - Source location of the mount. - - For volumes, this contains the storage location of the volume (within - `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains - the source (host) part of the bind-mount. For `tmpfs` mount points, this - field is empty. - type: "string" - example: "/var/lib/docker/volumes/myvolume/_data" - Destination: - description: | - Destination is the path relative to the container root (`/`) where - the `Source` is mounted inside the container. - type: "string" - example: "/usr/share/nginx/html/" - Driver: - description: | - Driver is the volume driver used to create the volume (if it is a volume). - type: "string" - example: "local" - Mode: - description: | - Mode is a comma separated list of options supplied by the user when - creating the bind/volume mount. - - The default is platform-specific (`"z"` on Linux, empty on Windows). - type: "string" - example: "z" - RW: - description: | - Whether the mount is mounted writable (read-write). - type: "boolean" - example: true - Propagation: - description: | - Propagation describes how mounts are propagated from the host into the - mount point, and vice-versa. Refer to the [Linux kernel documentation](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) - for details. This field is not used on Windows. - type: "string" - example: "" - - DeviceMapping: - type: "object" - description: "A device mapping between the host and container" - properties: - PathOnHost: - type: "string" - PathInContainer: - type: "string" - CgroupPermissions: - type: "string" - example: - PathOnHost: "/dev/deviceName" - PathInContainer: "/dev/deviceName" - CgroupPermissions: "mrw" - - DeviceRequest: - type: "object" - description: "A request for devices to be sent to device drivers" - properties: - Driver: - type: "string" - example: "nvidia" - Count: - type: "integer" - example: -1 - DeviceIDs: - type: "array" - items: - type: "string" - example: - - "0" - - "1" - - "GPU-fef8089b-4820-abfc-e83e-94318197576e" - Capabilities: - description: | - A list of capabilities; an OR list of AND lists of capabilities. - type: "array" - items: - type: "array" - items: - type: "string" - example: - # gpu AND nvidia AND compute - - ["gpu", "nvidia", "compute"] - Options: - description: | - Driver-specific options, specified as a key/value pairs. These options - are passed directly to the driver. - type: "object" - additionalProperties: - type: "string" - - ThrottleDevice: - type: "object" - properties: - Path: - description: "Device path" - type: "string" - Rate: - description: "Rate" - type: "integer" - format: "int64" - minimum: 0 - - Mount: - type: "object" - properties: - Target: - description: "Container path." - type: "string" - Source: - description: "Mount source (e.g. a volume name, a host path)." - type: "string" - Type: - description: | - The mount type. Available types: - - - `bind` Mounts a file or directory from the host into the container. Must exist prior to creating the container. - - `volume` Creates a volume with the given name and options (or uses a pre-existing volume with the same name and options). These are **not** removed when the container is removed. - - `image` Mounts an image. - - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. - - `cluster` a Swarm cluster volume - type: "string" - enum: - - "bind" - - "volume" - - "image" - - "tmpfs" - - "npipe" - - "cluster" - ReadOnly: - description: "Whether the mount should be read-only." - type: "boolean" - Consistency: - description: "The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`." - type: "string" - BindOptions: - description: "Optional configuration for the `bind` type." - type: "object" - properties: - Propagation: - description: "A propagation mode with the value `[r]private`, `[r]shared`, or `[r]slave`." - type: "string" - enum: - - "private" - - "rprivate" - - "shared" - - "rshared" - - "slave" - - "rslave" - NonRecursive: - description: "Disable recursive bind mount." - type: "boolean" - default: false - CreateMountpoint: - description: "Create mount point on host if missing" - type: "boolean" - default: false - ReadOnlyNonRecursive: - description: | - Make the mount non-recursively read-only, but still leave the mount recursive - (unless NonRecursive is set to `true` in conjunction). - - Added in v1.44, before that version all read-only mounts were - non-recursive by default. To match the previous behaviour this - will default to `true` for clients on versions prior to v1.44. - type: "boolean" - default: false - ReadOnlyForceRecursive: - description: "Raise an error if the mount cannot be made recursively read-only." - type: "boolean" - default: false - VolumeOptions: - description: "Optional configuration for the `volume` type." - type: "object" - properties: - NoCopy: - description: "Populate volume with data from the target." - type: "boolean" - default: false - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - DriverConfig: - description: "Map of driver specific options" - type: "object" - properties: - Name: - description: "Name of the driver to use to create the volume." - type: "string" - Options: - description: "key/value map of driver specific options." - type: "object" - additionalProperties: - type: "string" - Subpath: - description: "Source path inside the volume. Must be relative without any back traversals." - type: "string" - example: "dir-inside-volume/subdirectory" - ImageOptions: - description: "Optional configuration for the `image` type." - type: "object" - properties: - Subpath: - description: "Source path inside the image. Must be relative without any back traversals." - type: "string" - example: "dir-inside-image/subdirectory" - TmpfsOptions: - description: "Optional configuration for the `tmpfs` type." - type: "object" - properties: - SizeBytes: - description: "The size for the tmpfs mount in bytes." - type: "integer" - format: "int64" - Mode: - description: "The permission mode for the tmpfs mount in an integer." - type: "integer" - Options: - description: | - The options to be passed to the tmpfs mount. An array of arrays. - Flag options should be provided as 1-length arrays. Other types - should be provided as as 2-length arrays, where the first item is - the key and the second the value. - type: "array" - items: - type: "array" - minItems: 1 - maxItems: 2 - items: - type: "string" - example: - [["noexec"]] - - RestartPolicy: - description: | - The behavior to apply when the container exits. The default is not to - restart. - - An ever increasing delay (double the previous delay, starting at 100ms) is - added before each restart to prevent flooding the server. - type: "object" - properties: - Name: - type: "string" - description: | - - Empty string means not to restart - - `no` Do not automatically restart - - `always` Always restart - - `unless-stopped` Restart always except when the user has manually stopped the container - - `on-failure` Restart only when the container exit code is non-zero - enum: - - "" - - "no" - - "always" - - "unless-stopped" - - "on-failure" - MaximumRetryCount: - type: "integer" - description: | - If `on-failure` is used, the number of times to retry before giving up. - - Resources: - description: "A container's resources (cgroups config, ulimits, etc)" - type: "object" - properties: - # Applicable to all platforms - CpuShares: - description: | - An integer value representing this container's relative CPU weight - versus other containers. - type: "integer" - Memory: - description: "Memory limit in bytes." - type: "integer" - format: "int64" - default: 0 - # Applicable to UNIX platforms - CgroupParent: - description: | - Path to `cgroups` under which the container's `cgroup` is created. If - the path is not absolute, the path is considered to be relative to the - `cgroups` path of the init process. Cgroups are created if they do not - already exist. - type: "string" - BlkioWeight: - description: "Block IO weight (relative weight)." - type: "integer" - minimum: 0 - maximum: 1000 - BlkioWeightDevice: - description: | - Block IO weight (relative device weight) in the form: - - ``` - [{"Path": "device_path", "Weight": weight}] - ``` - type: "array" - items: - type: "object" - properties: - Path: - type: "string" - Weight: - type: "integer" - minimum: 0 - BlkioDeviceReadBps: - description: | - Limit read rate (bytes per second) from a device, in the form: - - ``` - [{"Path": "device_path", "Rate": rate}] - ``` - type: "array" - items: - $ref: "#/definitions/ThrottleDevice" - BlkioDeviceWriteBps: - description: | - Limit write rate (bytes per second) to a device, in the form: - - ``` - [{"Path": "device_path", "Rate": rate}] - ``` - type: "array" - items: - $ref: "#/definitions/ThrottleDevice" - BlkioDeviceReadIOps: - description: | - Limit read rate (IO per second) from a device, in the form: - - ``` - [{"Path": "device_path", "Rate": rate}] - ``` - type: "array" - items: - $ref: "#/definitions/ThrottleDevice" - BlkioDeviceWriteIOps: - description: | - Limit write rate (IO per second) to a device, in the form: - - ``` - [{"Path": "device_path", "Rate": rate}] - ``` - type: "array" - items: - $ref: "#/definitions/ThrottleDevice" - CpuPeriod: - description: "The length of a CPU period in microseconds." - type: "integer" - format: "int64" - CpuQuota: - description: | - Microseconds of CPU time that the container can get in a CPU period. - type: "integer" - format: "int64" - CpuRealtimePeriod: - description: | - The length of a CPU real-time period in microseconds. Set to 0 to - allocate no time allocated to real-time tasks. - type: "integer" - format: "int64" - CpuRealtimeRuntime: - description: | - The length of a CPU real-time runtime in microseconds. Set to 0 to - allocate no time allocated to real-time tasks. - type: "integer" - format: "int64" - CpusetCpus: - description: | - CPUs in which to allow execution (e.g., `0-3`, `0,1`). - type: "string" - example: "0-3" - CpusetMems: - description: | - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only - effective on NUMA systems. - type: "string" - Devices: - description: "A list of devices to add to the container." - type: "array" - items: - $ref: "#/definitions/DeviceMapping" - DeviceCgroupRules: - description: "a list of cgroup rules to apply to the container" - type: "array" - items: - type: "string" - example: "c 13:* rwm" - DeviceRequests: - description: | - A list of requests for devices to be sent to device drivers. - type: "array" - items: - $ref: "#/definitions/DeviceRequest" - KernelMemoryTCP: - description: | - Hard limit for kernel TCP buffer memory (in bytes). Depending on the - OCI runtime in use, this option may be ignored. It is no longer supported - by the default (runc) runtime. - - This field is omitted when empty. - type: "integer" - format: "int64" - MemoryReservation: - description: "Memory soft limit in bytes." - type: "integer" - format: "int64" - MemorySwap: - description: | - Total memory limit (memory + swap). Set as `-1` to enable unlimited - swap. - type: "integer" - format: "int64" - MemorySwappiness: - description: | - Tune a container's memory swappiness behavior. Accepts an integer - between 0 and 100. - type: "integer" - format: "int64" - minimum: 0 - maximum: 100 - NanoCpus: - description: "CPU quota in units of 10-9 CPUs." - type: "integer" - format: "int64" - OomKillDisable: - description: "Disable OOM Killer for the container." - type: "boolean" - Init: - description: | - Run an init inside the container that forwards signals and reaps - processes. This field is omitted if empty, and the default (as - configured on the daemon) is used. - type: "boolean" - x-nullable: true - PidsLimit: - description: | - Tune a container's PIDs limit. Set `0` or `-1` for unlimited, or `null` - to not change. - type: "integer" - format: "int64" - x-nullable: true - Ulimits: - description: | - A list of resource limits to set in the container. For example: - - ``` - {"Name": "nofile", "Soft": 1024, "Hard": 2048} - ``` - type: "array" - items: - type: "object" - properties: - Name: - description: "Name of ulimit" - type: "string" - Soft: - description: "Soft limit" - type: "integer" - Hard: - description: "Hard limit" - type: "integer" - # Applicable to Windows - CpuCount: - description: | - The number of usable CPUs (Windows only). - - On Windows Server containers, the processor resource controls are - mutually exclusive. The order of precedence is `CPUCount` first, then - `CPUShares`, and `CPUPercent` last. - type: "integer" - format: "int64" - CpuPercent: - description: | - The usable percentage of the available CPUs (Windows only). - - On Windows Server containers, the processor resource controls are - mutually exclusive. The order of precedence is `CPUCount` first, then - `CPUShares`, and `CPUPercent` last. - type: "integer" - format: "int64" - IOMaximumIOps: - description: "Maximum IOps for the container system drive (Windows only)" - type: "integer" - format: "int64" - IOMaximumBandwidth: - description: | - Maximum IO in bytes per second for the container system drive - (Windows only). - type: "integer" - format: "int64" - - Limit: - description: | - An object describing a limit on resources which can be requested by a task. - type: "object" - properties: - NanoCPUs: - type: "integer" - format: "int64" - example: 4000000000 - MemoryBytes: - type: "integer" - format: "int64" - example: 8272408576 - Pids: - description: | - Limits the maximum number of PIDs in the container. Set `0` for unlimited. - type: "integer" - format: "int64" - default: 0 - example: 100 - - ResourceObject: - description: | - An object describing the resources which can be advertised by a node and - requested by a task. - type: "object" - properties: - NanoCPUs: - type: "integer" - format: "int64" - example: 4000000000 - MemoryBytes: - type: "integer" - format: "int64" - example: 8272408576 - GenericResources: - $ref: "#/definitions/GenericResources" - - GenericResources: - description: | - User-defined resources can be either Integer resources (e.g, `SSD=3`) or - String resources (e.g, `GPU=UUID1`). - type: "array" - items: - type: "object" - properties: - NamedResourceSpec: - type: "object" - properties: - Kind: - type: "string" - Value: - type: "string" - DiscreteResourceSpec: - type: "object" - properties: - Kind: - type: "string" - Value: - type: "integer" - format: "int64" - example: - - DiscreteResourceSpec: - Kind: "SSD" - Value: 3 - - NamedResourceSpec: - Kind: "GPU" - Value: "UUID1" - - NamedResourceSpec: - Kind: "GPU" - Value: "UUID2" - - HealthConfig: - description: "A test to perform to check that the container is healthy." - type: "object" - properties: - Test: - description: | - The test to perform. Possible values are: - - - `[]` inherit healthcheck from image or parent image - - `["NONE"]` disable healthcheck - - `["CMD", args...]` exec arguments directly - - `["CMD-SHELL", command]` run command with system's default shell - type: "array" - items: - type: "string" - Interval: - description: | - The time to wait between checks in nanoseconds. It should be 0 or at - least 1000000 (1 ms). 0 means inherit. - type: "integer" - format: "int64" - Timeout: - description: | - The time to wait before considering the check to have hung. It should - be 0 or at least 1000000 (1 ms). 0 means inherit. - type: "integer" - format: "int64" - Retries: - description: | - The number of consecutive failures needed to consider a container as - unhealthy. 0 means inherit. - type: "integer" - StartPeriod: - description: | - Start period for the container to initialize before starting - health-retries countdown in nanoseconds. It should be 0 or at least - 1000000 (1 ms). 0 means inherit. - type: "integer" - format: "int64" - StartInterval: - description: | - The time to wait between checks in nanoseconds during the start period. - It should be 0 or at least 1000000 (1 ms). 0 means inherit. - type: "integer" - format: "int64" - - Health: - description: | - Health stores information about the container's healthcheck results. - type: "object" - x-nullable: true - properties: - Status: - description: | - Status is one of `none`, `starting`, `healthy` or `unhealthy` - - - "none" Indicates there is no healthcheck - - "starting" Starting indicates that the container is not yet ready - - "healthy" Healthy indicates that the container is running correctly - - "unhealthy" Unhealthy indicates that the container has a problem - type: "string" - enum: - - "none" - - "starting" - - "healthy" - - "unhealthy" - example: "healthy" - FailingStreak: - description: "FailingStreak is the number of consecutive failures" - type: "integer" - example: 0 - Log: - type: "array" - description: | - Log contains the last few results (oldest first) - items: - $ref: "#/definitions/HealthcheckResult" - - HealthcheckResult: - description: | - HealthcheckResult stores information about a single run of a healthcheck probe - type: "object" - x-nullable: true - properties: - Start: - description: | - Date and time at which this check started in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "date-time" - example: "2020-01-04T10:44:24.496525531Z" - End: - description: | - Date and time at which this check ended in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2020-01-04T10:45:21.364524523Z" - ExitCode: - description: | - ExitCode meanings: - - - `0` healthy - - `1` unhealthy - - `2` reserved (considered unhealthy) - - other values: error running probe - type: "integer" - example: 0 - Output: - description: "Output from last check" - type: "string" - - HostConfig: - description: "Container configuration that depends on the host we are running on" - allOf: - - $ref: "#/definitions/Resources" - - type: "object" - properties: - # Applicable to all platforms - Binds: - type: "array" - description: | - A list of volume bindings for this container. Each volume binding - is a string in one of these forms: - - - `host-src:container-dest[:options]` to bind-mount a host path - into the container. Both `host-src`, and `container-dest` must - be an _absolute_ path. - - `volume-name:container-dest[:options]` to bind-mount a volume - managed by a volume driver into the container. `container-dest` - must be an _absolute_ path. - - `options` is an optional, comma-delimited list of: - - - `nocopy` disables automatic copying of data from the container - path to the volume. The `nocopy` flag only applies to named volumes. - - `[ro|rw]` mounts a volume read-only or read-write, respectively. - If omitted or set to `rw`, volumes are mounted read-write. - - `[z|Z]` applies SELinux labels to allow or deny multiple containers - to read and write to the same volume. - - `z`: a _shared_ content label is applied to the content. This - label indicates that multiple containers can share the volume - content, for both reading and writing. - - `Z`: a _private unshared_ label is applied to the content. - This label indicates that only the current container can use - a private volume. Labeling systems such as SELinux require - proper labels to be placed on volume content that is mounted - into a container. Without a label, the security system can - prevent a container's processes from using the content. By - default, the labels set by the host operating system are not - modified. - - `[[r]shared|[r]slave|[r]private]` specifies mount - [propagation behavior](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt). - This only applies to bind-mounted volumes, not internal volumes - or named volumes. Mount propagation requires the source mount - point (the location where the source directory is mounted in the - host operating system) to have the correct propagation properties. - For shared volumes, the source mount point must be set to `shared`. - For slave volumes, the mount must be set to either `shared` or - `slave`. - items: - type: "string" - ContainerIDFile: - type: "string" - description: "Path to a file where the container ID is written" - example: "" - LogConfig: - type: "object" - description: "The logging configuration for this container" - properties: - Type: - description: |- - Name of the logging driver used for the container or "none" - if logging is disabled. - type: "string" - enum: - - "local" - - "json-file" - - "syslog" - - "journald" - - "gelf" - - "fluentd" - - "awslogs" - - "splunk" - - "etwlogs" - - "none" - Config: - description: |- - Driver-specific configuration options for the logging driver. - type: "object" - additionalProperties: - type: "string" - example: - "max-file": "5" - "max-size": "10m" - NetworkMode: - type: "string" - description: | - Network mode to use for this container. Supported standard values - are: `bridge`, `host`, `none`, and `container:`. Any - other value is taken as a custom network's name to which this - container should connect to. - PortBindings: - $ref: "#/definitions/PortMap" - RestartPolicy: - $ref: "#/definitions/RestartPolicy" - AutoRemove: - type: "boolean" - description: | - Automatically remove the container when the container's process - exits. This has no effect if `RestartPolicy` is set. - VolumeDriver: - type: "string" - description: "Driver that this container uses to mount volumes." - VolumesFrom: - type: "array" - description: | - A list of volumes to inherit from another container, specified in - the form `[:]`. - items: - type: "string" - Mounts: - description: | - Specification for mounts to be added to the container. - type: "array" - items: - $ref: "#/definitions/Mount" - ConsoleSize: - type: "array" - description: | - Initial console size, as an `[height, width]` array. - x-nullable: true - minItems: 2 - maxItems: 2 - items: - type: "integer" - minimum: 0 - example: [80, 64] - Annotations: - type: "object" - description: | - Arbitrary non-identifying metadata attached to container and - provided to the runtime when the container is started. - additionalProperties: - type: "string" - - # Applicable to UNIX platforms - CapAdd: - type: "array" - description: | - A list of kernel capabilities to add to the container. Conflicts - with option 'Capabilities'. - items: - type: "string" - CapDrop: - type: "array" - description: | - A list of kernel capabilities to drop from the container. Conflicts - with option 'Capabilities'. - items: - type: "string" - CgroupnsMode: - type: "string" - enum: - - "private" - - "host" - description: | - cgroup namespace mode for the container. Possible values are: - - - `"private"`: the container runs in its own private cgroup namespace - - `"host"`: use the host system's cgroup namespace - - If not specified, the daemon default is used, which can either be `"private"` - or `"host"`, depending on daemon version, kernel support and configuration. - Dns: - type: "array" - description: "A list of DNS servers for the container to use." - items: - type: "string" - DnsOptions: - type: "array" - description: "A list of DNS options." - items: - type: "string" - DnsSearch: - type: "array" - description: "A list of DNS search domains." - items: - type: "string" - ExtraHosts: - type: "array" - description: | - A list of hostnames/IP mappings to add to the container's `/etc/hosts` - file. Specified in the form `["hostname:IP"]`. - items: - type: "string" - GroupAdd: - type: "array" - description: | - A list of additional groups that the container process will run as. - items: - type: "string" - IpcMode: - type: "string" - description: | - IPC sharing mode for the container. Possible values are: - - - `"none"`: own private IPC namespace, with /dev/shm not mounted - - `"private"`: own private IPC namespace - - `"shareable"`: own private IPC namespace, with a possibility to share it with other containers - - `"container:"`: join another (shareable) container's IPC namespace - - `"host"`: use the host system's IPC namespace - - If not specified, daemon default is used, which can either be `"private"` - or `"shareable"`, depending on daemon version and configuration. - Cgroup: - type: "string" - description: "Cgroup to use for the container." - Links: - type: "array" - description: | - A list of links for the container in the form `container_name:alias`. - items: - type: "string" - OomScoreAdj: - type: "integer" - description: | - An integer value containing the score given to the container in - order to tune OOM killer preferences. - example: 500 - PidMode: - type: "string" - description: | - Set the PID (Process) Namespace mode for the container. It can be - either: - - - `"container:"`: joins another container's PID namespace - - `"host"`: use the host's PID namespace inside the container - Privileged: - type: "boolean" - description: |- - Gives the container full access to the host. - PublishAllPorts: - type: "boolean" - description: | - Allocates an ephemeral host port for all of a container's - exposed ports. - - Ports are de-allocated when the container stops and allocated when - the container starts. The allocated port might be changed when - restarting the container. - - The port is selected from the ephemeral port range that depends on - the kernel. For example, on Linux the range is defined by - `/proc/sys/net/ipv4/ip_local_port_range`. - ReadonlyRootfs: - type: "boolean" - description: "Mount the container's root filesystem as read only." - SecurityOpt: - type: "array" - description: | - A list of string values to customize labels for MLS systems, such - as SELinux. - items: - type: "string" - StorageOpt: - type: "object" - description: | - Storage driver options for this container, in the form `{"size": "120G"}`. - additionalProperties: - type: "string" - Tmpfs: - type: "object" - description: | - A map of container directories which should be replaced by tmpfs - mounts, and their corresponding mount options. For example: - - ``` - { "/run": "rw,noexec,nosuid,size=65536k" } - ``` - additionalProperties: - type: "string" - UTSMode: - type: "string" - description: "UTS namespace to use for the container." - UsernsMode: - type: "string" - description: | - Sets the usernamespace mode for the container when usernamespace - remapping option is enabled. - ShmSize: - type: "integer" - format: "int64" - description: | - Size of `/dev/shm` in bytes. If omitted, the system uses 64MB. - minimum: 0 - Sysctls: - type: "object" - x-nullable: true - description: |- - A list of kernel parameters (sysctls) to set in the container. - - This field is omitted if not set. - additionalProperties: - type: "string" - example: - "net.ipv4.ip_forward": "1" - Runtime: - type: "string" - x-nullable: true - description: |- - Runtime to use with this container. - # Applicable to Windows - Isolation: - type: "string" - description: | - Isolation technology of the container. (Windows only) - enum: - - "default" - - "process" - - "hyperv" - - "" - MaskedPaths: - type: "array" - description: | - The list of paths to be masked inside the container (this overrides - the default set of paths). - items: - type: "string" - example: - - "/proc/asound" - - "/proc/acpi" - - "/proc/kcore" - - "/proc/keys" - - "/proc/latency_stats" - - "/proc/timer_list" - - "/proc/timer_stats" - - "/proc/sched_debug" - - "/proc/scsi" - - "/sys/firmware" - - "/sys/devices/virtual/powercap" - ReadonlyPaths: - type: "array" - description: | - The list of paths to be set as read-only inside the container - (this overrides the default set of paths). - items: - type: "string" - example: - - "/proc/bus" - - "/proc/fs" - - "/proc/irq" - - "/proc/sys" - - "/proc/sysrq-trigger" - - ContainerConfig: - description: | - Configuration for a container that is portable between hosts. - type: "object" - properties: - Hostname: - description: | - The hostname to use for the container, as a valid RFC 1123 hostname. - type: "string" - example: "439f4e91bd1d" - Domainname: - description: | - The domain name to use for the container. - type: "string" - User: - description: |- - Commands run as this user inside the container. If omitted, commands - run as the user specified in the image the container was started from. - - Can be either user-name or UID, and optional group-name or GID, - separated by a colon (`[<:group-name|GID>]`). - type: "string" - example: "123:456" - AttachStdin: - description: "Whether to attach to `stdin`." - type: "boolean" - default: false - AttachStdout: - description: "Whether to attach to `stdout`." - type: "boolean" - default: true - AttachStderr: - description: "Whether to attach to `stderr`." - type: "boolean" - default: true - ExposedPorts: - description: | - An object mapping ports to an empty object in the form: - - `{"/": {}}` - type: "object" - x-nullable: true - additionalProperties: - type: "object" - enum: - - {} - default: {} - example: { - "80/tcp": {}, - "443/tcp": {} - } - Tty: - description: | - Attach standard streams to a TTY, including `stdin` if it is not closed. - type: "boolean" - default: false - OpenStdin: - description: "Open `stdin`" - type: "boolean" - default: false - StdinOnce: - description: "Close `stdin` after one attached client disconnects" - type: "boolean" - default: false - Env: - description: | - A list of environment variables to set inside the container in the - form `["VAR=value", ...]`. A variable without `=` is removed from the - environment, rather than to have an empty value. - type: "array" - items: - type: "string" - example: - - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - Cmd: - description: | - Command to run specified as a string or an array of strings. - type: "array" - items: - type: "string" - example: ["/bin/sh"] - Healthcheck: - $ref: "#/definitions/HealthConfig" - ArgsEscaped: - description: "Command is already escaped (Windows only)" - type: "boolean" - default: false - example: false - x-nullable: true - Image: - description: | - The name (or reference) of the image to use when creating the container, - or which was used when the container was created. - type: "string" - example: "example-image:1.0" - Volumes: - description: | - An object mapping mount point paths inside the container to empty - objects. - type: "object" - additionalProperties: - type: "object" - enum: - - {} - default: {} - WorkingDir: - description: "The working directory for commands to run in." - type: "string" - example: "/public/" - Entrypoint: - description: | - The entry point for the container as a string or an array of strings. - - If the array consists of exactly one empty string (`[""]`) then the - entry point is reset to system default (i.e., the entry point used by - docker when there is no `ENTRYPOINT` instruction in the `Dockerfile`). - type: "array" - items: - type: "string" - example: [] - NetworkDisabled: - description: "Disable networking for the container." - type: "boolean" - x-nullable: true - MacAddress: - description: | - MAC address of the container. - - Deprecated: this field is deprecated in API v1.44 and up. Use EndpointSettings.MacAddress instead. - type: "string" - x-nullable: true - OnBuild: - description: | - `ONBUILD` metadata that were defined in the image's `Dockerfile`. - type: "array" - x-nullable: true - items: - type: "string" - example: [] - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - StopSignal: - description: | - Signal to stop a container as a string or unsigned integer. - type: "string" - example: "SIGTERM" - x-nullable: true - StopTimeout: - description: "Timeout to stop a container in seconds." - type: "integer" - default: 10 - x-nullable: true - Shell: - description: | - Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell. - type: "array" - x-nullable: true - items: - type: "string" - example: ["/bin/sh", "-c"] - - ImageConfig: - description: | - Configuration of the image. These fields are used as defaults - when starting a container from the image. - type: "object" - properties: - User: - description: "The user that commands are run as inside the container." - type: "string" - example: "web:web" - ExposedPorts: - description: | - An object mapping ports to an empty object in the form: - - `{"/": {}}` - type: "object" - x-nullable: true - additionalProperties: - type: "object" - enum: - - {} - default: {} - example: { - "80/tcp": {}, - "443/tcp": {} - } - Env: - description: | - A list of environment variables to set inside the container in the - form `["VAR=value", ...]`. A variable without `=` is removed from the - environment, rather than to have an empty value. - type: "array" - items: - type: "string" - example: - - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - Cmd: - description: | - Command to run specified as a string or an array of strings. - type: "array" - items: - type: "string" - example: ["/bin/sh"] - Healthcheck: - $ref: "#/definitions/HealthConfig" - ArgsEscaped: - description: "Command is already escaped (Windows only)" - type: "boolean" - default: false - example: false - x-nullable: true - Volumes: - description: | - An object mapping mount point paths inside the container to empty - objects. - type: "object" - additionalProperties: - type: "object" - enum: - - {} - default: {} - example: - "/app/data": {} - "/app/config": {} - WorkingDir: - description: "The working directory for commands to run in." - type: "string" - example: "/public/" - Entrypoint: - description: | - The entry point for the container as a string or an array of strings. - - If the array consists of exactly one empty string (`[""]`) then the - entry point is reset to system default (i.e., the entry point used by - docker when there is no `ENTRYPOINT` instruction in the `Dockerfile`). - type: "array" - items: - type: "string" - example: [] - OnBuild: - description: | - `ONBUILD` metadata that were defined in the image's `Dockerfile`. - type: "array" - x-nullable: true - items: - type: "string" - example: [] - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - StopSignal: - description: | - Signal to stop a container as a string or unsigned integer. - type: "string" - example: "SIGTERM" - x-nullable: true - Shell: - description: | - Shell for when `RUN`, `CMD`, and `ENTRYPOINT` uses a shell. - type: "array" - x-nullable: true - items: - type: "string" - example: ["/bin/sh", "-c"] - # FIXME(thaJeztah): temporarily using a full example to remove some "omitempty" fields. Remove once the fields are removed. - example: - "User": "web:web" - "ExposedPorts": { - "80/tcp": {}, - "443/tcp": {} - } - "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] - "Cmd": ["/bin/sh"] - "Healthcheck": { - "Test": ["string"], - "Interval": 0, - "Timeout": 0, - "Retries": 0, - "StartPeriod": 0, - "StartInterval": 0 - } - "ArgsEscaped": true - "Volumes": { - "/app/data": {}, - "/app/config": {} - } - "WorkingDir": "/public/" - "Entrypoint": [] - "OnBuild": [] - "Labels": { - "com.example.some-label": "some-value", - "com.example.some-other-label": "some-other-value" - } - "StopSignal": "SIGTERM" - "Shell": ["/bin/sh", "-c"] - - NetworkingConfig: - description: | - NetworkingConfig represents the container's networking configuration for - each of its interfaces. - It is used for the networking configs specified in the `docker create` - and `docker network connect` commands. - type: "object" - properties: - EndpointsConfig: - description: | - A mapping of network name to endpoint configuration for that network. - The endpoint configuration can be left empty to connect to that - network with no particular endpoint configuration. - type: "object" - additionalProperties: - $ref: "#/definitions/EndpointSettings" - example: - # putting an example here, instead of using the example values from - # /definitions/EndpointSettings, because EndpointSettings contains - # operational data returned when inspecting a container that we don't - # accept here. - EndpointsConfig: - isolated_nw: - IPAMConfig: - IPv4Address: "172.20.30.33" - IPv6Address: "2001:db8:abcd::3033" - LinkLocalIPs: - - "169.254.34.68" - - "fe80::3468" - MacAddress: "02:42:ac:12:05:02" - Links: - - "container_1" - - "container_2" - Aliases: - - "server_x" - - "server_y" - database_nw: {} - - NetworkSettings: - description: "NetworkSettings exposes the network settings in the API" - type: "object" - properties: - Bridge: - description: | - Name of the default bridge interface when dockerd's --bridge flag is set. - type: "string" - example: "docker0" - SandboxID: - description: SandboxID uniquely represents a container's network stack. - type: "string" - example: "9d12daf2c33f5959c8bf90aa513e4f65b561738661003029ec84830cd503a0c3" - HairpinMode: - description: | - Indicates if hairpin NAT should be enabled on the virtual interface. - - Deprecated: This field is never set and will be removed in a future release. - type: "boolean" - example: false - LinkLocalIPv6Address: - description: | - IPv6 unicast address using the link-local prefix. - - Deprecated: This field is never set and will be removed in a future release. - type: "string" - example: "" - LinkLocalIPv6PrefixLen: - description: | - Prefix length of the IPv6 unicast address. - - Deprecated: This field is never set and will be removed in a future release. - type: "integer" - example: "" - Ports: - $ref: "#/definitions/PortMap" - SandboxKey: - description: SandboxKey is the full path of the netns handle - type: "string" - example: "/var/run/docker/netns/8ab54b426c38" - - SecondaryIPAddresses: - description: "Deprecated: This field is never set and will be removed in a future release." - type: "array" - items: - $ref: "#/definitions/Address" - x-nullable: true - - SecondaryIPv6Addresses: - description: "Deprecated: This field is never set and will be removed in a future release." - type: "array" - items: - $ref: "#/definitions/Address" - x-nullable: true - - # TODO properties below are part of DefaultNetworkSettings, which is - # marked as deprecated since Docker 1.9 and to be removed in Docker v17.12 - EndpointID: - description: | - EndpointID uniquely represents a service endpoint in a Sandbox. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "b88f5b905aabf2893f3cbc4ee42d1ea7980bbc0a92e2c8922b1e1795298afb0b" - Gateway: - description: | - Gateway address for the default "bridge" network. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "172.17.0.1" - GlobalIPv6Address: - description: | - Global IPv6 address for the default "bridge" network. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "2001:db8::5689" - GlobalIPv6PrefixLen: - description: | - Mask length of the global IPv6 address. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "integer" - example: 64 - IPAddress: - description: | - IPv4 address for the default "bridge" network. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "172.17.0.4" - IPPrefixLen: - description: | - Mask length of the IPv4 address. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "integer" - example: 16 - IPv6Gateway: - description: | - IPv6 gateway address for this network. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "2001:db8:2::100" - MacAddress: - description: | - MAC address for the container on the default "bridge" network. - -


- - > **Deprecated**: This field is only propagated when attached to the - > default "bridge" network. Use the information from the "bridge" - > network inside the `Networks` map instead, which contains the same - > information. This field was deprecated in Docker 1.9 and is scheduled - > to be removed in Docker 17.12.0 - type: "string" - example: "02:42:ac:11:00:04" - Networks: - description: | - Information about all networks that the container is connected to. - type: "object" - additionalProperties: - $ref: "#/definitions/EndpointSettings" - - Address: - description: Address represents an IPv4 or IPv6 IP address. - type: "object" - properties: - Addr: - description: IP address. - type: "string" - PrefixLen: - description: Mask length of the IP address. - type: "integer" - - PortMap: - description: | - PortMap describes the mapping of container ports to host ports, using the - container's port-number and protocol as key in the format `/`, - for example, `80/udp`. - - If a container's port is mapped for multiple protocols, separate entries - are added to the mapping table. - type: "object" - additionalProperties: - type: "array" - x-nullable: true - items: - $ref: "#/definitions/PortBinding" - example: - "443/tcp": - - HostIp: "127.0.0.1" - HostPort: "4443" - "80/tcp": - - HostIp: "0.0.0.0" - HostPort: "80" - - HostIp: "0.0.0.0" - HostPort: "8080" - "80/udp": - - HostIp: "0.0.0.0" - HostPort: "80" - "53/udp": - - HostIp: "0.0.0.0" - HostPort: "53" - "2377/tcp": null - - PortBinding: - description: | - PortBinding represents a binding between a host IP address and a host - port. - type: "object" - properties: - HostIp: - description: "Host IP address that the container's port is mapped to." - type: "string" - example: "127.0.0.1" - HostPort: - description: "Host port number that the container's port is mapped to." - type: "string" - example: "4443" - - DriverData: - description: | - Information about the storage driver used to store the container's and - image's filesystem. - type: "object" - required: [Name, Data] - properties: - Name: - description: "Name of the storage driver." - type: "string" - x-nullable: false - example: "overlay2" - Data: - description: | - Low-level storage metadata, provided as key/value pairs. - - This information is driver-specific, and depends on the storage-driver - in use, and should be used for informational purposes only. - type: "object" - x-nullable: false - additionalProperties: - type: "string" - example: { - "MergedDir": "/var/lib/docker/overlay2/ef749362d13333e65fc95c572eb525abbe0052e16e086cb64bc3b98ae9aa6d74/merged", - "UpperDir": "/var/lib/docker/overlay2/ef749362d13333e65fc95c572eb525abbe0052e16e086cb64bc3b98ae9aa6d74/diff", - "WorkDir": "/var/lib/docker/overlay2/ef749362d13333e65fc95c572eb525abbe0052e16e086cb64bc3b98ae9aa6d74/work" - } - - FilesystemChange: - description: | - Change in the container's filesystem. - type: "object" - required: [Path, Kind] - properties: - Path: - description: | - Path to file or directory that has changed. - type: "string" - x-nullable: false - Kind: - $ref: "#/definitions/ChangeType" - - ChangeType: - description: | - Kind of change - - Can be one of: - - - `0`: Modified ("C") - - `1`: Added ("A") - - `2`: Deleted ("D") - type: "integer" - format: "uint8" - enum: [0, 1, 2] - x-nullable: false - - ImageInspect: - description: | - Information about an image in the local image cache. - type: "object" - properties: - Id: - description: | - ID is the content-addressable ID of an image. - - This identifier is a content-addressable digest calculated from the - image's configuration (which includes the digests of layers used by - the image). - - Note that this digest differs from the `RepoDigests` below, which - holds digests of image manifests that reference the image. - type: "string" - x-nullable: false - example: "sha256:ec3f0931a6e6b6855d76b2d7b0be30e81860baccd891b2e243280bf1cd8ad710" - Descriptor: - description: | - Descriptor is an OCI descriptor of the image target. - In case of a multi-platform image, this descriptor points to the OCI index - or a manifest list. - - This field is only present if the daemon provides a multi-platform image store. - - WARNING: This is experimental and may change at any time without any backward - compatibility. - x-nullable: true - $ref: "#/definitions/OCIDescriptor" - Manifests: - description: | - Manifests is a list of image manifests available in this image. It - provides a more detailed view of the platform-specific image manifests or - other image-attached data like build attestations. - - Only available if the daemon provides a multi-platform image store - and the `manifests` option is set in the inspect request. - - WARNING: This is experimental and may change at any time without any backward - compatibility. - type: "array" - x-nullable: true - items: - $ref: "#/definitions/ImageManifestSummary" - RepoTags: - description: | - List of image names/tags in the local image cache that reference this - image. - - Multiple image tags can refer to the same image, and this list may be - empty if no tags reference the image, in which case the image is - "untagged", in which case it can still be referenced by its ID. - type: "array" - items: - type: "string" - example: - - "example:1.0" - - "example:latest" - - "example:stable" - - "internal.registry.example.com:5000/example:1.0" - RepoDigests: - description: | - List of content-addressable digests of locally available image manifests - that the image is referenced from. Multiple manifests can refer to the - same image. - - These digests are usually only available if the image was either pulled - from a registry, or if the image was pushed to a registry, which is when - the manifest is generated and its digest calculated. - type: "array" - items: - type: "string" - example: - - "example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb" - - "internal.registry.example.com:5000/example@sha256:b69959407d21e8a062e0416bf13405bb2b71ed7a84dde4158ebafacfa06f5578" - Parent: - description: | - ID of the parent image. - - Depending on how the image was created, this field may be empty and - is only set for images that were built/created locally. This field - is empty if the image was pulled from an image registry. - type: "string" - x-nullable: false - example: "" - Comment: - description: | - Optional message that was set when committing or importing the image. - type: "string" - x-nullable: false - example: "" - Created: - description: | - Date and time at which the image was created, formatted in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - - This information is only available if present in the image, - and omitted otherwise. - type: "string" - format: "dateTime" - x-nullable: true - example: "2022-02-04T21:20:12.497794809Z" - DockerVersion: - description: | - The version of Docker that was used to build the image. - - Depending on how the image was created, this field may be empty. - type: "string" - x-nullable: false - example: "27.0.1" - Author: - description: | - Name of the author that was specified when committing the image, or as - specified through MAINTAINER (deprecated) in the Dockerfile. - type: "string" - x-nullable: false - example: "" - Config: - $ref: "#/definitions/ImageConfig" - Architecture: - description: | - Hardware CPU architecture that the image runs on. - type: "string" - x-nullable: false - example: "arm" - Variant: - description: | - CPU architecture variant (presently ARM-only). - type: "string" - x-nullable: true - example: "v7" - Os: - description: | - Operating System the image is built to run on. - type: "string" - x-nullable: false - example: "linux" - OsVersion: - description: | - Operating System version the image is built to run on (especially - for Windows). - type: "string" - example: "" - x-nullable: true - Size: - description: | - Total size of the image including all layers it is composed of. - type: "integer" - format: "int64" - x-nullable: false - example: 1239828 - VirtualSize: - description: | - Total size of the image including all layers it is composed of. - - Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - type: "integer" - format: "int64" - example: 1239828 - GraphDriver: - $ref: "#/definitions/DriverData" - RootFS: - description: | - Information about the image's RootFS, including the layer IDs. - type: "object" - required: [Type] - properties: - Type: - type: "string" - x-nullable: false - example: "layers" - Layers: - type: "array" - items: - type: "string" - example: - - "sha256:1834950e52ce4d5a88a1bbd131c537f4d0e56d10ff0dd69e66be3b7dfa9df7e6" - - "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" - Metadata: - description: | - Additional metadata of the image in the local cache. This information - is local to the daemon, and not part of the image itself. - type: "object" - properties: - LastTagTime: - description: | - Date and time at which the image was last tagged in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - - This information is only available if the image was tagged locally, - and omitted otherwise. - type: "string" - format: "dateTime" - example: "2022-02-28T14:40:02.623929178Z" - x-nullable: true - - ImageSummary: - type: "object" - x-go-name: "Summary" - required: - - Id - - ParentId - - RepoTags - - RepoDigests - - Created - - Size - - SharedSize - - Labels - - Containers - properties: - Id: - description: | - ID is the content-addressable ID of an image. - - This identifier is a content-addressable digest calculated from the - image's configuration (which includes the digests of layers used by - the image). - - Note that this digest differs from the `RepoDigests` below, which - holds digests of image manifests that reference the image. - type: "string" - x-nullable: false - example: "sha256:ec3f0931a6e6b6855d76b2d7b0be30e81860baccd891b2e243280bf1cd8ad710" - ParentId: - description: | - ID of the parent image. - - Depending on how the image was created, this field may be empty and - is only set for images that were built/created locally. This field - is empty if the image was pulled from an image registry. - type: "string" - x-nullable: false - example: "" - RepoTags: - description: | - List of image names/tags in the local image cache that reference this - image. - - Multiple image tags can refer to the same image, and this list may be - empty if no tags reference the image, in which case the image is - "untagged", in which case it can still be referenced by its ID. - type: "array" - x-nullable: false - items: - type: "string" - example: - - "example:1.0" - - "example:latest" - - "example:stable" - - "internal.registry.example.com:5000/example:1.0" - RepoDigests: - description: | - List of content-addressable digests of locally available image manifests - that the image is referenced from. Multiple manifests can refer to the - same image. - - These digests are usually only available if the image was either pulled - from a registry, or if the image was pushed to a registry, which is when - the manifest is generated and its digest calculated. - type: "array" - x-nullable: false - items: - type: "string" - example: - - "example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb" - - "internal.registry.example.com:5000/example@sha256:b69959407d21e8a062e0416bf13405bb2b71ed7a84dde4158ebafacfa06f5578" - Created: - description: | - Date and time at which the image was created as a Unix timestamp - (number of seconds since EPOCH). - type: "integer" - x-nullable: false - example: "1644009612" - Size: - description: | - Total size of the image including all layers it is composed of. - type: "integer" - format: "int64" - x-nullable: false - example: 172064416 - SharedSize: - description: | - Total size of image layers that are shared between this image and other - images. - - This size is not calculated by default. `-1` indicates that the value - has not been set / calculated. - type: "integer" - format: "int64" - x-nullable: false - example: 1239828 - VirtualSize: - description: |- - Total size of the image including all layers it is composed of. - - Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - type: "integer" - format: "int64" - example: 172064416 - Labels: - description: "User-defined key/value metadata." - type: "object" - x-nullable: false - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - Containers: - description: | - Number of containers using this image. Includes both stopped and running - containers. - - `-1` indicates that the value has not been set / calculated. - x-nullable: false - type: "integer" - example: 2 - Manifests: - description: | - Manifests is a list of manifests available in this image. - It provides a more detailed view of the platform-specific image manifests - or other image-attached data like build attestations. - - WARNING: This is experimental and may change at any time without any backward - compatibility. - type: "array" - x-nullable: false - x-omitempty: true - items: - $ref: "#/definitions/ImageManifestSummary" - Descriptor: - description: | - Descriptor is an OCI descriptor of the image target. - In case of a multi-platform image, this descriptor points to the OCI index - or a manifest list. - - This field is only present if the daemon provides a multi-platform image store. - - WARNING: This is experimental and may change at any time without any backward - compatibility. - x-nullable: true - $ref: "#/definitions/OCIDescriptor" - - AuthConfig: - type: "object" - properties: - username: - type: "string" - password: - type: "string" - email: - type: "string" - serveraddress: - type: "string" - example: - username: "hannibal" - password: "xxxx" - serveraddress: "https://index.docker.io/v1/" - - ProcessConfig: - type: "object" - properties: - privileged: - type: "boolean" - user: - type: "string" - tty: - type: "boolean" - entrypoint: - type: "string" - arguments: - type: "array" - items: - type: "string" - - Volume: - type: "object" - required: [Name, Driver, Mountpoint, Labels, Scope, Options] - properties: - Name: - type: "string" - description: "Name of the volume." - x-nullable: false - example: "tardis" - Driver: - type: "string" - description: "Name of the volume driver used by the volume." - x-nullable: false - example: "custom" - Mountpoint: - type: "string" - description: "Mount path of the volume on the host." - x-nullable: false - example: "/var/lib/docker/volumes/tardis" - CreatedAt: - type: "string" - format: "dateTime" - description: "Date/Time the volume was created." - example: "2016-06-07T20:31:11.853781916Z" - Status: - type: "object" - description: | - Low-level details about the volume, provided by the volume driver. - Details are returned as a map with key/value pairs: - `{"key":"value","key2":"value2"}`. - - The `Status` field is optional, and is omitted if the volume driver - does not support this feature. - additionalProperties: - type: "object" - example: - hello: "world" - Labels: - type: "object" - description: "User-defined key/value metadata." - x-nullable: false - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - Scope: - type: "string" - description: | - The level at which the volume exists. Either `global` for cluster-wide, - or `local` for machine level. - default: "local" - x-nullable: false - enum: ["local", "global"] - example: "local" - ClusterVolume: - $ref: "#/definitions/ClusterVolume" - Options: - type: "object" - description: | - The driver specific options used when creating the volume. - additionalProperties: - type: "string" - example: - device: "tmpfs" - o: "size=100m,uid=1000" - type: "tmpfs" - UsageData: - type: "object" - x-nullable: true - x-go-name: "UsageData" - required: [Size, RefCount] - description: | - Usage details about the volume. This information is used by the - `GET /system/df` endpoint, and omitted in other endpoints. - properties: - Size: - type: "integer" - format: "int64" - default: -1 - description: | - Amount of disk space used by the volume (in bytes). This information - is only available for volumes created with the `"local"` volume - driver. For volumes created with other volume drivers, this field - is set to `-1` ("not available") - x-nullable: false - RefCount: - type: "integer" - format: "int64" - default: -1 - description: | - The number of containers referencing this volume. This field - is set to `-1` if the reference-count is not available. - x-nullable: false - - VolumeCreateOptions: - description: "Volume configuration" - type: "object" - title: "VolumeConfig" - x-go-name: "CreateOptions" - properties: - Name: - description: | - The new volume's name. If not specified, Docker generates a name. - type: "string" - x-nullable: false - example: "tardis" - Driver: - description: "Name of the volume driver to use." - type: "string" - default: "local" - x-nullable: false - example: "custom" - DriverOpts: - description: | - A mapping of driver options and values. These options are - passed directly to the driver and are driver specific. - type: "object" - additionalProperties: - type: "string" - example: - device: "tmpfs" - o: "size=100m,uid=1000" - type: "tmpfs" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - ClusterVolumeSpec: - $ref: "#/definitions/ClusterVolumeSpec" - - VolumeListResponse: - type: "object" - title: "VolumeListResponse" - x-go-name: "ListResponse" - description: "Volume list response" - properties: - Volumes: - type: "array" - description: "List of volumes" - items: - $ref: "#/definitions/Volume" - Warnings: - type: "array" - description: | - Warnings that occurred when fetching the list of volumes. - items: - type: "string" - example: [] - - Network: - type: "object" - properties: - Name: - description: | - Name of the network. - type: "string" - example: "my_network" - Id: - description: | - ID that uniquely identifies a network on a single machine. - type: "string" - example: "7d86d31b1478e7cca9ebed7e73aa0fdeec46c5ca29497431d3007d2d9e15ed99" - Created: - description: | - Date and time at which the network was created in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2016-10-19T04:33:30.360899459Z" - Scope: - description: | - The level at which the network exists (e.g. `swarm` for cluster-wide - or `local` for machine level) - type: "string" - example: "local" - Driver: - description: | - The name of the driver used to create the network (e.g. `bridge`, - `overlay`). - type: "string" - example: "overlay" - EnableIPv4: - description: | - Whether the network was created with IPv4 enabled. - type: "boolean" - example: true - EnableIPv6: - description: | - Whether the network was created with IPv6 enabled. - type: "boolean" - example: false - IPAM: - $ref: "#/definitions/IPAM" - Internal: - description: | - Whether the network is created to only allow internal networking - connectivity. - type: "boolean" - default: false - example: false - Attachable: - description: | - Whether a global / swarm scope network is manually attachable by regular - containers from workers in swarm mode. - type: "boolean" - default: false - example: false - Ingress: - description: | - Whether the network is providing the routing-mesh for the swarm cluster. - type: "boolean" - default: false - example: false - ConfigFrom: - $ref: "#/definitions/ConfigReference" - ConfigOnly: - description: | - Whether the network is a config-only network. Config-only networks are - placeholder networks for network configurations to be used by other - networks. Config-only networks cannot be used directly to run containers - or services. - type: "boolean" - default: false - Containers: - description: | - Contains endpoints attached to the network. - type: "object" - additionalProperties: - $ref: "#/definitions/NetworkContainer" - example: - 19a4d5d687db25203351ed79d478946f861258f018fe384f229f2efa4b23513c: - Name: "test" - EndpointID: "628cadb8bcb92de107b2a1e516cbffe463e321f548feb37697cce00ad694f21a" - MacAddress: "02:42:ac:13:00:02" - IPv4Address: "172.19.0.2/16" - IPv6Address: "" - Options: - description: | - Network-specific options uses when creating the network. - type: "object" - additionalProperties: - type: "string" - example: - com.docker.network.bridge.default_bridge: "true" - com.docker.network.bridge.enable_icc: "true" - com.docker.network.bridge.enable_ip_masquerade: "true" - com.docker.network.bridge.host_binding_ipv4: "0.0.0.0" - com.docker.network.bridge.name: "docker0" - com.docker.network.driver.mtu: "1500" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - Peers: - description: | - List of peer nodes for an overlay network. This field is only present - for overlay networks, and omitted for other network types. - type: "array" - items: - $ref: "#/definitions/PeerInfo" - x-nullable: true - # TODO: Add Services (only present when "verbose" is set). - - ConfigReference: - description: | - The config-only network source to provide the configuration for - this network. - type: "object" - properties: - Network: - description: | - The name of the config-only network that provides the network's - configuration. The specified network must be an existing config-only - network. Only network names are allowed, not network IDs. - type: "string" - example: "config_only_network_01" - - IPAM: - type: "object" - properties: - Driver: - description: "Name of the IPAM driver to use." - type: "string" - default: "default" - example: "default" - Config: - description: | - List of IPAM configuration options, specified as a map: - - ``` - {"Subnet": , "IPRange": , "Gateway": , "AuxAddress": } - ``` - type: "array" - items: - $ref: "#/definitions/IPAMConfig" - Options: - description: "Driver-specific options, specified as a map." - type: "object" - additionalProperties: - type: "string" - example: - foo: "bar" - - IPAMConfig: - type: "object" - properties: - Subnet: - type: "string" - example: "172.20.0.0/16" - IPRange: - type: "string" - example: "172.20.10.0/24" - Gateway: - type: "string" - example: "172.20.10.11" - AuxiliaryAddresses: - type: "object" - additionalProperties: - type: "string" - - NetworkContainer: - type: "object" - properties: - Name: - type: "string" - example: "container_1" - EndpointID: - type: "string" - example: "628cadb8bcb92de107b2a1e516cbffe463e321f548feb37697cce00ad694f21a" - MacAddress: - type: "string" - example: "02:42:ac:13:00:02" - IPv4Address: - type: "string" - example: "172.19.0.2/16" - IPv6Address: - type: "string" - example: "" - - PeerInfo: - description: | - PeerInfo represents one peer of an overlay network. - type: "object" - properties: - Name: - description: - ID of the peer-node in the Swarm cluster. - type: "string" - example: "6869d7c1732b" - IP: - description: - IP-address of the peer-node in the Swarm cluster. - type: "string" - example: "10.133.77.91" - - NetworkCreateResponse: - description: "OK response to NetworkCreate operation" - type: "object" - title: "NetworkCreateResponse" - x-go-name: "CreateResponse" - required: [Id, Warning] - properties: - Id: - description: "The ID of the created network." - type: "string" - x-nullable: false - example: "b5c4fc71e8022147cd25de22b22173de4e3b170134117172eb595cb91b4e7e5d" - Warning: - description: "Warnings encountered when creating the container" - type: "string" - x-nullable: false - example: "" - - BuildInfo: - type: "object" - properties: - id: - type: "string" - stream: - type: "string" - error: - type: "string" - x-nullable: true - description: |- - errors encountered during the operation. - - - > **Deprecated**: This field is deprecated since API v1.4, and will be omitted in a future API version. Use the information in errorDetail instead. - errorDetail: - $ref: "#/definitions/ErrorDetail" - status: - type: "string" - progress: - type: "string" - x-nullable: true - description: |- - Progress is a pre-formatted presentation of progressDetail. - - - > **Deprecated**: This field is deprecated since API v1.8, and will be omitted in a future API version. Use the information in progressDetail instead. - progressDetail: - $ref: "#/definitions/ProgressDetail" - aux: - $ref: "#/definitions/ImageID" - - BuildCache: - type: "object" - description: | - BuildCache contains information about a build cache record. - properties: - ID: - type: "string" - description: | - Unique ID of the build cache record. - example: "ndlpt0hhvkqcdfkputsk4cq9c" - Parent: - description: | - ID of the parent build cache record. - - > **Deprecated**: This field is deprecated, and omitted if empty. - type: "string" - x-nullable: true - example: "" - Parents: - description: | - List of parent build cache record IDs. - type: "array" - items: - type: "string" - x-nullable: true - example: ["hw53o5aio51xtltp5xjp8v7fx"] - Type: - type: "string" - description: | - Cache record type. - example: "regular" - # see https://github.com/moby/buildkit/blob/fce4a32258dc9d9664f71a4831d5de10f0670677/client/diskusage.go#L75-L84 - enum: - - "internal" - - "frontend" - - "source.local" - - "source.git.checkout" - - "exec.cachemount" - - "regular" - Description: - type: "string" - description: | - Description of the build-step that produced the build cache. - example: "mount / from exec /bin/sh -c echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' > /etc/apt/apt.conf.d/keep-cache" - InUse: - type: "boolean" - description: | - Indicates if the build cache is in use. - example: false - Shared: - type: "boolean" - description: | - Indicates if the build cache is shared. - example: true - Size: - description: | - Amount of disk space used by the build cache (in bytes). - type: "integer" - example: 51 - CreatedAt: - description: | - Date and time at which the build cache was created in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2016-08-18T10:44:24.496525531Z" - LastUsedAt: - description: | - Date and time at which the build cache was last used in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - x-nullable: true - example: "2017-08-09T07:09:37.632105588Z" - UsageCount: - type: "integer" - example: 26 - - ImageID: - type: "object" - description: "Image ID or Digest" - properties: - ID: - type: "string" - example: - ID: "sha256:85f05633ddc1c50679be2b16a0479ab6f7637f8884e0cfe0f4d20e1ebb3d6e7c" - - CreateImageInfo: - type: "object" - properties: - id: - type: "string" - error: - type: "string" - x-nullable: true - description: |- - errors encountered during the operation. - - - > **Deprecated**: This field is deprecated since API v1.4, and will be omitted in a future API version. Use the information in errorDetail instead. - errorDetail: - $ref: "#/definitions/ErrorDetail" - status: - type: "string" - progress: - type: "string" - x-nullable: true - description: |- - Progress is a pre-formatted presentation of progressDetail. - - - > **Deprecated**: This field is deprecated since API v1.8, and will be omitted in a future API version. Use the information in progressDetail instead. - progressDetail: - $ref: "#/definitions/ProgressDetail" - - PushImageInfo: - type: "object" - properties: - error: - type: "string" - x-nullable: true - description: |- - errors encountered during the operation. - - - > **Deprecated**: This field is deprecated since API v1.4, and will be omitted in a future API version. Use the information in errorDetail instead. - errorDetail: - $ref: "#/definitions/ErrorDetail" - status: - type: "string" - progress: - type: "string" - x-nullable: true - description: |- - Progress is a pre-formatted presentation of progressDetail. - - - > **Deprecated**: This field is deprecated since API v1.8, and will be omitted in a future API version. Use the information in progressDetail instead. - progressDetail: - $ref: "#/definitions/ProgressDetail" - - DeviceInfo: - type: "object" - description: | - DeviceInfo represents a device that can be used by a container. - properties: - Source: - type: "string" - example: "cdi" - description: | - The origin device driver. - ID: - type: "string" - example: "vendor.com/gpu=0" - description: | - The unique identifier for the device within its source driver. - For CDI devices, this would be an FQDN like "vendor.com/gpu=0". - - ErrorDetail: - type: "object" - properties: - code: - type: "integer" - message: - type: "string" - - ProgressDetail: - type: "object" - properties: - current: - type: "integer" - total: - type: "integer" - - ErrorResponse: - description: "Represents an error." - type: "object" - required: ["message"] - properties: - message: - description: "The error message." - type: "string" - x-nullable: false - example: - message: "Something went wrong." - - IDResponse: - description: "Response to an API call that returns just an Id" - type: "object" - x-go-name: "IDResponse" - required: ["Id"] - properties: - Id: - description: "The id of the newly created object." - type: "string" - x-nullable: false - - EndpointSettings: - description: "Configuration for a network endpoint." - type: "object" - properties: - # Configurations - IPAMConfig: - $ref: "#/definitions/EndpointIPAMConfig" - Links: - type: "array" - items: - type: "string" - example: - - "container_1" - - "container_2" - MacAddress: - description: | - MAC address for the endpoint on this network. The network driver might ignore this parameter. - type: "string" - example: "02:42:ac:11:00:04" - Aliases: - type: "array" - items: - type: "string" - example: - - "server_x" - - "server_y" - DriverOpts: - description: | - DriverOpts is a mapping of driver options and values. These options - are passed directly to the driver and are driver specific. - type: "object" - x-nullable: true - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - GwPriority: - description: | - This property determines which endpoint will provide the default - gateway for a container. The endpoint with the highest priority will - be used. If multiple endpoints have the same priority, endpoints are - lexicographically sorted based on their network name, and the one - that sorts first is picked. - type: "number" - example: - - 10 - - # Operational data - NetworkID: - description: | - Unique ID of the network. - type: "string" - example: "08754567f1f40222263eab4102e1c733ae697e8e354aa9cd6e18d7402835292a" - EndpointID: - description: | - Unique ID for the service endpoint in a Sandbox. - type: "string" - example: "b88f5b905aabf2893f3cbc4ee42d1ea7980bbc0a92e2c8922b1e1795298afb0b" - Gateway: - description: | - Gateway address for this network. - type: "string" - example: "172.17.0.1" - IPAddress: - description: | - IPv4 address. - type: "string" - example: "172.17.0.4" - IPPrefixLen: - description: | - Mask length of the IPv4 address. - type: "integer" - example: 16 - IPv6Gateway: - description: | - IPv6 gateway address. - type: "string" - example: "2001:db8:2::100" - GlobalIPv6Address: - description: | - Global IPv6 address. - type: "string" - example: "2001:db8::5689" - GlobalIPv6PrefixLen: - description: | - Mask length of the global IPv6 address. - type: "integer" - format: "int64" - example: 64 - DNSNames: - description: | - List of all DNS names an endpoint has on a specific network. This - list is based on the container name, network aliases, container short - ID, and hostname. - - These DNS names are non-fully qualified but can contain several dots. - You can get fully qualified DNS names by appending `.`. - For instance, if container name is `my.ctr` and the network is named - `testnet`, `DNSNames` will contain `my.ctr` and the FQDN will be - `my.ctr.testnet`. - type: array - items: - type: string - example: ["foobar", "server_x", "server_y", "my.ctr"] - - EndpointIPAMConfig: - description: | - EndpointIPAMConfig represents an endpoint's IPAM configuration. - type: "object" - x-nullable: true - properties: - IPv4Address: - type: "string" - example: "172.20.30.33" - IPv6Address: - type: "string" - example: "2001:db8:abcd::3033" - LinkLocalIPs: - type: "array" - items: - type: "string" - example: - - "169.254.34.68" - - "fe80::3468" - - PluginMount: - type: "object" - x-nullable: false - required: [Name, Description, Settable, Source, Destination, Type, Options] - properties: - Name: - type: "string" - x-nullable: false - example: "some-mount" - Description: - type: "string" - x-nullable: false - example: "This is a mount that's used by the plugin." - Settable: - type: "array" - items: - type: "string" - Source: - type: "string" - example: "/var/lib/docker/plugins/" - Destination: - type: "string" - x-nullable: false - example: "/mnt/state" - Type: - type: "string" - x-nullable: false - example: "bind" - Options: - type: "array" - items: - type: "string" - example: - - "rbind" - - "rw" - - PluginDevice: - type: "object" - required: [Name, Description, Settable, Path] - x-nullable: false - properties: - Name: - type: "string" - x-nullable: false - Description: - type: "string" - x-nullable: false - Settable: - type: "array" - items: - type: "string" - Path: - type: "string" - example: "/dev/fuse" - - PluginEnv: - type: "object" - x-nullable: false - required: [Name, Description, Settable, Value] - properties: - Name: - x-nullable: false - type: "string" - Description: - x-nullable: false - type: "string" - Settable: - type: "array" - items: - type: "string" - Value: - type: "string" - - PluginInterfaceType: - type: "object" - x-nullable: false - required: [Prefix, Capability, Version] - properties: - Prefix: - type: "string" - x-nullable: false - Capability: - type: "string" - x-nullable: false - Version: - type: "string" - x-nullable: false - - PluginPrivilege: - description: | - Describes a permission the user has to accept upon installing - the plugin. - type: "object" - x-go-name: "PluginPrivilege" - properties: - Name: - type: "string" - example: "network" - Description: - type: "string" - Value: - type: "array" - items: - type: "string" - example: - - "host" - - Plugin: - description: "A plugin for the Engine API" - type: "object" - required: [Settings, Enabled, Config, Name] - properties: - Id: - type: "string" - example: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078" - Name: - type: "string" - x-nullable: false - example: "tiborvass/sample-volume-plugin" - Enabled: - description: - True if the plugin is running. False if the plugin is not running, - only installed. - type: "boolean" - x-nullable: false - example: true - Settings: - description: "Settings that can be modified by users." - type: "object" - x-nullable: false - required: [Args, Devices, Env, Mounts] - properties: - Mounts: - type: "array" - items: - $ref: "#/definitions/PluginMount" - Env: - type: "array" - items: - type: "string" - example: - - "DEBUG=0" - Args: - type: "array" - items: - type: "string" - Devices: - type: "array" - items: - $ref: "#/definitions/PluginDevice" - PluginReference: - description: "plugin remote reference used to push/pull the plugin" - type: "string" - x-nullable: false - example: "localhost:5000/tiborvass/sample-volume-plugin:latest" - Config: - description: "The config of a plugin." - type: "object" - x-nullable: false - required: - - Description - - Documentation - - Interface - - Entrypoint - - WorkDir - - Network - - Linux - - PidHost - - PropagatedMount - - IpcHost - - Mounts - - Env - - Args - properties: - DockerVersion: - description: "Docker Version used to create the plugin" - type: "string" - x-nullable: false - example: "17.06.0-ce" - Description: - type: "string" - x-nullable: false - example: "A sample volume plugin for Docker" - Documentation: - type: "string" - x-nullable: false - example: "https://docs.docker.com/engine/extend/plugins/" - Interface: - description: "The interface between Docker and the plugin" - x-nullable: false - type: "object" - required: [Types, Socket] - properties: - Types: - type: "array" - items: - $ref: "#/definitions/PluginInterfaceType" - example: - - "docker.volumedriver/1.0" - Socket: - type: "string" - x-nullable: false - example: "plugins.sock" - ProtocolScheme: - type: "string" - example: "some.protocol/v1.0" - description: "Protocol to use for clients connecting to the plugin." - enum: - - "" - - "moby.plugins.http/v1" - Entrypoint: - type: "array" - items: - type: "string" - example: - - "/usr/bin/sample-volume-plugin" - - "/data" - WorkDir: - type: "string" - x-nullable: false - example: "/bin/" - User: - type: "object" - x-nullable: false - properties: - UID: - type: "integer" - format: "uint32" - example: 1000 - GID: - type: "integer" - format: "uint32" - example: 1000 - Network: - type: "object" - x-nullable: false - required: [Type] - properties: - Type: - x-nullable: false - type: "string" - example: "host" - Linux: - type: "object" - x-nullable: false - required: [Capabilities, AllowAllDevices, Devices] - properties: - Capabilities: - type: "array" - items: - type: "string" - example: - - "CAP_SYS_ADMIN" - - "CAP_SYSLOG" - AllowAllDevices: - type: "boolean" - x-nullable: false - example: false - Devices: - type: "array" - items: - $ref: "#/definitions/PluginDevice" - PropagatedMount: - type: "string" - x-nullable: false - example: "/mnt/volumes" - IpcHost: - type: "boolean" - x-nullable: false - example: false - PidHost: - type: "boolean" - x-nullable: false - example: false - Mounts: - type: "array" - items: - $ref: "#/definitions/PluginMount" - Env: - type: "array" - items: - $ref: "#/definitions/PluginEnv" - example: - - Name: "DEBUG" - Description: "If set, prints debug messages" - Settable: null - Value: "0" - Args: - type: "object" - x-nullable: false - required: [Name, Description, Settable, Value] - properties: - Name: - x-nullable: false - type: "string" - example: "args" - Description: - x-nullable: false - type: "string" - example: "command line arguments" - Settable: - type: "array" - items: - type: "string" - Value: - type: "array" - items: - type: "string" - rootfs: - type: "object" - properties: - type: - type: "string" - example: "layers" - diff_ids: - type: "array" - items: - type: "string" - example: - - "sha256:675532206fbf3030b8458f88d6e26d4eb1577688a25efec97154c94e8b6b4887" - - "sha256:e216a057b1cb1efc11f8a268f37ef62083e70b1b38323ba252e25ac88904a7e8" - - ObjectVersion: - description: | - The version number of the object such as node, service, etc. This is needed - to avoid conflicting writes. The client must send the version number along - with the modified specification when updating these objects. - - This approach ensures safe concurrency and determinism in that the change - on the object may not be applied if the version number has changed from the - last read. In other words, if two update requests specify the same base - version, only one of the requests can succeed. As a result, two separate - update requests that happen at the same time will not unintentionally - overwrite each other. - type: "object" - properties: - Index: - type: "integer" - format: "uint64" - example: 373531 - - NodeSpec: - type: "object" - properties: - Name: - description: "Name for the node." - type: "string" - example: "my-node" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - Role: - description: "Role of the node." - type: "string" - enum: - - "worker" - - "manager" - example: "manager" - Availability: - description: "Availability of the node." - type: "string" - enum: - - "active" - - "pause" - - "drain" - example: "active" - example: - Availability: "active" - Name: "node-name" - Role: "manager" - Labels: - foo: "bar" - - Node: - type: "object" - properties: - ID: - type: "string" - example: "24ifsmvkjbyhk" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - description: | - Date and time at which the node was added to the swarm in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2016-08-18T10:44:24.496525531Z" - UpdatedAt: - description: | - Date and time at which the node was last updated in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2017-08-09T07:09:37.632105588Z" - Spec: - $ref: "#/definitions/NodeSpec" - Description: - $ref: "#/definitions/NodeDescription" - Status: - $ref: "#/definitions/NodeStatus" - ManagerStatus: - $ref: "#/definitions/ManagerStatus" - - NodeDescription: - description: | - NodeDescription encapsulates the properties of the Node as reported by the - agent. - type: "object" - properties: - Hostname: - type: "string" - example: "bf3067039e47" - Platform: - $ref: "#/definitions/Platform" - Resources: - $ref: "#/definitions/ResourceObject" - Engine: - $ref: "#/definitions/EngineDescription" - TLSInfo: - $ref: "#/definitions/TLSInfo" - - Platform: - description: | - Platform represents the platform (Arch/OS). - type: "object" - properties: - Architecture: - description: | - Architecture represents the hardware architecture (for example, - `x86_64`). - type: "string" - example: "x86_64" - OS: - description: | - OS represents the Operating System (for example, `linux` or `windows`). - type: "string" - example: "linux" - - EngineDescription: - description: "EngineDescription provides information about an engine." - type: "object" - properties: - EngineVersion: - type: "string" - example: "17.06.0" - Labels: - type: "object" - additionalProperties: - type: "string" - example: - foo: "bar" - Plugins: - type: "array" - items: - type: "object" - properties: - Type: - type: "string" - Name: - type: "string" - example: - - Type: "Log" - Name: "awslogs" - - Type: "Log" - Name: "fluentd" - - Type: "Log" - Name: "gcplogs" - - Type: "Log" - Name: "gelf" - - Type: "Log" - Name: "journald" - - Type: "Log" - Name: "json-file" - - Type: "Log" - Name: "splunk" - - Type: "Log" - Name: "syslog" - - Type: "Network" - Name: "bridge" - - Type: "Network" - Name: "host" - - Type: "Network" - Name: "ipvlan" - - Type: "Network" - Name: "macvlan" - - Type: "Network" - Name: "null" - - Type: "Network" - Name: "overlay" - - Type: "Volume" - Name: "local" - - Type: "Volume" - Name: "localhost:5000/vieux/sshfs:latest" - - Type: "Volume" - Name: "vieux/sshfs:latest" - - TLSInfo: - description: | - Information about the issuer of leaf TLS certificates and the trusted root - CA certificate. - type: "object" - properties: - TrustRoot: - description: | - The root CA certificate(s) that are used to validate leaf TLS - certificates. - type: "string" - CertIssuerSubject: - description: - The base64-url-safe-encoded raw subject bytes of the issuer. - type: "string" - CertIssuerPublicKey: - description: | - The base64-url-safe-encoded raw public key bytes of the issuer. - type: "string" - example: - TrustRoot: | - -----BEGIN CERTIFICATE----- - MIIBajCCARCgAwIBAgIUbYqrLSOSQHoxD8CwG6Bi2PJi9c8wCgYIKoZIzj0EAwIw - EzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwNDI0MjE0MzAwWhcNMzcwNDE5MjE0 - MzAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH - A0IABJk/VyMPYdaqDXJb/VXh5n/1Yuv7iNrxV3Qb3l06XD46seovcDWs3IZNV1lf - 3Skyr0ofcchipoiHkXBODojJydSjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB - Af8EBTADAQH/MB0GA1UdDgQWBBRUXxuRcnFjDfR/RIAUQab8ZV/n4jAKBggqhkjO - PQQDAgNIADBFAiAy+JTe6Uc3KyLCMiqGl2GyWGQqQDEcO3/YG36x7om65AIhAJvz - pxv6zFeVEkAEEkqIYi0omA9+CjanB/6Bz4n1uw8H - -----END CERTIFICATE----- - CertIssuerSubject: "MBMxETAPBgNVBAMTCHN3YXJtLWNh" - CertIssuerPublicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmT9XIw9h1qoNclv9VeHmf/Vi6/uI2vFXdBveXTpcPjqx6i9wNazchk1XWV/dKTKvSh9xyGKmiIeRcE4OiMnJ1A==" - - NodeStatus: - description: | - NodeStatus represents the status of a node. - - It provides the current status of the node, as seen by the manager. - type: "object" - properties: - State: - $ref: "#/definitions/NodeState" - Message: - type: "string" - example: "" - Addr: - description: "IP address of the node." - type: "string" - example: "172.17.0.2" - - NodeState: - description: "NodeState represents the state of a node." - type: "string" - enum: - - "unknown" - - "down" - - "ready" - - "disconnected" - example: "ready" - - ManagerStatus: - description: | - ManagerStatus represents the status of a manager. - - It provides the current status of a node's manager component, if the node - is a manager. - x-nullable: true - type: "object" - properties: - Leader: - type: "boolean" - default: false - example: true - Reachability: - $ref: "#/definitions/Reachability" - Addr: - description: | - The IP address and port at which the manager is reachable. - type: "string" - example: "10.0.0.46:2377" - - Reachability: - description: "Reachability represents the reachability of a node." - type: "string" - enum: - - "unknown" - - "unreachable" - - "reachable" - example: "reachable" - - SwarmSpec: - description: "User modifiable swarm configuration." - type: "object" - properties: - Name: - description: "Name of the swarm." - type: "string" - example: "default" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.corp.type: "production" - com.example.corp.department: "engineering" - Orchestration: - description: "Orchestration configuration." - type: "object" - x-nullable: true - properties: - TaskHistoryRetentionLimit: - description: | - The number of historic tasks to keep per instance or node. If - negative, never remove completed or failed tasks. - type: "integer" - format: "int64" - example: 10 - Raft: - description: "Raft configuration." - type: "object" - properties: - SnapshotInterval: - description: "The number of log entries between snapshots." - type: "integer" - format: "uint64" - example: 10000 - KeepOldSnapshots: - description: | - The number of snapshots to keep beyond the current snapshot. - type: "integer" - format: "uint64" - LogEntriesForSlowFollowers: - description: | - The number of log entries to keep around to sync up slow followers - after a snapshot is created. - type: "integer" - format: "uint64" - example: 500 - ElectionTick: - description: | - The number of ticks that a follower will wait for a message from - the leader before becoming a candidate and starting an election. - `ElectionTick` must be greater than `HeartbeatTick`. - - A tick currently defaults to one second, so these translate - directly to seconds currently, but this is NOT guaranteed. - type: "integer" - example: 3 - HeartbeatTick: - description: | - The number of ticks between heartbeats. Every HeartbeatTick ticks, - the leader will send a heartbeat to the followers. - - A tick currently defaults to one second, so these translate - directly to seconds currently, but this is NOT guaranteed. - type: "integer" - example: 1 - Dispatcher: - description: "Dispatcher configuration." - type: "object" - x-nullable: true - properties: - HeartbeatPeriod: - description: | - The delay for an agent to send a heartbeat to the dispatcher. - type: "integer" - format: "int64" - example: 5000000000 - CAConfig: - description: "CA configuration." - type: "object" - x-nullable: true - properties: - NodeCertExpiry: - description: "The duration node certificates are issued for." - type: "integer" - format: "int64" - example: 7776000000000000 - ExternalCAs: - description: | - Configuration for forwarding signing requests to an external - certificate authority. - type: "array" - items: - type: "object" - properties: - Protocol: - description: | - Protocol for communication with the external CA (currently - only `cfssl` is supported). - type: "string" - enum: - - "cfssl" - default: "cfssl" - URL: - description: | - URL where certificate signing requests should be sent. - type: "string" - Options: - description: | - An object with key/value pairs that are interpreted as - protocol-specific options for the external CA driver. - type: "object" - additionalProperties: - type: "string" - CACert: - description: | - The root CA certificate (in PEM format) this external CA uses - to issue TLS certificates (assumed to be to the current swarm - root CA certificate if not provided). - type: "string" - SigningCACert: - description: | - The desired signing CA certificate for all swarm node TLS leaf - certificates, in PEM format. - type: "string" - SigningCAKey: - description: | - The desired signing CA key for all swarm node TLS leaf certificates, - in PEM format. - type: "string" - ForceRotate: - description: | - An integer whose purpose is to force swarm to generate a new - signing CA certificate and key, if none have been specified in - `SigningCACert` and `SigningCAKey` - format: "uint64" - type: "integer" - EncryptionConfig: - description: "Parameters related to encryption-at-rest." - type: "object" - properties: - AutoLockManagers: - description: | - If set, generate a key and use it to lock data stored on the - managers. - type: "boolean" - example: false - TaskDefaults: - description: "Defaults for creating tasks in this cluster." - type: "object" - properties: - LogDriver: - description: | - The log driver to use for tasks created in the orchestrator if - unspecified by a service. - - Updating this value only affects new tasks. Existing tasks continue - to use their previously configured log driver until recreated. - type: "object" - properties: - Name: - description: | - The log driver to use as a default for new tasks. - type: "string" - example: "json-file" - Options: - description: | - Driver-specific options for the selected log driver, specified - as key/value pairs. - type: "object" - additionalProperties: - type: "string" - example: - "max-file": "10" - "max-size": "100m" - - # The Swarm information for `GET /info`. It is the same as `GET /swarm`, but - # without `JoinTokens`. - ClusterInfo: - description: | - ClusterInfo represents information about the swarm as is returned by the - "/info" endpoint. Join-tokens are not included. - x-nullable: true - type: "object" - properties: - ID: - description: "The ID of the swarm." - type: "string" - example: "abajmipo7b4xz5ip2nrla6b11" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - description: | - Date and time at which the swarm was initialised in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2016-08-18T10:44:24.496525531Z" - UpdatedAt: - description: | - Date and time at which the swarm was last updated in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - example: "2017-08-09T07:09:37.632105588Z" - Spec: - $ref: "#/definitions/SwarmSpec" - TLSInfo: - $ref: "#/definitions/TLSInfo" - RootRotationInProgress: - description: | - Whether there is currently a root CA rotation in progress for the swarm - type: "boolean" - example: false - DataPathPort: - description: | - DataPathPort specifies the data path port number for data traffic. - Acceptable port range is 1024 to 49151. - If no port is set or is set to 0, the default port (4789) is used. - type: "integer" - format: "uint32" - default: 4789 - example: 4789 - DefaultAddrPool: - description: | - Default Address Pool specifies default subnet pools for global scope - networks. - type: "array" - items: - type: "string" - format: "CIDR" - example: ["10.10.0.0/16", "20.20.0.0/16"] - SubnetSize: - description: | - SubnetSize specifies the subnet size of the networks created from the - default subnet pool. - type: "integer" - format: "uint32" - maximum: 29 - default: 24 - example: 24 - - JoinTokens: - description: | - JoinTokens contains the tokens workers and managers need to join the swarm. - type: "object" - properties: - Worker: - description: | - The token workers can use to join the swarm. - type: "string" - example: "SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-1awxwuwd3z9j1z3puu7rcgdbx" - Manager: - description: | - The token managers can use to join the swarm. - type: "string" - example: "SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2" - - Swarm: - type: "object" - allOf: - - $ref: "#/definitions/ClusterInfo" - - type: "object" - properties: - JoinTokens: - $ref: "#/definitions/JoinTokens" - - TaskSpec: - description: "User modifiable task configuration." - type: "object" - properties: - PluginSpec: - type: "object" - description: | - Plugin spec for the service. *(Experimental release only.)* - -


- - > **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are - > mutually exclusive. PluginSpec is only used when the Runtime field - > is set to `plugin`. NetworkAttachmentSpec is used when the Runtime - > field is set to `attachment`. - properties: - Name: - description: "The name or 'alias' to use for the plugin." - type: "string" - Remote: - description: "The plugin image reference to use." - type: "string" - Disabled: - description: "Disable the plugin once scheduled." - type: "boolean" - PluginPrivilege: - type: "array" - items: - $ref: "#/definitions/PluginPrivilege" - ContainerSpec: - type: "object" - description: | - Container spec for the service. - -


- - > **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are - > mutually exclusive. PluginSpec is only used when the Runtime field - > is set to `plugin`. NetworkAttachmentSpec is used when the Runtime - > field is set to `attachment`. - properties: - Image: - description: "The image name to use for the container" - type: "string" - Labels: - description: "User-defined key/value data." - type: "object" - additionalProperties: - type: "string" - Command: - description: "The command to be run in the image." - type: "array" - items: - type: "string" - Args: - description: "Arguments to the command." - type: "array" - items: - type: "string" - Hostname: - description: | - The hostname to use for the container, as a valid - [RFC 1123](https://tools.ietf.org/html/rfc1123) hostname. - type: "string" - Env: - description: | - A list of environment variables in the form `VAR=value`. - type: "array" - items: - type: "string" - Dir: - description: "The working directory for commands to run in." - type: "string" - User: - description: "The user inside the container." - type: "string" - Groups: - type: "array" - description: | - A list of additional groups that the container process will run as. - items: - type: "string" - Privileges: - type: "object" - description: "Security options for the container" - properties: - CredentialSpec: - type: "object" - description: "CredentialSpec for managed service account (Windows only)" - properties: - Config: - type: "string" - example: "0bt9dmxjvjiqermk6xrop3ekq" - description: | - Load credential spec from a Swarm Config with the given ID. - The specified config must also be present in the Configs - field with the Runtime property set. - -


- - - > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, - > and `CredentialSpec.Config` are mutually exclusive. - File: - type: "string" - example: "spec.json" - description: | - Load credential spec from this file. The file is read by - the daemon, and must be present in the `CredentialSpecs` - subdirectory in the docker data directory, which defaults - to `C:\ProgramData\Docker\` on Windows. - - For example, specifying `spec.json` loads - `C:\ProgramData\Docker\CredentialSpecs\spec.json`. - -


- - > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, - > and `CredentialSpec.Config` are mutually exclusive. - Registry: - type: "string" - description: | - Load credential spec from this value in the Windows - registry. The specified registry value must be located in: - - `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs` - -


- - - > **Note**: `CredentialSpec.File`, `CredentialSpec.Registry`, - > and `CredentialSpec.Config` are mutually exclusive. - SELinuxContext: - type: "object" - description: "SELinux labels of the container" - properties: - Disable: - type: "boolean" - description: "Disable SELinux" - User: - type: "string" - description: "SELinux user label" - Role: - type: "string" - description: "SELinux role label" - Type: - type: "string" - description: "SELinux type label" - Level: - type: "string" - description: "SELinux level label" - Seccomp: - type: "object" - description: "Options for configuring seccomp on the container" - properties: - Mode: - type: "string" - enum: - - "default" - - "unconfined" - - "custom" - Profile: - description: "The custom seccomp profile as a json object" - type: "string" - AppArmor: - type: "object" - description: "Options for configuring AppArmor on the container" - properties: - Mode: - type: "string" - enum: - - "default" - - "disabled" - NoNewPrivileges: - type: "boolean" - description: "Configuration of the no_new_privs bit in the container" - - TTY: - description: "Whether a pseudo-TTY should be allocated." - type: "boolean" - OpenStdin: - description: "Open `stdin`" - type: "boolean" - ReadOnly: - description: "Mount the container's root filesystem as read only." - type: "boolean" - Mounts: - description: | - Specification for mounts to be added to containers created as part - of the service. - type: "array" - items: - $ref: "#/definitions/Mount" - StopSignal: - description: "Signal to stop the container." - type: "string" - StopGracePeriod: - description: | - Amount of time to wait for the container to terminate before - forcefully killing it. - type: "integer" - format: "int64" - HealthCheck: - $ref: "#/definitions/HealthConfig" - Hosts: - type: "array" - description: | - A list of hostname/IP mappings to add to the container's `hosts` - file. The format of extra hosts is specified in the - [hosts(5)](http://man7.org/linux/man-pages/man5/hosts.5.html) - man page: - - IP_address canonical_hostname [aliases...] - items: - type: "string" - DNSConfig: - description: | - Specification for DNS related configurations in resolver configuration - file (`resolv.conf`). - type: "object" - properties: - Nameservers: - description: "The IP addresses of the name servers." - type: "array" - items: - type: "string" - Search: - description: "A search list for host-name lookup." - type: "array" - items: - type: "string" - Options: - description: | - A list of internal resolver variables to be modified (e.g., - `debug`, `ndots:3`, etc.). - type: "array" - items: - type: "string" - Secrets: - description: | - Secrets contains references to zero or more secrets that will be - exposed to the service. - type: "array" - items: - type: "object" - properties: - File: - description: | - File represents a specific target that is backed by a file. - type: "object" - properties: - Name: - description: | - Name represents the final filename in the filesystem. - type: "string" - UID: - description: "UID represents the file UID." - type: "string" - GID: - description: "GID represents the file GID." - type: "string" - Mode: - description: "Mode represents the FileMode of the file." - type: "integer" - format: "uint32" - SecretID: - description: | - SecretID represents the ID of the specific secret that we're - referencing. - type: "string" - SecretName: - description: | - SecretName is the name of the secret that this references, - but this is just provided for lookup/display purposes. The - secret in the reference will be identified by its ID. - type: "string" - OomScoreAdj: - type: "integer" - format: "int64" - description: | - An integer value containing the score given to the container in - order to tune OOM killer preferences. - example: 0 - Configs: - description: | - Configs contains references to zero or more configs that will be - exposed to the service. - type: "array" - items: - type: "object" - properties: - File: - description: | - File represents a specific target that is backed by a file. - -


- - > **Note**: `Configs.File` and `Configs.Runtime` are mutually exclusive - type: "object" - properties: - Name: - description: | - Name represents the final filename in the filesystem. - type: "string" - UID: - description: "UID represents the file UID." - type: "string" - GID: - description: "GID represents the file GID." - type: "string" - Mode: - description: "Mode represents the FileMode of the file." - type: "integer" - format: "uint32" - Runtime: - description: | - Runtime represents a target that is not mounted into the - container but is used by the task - -


- - > **Note**: `Configs.File` and `Configs.Runtime` are mutually - > exclusive - type: "object" - ConfigID: - description: | - ConfigID represents the ID of the specific config that we're - referencing. - type: "string" - ConfigName: - description: | - ConfigName is the name of the config that this references, - but this is just provided for lookup/display purposes. The - config in the reference will be identified by its ID. - type: "string" - Isolation: - type: "string" - description: | - Isolation technology of the containers running the service. - (Windows only) - enum: - - "default" - - "process" - - "hyperv" - - "" - Init: - description: | - Run an init inside the container that forwards signals and reaps - processes. This field is omitted if empty, and the default (as - configured on the daemon) is used. - type: "boolean" - x-nullable: true - Sysctls: - description: | - Set kernel namedspaced parameters (sysctls) in the container. - The Sysctls option on services accepts the same sysctls as the - are supported on containers. Note that while the same sysctls are - supported, no guarantees or checks are made about their - suitability for a clustered environment, and it's up to the user - to determine whether a given sysctl will work properly in a - Service. - type: "object" - additionalProperties: - type: "string" - # This option is not used by Windows containers - CapabilityAdd: - type: "array" - description: | - A list of kernel capabilities to add to the default set - for the container. - items: - type: "string" - example: - - "CAP_NET_RAW" - - "CAP_SYS_ADMIN" - - "CAP_SYS_CHROOT" - - "CAP_SYSLOG" - CapabilityDrop: - type: "array" - description: | - A list of kernel capabilities to drop from the default set - for the container. - items: - type: "string" - example: - - "CAP_NET_RAW" - Ulimits: - description: | - A list of resource limits to set in the container. For example: `{"Name": "nofile", "Soft": 1024, "Hard": 2048}`" - type: "array" - items: - type: "object" - properties: - Name: - description: "Name of ulimit" - type: "string" - Soft: - description: "Soft limit" - type: "integer" - Hard: - description: "Hard limit" - type: "integer" - NetworkAttachmentSpec: - description: | - Read-only spec type for non-swarm containers attached to swarm overlay - networks. - -


- - > **Note**: ContainerSpec, NetworkAttachmentSpec, and PluginSpec are - > mutually exclusive. PluginSpec is only used when the Runtime field - > is set to `plugin`. NetworkAttachmentSpec is used when the Runtime - > field is set to `attachment`. - type: "object" - properties: - ContainerID: - description: "ID of the container represented by this task" - type: "string" - Resources: - description: | - Resource requirements which apply to each individual container created - as part of the service. - type: "object" - properties: - Limits: - description: "Define resources limits." - $ref: "#/definitions/Limit" - Reservations: - description: "Define resources reservation." - $ref: "#/definitions/ResourceObject" - RestartPolicy: - description: | - Specification for the restart policy which applies to containers - created as part of this service. - type: "object" - properties: - Condition: - description: "Condition for restart." - type: "string" - enum: - - "none" - - "on-failure" - - "any" - Delay: - description: "Delay between restart attempts." - type: "integer" - format: "int64" - MaxAttempts: - description: | - Maximum attempts to restart a given container before giving up - (default value is 0, which is ignored). - type: "integer" - format: "int64" - default: 0 - Window: - description: | - Windows is the time window used to evaluate the restart policy - (default value is 0, which is unbounded). - type: "integer" - format: "int64" - default: 0 - Placement: - type: "object" - properties: - Constraints: - description: | - An array of constraint expressions to limit the set of nodes where - a task can be scheduled. Constraint expressions can either use a - _match_ (`==`) or _exclude_ (`!=`) rule. Multiple constraints find - nodes that satisfy every expression (AND match). Constraints can - match node or Docker Engine labels as follows: - - node attribute | matches | example - ---------------------|--------------------------------|----------------------------------------------- - `node.id` | Node ID | `node.id==2ivku8v2gvtg4` - `node.hostname` | Node hostname | `node.hostname!=node-2` - `node.role` | Node role (`manager`/`worker`) | `node.role==manager` - `node.platform.os` | Node operating system | `node.platform.os==windows` - `node.platform.arch` | Node architecture | `node.platform.arch==x86_64` - `node.labels` | User-defined node labels | `node.labels.security==high` - `engine.labels` | Docker Engine's labels | `engine.labels.operatingsystem==ubuntu-24.04` - - `engine.labels` apply to Docker Engine labels like operating system, - drivers, etc. Swarm administrators add `node.labels` for operational - purposes by using the [`node update endpoint`](#operation/NodeUpdate). - - type: "array" - items: - type: "string" - example: - - "node.hostname!=node3.corp.example.com" - - "node.role!=manager" - - "node.labels.type==production" - - "node.platform.os==linux" - - "node.platform.arch==x86_64" - Preferences: - description: | - Preferences provide a way to make the scheduler aware of factors - such as topology. They are provided in order from highest to - lowest precedence. - type: "array" - items: - type: "object" - properties: - Spread: - type: "object" - properties: - SpreadDescriptor: - description: | - label descriptor, such as `engine.labels.az`. - type: "string" - example: - - Spread: - SpreadDescriptor: "node.labels.datacenter" - - Spread: - SpreadDescriptor: "node.labels.rack" - MaxReplicas: - description: | - Maximum number of replicas for per node (default value is 0, which - is unlimited) - type: "integer" - format: "int64" - default: 0 - Platforms: - description: | - Platforms stores all the platforms that the service's image can - run on. This field is used in the platform filter for scheduling. - If empty, then the platform filter is off, meaning there are no - scheduling restrictions. - type: "array" - items: - $ref: "#/definitions/Platform" - ForceUpdate: - description: | - A counter that triggers an update even if no relevant parameters have - been changed. - type: "integer" - Runtime: - description: | - Runtime is the type of runtime specified for the task executor. - type: "string" - Networks: - description: "Specifies which networks the service should attach to." - type: "array" - items: - $ref: "#/definitions/NetworkAttachmentConfig" - LogDriver: - description: | - Specifies the log driver to use for tasks created from this spec. If - not present, the default one for the swarm will be used, finally - falling back to the engine default if not specified. - type: "object" - properties: - Name: - type: "string" - Options: - type: "object" - additionalProperties: - type: "string" - - TaskState: - type: "string" - enum: - - "new" - - "allocated" - - "pending" - - "assigned" - - "accepted" - - "preparing" - - "ready" - - "starting" - - "running" - - "complete" - - "shutdown" - - "failed" - - "rejected" - - "remove" - - "orphaned" - - ContainerStatus: - type: "object" - description: "represents the status of a container." - properties: - ContainerID: - type: "string" - PID: - type: "integer" - ExitCode: - type: "integer" - - PortStatus: - type: "object" - description: "represents the port status of a task's host ports whose service has published host ports" - properties: - Ports: - type: "array" - items: - $ref: "#/definitions/EndpointPortConfig" - - TaskStatus: - type: "object" - description: "represents the status of a task." - properties: - Timestamp: - type: "string" - format: "dateTime" - State: - $ref: "#/definitions/TaskState" - Message: - type: "string" - Err: - type: "string" - ContainerStatus: - $ref: "#/definitions/ContainerStatus" - PortStatus: - $ref: "#/definitions/PortStatus" - - Task: - type: "object" - properties: - ID: - description: "The ID of the task." - type: "string" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - type: "string" - format: "dateTime" - UpdatedAt: - type: "string" - format: "dateTime" - Name: - description: "Name of the task." - type: "string" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - Spec: - $ref: "#/definitions/TaskSpec" - ServiceID: - description: "The ID of the service this task is part of." - type: "string" - Slot: - type: "integer" - NodeID: - description: "The ID of the node that this task is on." - type: "string" - AssignedGenericResources: - $ref: "#/definitions/GenericResources" - Status: - $ref: "#/definitions/TaskStatus" - DesiredState: - $ref: "#/definitions/TaskState" - JobIteration: - description: | - If the Service this Task belongs to is a job-mode service, contains - the JobIteration of the Service this Task was created for. Absent if - the Task was created for a Replicated or Global Service. - $ref: "#/definitions/ObjectVersion" - example: - ID: "0kzzo1i0y4jz6027t0k7aezc7" - Version: - Index: 71 - CreatedAt: "2016-06-07T21:07:31.171892745Z" - UpdatedAt: "2016-06-07T21:07:31.376370513Z" - Spec: - ContainerSpec: - Image: "redis" - Resources: - Limits: {} - Reservations: {} - RestartPolicy: - Condition: "any" - MaxAttempts: 0 - Placement: {} - ServiceID: "9mnpnzenvg8p8tdbtq4wvbkcz" - Slot: 1 - NodeID: "60gvrl6tm78dmak4yl7srz94v" - Status: - Timestamp: "2016-06-07T21:07:31.290032978Z" - State: "running" - Message: "started" - ContainerStatus: - ContainerID: "e5d62702a1b48d01c3e02ca1e0212a250801fa8d67caca0b6f35919ebc12f035" - PID: 677 - DesiredState: "running" - NetworksAttachments: - - Network: - ID: "4qvuz4ko70xaltuqbt8956gd1" - Version: - Index: 18 - CreatedAt: "2016-06-07T20:31:11.912919752Z" - UpdatedAt: "2016-06-07T21:07:29.955277358Z" - Spec: - Name: "ingress" - Labels: - com.docker.swarm.internal: "true" - DriverConfiguration: {} - IPAMOptions: - Driver: {} - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - DriverState: - Name: "overlay" - Options: - com.docker.network.driver.overlay.vxlanid_list: "256" - IPAMOptions: - Driver: - Name: "default" - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - Addresses: - - "10.255.0.10/16" - AssignedGenericResources: - - DiscreteResourceSpec: - Kind: "SSD" - Value: 3 - - NamedResourceSpec: - Kind: "GPU" - Value: "UUID1" - - NamedResourceSpec: - Kind: "GPU" - Value: "UUID2" - - ServiceSpec: - description: "User modifiable configuration for a service." - type: object - properties: - Name: - description: "Name of the service." - type: "string" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - TaskTemplate: - $ref: "#/definitions/TaskSpec" - Mode: - description: "Scheduling mode for the service." - type: "object" - properties: - Replicated: - type: "object" - properties: - Replicas: - type: "integer" - format: "int64" - Global: - type: "object" - ReplicatedJob: - description: | - The mode used for services with a finite number of tasks that run - to a completed state. - type: "object" - properties: - MaxConcurrent: - description: | - The maximum number of replicas to run simultaneously. - type: "integer" - format: "int64" - default: 1 - TotalCompletions: - description: | - The total number of replicas desired to reach the Completed - state. If unset, will default to the value of `MaxConcurrent` - type: "integer" - format: "int64" - GlobalJob: - description: | - The mode used for services which run a task to the completed state - on each valid node. - type: "object" - UpdateConfig: - description: "Specification for the update strategy of the service." - type: "object" - properties: - Parallelism: - description: | - Maximum number of tasks to be updated in one iteration (0 means - unlimited parallelism). - type: "integer" - format: "int64" - Delay: - description: "Amount of time between updates, in nanoseconds." - type: "integer" - format: "int64" - FailureAction: - description: | - Action to take if an updated task fails to run, or stops running - during the update. - type: "string" - enum: - - "continue" - - "pause" - - "rollback" - Monitor: - description: | - Amount of time to monitor each updated task for failures, in - nanoseconds. - type: "integer" - format: "int64" - MaxFailureRatio: - description: | - The fraction of tasks that may fail during an update before the - failure action is invoked, specified as a floating point number - between 0 and 1. - type: "number" - default: 0 - Order: - description: | - The order of operations when rolling out an updated task. Either - the old task is shut down before the new task is started, or the - new task is started before the old task is shut down. - type: "string" - enum: - - "stop-first" - - "start-first" - RollbackConfig: - description: "Specification for the rollback strategy of the service." - type: "object" - properties: - Parallelism: - description: | - Maximum number of tasks to be rolled back in one iteration (0 means - unlimited parallelism). - type: "integer" - format: "int64" - Delay: - description: | - Amount of time between rollback iterations, in nanoseconds. - type: "integer" - format: "int64" - FailureAction: - description: | - Action to take if an rolled back task fails to run, or stops - running during the rollback. - type: "string" - enum: - - "continue" - - "pause" - Monitor: - description: | - Amount of time to monitor each rolled back task for failures, in - nanoseconds. - type: "integer" - format: "int64" - MaxFailureRatio: - description: | - The fraction of tasks that may fail during a rollback before the - failure action is invoked, specified as a floating point number - between 0 and 1. - type: "number" - default: 0 - Order: - description: | - The order of operations when rolling back a task. Either the old - task is shut down before the new task is started, or the new task - is started before the old task is shut down. - type: "string" - enum: - - "stop-first" - - "start-first" - Networks: - description: | - Specifies which networks the service should attach to. - - Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead. - type: "array" - items: - $ref: "#/definitions/NetworkAttachmentConfig" - - EndpointSpec: - $ref: "#/definitions/EndpointSpec" - - EndpointPortConfig: - type: "object" - properties: - Name: - type: "string" - Protocol: - type: "string" - enum: - - "tcp" - - "udp" - - "sctp" - TargetPort: - description: "The port inside the container." - type: "integer" - PublishedPort: - description: "The port on the swarm hosts." - type: "integer" - PublishMode: - description: | - The mode in which port is published. - -


- - - "ingress" makes the target port accessible on every node, - regardless of whether there is a task for the service running on - that node or not. - - "host" bypasses the routing mesh and publish the port directly on - the swarm node where that service is running. - - type: "string" - enum: - - "ingress" - - "host" - default: "ingress" - example: "ingress" - - EndpointSpec: - description: "Properties that can be configured to access and load balance a service." - type: "object" - properties: - Mode: - description: | - The mode of resolution to use for internal load balancing between tasks. - type: "string" - enum: - - "vip" - - "dnsrr" - default: "vip" - Ports: - description: | - List of exposed ports that this service is accessible on from the - outside. Ports can only be provided if `vip` resolution mode is used. - type: "array" - items: - $ref: "#/definitions/EndpointPortConfig" - - Service: - type: "object" - properties: - ID: - type: "string" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - type: "string" - format: "dateTime" - UpdatedAt: - type: "string" - format: "dateTime" - Spec: - $ref: "#/definitions/ServiceSpec" - Endpoint: - type: "object" - properties: - Spec: - $ref: "#/definitions/EndpointSpec" - Ports: - type: "array" - items: - $ref: "#/definitions/EndpointPortConfig" - VirtualIPs: - type: "array" - items: - type: "object" - properties: - NetworkID: - type: "string" - Addr: - type: "string" - UpdateStatus: - description: "The status of a service update." - type: "object" - properties: - State: - type: "string" - enum: - - "updating" - - "paused" - - "completed" - StartedAt: - type: "string" - format: "dateTime" - CompletedAt: - type: "string" - format: "dateTime" - Message: - type: "string" - ServiceStatus: - description: | - The status of the service's tasks. Provided only when requested as - part of a ServiceList operation. - type: "object" - properties: - RunningTasks: - description: | - The number of tasks for the service currently in the Running state. - type: "integer" - format: "uint64" - example: 7 - DesiredTasks: - description: | - The number of tasks for the service desired to be running. - For replicated services, this is the replica count from the - service spec. For global services, this is computed by taking - count of all tasks for the service with a Desired State other - than Shutdown. - type: "integer" - format: "uint64" - example: 10 - CompletedTasks: - description: | - The number of tasks for a job that are in the Completed state. - This field must be cross-referenced with the service type, as the - value of 0 may mean the service is not in a job mode, or it may - mean the job-mode service has no tasks yet Completed. - type: "integer" - format: "uint64" - JobStatus: - description: | - The status of the service when it is in one of ReplicatedJob or - GlobalJob modes. Absent on Replicated and Global mode services. The - JobIteration is an ObjectVersion, but unlike the Service's version, - does not need to be sent with an update request. - type: "object" - properties: - JobIteration: - description: | - JobIteration is a value increased each time a Job is executed, - successfully or otherwise. "Executed", in this case, means the - job as a whole has been started, not that an individual Task has - been launched. A job is "Executed" when its ServiceSpec is - updated. JobIteration can be used to disambiguate Tasks belonging - to different executions of a job. Though JobIteration will - increase with each subsequent execution, it may not necessarily - increase by 1, and so JobIteration should not be used to - $ref: "#/definitions/ObjectVersion" - LastExecution: - description: | - The last time, as observed by the server, that this job was - started. - type: "string" - format: "dateTime" - example: - ID: "9mnpnzenvg8p8tdbtq4wvbkcz" - Version: - Index: 19 - CreatedAt: "2016-06-07T21:05:51.880065305Z" - UpdatedAt: "2016-06-07T21:07:29.962229872Z" - Spec: - Name: "hopeful_cori" - TaskTemplate: - ContainerSpec: - Image: "redis" - Resources: - Limits: {} - Reservations: {} - RestartPolicy: - Condition: "any" - MaxAttempts: 0 - Placement: {} - ForceUpdate: 0 - Mode: - Replicated: - Replicas: 1 - UpdateConfig: - Parallelism: 1 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - RollbackConfig: - Parallelism: 1 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - EndpointSpec: - Mode: "vip" - Ports: - - - Protocol: "tcp" - TargetPort: 6379 - PublishedPort: 30001 - Endpoint: - Spec: - Mode: "vip" - Ports: - - - Protocol: "tcp" - TargetPort: 6379 - PublishedPort: 30001 - Ports: - - - Protocol: "tcp" - TargetPort: 6379 - PublishedPort: 30001 - VirtualIPs: - - - NetworkID: "4qvuz4ko70xaltuqbt8956gd1" - Addr: "10.255.0.2/16" - - - NetworkID: "4qvuz4ko70xaltuqbt8956gd1" - Addr: "10.255.0.3/16" - - ImageDeleteResponseItem: - type: "object" - x-go-name: "DeleteResponse" - properties: - Untagged: - description: "The image ID of an image that was untagged" - type: "string" - Deleted: - description: "The image ID of an image that was deleted" - type: "string" - - ServiceCreateResponse: - type: "object" - description: | - contains the information returned to a client on the - creation of a new service. - properties: - ID: - description: "The ID of the created service." - type: "string" - x-nullable: false - example: "ak7w3gjqoa3kuz8xcpnyy0pvl" - Warnings: - description: | - Optional warning message. - - FIXME(thaJeztah): this should have "omitempty" in the generated type. - type: "array" - x-nullable: true - items: - type: "string" - example: - - "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found" - - ServiceUpdateResponse: - type: "object" - properties: - Warnings: - description: "Optional warning messages" - type: "array" - items: - type: "string" - example: - Warnings: - - "unable to pin image doesnotexist:latest to digest: image library/doesnotexist:latest not found" - - ContainerInspectResponse: - type: "object" - title: "ContainerInspectResponse" - x-go-name: "InspectResponse" - properties: - Id: - description: |- - The ID of this container as a 128-bit (64-character) hexadecimal string (32 bytes). - type: "string" - x-go-name: "ID" - minLength: 64 - maxLength: 64 - pattern: "^[0-9a-fA-F]{64}$" - example: "aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf" - Created: - description: |- - Date and time at which the container was created, formatted in - [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format with nano-seconds. - type: "string" - format: "dateTime" - x-nullable: true - example: "2025-02-17T17:43:39.64001363Z" - Path: - description: |- - The path to the command being run - type: "string" - example: "/bin/sh" - Args: - description: "The arguments to the command being run" - type: "array" - items: - type: "string" - example: - - "-c" - - "exit 9" - State: - $ref: "#/definitions/ContainerState" - Image: - description: |- - The ID (digest) of the image that this container was created from. - type: "string" - example: "sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782" - ResolvConfPath: - description: |- - Location of the `/etc/resolv.conf` generated for the container on the - host. - - This file is managed through the docker daemon, and should not be - accessed or modified by other tools. - type: "string" - example: "/var/lib/docker/containers/aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf/resolv.conf" - HostnamePath: - description: |- - Location of the `/etc/hostname` generated for the container on the - host. - - This file is managed through the docker daemon, and should not be - accessed or modified by other tools. - type: "string" - example: "/var/lib/docker/containers/aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf/hostname" - HostsPath: - description: |- - Location of the `/etc/hosts` generated for the container on the - host. - - This file is managed through the docker daemon, and should not be - accessed or modified by other tools. - type: "string" - example: "/var/lib/docker/containers/aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf/hosts" - LogPath: - description: |- - Location of the file used to buffer the container's logs. Depending on - the logging-driver used for the container, this field may be omitted. - - This file is managed through the docker daemon, and should not be - accessed or modified by other tools. - type: "string" - x-nullable: true - example: "/var/lib/docker/containers/5b7c7e2b992aa426584ce6c47452756066be0e503a08b4516a433a54d2f69e59/5b7c7e2b992aa426584ce6c47452756066be0e503a08b4516a433a54d2f69e59-json.log" - Name: - description: |- - The name associated with this container. - - For historic reasons, the name may be prefixed with a forward-slash (`/`). - type: "string" - example: "/funny_chatelet" - RestartCount: - description: |- - Number of times the container was restarted since it was created, - or since daemon was started. - type: "integer" - example: 0 - Driver: - description: |- - The storage-driver used for the container's filesystem (graph-driver - or snapshotter). - type: "string" - example: "overlayfs" - Platform: - description: |- - The platform (operating system) for which the container was created. - - This field was introduced for the experimental "LCOW" (Linux Containers - On Windows) features, which has been removed. In most cases, this field - is equal to the host's operating system (`linux` or `windows`). - type: "string" - example: "linux" - ImageManifestDescriptor: - $ref: "#/definitions/OCIDescriptor" - description: |- - OCI descriptor of the platform-specific manifest of the image - the container was created from. - - Note: Only available if the daemon provides a multi-platform - image store. - MountLabel: - description: |- - SELinux mount label set for the container. - type: "string" - example: "" - ProcessLabel: - description: |- - SELinux process label set for the container. - type: "string" - example: "" - AppArmorProfile: - description: |- - The AppArmor profile set for the container. - type: "string" - example: "" - ExecIDs: - description: |- - IDs of exec instances that are running in the container. - type: "array" - items: - type: "string" - x-nullable: true - example: - - "b35395de42bc8abd327f9dd65d913b9ba28c74d2f0734eeeae84fa1c616a0fca" - - "3fc1232e5cd20c8de182ed81178503dc6437f4e7ef12b52cc5e8de020652f1c4" - HostConfig: - $ref: "#/definitions/HostConfig" - GraphDriver: - $ref: "#/definitions/DriverData" - SizeRw: - description: |- - The size of files that have been created or changed by this container. - - This field is omitted by default, and only set when size is requested - in the API request. - type: "integer" - format: "int64" - x-nullable: true - example: "122880" - SizeRootFs: - description: |- - The total size of all files in the read-only layers from the image - that the container uses. These layers can be shared between containers. - - This field is omitted by default, and only set when size is requested - in the API request. - type: "integer" - format: "int64" - x-nullable: true - example: "1653948416" - Mounts: - description: |- - List of mounts used by the container. - type: "array" - items: - $ref: "#/definitions/MountPoint" - Config: - $ref: "#/definitions/ContainerConfig" - NetworkSettings: - $ref: "#/definitions/NetworkSettings" - - ContainerSummary: - type: "object" - properties: - Id: - description: |- - The ID of this container as a 128-bit (64-character) hexadecimal string (32 bytes). - type: "string" - x-go-name: "ID" - minLength: 64 - maxLength: 64 - pattern: "^[0-9a-fA-F]{64}$" - example: "aa86eacfb3b3ed4cd362c1e88fc89a53908ad05fb3a4103bca3f9b28292d14bf" - Names: - description: |- - The names associated with this container. Most containers have a single - name, but when using legacy "links", the container can have multiple - names. - - For historic reasons, names are prefixed with a forward-slash (`/`). - type: "array" - items: - type: "string" - example: - - "/funny_chatelet" - Image: - description: |- - The name or ID of the image used to create the container. - - This field shows the image reference as was specified when creating the container, - which can be in its canonical form (e.g., `docker.io/library/ubuntu:latest` - or `docker.io/library/ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782`), - short form (e.g., `ubuntu:latest`)), or the ID(-prefix) of the image (e.g., `72297848456d`). - - The content of this field can be updated at runtime if the image used to - create the container is untagged, in which case the field is updated to - contain the the image ID (digest) it was resolved to in its canonical, - non-truncated form (e.g., `sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782`). - type: "string" - example: "docker.io/library/ubuntu:latest" - ImageID: - description: |- - The ID (digest) of the image that this container was created from. - type: "string" - example: "sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782" - ImageManifestDescriptor: - $ref: "#/definitions/OCIDescriptor" - x-nullable: true - description: | - OCI descriptor of the platform-specific manifest of the image - the container was created from. - - Note: Only available if the daemon provides a multi-platform - image store. - - This field is not populated in the `GET /system/df` endpoint. - Command: - description: "Command to run when starting the container" - type: "string" - example: "/bin/bash" - Created: - description: |- - Date and time at which the container was created as a Unix timestamp - (number of seconds since EPOCH). - type: "integer" - format: "int64" - example: "1739811096" - Ports: - description: |- - Port-mappings for the container. - type: "array" - items: - $ref: "#/definitions/Port" - SizeRw: - description: |- - The size of files that have been created or changed by this container. - - This field is omitted by default, and only set when size is requested - in the API request. - type: "integer" - format: "int64" - x-nullable: true - example: "122880" - SizeRootFs: - description: |- - The total size of all files in the read-only layers from the image - that the container uses. These layers can be shared between containers. - - This field is omitted by default, and only set when size is requested - in the API request. - type: "integer" - format: "int64" - x-nullable: true - example: "1653948416" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.vendor: "Acme" - com.example.license: "GPL" - com.example.version: "1.0" - State: - description: | - The state of this container. - type: "string" - enum: - - "created" - - "running" - - "paused" - - "restarting" - - "exited" - - "removing" - - "dead" - example: "running" - Status: - description: |- - Additional human-readable status of this container (e.g. `Exit 0`) - type: "string" - example: "Up 4 days" - HostConfig: - type: "object" - description: |- - Summary of host-specific runtime information of the container. This - is a reduced set of information in the container's "HostConfig" as - available in the container "inspect" response. - properties: - NetworkMode: - description: |- - Networking mode (`host`, `none`, `container:`) or name of the - primary network the container is using. - - This field is primarily for backward compatibility. The container - can be connected to multiple networks for which information can be - found in the `NetworkSettings.Networks` field, which enumerates - settings per network. - type: "string" - example: "mynetwork" - Annotations: - description: |- - Arbitrary key-value metadata attached to the container. - type: "object" - x-nullable: true - additionalProperties: - type: "string" - example: - io.kubernetes.docker.type: "container" - io.kubernetes.sandbox.id: "3befe639bed0fd6afdd65fd1fa84506756f59360ec4adc270b0fdac9be22b4d3" - NetworkSettings: - description: |- - Summary of the container's network settings - type: "object" - properties: - Networks: - type: "object" - description: |- - Summary of network-settings for each network the container is - attached to. - additionalProperties: - $ref: "#/definitions/EndpointSettings" - Mounts: - type: "array" - description: |- - List of mounts used by the container. - items: - $ref: "#/definitions/MountPoint" - - Driver: - description: "Driver represents a driver (network, logging, secrets)." - type: "object" - required: [Name] - properties: - Name: - description: "Name of the driver." - type: "string" - x-nullable: false - example: "some-driver" - Options: - description: "Key/value map of driver-specific options." - type: "object" - x-nullable: false - additionalProperties: - type: "string" - example: - OptionA: "value for driver-specific option A" - OptionB: "value for driver-specific option B" - - SecretSpec: - type: "object" - properties: - Name: - description: "User-defined name of the secret." - type: "string" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - Data: - description: | - Data is the data to store as a secret, formatted as a Base64-url-safe-encoded - ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string. - It must be empty if the Driver field is set, in which case the data is - loaded from an external secret store. The maximum allowed size is 500KB, - as defined in [MaxSecretSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/api/validation#MaxSecretSize). - - This field is only used to _create_ a secret, and is not returned by - other endpoints. - type: "string" - example: "" - Driver: - description: | - Name of the secrets driver used to fetch the secret's value from an - external secret store. - $ref: "#/definitions/Driver" - Templating: - description: | - Templating driver, if applicable - - Templating controls whether and how to evaluate the config payload as - a template. If no driver is set, no templating is used. - $ref: "#/definitions/Driver" - - Secret: - type: "object" - properties: - ID: - type: "string" - example: "blt1owaxmitz71s9v5zh81zun" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - type: "string" - format: "dateTime" - example: "2017-07-20T13:55:28.678958722Z" - UpdatedAt: - type: "string" - format: "dateTime" - example: "2017-07-20T13:55:28.678958722Z" - Spec: - $ref: "#/definitions/SecretSpec" - - ConfigSpec: - type: "object" - properties: - Name: - description: "User-defined name of the config." - type: "string" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - Data: - description: | - Data is the data to store as a config, formatted as a Base64-url-safe-encoded - ([RFC 4648](https://tools.ietf.org/html/rfc4648#section-5)) string. - The maximum allowed size is 1000KB, as defined in [MaxConfigSize](https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize). - type: "string" - Templating: - description: | - Templating driver, if applicable - - Templating controls whether and how to evaluate the config payload as - a template. If no driver is set, no templating is used. - $ref: "#/definitions/Driver" - - Config: - type: "object" - properties: - ID: - type: "string" - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - type: "string" - format: "dateTime" - UpdatedAt: - type: "string" - format: "dateTime" - Spec: - $ref: "#/definitions/ConfigSpec" - - ContainerState: - description: | - ContainerState stores container's running state. It's part of ContainerJSONBase - and will be returned by the "inspect" command. - type: "object" - x-nullable: true - properties: - Status: - description: | - String representation of the container state. Can be one of "created", - "running", "paused", "restarting", "removing", "exited", or "dead". - type: "string" - enum: ["created", "running", "paused", "restarting", "removing", "exited", "dead"] - example: "running" - Running: - description: | - Whether this container is running. - - Note that a running container can be _paused_. The `Running` and `Paused` - booleans are not mutually exclusive: - - When pausing a container (on Linux), the freezer cgroup is used to suspend - all processes in the container. Freezing the process requires the process to - be running. As a result, paused containers are both `Running` _and_ `Paused`. - - Use the `Status` field instead to determine if a container's state is "running". - type: "boolean" - example: true - Paused: - description: "Whether this container is paused." - type: "boolean" - example: false - Restarting: - description: "Whether this container is restarting." - type: "boolean" - example: false - OOMKilled: - description: | - Whether a process within this container has been killed because it ran - out of memory since the container was last started. - type: "boolean" - example: false - Dead: - type: "boolean" - example: false - Pid: - description: "The process ID of this container" - type: "integer" - example: 1234 - ExitCode: - description: "The last exit code of this container" - type: "integer" - example: 0 - Error: - type: "string" - StartedAt: - description: "The time when this container was last started." - type: "string" - example: "2020-01-06T09:06:59.461876391Z" - FinishedAt: - description: "The time when this container last exited." - type: "string" - example: "2020-01-06T09:07:59.461876391Z" - Health: - $ref: "#/definitions/Health" - - ContainerCreateResponse: - description: "OK response to ContainerCreate operation" - type: "object" - title: "ContainerCreateResponse" - x-go-name: "CreateResponse" - required: [Id, Warnings] - properties: - Id: - description: "The ID of the created container" - type: "string" - x-nullable: false - example: "ede54ee1afda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c743" - Warnings: - description: "Warnings encountered when creating the container" - type: "array" - x-nullable: false - items: - type: "string" - example: [] - - ContainerUpdateResponse: - type: "object" - title: "ContainerUpdateResponse" - x-go-name: "UpdateResponse" - description: |- - Response for a successful container-update. - properties: - Warnings: - type: "array" - description: |- - Warnings encountered when updating the container. - items: - type: "string" - example: ["Published ports are discarded when using host network mode"] - - ContainerStatsResponse: - description: | - Statistics sample for a container. - type: "object" - x-go-name: "StatsResponse" - title: "ContainerStatsResponse" - properties: - name: - description: "Name of the container" - type: "string" - x-nullable: true - example: "boring_wozniak" - id: - description: "ID of the container" - type: "string" - x-nullable: true - example: "ede54ee1afda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c743" - read: - description: | - Date and time at which this sample was collected. - The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) - with nano-seconds. - type: "string" - format: "date-time" - example: "2025-01-16T13:55:22.165243637Z" - preread: - description: | - Date and time at which this first sample was collected. This field - is not propagated if the "one-shot" option is set. If the "one-shot" - option is set, this field may be omitted, empty, or set to a default - date (`0001-01-01T00:00:00Z`). - - The value is formatted as [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) - with nano-seconds. - type: "string" - format: "date-time" - example: "2025-01-16T13:55:21.160452595Z" - pids_stats: - $ref: "#/definitions/ContainerPidsStats" - blkio_stats: - $ref: "#/definitions/ContainerBlkioStats" - num_procs: - description: | - The number of processors on the system. - - This field is Windows-specific and always zero for Linux containers. - type: "integer" - format: "uint32" - example: 16 - storage_stats: - $ref: "#/definitions/ContainerStorageStats" - cpu_stats: - $ref: "#/definitions/ContainerCPUStats" - precpu_stats: - $ref: "#/definitions/ContainerCPUStats" - memory_stats: - $ref: "#/definitions/ContainerMemoryStats" - networks: - description: | - Network statistics for the container per interface. - - This field is omitted if the container has no networking enabled. - x-nullable: true - additionalProperties: - $ref: "#/definitions/ContainerNetworkStats" - example: - eth0: - rx_bytes: 5338 - rx_dropped: 0 - rx_errors: 0 - rx_packets: 36 - tx_bytes: 648 - tx_dropped: 0 - tx_errors: 0 - tx_packets: 8 - eth5: - rx_bytes: 4641 - rx_dropped: 0 - rx_errors: 0 - rx_packets: 26 - tx_bytes: 690 - tx_dropped: 0 - tx_errors: 0 - tx_packets: 9 - - ContainerBlkioStats: - description: | - BlkioStats stores all IO service stats for data read and write. - - This type is Linux-specific and holds many fields that are specific to cgroups v1. - On a cgroup v2 host, all fields other than `io_service_bytes_recursive` - are omitted or `null`. - - This type is only populated on Linux and omitted for Windows containers. - type: "object" - x-go-name: "BlkioStats" - x-nullable: true - properties: - io_service_bytes_recursive: - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_serviced_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_queue_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_service_time_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_wait_time_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_merged_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - io_time_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - sectors_recursive: - description: | - This field is only available when using Linux containers with - cgroups v1. It is omitted or `null` when using cgroups v2. - x-nullable: true - type: "array" - items: - $ref: "#/definitions/ContainerBlkioStatEntry" - example: - io_service_bytes_recursive: [ - {"major": 254, "minor": 0, "op": "read", "value": 7593984}, - {"major": 254, "minor": 0, "op": "write", "value": 100} - ] - io_serviced_recursive: null - io_queue_recursive: null - io_service_time_recursive: null - io_wait_time_recursive: null - io_merged_recursive: null - io_time_recursive: null - sectors_recursive: null - - ContainerBlkioStatEntry: - description: | - Blkio stats entry. - - This type is Linux-specific and omitted for Windows containers. - type: "object" - x-go-name: "BlkioStatEntry" - x-nullable: true - properties: - major: - type: "integer" - format: "uint64" - example: 254 - minor: - type: "integer" - format: "uint64" - example: 0 - op: - type: "string" - example: "read" - value: - type: "integer" - format: "uint64" - example: 7593984 - - ContainerCPUStats: - description: | - CPU related info of the container - type: "object" - x-go-name: "CPUStats" - x-nullable: true - properties: - cpu_usage: - $ref: "#/definitions/ContainerCPUUsage" - system_cpu_usage: - description: | - System Usage. - - This field is Linux-specific and omitted for Windows containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 5 - online_cpus: - description: | - Number of online CPUs. - - This field is Linux-specific and omitted for Windows containers. - type: "integer" - format: "uint32" - x-nullable: true - example: 5 - throttling_data: - $ref: "#/definitions/ContainerThrottlingData" - - ContainerCPUUsage: - description: | - All CPU stats aggregated since container inception. - type: "object" - x-go-name: "CPUUsage" - x-nullable: true - properties: - total_usage: - description: | - Total CPU time consumed in nanoseconds (Linux) or 100's of nanoseconds (Windows). - type: "integer" - format: "uint64" - example: 29912000 - percpu_usage: - description: | - Total CPU time (in nanoseconds) consumed per core (Linux). - - This field is Linux-specific when using cgroups v1. It is omitted - when using cgroups v2 and Windows containers. - type: "array" - x-nullable: true - items: - type: "integer" - format: "uint64" - example: 29912000 - - usage_in_kernelmode: - description: | - Time (in nanoseconds) spent by tasks of the cgroup in kernel mode (Linux), - or time spent (in 100's of nanoseconds) by all container processes in - kernel mode (Windows). - - Not populated for Windows containers using Hyper-V isolation. - type: "integer" - format: "uint64" - example: 21994000 - usage_in_usermode: - description: | - Time (in nanoseconds) spent by tasks of the cgroup in user mode (Linux), - or time spent (in 100's of nanoseconds) by all container processes in - kernel mode (Windows). - - Not populated for Windows containers using Hyper-V isolation. - type: "integer" - format: "uint64" - example: 7918000 - - ContainerPidsStats: - description: | - PidsStats contains Linux-specific stats of a container's process-IDs (PIDs). - - This type is Linux-specific and omitted for Windows containers. - type: "object" - x-go-name: "PidsStats" - x-nullable: true - properties: - current: - description: | - Current is the number of PIDs in the cgroup. - type: "integer" - format: "uint64" - x-nullable: true - example: 5 - limit: - description: | - Limit is the hard limit on the number of pids in the cgroup. - A "Limit" of 0 means that there is no limit. - type: "integer" - format: "uint64" - x-nullable: true - example: "18446744073709551615" - - ContainerThrottlingData: - description: | - CPU throttling stats of the container. - - This type is Linux-specific and omitted for Windows containers. - type: "object" - x-go-name: "ThrottlingData" - x-nullable: true - properties: - periods: - description: | - Number of periods with throttling active. - type: "integer" - format: "uint64" - example: 0 - throttled_periods: - description: | - Number of periods when the container hit its throttling limit. - type: "integer" - format: "uint64" - example: 0 - throttled_time: - description: | - Aggregated time (in nanoseconds) the container was throttled for. - type: "integer" - format: "uint64" - example: 0 - - ContainerMemoryStats: - description: | - Aggregates all memory stats since container inception on Linux. - Windows returns stats for commit and private working set only. - type: "object" - x-go-name: "MemoryStats" - properties: - usage: - description: | - Current `res_counter` usage for memory. - - This field is Linux-specific and omitted for Windows containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - max_usage: - description: | - Maximum usage ever recorded. - - This field is Linux-specific and only supported on cgroups v1. - It is omitted when using cgroups v2 and for Windows containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - stats: - description: | - All the stats exported via memory.stat. when using cgroups v2. - - This field is Linux-specific and omitted for Windows containers. - type: "object" - additionalProperties: - type: "integer" - format: "uint64" - x-nullable: true - example: - { - "active_anon": 1572864, - "active_file": 5115904, - "anon": 1572864, - "anon_thp": 0, - "file": 7626752, - "file_dirty": 0, - "file_mapped": 2723840, - "file_writeback": 0, - "inactive_anon": 0, - "inactive_file": 2510848, - "kernel_stack": 16384, - "pgactivate": 0, - "pgdeactivate": 0, - "pgfault": 2042, - "pglazyfree": 0, - "pglazyfreed": 0, - "pgmajfault": 45, - "pgrefill": 0, - "pgscan": 0, - "pgsteal": 0, - "shmem": 0, - "slab": 1180928, - "slab_reclaimable": 725576, - "slab_unreclaimable": 455352, - "sock": 0, - "thp_collapse_alloc": 0, - "thp_fault_alloc": 1, - "unevictable": 0, - "workingset_activate": 0, - "workingset_nodereclaim": 0, - "workingset_refault": 0 - } - failcnt: - description: | - Number of times memory usage hits limits. - - This field is Linux-specific and only supported on cgroups v1. - It is omitted when using cgroups v2 and for Windows containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - limit: - description: | - This field is Linux-specific and omitted for Windows containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 8217579520 - commitbytes: - description: | - Committed bytes. - - This field is Windows-specific and omitted for Linux containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - commitpeakbytes: - description: | - Peak committed bytes. - - This field is Windows-specific and omitted for Linux containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - privateworkingset: - description: | - Private working set. - - This field is Windows-specific and omitted for Linux containers. - type: "integer" - format: "uint64" - x-nullable: true - example: 0 - - ContainerNetworkStats: - description: | - Aggregates the network stats of one container - type: "object" - x-go-name: "NetworkStats" - x-nullable: true - properties: - rx_bytes: - description: | - Bytes received. Windows and Linux. - type: "integer" - format: "uint64" - example: 5338 - rx_packets: - description: | - Packets received. Windows and Linux. - type: "integer" - format: "uint64" - example: 36 - rx_errors: - description: | - Received errors. Not used on Windows. - - This field is Linux-specific and always zero for Windows containers. - type: "integer" - format: "uint64" - example: 0 - rx_dropped: - description: | - Incoming packets dropped. Windows and Linux. - type: "integer" - format: "uint64" - example: 0 - tx_bytes: - description: | - Bytes sent. Windows and Linux. - type: "integer" - format: "uint64" - example: 1200 - tx_packets: - description: | - Packets sent. Windows and Linux. - type: "integer" - format: "uint64" - example: 12 - tx_errors: - description: | - Sent errors. Not used on Windows. - - This field is Linux-specific and always zero for Windows containers. - type: "integer" - format: "uint64" - example: 0 - tx_dropped: - description: | - Outgoing packets dropped. Windows and Linux. - type: "integer" - format: "uint64" - example: 0 - endpoint_id: - description: | - Endpoint ID. Not used on Linux. - - This field is Windows-specific and omitted for Linux containers. - type: "string" - x-nullable: true - instance_id: - description: | - Instance ID. Not used on Linux. - - This field is Windows-specific and omitted for Linux containers. - type: "string" - x-nullable: true - - ContainerStorageStats: - description: | - StorageStats is the disk I/O stats for read/write on Windows. - - This type is Windows-specific and omitted for Linux containers. - type: "object" - x-go-name: "StorageStats" - x-nullable: true - properties: - read_count_normalized: - type: "integer" - format: "uint64" - x-nullable: true - example: 7593984 - read_size_bytes: - type: "integer" - format: "uint64" - x-nullable: true - example: 7593984 - write_count_normalized: - type: "integer" - format: "uint64" - x-nullable: true - example: 7593984 - write_size_bytes: - type: "integer" - format: "uint64" - x-nullable: true - example: 7593984 - - ContainerTopResponse: - type: "object" - x-go-name: "TopResponse" - title: "ContainerTopResponse" - description: |- - Container "top" response. - properties: - Titles: - description: "The ps column titles" - type: "array" - items: - type: "string" - example: - Titles: - - "UID" - - "PID" - - "PPID" - - "C" - - "STIME" - - "TTY" - - "TIME" - - "CMD" - Processes: - description: |- - Each process running in the container, where each process - is an array of values corresponding to the titles. - type: "array" - items: - type: "array" - items: - type: "string" - example: - Processes: - - - - "root" - - "13642" - - "882" - - "0" - - "17:03" - - "pts/0" - - "00:00:00" - - "/bin/bash" - - - - "root" - - "13735" - - "13642" - - "0" - - "17:06" - - "pts/0" - - "00:00:00" - - "sleep 10" - - ContainerWaitResponse: - description: "OK response to ContainerWait operation" - type: "object" - x-go-name: "WaitResponse" - title: "ContainerWaitResponse" - required: [StatusCode] - properties: - StatusCode: - description: "Exit code of the container" - type: "integer" - format: "int64" - x-nullable: false - Error: - $ref: "#/definitions/ContainerWaitExitError" - - ContainerWaitExitError: - description: "container waiting error, if any" - type: "object" - x-go-name: "WaitExitError" - properties: - Message: - description: "Details of an error" - type: "string" - - SystemVersion: - type: "object" - description: | - Response of Engine API: GET "/version" - properties: - Platform: - type: "object" - required: [Name] - properties: - Name: - type: "string" - Components: - type: "array" - description: | - Information about system components - items: - type: "object" - x-go-name: ComponentVersion - required: [Name, Version] - properties: - Name: - description: | - Name of the component - type: "string" - example: "Engine" - Version: - description: | - Version of the component - type: "string" - x-nullable: false - example: "27.0.1" - Details: - description: | - Key/value pairs of strings with additional information about the - component. These values are intended for informational purposes - only, and their content is not defined, and not part of the API - specification. - - These messages can be printed by the client as information to the user. - type: "object" - x-nullable: true - Version: - description: "The version of the daemon" - type: "string" - example: "27.0.1" - ApiVersion: - description: | - The default (and highest) API version that is supported by the daemon - type: "string" - example: "1.47" - MinAPIVersion: - description: | - The minimum API version that is supported by the daemon - type: "string" - example: "1.24" - GitCommit: - description: | - The Git commit of the source code that was used to build the daemon - type: "string" - example: "48a66213fe" - GoVersion: - description: | - The version Go used to compile the daemon, and the version of the Go - runtime in use. - type: "string" - example: "go1.22.7" - Os: - description: | - The operating system that the daemon is running on ("linux" or "windows") - type: "string" - example: "linux" - Arch: - description: | - The architecture that the daemon is running on - type: "string" - example: "amd64" - KernelVersion: - description: | - The kernel version (`uname -r`) that the daemon is running on. - - This field is omitted when empty. - type: "string" - example: "6.8.0-31-generic" - Experimental: - description: | - Indicates if the daemon is started with experimental features enabled. - - This field is omitted when empty / false. - type: "boolean" - example: true - BuildTime: - description: | - The date and time that the daemon was compiled. - type: "string" - example: "2020-06-22T15:49:27.000000000+00:00" - - SystemInfo: - type: "object" - properties: - ID: - description: | - Unique identifier of the daemon. - -


- - > **Note**: The format of the ID itself is not part of the API, and - > should not be considered stable. - type: "string" - example: "7TRN:IPZB:QYBB:VPBQ:UMPP:KARE:6ZNR:XE6T:7EWV:PKF4:ZOJD:TPYS" - Containers: - description: "Total number of containers on the host." - type: "integer" - example: 14 - ContainersRunning: - description: | - Number of containers with status `"running"`. - type: "integer" - example: 3 - ContainersPaused: - description: | - Number of containers with status `"paused"`. - type: "integer" - example: 1 - ContainersStopped: - description: | - Number of containers with status `"stopped"`. - type: "integer" - example: 10 - Images: - description: | - Total number of images on the host. - - Both _tagged_ and _untagged_ (dangling) images are counted. - type: "integer" - example: 508 - Driver: - description: "Name of the storage driver in use." - type: "string" - example: "overlay2" - DriverStatus: - description: | - Information specific to the storage driver, provided as - "label" / "value" pairs. - - This information is provided by the storage driver, and formatted - in a way consistent with the output of `docker info` on the command - line. - -


- - > **Note**: The information returned in this field, including the - > formatting of values and labels, should not be considered stable, - > and may change without notice. - type: "array" - items: - type: "array" - items: - type: "string" - example: - - ["Backing Filesystem", "extfs"] - - ["Supports d_type", "true"] - - ["Native Overlay Diff", "true"] - DockerRootDir: - description: | - Root directory of persistent Docker state. - - Defaults to `/var/lib/docker` on Linux, and `C:\ProgramData\docker` - on Windows. - type: "string" - example: "/var/lib/docker" - Plugins: - $ref: "#/definitions/PluginsInfo" - MemoryLimit: - description: "Indicates if the host has memory limit support enabled." - type: "boolean" - example: true - SwapLimit: - description: "Indicates if the host has memory swap limit support enabled." - type: "boolean" - example: true - KernelMemoryTCP: - description: | - Indicates if the host has kernel memory TCP limit support enabled. This - field is omitted if not supported. - - Kernel memory TCP limits are not supported when using cgroups v2, which - does not support the corresponding `memory.kmem.tcp.limit_in_bytes` cgroup. - type: "boolean" - example: true - CpuCfsPeriod: - description: | - Indicates if CPU CFS(Completely Fair Scheduler) period is supported by - the host. - type: "boolean" - example: true - CpuCfsQuota: - description: | - Indicates if CPU CFS(Completely Fair Scheduler) quota is supported by - the host. - type: "boolean" - example: true - CPUShares: - description: | - Indicates if CPU Shares limiting is supported by the host. - type: "boolean" - example: true - CPUSet: - description: | - Indicates if CPUsets (cpuset.cpus, cpuset.mems) are supported by the host. - - See [cpuset(7)](https://www.kernel.org/doc/Documentation/cgroup-v1/cpusets.txt) - type: "boolean" - example: true - PidsLimit: - description: "Indicates if the host kernel has PID limit support enabled." - type: "boolean" - example: true - OomKillDisable: - description: "Indicates if OOM killer disable is supported on the host." - type: "boolean" - IPv4Forwarding: - description: "Indicates IPv4 forwarding is enabled." - type: "boolean" - example: true - BridgeNfIptables: - description: | - Indicates if `bridge-nf-call-iptables` is available on the host when - the daemon was started. - -


- - > **Deprecated**: netfilter module is now loaded on-demand and no longer - > during daemon startup, making this field obsolete. This field is always - > `false` and will be removed in a API v1.49. - type: "boolean" - example: false - BridgeNfIp6tables: - description: | - Indicates if `bridge-nf-call-ip6tables` is available on the host. - -


- - > **Deprecated**: netfilter module is now loaded on-demand, and no longer - > during daemon startup, making this field obsolete. This field is always - > `false` and will be removed in a API v1.49. - type: "boolean" - example: false - Debug: - description: | - Indicates if the daemon is running in debug-mode / with debug-level - logging enabled. - type: "boolean" - example: true - NFd: - description: | - The total number of file Descriptors in use by the daemon process. - - This information is only returned if debug-mode is enabled. - type: "integer" - example: 64 - NGoroutines: - description: | - The number of goroutines that currently exist. - - This information is only returned if debug-mode is enabled. - type: "integer" - example: 174 - SystemTime: - description: | - Current system-time in [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) - format with nano-seconds. - type: "string" - example: "2017-08-08T20:28:29.06202363Z" - LoggingDriver: - description: | - The logging driver to use as a default for new containers. - type: "string" - CgroupDriver: - description: | - The driver to use for managing cgroups. - type: "string" - enum: ["cgroupfs", "systemd", "none"] - default: "cgroupfs" - example: "cgroupfs" - CgroupVersion: - description: | - The version of the cgroup. - type: "string" - enum: ["1", "2"] - default: "1" - example: "1" - NEventsListener: - description: "Number of event listeners subscribed." - type: "integer" - example: 30 - KernelVersion: - description: | - Kernel version of the host. - - On Linux, this information obtained from `uname`. On Windows this - information is queried from the HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ - registry value, for example _"10.0 14393 (14393.1198.amd64fre.rs1_release_sec.170427-1353)"_. - type: "string" - example: "6.8.0-31-generic" - OperatingSystem: - description: | - Name of the host's operating system, for example: "Ubuntu 24.04 LTS" - or "Windows Server 2016 Datacenter" - type: "string" - example: "Ubuntu 24.04 LTS" - OSVersion: - description: | - Version of the host's operating system - -


- - > **Note**: The information returned in this field, including its - > very existence, and the formatting of values, should not be considered - > stable, and may change without notice. - type: "string" - example: "24.04" - OSType: - description: | - Generic type of the operating system of the host, as returned by the - Go runtime (`GOOS`). - - Currently returned values are "linux" and "windows". A full list of - possible values can be found in the [Go documentation](https://go.dev/doc/install/source#environment). - type: "string" - example: "linux" - Architecture: - description: | - Hardware architecture of the host, as returned by the Go runtime - (`GOARCH`). - - A full list of possible values can be found in the [Go documentation](https://go.dev/doc/install/source#environment). - type: "string" - example: "x86_64" - NCPU: - description: | - The number of logical CPUs usable by the daemon. - - The number of available CPUs is checked by querying the operating - system when the daemon starts. Changes to operating system CPU - allocation after the daemon is started are not reflected. - type: "integer" - example: 4 - MemTotal: - description: | - Total amount of physical memory available on the host, in bytes. - type: "integer" - format: "int64" - example: 2095882240 - - IndexServerAddress: - description: | - Address / URL of the index server that is used for image search, - and as a default for user authentication for Docker Hub and Docker Cloud. - default: "https://index.docker.io/v1/" - type: "string" - example: "https://index.docker.io/v1/" - RegistryConfig: - $ref: "#/definitions/RegistryServiceConfig" - GenericResources: - $ref: "#/definitions/GenericResources" - HttpProxy: - description: | - HTTP-proxy configured for the daemon. This value is obtained from the - [`HTTP_PROXY`](https://www.gnu.org/software/wget/manual/html_node/Proxies.html) environment variable. - Credentials ([user info component](https://tools.ietf.org/html/rfc3986#section-3.2.1)) in the proxy URL - are masked in the API response. - - Containers do not automatically inherit this configuration. - type: "string" - example: "http://xxxxx:xxxxx@proxy.corp.example.com:8080" - HttpsProxy: - description: | - HTTPS-proxy configured for the daemon. This value is obtained from the - [`HTTPS_PROXY`](https://www.gnu.org/software/wget/manual/html_node/Proxies.html) environment variable. - Credentials ([user info component](https://tools.ietf.org/html/rfc3986#section-3.2.1)) in the proxy URL - are masked in the API response. - - Containers do not automatically inherit this configuration. - type: "string" - example: "https://xxxxx:xxxxx@proxy.corp.example.com:4443" - NoProxy: - description: | - Comma-separated list of domain extensions for which no proxy should be - used. This value is obtained from the [`NO_PROXY`](https://www.gnu.org/software/wget/manual/html_node/Proxies.html) - environment variable. - - Containers do not automatically inherit this configuration. - type: "string" - example: "*.local, 169.254/16" - Name: - description: "Hostname of the host." - type: "string" - example: "node5.corp.example.com" - Labels: - description: | - User-defined labels (key/value metadata) as set on the daemon. - -


- - > **Note**: When part of a Swarm, nodes can both have _daemon_ labels, - > set through the daemon configuration, and _node_ labels, set from a - > manager node in the Swarm. Node labels are not included in this - > field. Node labels can be retrieved using the `/nodes/(id)` endpoint - > on a manager node in the Swarm. - type: "array" - items: - type: "string" - example: ["storage=ssd", "production"] - ExperimentalBuild: - description: | - Indicates if experimental features are enabled on the daemon. - type: "boolean" - example: true - ServerVersion: - description: | - Version string of the daemon. - type: "string" - example: "27.0.1" - Runtimes: - description: | - List of [OCI compliant](https://github.com/opencontainers/runtime-spec) - runtimes configured on the daemon. Keys hold the "name" used to - reference the runtime. - - The Docker daemon relies on an OCI compliant runtime (invoked via the - `containerd` daemon) as its interface to the Linux kernel namespaces, - cgroups, and SELinux. - - The default runtime is `runc`, and automatically configured. Additional - runtimes can be configured by the user and will be listed here. - type: "object" - additionalProperties: - $ref: "#/definitions/Runtime" - default: - runc: - path: "runc" - example: - runc: - path: "runc" - runc-master: - path: "/go/bin/runc" - custom: - path: "/usr/local/bin/my-oci-runtime" - runtimeArgs: ["--debug", "--systemd-cgroup=false"] - DefaultRuntime: - description: | - Name of the default OCI runtime that is used when starting containers. - - The default can be overridden per-container at create time. - type: "string" - default: "runc" - example: "runc" - Swarm: - $ref: "#/definitions/SwarmInfo" - LiveRestoreEnabled: - description: | - Indicates if live restore is enabled. - - If enabled, containers are kept running when the daemon is shutdown - or upon daemon start if running containers are detected. - type: "boolean" - default: false - example: false - Isolation: - description: | - Represents the isolation technology to use as a default for containers. - The supported values are platform-specific. - - If no isolation value is specified on daemon start, on Windows client, - the default is `hyperv`, and on Windows server, the default is `process`. - - This option is currently not used on other platforms. - default: "default" - type: "string" - enum: - - "default" - - "hyperv" - - "process" - - "" - InitBinary: - description: | - Name and, optional, path of the `docker-init` binary. - - If the path is omitted, the daemon searches the host's `$PATH` for the - binary and uses the first result. - type: "string" - example: "docker-init" - ContainerdCommit: - $ref: "#/definitions/Commit" - RuncCommit: - $ref: "#/definitions/Commit" - InitCommit: - $ref: "#/definitions/Commit" - SecurityOptions: - description: | - List of security features that are enabled on the daemon, such as - apparmor, seccomp, SELinux, user-namespaces (userns), rootless and - no-new-privileges. - - Additional configuration options for each security feature may - be present, and are included as a comma-separated list of key/value - pairs. - type: "array" - items: - type: "string" - example: - - "name=apparmor" - - "name=seccomp,profile=default" - - "name=selinux" - - "name=userns" - - "name=rootless" - ProductLicense: - description: | - Reports a summary of the product license on the daemon. - - If a commercial license has been applied to the daemon, information - such as number of nodes, and expiration are included. - type: "string" - example: "Community Engine" - DefaultAddressPools: - description: | - List of custom default address pools for local networks, which can be - specified in the daemon.json file or dockerd option. - - Example: a Base "10.10.0.0/16" with Size 24 will define the set of 256 - 10.10.[0-255].0/24 address pools. - type: "array" - items: - type: "object" - properties: - Base: - description: "The network address in CIDR format" - type: "string" - example: "10.10.0.0/16" - Size: - description: "The network pool size" - type: "integer" - example: "24" - FirewallBackend: - $ref: "#/definitions/FirewallInfo" - DiscoveredDevices: - description: | - List of devices discovered by device drivers. - - Each device includes information about its source driver, kind, name, - and additional driver-specific attributes. - type: "array" - items: - $ref: "#/definitions/DeviceInfo" - Warnings: - description: | - List of warnings / informational messages about missing features, or - issues related to the daemon configuration. - - These messages can be printed by the client as information to the user. - type: "array" - items: - type: "string" - example: - - "WARNING: No memory limit support" - CDISpecDirs: - description: | - List of directories where (Container Device Interface) CDI - specifications are located. - - These specifications define vendor-specific modifications to an OCI - runtime specification for a container being created. - - An empty list indicates that CDI device injection is disabled. - - Note that since using CDI device injection requires the daemon to have - experimental enabled. For non-experimental daemons an empty list will - always be returned. - type: "array" - items: - type: "string" - example: - - "/etc/cdi" - - "/var/run/cdi" - Containerd: - $ref: "#/definitions/ContainerdInfo" - - ContainerdInfo: - description: | - Information for connecting to the containerd instance that is used by the daemon. - This is included for debugging purposes only. - type: "object" - x-nullable: true - properties: - Address: - description: "The address of the containerd socket." - type: "string" - example: "/run/containerd/containerd.sock" - Namespaces: - description: | - The namespaces that the daemon uses for running containers and - plugins in containerd. These namespaces can be configured in the - daemon configuration, and are considered to be used exclusively - by the daemon, Tampering with the containerd instance may cause - unexpected behavior. - - As these namespaces are considered to be exclusively accessed - by the daemon, it is not recommended to change these values, - or to change them to a value that is used by other systems, - such as cri-containerd. - type: "object" - properties: - Containers: - description: | - The default containerd namespace used for containers managed - by the daemon. - - The default namespace for containers is "moby", but will be - suffixed with the `.` of the remapped `root` if - user-namespaces are enabled and the containerd image-store - is used. - type: "string" - default: "moby" - example: "moby" - Plugins: - description: | - The default containerd namespace used for plugins managed by - the daemon. - - The default namespace for plugins is "plugins.moby", but will be - suffixed with the `.` of the remapped `root` if - user-namespaces are enabled and the containerd image-store - is used. - type: "string" - default: "plugins.moby" - example: "plugins.moby" - - FirewallInfo: - description: | - Information about the daemon's firewalling configuration. - - This field is currently only used on Linux, and omitted on other platforms. - type: "object" - x-nullable: true - properties: - Driver: - description: | - The name of the firewall backend driver. - type: "string" - example: "nftables" - Info: - description: | - Information about the firewall backend, provided as - "label" / "value" pairs. - -


- - > **Note**: The information returned in this field, including the - > formatting of values and labels, should not be considered stable, - > and may change without notice. - type: "array" - items: - type: "array" - items: - type: "string" - example: - - ["ReloadedAt", "2025-01-01T00:00:00Z"] - - # PluginsInfo is a temp struct holding Plugins name - # registered with docker daemon. It is used by Info struct - PluginsInfo: - description: | - Available plugins per type. - -


- - > **Note**: Only unmanaged (V1) plugins are included in this list. - > V1 plugins are "lazily" loaded, and are not returned in this list - > if there is no resource using the plugin. - type: "object" - properties: - Volume: - description: "Names of available volume-drivers, and network-driver plugins." - type: "array" - items: - type: "string" - example: ["local"] - Network: - description: "Names of available network-drivers, and network-driver plugins." - type: "array" - items: - type: "string" - example: ["bridge", "host", "ipvlan", "macvlan", "null", "overlay"] - Authorization: - description: "Names of available authorization plugins." - type: "array" - items: - type: "string" - example: ["img-authz-plugin", "hbm"] - Log: - description: "Names of available logging-drivers, and logging-driver plugins." - type: "array" - items: - type: "string" - example: ["awslogs", "fluentd", "gcplogs", "gelf", "journald", "json-file", "splunk", "syslog"] - - - RegistryServiceConfig: - description: | - RegistryServiceConfig stores daemon registry services configuration. - type: "object" - x-nullable: true - properties: - InsecureRegistryCIDRs: - description: | - List of IP ranges of insecure registries, using the CIDR syntax - ([RFC 4632](https://tools.ietf.org/html/4632)). Insecure registries - accept un-encrypted (HTTP) and/or untrusted (HTTPS with certificates - from unknown CAs) communication. - - By default, local registries (`::1/128` and `127.0.0.0/8`) are configured as - insecure. All other registries are secure. Communicating with an - insecure registry is not possible if the daemon assumes that registry - is secure. - - This configuration override this behavior, insecure communication with - registries whose resolved IP address is within the subnet described by - the CIDR syntax. - - Registries can also be marked insecure by hostname. Those registries - are listed under `IndexConfigs` and have their `Secure` field set to - `false`. - - > **Warning**: Using this option can be useful when running a local - > registry, but introduces security vulnerabilities. This option - > should therefore ONLY be used for testing purposes. For increased - > security, users should add their CA to their system's list of trusted - > CAs instead of enabling this option. - type: "array" - items: - type: "string" - example: ["::1/128", "127.0.0.0/8"] - IndexConfigs: - type: "object" - additionalProperties: - $ref: "#/definitions/IndexInfo" - example: - "127.0.0.1:5000": - "Name": "127.0.0.1:5000" - "Mirrors": [] - "Secure": false - "Official": false - "[2001:db8:a0b:12f0::1]:80": - "Name": "[2001:db8:a0b:12f0::1]:80" - "Mirrors": [] - "Secure": false - "Official": false - "docker.io": - Name: "docker.io" - Mirrors: ["https://hub-mirror.corp.example.com:5000/"] - Secure: true - Official: true - "registry.internal.corp.example.com:3000": - Name: "registry.internal.corp.example.com:3000" - Mirrors: [] - Secure: false - Official: false - Mirrors: - description: | - List of registry URLs that act as a mirror for the official - (`docker.io`) registry. - - type: "array" - items: - type: "string" - example: - - "https://hub-mirror.corp.example.com:5000/" - - "https://[2001:db8:a0b:12f0::1]/" - - IndexInfo: - description: - IndexInfo contains information about a registry. - type: "object" - x-nullable: true - properties: - Name: - description: | - Name of the registry, such as "docker.io". - type: "string" - example: "docker.io" - Mirrors: - description: | - List of mirrors, expressed as URIs. - type: "array" - items: - type: "string" - example: - - "https://hub-mirror.corp.example.com:5000/" - - "https://registry-2.docker.io/" - - "https://registry-3.docker.io/" - Secure: - description: | - Indicates if the registry is part of the list of insecure - registries. - - If `false`, the registry is insecure. Insecure registries accept - un-encrypted (HTTP) and/or untrusted (HTTPS with certificates from - unknown CAs) communication. - - > **Warning**: Insecure registries can be useful when running a local - > registry. However, because its use creates security vulnerabilities - > it should ONLY be enabled for testing purposes. For increased - > security, users should add their CA to their system's list of - > trusted CAs instead of enabling this option. - type: "boolean" - example: true - Official: - description: | - Indicates whether this is an official registry (i.e., Docker Hub / docker.io) - type: "boolean" - example: true - - Runtime: - description: | - Runtime describes an [OCI compliant](https://github.com/opencontainers/runtime-spec) - runtime. - - The runtime is invoked by the daemon via the `containerd` daemon. OCI - runtimes act as an interface to the Linux kernel namespaces, cgroups, - and SELinux. - type: "object" - properties: - path: - description: | - Name and, optional, path, of the OCI executable binary. - - If the path is omitted, the daemon searches the host's `$PATH` for the - binary and uses the first result. - type: "string" - example: "/usr/local/bin/my-oci-runtime" - runtimeArgs: - description: | - List of command-line arguments to pass to the runtime when invoked. - type: "array" - x-nullable: true - items: - type: "string" - example: ["--debug", "--systemd-cgroup=false"] - status: - description: | - Information specific to the runtime. - - While this API specification does not define data provided by runtimes, - the following well-known properties may be provided by runtimes: - - `org.opencontainers.runtime-spec.features`: features structure as defined - in the [OCI Runtime Specification](https://github.com/opencontainers/runtime-spec/blob/main/features.md), - in a JSON string representation. - -


- - > **Note**: The information returned in this field, including the - > formatting of values and labels, should not be considered stable, - > and may change without notice. - type: "object" - x-nullable: true - additionalProperties: - type: "string" - example: - "org.opencontainers.runtime-spec.features": "{\"ociVersionMin\":\"1.0.0\",\"ociVersionMax\":\"1.1.0\",\"...\":\"...\"}" - - Commit: - description: | - Commit holds the Git-commit (SHA1) that a binary was built from, as - reported in the version-string of external tools, such as `containerd`, - or `runC`. - type: "object" - properties: - ID: - description: "Actual commit ID of external tool." - type: "string" - example: "cfb82a876ecc11b5ca0977d1733adbe58599088a" - - SwarmInfo: - description: | - Represents generic information about swarm. - type: "object" - properties: - NodeID: - description: "Unique identifier of for this node in the swarm." - type: "string" - default: "" - example: "k67qz4598weg5unwwffg6z1m1" - NodeAddr: - description: | - IP address at which this node can be reached by other nodes in the - swarm. - type: "string" - default: "" - example: "10.0.0.46" - LocalNodeState: - $ref: "#/definitions/LocalNodeState" - ControlAvailable: - type: "boolean" - default: false - example: true - Error: - type: "string" - default: "" - RemoteManagers: - description: | - List of ID's and addresses of other managers in the swarm. - type: "array" - default: null - x-nullable: true - items: - $ref: "#/definitions/PeerNode" - example: - - NodeID: "71izy0goik036k48jg985xnds" - Addr: "10.0.0.158:2377" - - NodeID: "79y6h1o4gv8n120drcprv5nmc" - Addr: "10.0.0.159:2377" - - NodeID: "k67qz4598weg5unwwffg6z1m1" - Addr: "10.0.0.46:2377" - Nodes: - description: "Total number of nodes in the swarm." - type: "integer" - x-nullable: true - example: 4 - Managers: - description: "Total number of managers in the swarm." - type: "integer" - x-nullable: true - example: 3 - Cluster: - $ref: "#/definitions/ClusterInfo" - - LocalNodeState: - description: "Current local status of this node." - type: "string" - default: "" - enum: - - "" - - "inactive" - - "pending" - - "active" - - "error" - - "locked" - example: "active" - - PeerNode: - description: "Represents a peer-node in the swarm" - type: "object" - properties: - NodeID: - description: "Unique identifier of for this node in the swarm." - type: "string" - Addr: - description: | - IP address and ports at which this node can be reached. - type: "string" - - NetworkAttachmentConfig: - description: | - Specifies how a service should be attached to a particular network. - type: "object" - properties: - Target: - description: | - The target network for attachment. Must be a network name or ID. - type: "string" - Aliases: - description: | - Discoverable alternate names for the service on this network. - type: "array" - items: - type: "string" - DriverOpts: - description: | - Driver attachment options for the network target. - type: "object" - additionalProperties: - type: "string" - - EventActor: - description: | - Actor describes something that generates events, like a container, network, - or a volume. - type: "object" - properties: - ID: - description: "The ID of the object emitting the event" - type: "string" - example: "ede54ee1afda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c743" - Attributes: - description: | - Various key/value attributes of the object, depending on its type. - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-label-value" - image: "alpine:latest" - name: "my-container" - - EventMessage: - description: | - EventMessage represents the information an event contains. - type: "object" - title: "SystemEventsResponse" - properties: - Type: - description: "The type of object emitting the event" - type: "string" - enum: ["builder", "config", "container", "daemon", "image", "network", "node", "plugin", "secret", "service", "volume"] - example: "container" - Action: - description: "The type of event" - type: "string" - example: "create" - Actor: - $ref: "#/definitions/EventActor" - scope: - description: | - Scope of the event. Engine events are `local` scope. Cluster (Swarm) - events are `swarm` scope. - type: "string" - enum: ["local", "swarm"] - time: - description: "Timestamp of event" - type: "integer" - format: "int64" - example: 1629574695 - timeNano: - description: "Timestamp of event, with nanosecond accuracy" - type: "integer" - format: "int64" - example: 1629574695515050031 - - OCIDescriptor: - type: "object" - x-go-name: Descriptor - description: | - A descriptor struct containing digest, media type, and size, as defined in - the [OCI Content Descriptors Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md). - properties: - mediaType: - description: | - The media type of the object this schema refers to. - type: "string" - example: "application/vnd.oci.image.manifest.v1+json" - digest: - description: | - The digest of the targeted content. - type: "string" - example: "sha256:c0537ff6a5218ef531ece93d4984efc99bbf3f7497c0a7726c88e2bb7584dc96" - size: - description: | - The size in bytes of the blob. - type: "integer" - format: "int64" - example: 424 - urls: - description: |- - List of URLs from which this object MAY be downloaded. - type: "array" - items: - type: "string" - format: "uri" - x-nullable: true - annotations: - description: |- - Arbitrary metadata relating to the targeted content. - type: "object" - x-nullable: true - additionalProperties: - type: "string" - example: - "com.docker.official-images.bashbrew.arch": "amd64" - "org.opencontainers.image.base.digest": "sha256:0d0ef5c914d3ea700147da1bd050c59edb8bb12ca312f3800b29d7c8087eabd8" - "org.opencontainers.image.base.name": "scratch" - "org.opencontainers.image.created": "2025-01-27T00:00:00Z" - "org.opencontainers.image.revision": "9fabb4bad5138435b01857e2fe9363e2dc5f6a79" - "org.opencontainers.image.source": "https://git.launchpad.net/cloud-images/+oci/ubuntu-base" - "org.opencontainers.image.url": "https://hub.docker.com/_/ubuntu" - "org.opencontainers.image.version": "24.04" - data: - type: string - x-nullable: true - description: |- - Data is an embedding of the targeted content. This is encoded as a base64 - string when marshalled to JSON (automatically, by encoding/json). If - present, Data can be used directly to avoid fetching the targeted content. - example: null - platform: - $ref: "#/definitions/OCIPlatform" - artifactType: - description: |- - ArtifactType is the IANA media type of this artifact. - type: "string" - x-nullable: true - example: null - - OCIPlatform: - type: "object" - x-go-name: Platform - x-nullable: true - description: | - Describes the platform which the image in the manifest runs on, as defined - in the [OCI Image Index Specification](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md). - properties: - architecture: - description: | - The CPU architecture, for example `amd64` or `ppc64`. - type: "string" - example: "arm" - os: - description: | - The operating system, for example `linux` or `windows`. - type: "string" - example: "windows" - os.version: - description: | - Optional field specifying the operating system version, for example on - Windows `10.0.19041.1165`. - type: "string" - example: "10.0.19041.1165" - os.features: - description: | - Optional field specifying an array of strings, each listing a required - OS feature (for example on Windows `win32k`). - type: "array" - items: - type: "string" - example: - - "win32k" - variant: - description: | - Optional field specifying a variant of the CPU, for example `v7` to - specify ARMv7 when architecture is `arm`. - type: "string" - example: "v7" - - DistributionInspect: - type: "object" - x-go-name: DistributionInspect - title: "DistributionInspectResponse" - required: [Descriptor, Platforms] - description: | - Describes the result obtained from contacting the registry to retrieve - image metadata. - properties: - Descriptor: - $ref: "#/definitions/OCIDescriptor" - Platforms: - type: "array" - description: | - An array containing all platforms supported by the image. - items: - $ref: "#/definitions/OCIPlatform" - - ClusterVolume: - type: "object" - description: | - Options and information specific to, and only present on, Swarm CSI - cluster volumes. - properties: - ID: - type: "string" - description: | - The Swarm ID of this volume. Because cluster volumes are Swarm - objects, they have an ID, unlike non-cluster volumes. This ID can - be used to refer to the Volume instead of the name. - Version: - $ref: "#/definitions/ObjectVersion" - CreatedAt: - type: "string" - format: "dateTime" - UpdatedAt: - type: "string" - format: "dateTime" - Spec: - $ref: "#/definitions/ClusterVolumeSpec" - Info: - type: "object" - description: | - Information about the global status of the volume. - properties: - CapacityBytes: - type: "integer" - format: "int64" - description: | - The capacity of the volume in bytes. A value of 0 indicates that - the capacity is unknown. - VolumeContext: - type: "object" - description: | - A map of strings to strings returned from the storage plugin when - the volume is created. - additionalProperties: - type: "string" - VolumeID: - type: "string" - description: | - The ID of the volume as returned by the CSI storage plugin. This - is distinct from the volume's ID as provided by Docker. This ID - is never used by the user when communicating with Docker to refer - to this volume. If the ID is blank, then the Volume has not been - successfully created in the plugin yet. - AccessibleTopology: - type: "array" - description: | - The topology this volume is actually accessible from. - items: - $ref: "#/definitions/Topology" - PublishStatus: - type: "array" - description: | - The status of the volume as it pertains to its publishing and use on - specific nodes - items: - type: "object" - properties: - NodeID: - type: "string" - description: | - The ID of the Swarm node the volume is published on. - State: - type: "string" - description: | - The published state of the volume. - * `pending-publish` The volume should be published to this node, but the call to the controller plugin to do so has not yet been successfully completed. - * `published` The volume is published successfully to the node. - * `pending-node-unpublish` The volume should be unpublished from the node, and the manager is awaiting confirmation from the worker that it has done so. - * `pending-controller-unpublish` The volume is successfully unpublished from the node, but has not yet been successfully unpublished on the controller. - enum: - - "pending-publish" - - "published" - - "pending-node-unpublish" - - "pending-controller-unpublish" - PublishContext: - type: "object" - description: | - A map of strings to strings returned by the CSI controller - plugin when a volume is published. - additionalProperties: - type: "string" - - ClusterVolumeSpec: - type: "object" - description: | - Cluster-specific options used to create the volume. - properties: - Group: - type: "string" - description: | - Group defines the volume group of this volume. Volumes belonging to - the same group can be referred to by group name when creating - Services. Referring to a volume by group instructs Swarm to treat - volumes in that group interchangeably for the purpose of scheduling. - Volumes with an empty string for a group technically all belong to - the same, emptystring group. - AccessMode: - type: "object" - description: | - Defines how the volume is used by tasks. - properties: - Scope: - type: "string" - description: | - The set of nodes this volume can be used on at one time. - - `single` The volume may only be scheduled to one node at a time. - - `multi` the volume may be scheduled to any supported number of nodes at a time. - default: "single" - enum: ["single", "multi"] - x-nullable: false - Sharing: - type: "string" - description: | - The number and way that different tasks can use this volume - at one time. - - `none` The volume may only be used by one task at a time. - - `readonly` The volume may be used by any number of tasks, but they all must mount the volume as readonly - - `onewriter` The volume may be used by any number of tasks, but only one may mount it as read/write. - - `all` The volume may have any number of readers and writers. - default: "none" - enum: ["none", "readonly", "onewriter", "all"] - x-nullable: false - MountVolume: - type: "object" - description: | - Options for using this volume as a Mount-type volume. - - Either MountVolume or BlockVolume, but not both, must be - present. - properties: - FsType: - type: "string" - description: | - Specifies the filesystem type for the mount volume. - Optional. - MountFlags: - type: "array" - description: | - Flags to pass when mounting the volume. Optional. - items: - type: "string" - BlockVolume: - type: "object" - description: | - Options for using this volume as a Block-type volume. - Intentionally empty. - Secrets: - type: "array" - description: | - Swarm Secrets that are passed to the CSI storage plugin when - operating on this volume. - items: - type: "object" - description: | - One cluster volume secret entry. Defines a key-value pair that - is passed to the plugin. - properties: - Key: - type: "string" - description: | - Key is the name of the key of the key-value pair passed to - the plugin. - Secret: - type: "string" - description: | - Secret is the swarm Secret object from which to read data. - This can be a Secret name or ID. The Secret data is - retrieved by swarm and used as the value of the key-value - pair passed to the plugin. - AccessibilityRequirements: - type: "object" - description: | - Requirements for the accessible topology of the volume. These - fields are optional. For an in-depth description of what these - fields mean, see the CSI specification. - properties: - Requisite: - type: "array" - description: | - A list of required topologies, at least one of which the - volume must be accessible from. - items: - $ref: "#/definitions/Topology" - Preferred: - type: "array" - description: | - A list of topologies that the volume should attempt to be - provisioned in. - items: - $ref: "#/definitions/Topology" - CapacityRange: - type: "object" - description: | - The desired capacity that the volume should be created with. If - empty, the plugin will decide the capacity. - properties: - RequiredBytes: - type: "integer" - format: "int64" - description: | - The volume must be at least this big. The value of 0 - indicates an unspecified minimum - LimitBytes: - type: "integer" - format: "int64" - description: | - The volume must not be bigger than this. The value of 0 - indicates an unspecified maximum. - Availability: - type: "string" - description: | - The availability of the volume for use in tasks. - - `active` The volume is fully available for scheduling on the cluster - - `pause` No new workloads should use the volume, but existing workloads are not stopped. - - `drain` All workloads using this volume should be stopped and rescheduled, and no new ones should be started. - default: "active" - x-nullable: false - enum: - - "active" - - "pause" - - "drain" - - Topology: - description: | - A map of topological domains to topological segments. For in depth - details, see documentation for the Topology object in the CSI - specification. - type: "object" - additionalProperties: - type: "string" - - ImageManifestSummary: - x-go-name: "ManifestSummary" - description: | - ImageManifestSummary represents a summary of an image manifest. - type: "object" - required: ["ID", "Descriptor", "Available", "Size", "Kind"] - properties: - ID: - description: | - ID is the content-addressable ID of an image and is the same as the - digest of the image manifest. - type: "string" - example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f" - Descriptor: - $ref: "#/definitions/OCIDescriptor" - Available: - description: Indicates whether all the child content (image config, layers) is fully available locally. - type: "boolean" - example: true - Size: - type: "object" - x-nullable: false - required: ["Content", "Total"] - properties: - Total: - type: "integer" - format: "int64" - example: 8213251 - description: | - Total is the total size (in bytes) of all the locally present - data (both distributable and non-distributable) that's related to - this manifest and its children. - This equal to the sum of [Content] size AND all the sizes in the - [Size] struct present in the Kind-specific data struct. - For example, for an image kind (Kind == "image") - this would include the size of the image content and unpacked - image snapshots ([Size.Content] + [ImageData.Size.Unpacked]). - Content: - description: | - Content is the size (in bytes) of all the locally present - content in the content store (e.g. image config, layers) - referenced by this manifest and its children. - This only includes blobs in the content store. - type: "integer" - format: "int64" - example: 3987495 - Kind: - type: "string" - example: "image" - enum: - - "image" - - "attestation" - - "unknown" - description: | - The kind of the manifest. - - kind | description - -------------|----------------------------------------------------------- - image | Image manifest that can be used to start a container. - attestation | Attestation manifest produced by the Buildkit builder for a specific image manifest. - ImageData: - description: | - The image data for the image manifest. - This field is only populated when Kind is "image". - type: "object" - x-nullable: true - x-omitempty: true - required: ["Platform", "Containers", "Size", "UnpackedSize"] - properties: - Platform: - $ref: "#/definitions/OCIPlatform" - description: | - OCI platform of the image. This will be the platform specified in the - manifest descriptor from the index/manifest list. - If it's not available, it will be obtained from the image config. - Containers: - description: | - The IDs of the containers that are using this image. - type: "array" - items: - type: "string" - example: ["ede54ee1fda366ab42f824e8a5ffd195155d853ceaec74a927f249ea270c7430", "abadbce344c096744d8d6071a90d474d28af8f1034b5ea9fb03c3f4bfc6d005e"] - Size: - type: "object" - x-nullable: false - required: ["Unpacked"] - properties: - Unpacked: - type: "integer" - format: "int64" - example: 3987495 - description: | - Unpacked is the size (in bytes) of the locally unpacked - (uncompressed) image content that's directly usable by the containers - running this image. - It's independent of the distributable content - e.g. - the image might still have an unpacked data that's still used by - some container even when the distributable/compressed content is - already gone. - AttestationData: - description: | - The image data for the attestation manifest. - This field is only populated when Kind is "attestation". - type: "object" - x-nullable: true - x-omitempty: true - required: ["For"] - properties: - For: - description: | - The digest of the image manifest that this attestation is for. - type: "string" - example: "sha256:95869fbcf224d947ace8d61d0e931d49e31bb7fc67fffbbe9c3198c33aa8e93f" - -paths: - /containers/json: - get: - summary: "List containers" - description: | - Returns a list of containers. For details on the format, see the - [inspect endpoint](#operation/ContainerInspect). - - Note that it uses a different, smaller representation of a container - than inspecting a single container. For example, the list of linked - containers is not propagated . - operationId: "ContainerList" - produces: - - "application/json" - parameters: - - name: "all" - in: "query" - description: | - Return all containers. By default, only running containers are shown. - type: "boolean" - default: false - - name: "limit" - in: "query" - description: | - Return this number of most recently created containers, including - non-running ones. - type: "integer" - - name: "size" - in: "query" - description: | - Return the size of container as fields `SizeRw` and `SizeRootFs`. - type: "boolean" - default: false - - name: "filters" - in: "query" - description: | - Filters to process on the container list, encoded as JSON (a - `map[string][]string`). For example, `{"status": ["paused"]}` will - only return paused containers. - - Available filters: - - - `ancestor`=(`[:]`, ``, or ``) - - `before`=(`` or ``) - - `expose`=(`[/]`|`/[]`) - - `exited=` containers with exit code of `` - - `health`=(`starting`|`healthy`|`unhealthy`|`none`) - - `id=` a container's ID - - `isolation=`(`default`|`process`|`hyperv`) (Windows daemon only) - - `is-task=`(`true`|`false`) - - `label=key` or `label="key=value"` of a container label - - `name=` a container's name - - `network`=(`` or ``) - - `publish`=(`[/]`|`/[]`) - - `since`=(`` or ``) - - `status=`(`created`|`restarting`|`running`|`removing`|`paused`|`exited`|`dead`) - - `volume`=(`` or ``) - type: "string" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/ContainerSummary" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Container"] - /containers/create: - post: - summary: "Create a container" - operationId: "ContainerCreate" - consumes: - - "application/json" - - "application/octet-stream" - produces: - - "application/json" - parameters: - - name: "name" - in: "query" - description: | - Assign the specified name to the container. Must match - `/?[a-zA-Z0-9][a-zA-Z0-9_.-]+`. - type: "string" - pattern: "^/?[a-zA-Z0-9][a-zA-Z0-9_.-]+$" - - name: "platform" - in: "query" - description: | - Platform in the format `os[/arch[/variant]]` used for image lookup. - - When specified, the daemon checks if the requested image is present - in the local image cache with the given OS and Architecture, and - otherwise returns a `404` status. - - If the option is not set, the host's native OS and Architecture are - used to look up the image in the image cache. However, if no platform - is passed and the given image does exist in the local image cache, - but its OS or architecture does not match, the container is created - with the available image, and a warning is added to the `Warnings` - field in the response, for example; - - WARNING: The requested image's platform (linux/arm64/v8) does not - match the detected host platform (linux/amd64) and no - specific platform was requested - - type: "string" - default: "" - - name: "body" - in: "body" - description: "Container to create" - schema: - allOf: - - $ref: "#/definitions/ContainerConfig" - - type: "object" - properties: - HostConfig: - $ref: "#/definitions/HostConfig" - NetworkingConfig: - $ref: "#/definitions/NetworkingConfig" - example: - Hostname: "" - Domainname: "" - User: "" - AttachStdin: false - AttachStdout: true - AttachStderr: true - Tty: false - OpenStdin: false - StdinOnce: false - Env: - - "FOO=bar" - - "BAZ=quux" - Cmd: - - "date" - Entrypoint: "" - Image: "ubuntu" - Labels: - com.example.vendor: "Acme" - com.example.license: "GPL" - com.example.version: "1.0" - Volumes: - /volumes/data: {} - WorkingDir: "" - NetworkDisabled: false - MacAddress: "12:34:56:78:9a:bc" - ExposedPorts: - 22/tcp: {} - StopSignal: "SIGTERM" - StopTimeout: 10 - HostConfig: - Binds: - - "/tmp:/tmp" - Links: - - "redis3:redis" - Memory: 0 - MemorySwap: 0 - MemoryReservation: 0 - NanoCpus: 500000 - CpuPercent: 80 - CpuShares: 512 - CpuPeriod: 100000 - CpuRealtimePeriod: 1000000 - CpuRealtimeRuntime: 10000 - CpuQuota: 50000 - CpusetCpus: "0,1" - CpusetMems: "0,1" - MaximumIOps: 0 - MaximumIOBps: 0 - BlkioWeight: 300 - BlkioWeightDevice: - - {} - BlkioDeviceReadBps: - - {} - BlkioDeviceReadIOps: - - {} - BlkioDeviceWriteBps: - - {} - BlkioDeviceWriteIOps: - - {} - DeviceRequests: - - Driver: "nvidia" - Count: -1 - DeviceIDs": ["0", "1", "GPU-fef8089b-4820-abfc-e83e-94318197576e"] - Capabilities: [["gpu", "nvidia", "compute"]] - Options: - property1: "string" - property2: "string" - MemorySwappiness: 60 - OomKillDisable: false - OomScoreAdj: 500 - PidMode: "" - PidsLimit: 0 - PortBindings: - 22/tcp: - - HostPort: "11022" - PublishAllPorts: false - Privileged: false - ReadonlyRootfs: false - Dns: - - "8.8.8.8" - DnsOptions: - - "" - DnsSearch: - - "" - VolumesFrom: - - "parent" - - "other:ro" - CapAdd: - - "NET_ADMIN" - CapDrop: - - "MKNOD" - GroupAdd: - - "newgroup" - RestartPolicy: - Name: "" - MaximumRetryCount: 0 - AutoRemove: true - NetworkMode: "bridge" - Devices: [] - Ulimits: - - {} - LogConfig: - Type: "json-file" - Config: {} - SecurityOpt: [] - StorageOpt: {} - CgroupParent: "" - VolumeDriver: "" - ShmSize: 67108864 - NetworkingConfig: - EndpointsConfig: - isolated_nw: - IPAMConfig: - IPv4Address: "172.20.30.33" - IPv6Address: "2001:db8:abcd::3033" - LinkLocalIPs: - - "169.254.34.68" - - "fe80::3468" - Links: - - "container_1" - - "container_2" - Aliases: - - "server_x" - - "server_y" - database_nw: {} - - required: true - responses: - 201: - description: "Container created successfully" - schema: - $ref: "#/definitions/ContainerCreateResponse" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such image" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such image: c2ada9df5af8" - 409: - description: "conflict" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Container"] - /containers/{id}/json: - get: - summary: "Inspect a container" - description: "Return low-level information about a container." - operationId: "ContainerInspect" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/ContainerInspectResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "size" - in: "query" - type: "boolean" - default: false - description: "Return the size of container as fields `SizeRw` and `SizeRootFs`" - tags: ["Container"] - /containers/{id}/top: - get: - summary: "List processes running inside a container" - description: | - On Unix systems, this is done by running the `ps` command. This endpoint - is not supported on Windows. - operationId: "ContainerTop" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/ContainerTopResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "ps_args" - in: "query" - description: "The arguments to pass to `ps`. For example, `aux`" - type: "string" - default: "-ef" - tags: ["Container"] - /containers/{id}/logs: - get: - summary: "Get container logs" - description: | - Get `stdout` and `stderr` logs from a container. - - Note: This endpoint works only for containers with the `json-file` or - `journald` logging driver. - produces: - - "application/vnd.docker.raw-stream" - - "application/vnd.docker.multiplexed-stream" - operationId: "ContainerLogs" - responses: - 200: - description: | - logs returned as a stream in response body. - For the stream format, [see the documentation for the attach endpoint](#operation/ContainerAttach). - Note that unlike the attach endpoint, the logs endpoint does not - upgrade the connection and does not set Content-Type. - schema: - type: "string" - format: "binary" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "follow" - in: "query" - description: "Keep connection after returning logs." - type: "boolean" - default: false - - name: "stdout" - in: "query" - description: "Return logs from `stdout`" - type: "boolean" - default: false - - name: "stderr" - in: "query" - description: "Return logs from `stderr`" - type: "boolean" - default: false - - name: "since" - in: "query" - description: "Only return logs since this time, as a UNIX timestamp" - type: "integer" - default: 0 - - name: "until" - in: "query" - description: "Only return logs before this time, as a UNIX timestamp" - type: "integer" - default: 0 - - name: "timestamps" - in: "query" - description: "Add timestamps to every log line" - type: "boolean" - default: false - - name: "tail" - in: "query" - description: | - Only return this number of log lines from the end of the logs. - Specify as an integer or `all` to output all log lines. - type: "string" - default: "all" - tags: ["Container"] - /containers/{id}/changes: - get: - summary: "Get changes on a container’s filesystem" - description: | - Returns which files in a container's filesystem have been added, deleted, - or modified. The `Kind` of modification can be one of: - - - `0`: Modified ("C") - - `1`: Added ("A") - - `2`: Deleted ("D") - operationId: "ContainerChanges" - produces: ["application/json"] - responses: - 200: - description: "The list of changes" - schema: - type: "array" - items: - $ref: "#/definitions/FilesystemChange" - examples: - application/json: - - Path: "/dev" - Kind: 0 - - Path: "/dev/kmsg" - Kind: 1 - - Path: "/test" - Kind: 1 - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - tags: ["Container"] - /containers/{id}/export: - get: - summary: "Export a container" - description: "Export the contents of a container as a tarball." - operationId: "ContainerExport" - produces: - - "application/octet-stream" - responses: - 200: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - tags: ["Container"] - /containers/{id}/stats: - get: - summary: "Get container stats based on resource usage" - description: | - This endpoint returns a live stream of a container’s resource usage - statistics. - - The `precpu_stats` is the CPU statistic of the *previous* read, and is - used to calculate the CPU usage percentage. It is not an exact copy - of the `cpu_stats` field. - - If either `precpu_stats.online_cpus` or `cpu_stats.online_cpus` is - nil then for compatibility with older daemons the length of the - corresponding `cpu_usage.percpu_usage` array should be used. - - On a cgroup v2 host, the following fields are not set - * `blkio_stats`: all fields other than `io_service_bytes_recursive` - * `cpu_stats`: `cpu_usage.percpu_usage` - * `memory_stats`: `max_usage` and `failcnt` - Also, `memory_stats.stats` fields are incompatible with cgroup v1. - - To calculate the values shown by the `stats` command of the docker cli tool - the following formulas can be used: - * used_memory = `memory_stats.usage - memory_stats.stats.cache` - * available_memory = `memory_stats.limit` - * Memory usage % = `(used_memory / available_memory) * 100.0` - * cpu_delta = `cpu_stats.cpu_usage.total_usage - precpu_stats.cpu_usage.total_usage` - * system_cpu_delta = `cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage` - * number_cpus = `length(cpu_stats.cpu_usage.percpu_usage)` or `cpu_stats.online_cpus` - * CPU usage % = `(cpu_delta / system_cpu_delta) * number_cpus * 100.0` - operationId: "ContainerStats" - produces: ["application/json"] - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/ContainerStatsResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "stream" - in: "query" - description: | - Stream the output. If false, the stats will be output once and then - it will disconnect. - type: "boolean" - default: true - - name: "one-shot" - in: "query" - description: | - Only get a single stat instead of waiting for 2 cycles. Must be used - with `stream=false`. - type: "boolean" - default: false - tags: ["Container"] - /containers/{id}/resize: - post: - summary: "Resize a container TTY" - description: "Resize the TTY for a container." - operationId: "ContainerResize" - consumes: - - "application/octet-stream" - produces: - - "text/plain" - responses: - 200: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "cannot resize container" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "h" - in: "query" - required: true - description: "Height of the TTY session in characters" - type: "integer" - - name: "w" - in: "query" - required: true - description: "Width of the TTY session in characters" - type: "integer" - tags: ["Container"] - /containers/{id}/start: - post: - summary: "Start a container" - operationId: "ContainerStart" - responses: - 204: - description: "no error" - 304: - description: "container already started" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "detachKeys" - in: "query" - description: | - Override the key sequence for detaching a container. Format is a - single character `[a-Z]` or `ctrl-` where `` is one - of: `a-z`, `@`, `^`, `[`, `,` or `_`. - type: "string" - tags: ["Container"] - /containers/{id}/stop: - post: - summary: "Stop a container" - operationId: "ContainerStop" - responses: - 204: - description: "no error" - 304: - description: "container already stopped" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "signal" - in: "query" - description: | - Signal to send to the container as an integer or string (e.g. `SIGINT`). - type: "string" - - name: "t" - in: "query" - description: "Number of seconds to wait before killing the container" - type: "integer" - tags: ["Container"] - /containers/{id}/restart: - post: - summary: "Restart a container" - operationId: "ContainerRestart" - responses: - 204: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "signal" - in: "query" - description: | - Signal to send to the container as an integer or string (e.g. `SIGINT`). - type: "string" - - name: "t" - in: "query" - description: "Number of seconds to wait before killing the container" - type: "integer" - tags: ["Container"] - /containers/{id}/kill: - post: - summary: "Kill a container" - description: | - Send a POSIX signal to a container, defaulting to killing to the - container. - operationId: "ContainerKill" - responses: - 204: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 409: - description: "container is not running" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "Container d37cde0fe4ad63c3a7252023b2f9800282894247d145cb5933ddf6e52cc03a28 is not running" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "signal" - in: "query" - description: | - Signal to send to the container as an integer or string (e.g. `SIGINT`). - type: "string" - default: "SIGKILL" - tags: ["Container"] - /containers/{id}/update: - post: - summary: "Update a container" - description: | - Change various configuration options of a container without having to - recreate it. - operationId: "ContainerUpdate" - consumes: ["application/json"] - produces: ["application/json"] - responses: - 200: - description: "The container has been updated." - schema: - $ref: "#/definitions/ContainerUpdateResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "update" - in: "body" - required: true - schema: - allOf: - - $ref: "#/definitions/Resources" - - type: "object" - properties: - RestartPolicy: - $ref: "#/definitions/RestartPolicy" - example: - BlkioWeight: 300 - CpuShares: 512 - CpuPeriod: 100000 - CpuQuota: 50000 - CpuRealtimePeriod: 1000000 - CpuRealtimeRuntime: 10000 - CpusetCpus: "0,1" - CpusetMems: "0" - Memory: 314572800 - MemorySwap: 514288000 - MemoryReservation: 209715200 - RestartPolicy: - MaximumRetryCount: 4 - Name: "on-failure" - tags: ["Container"] - /containers/{id}/rename: - post: - summary: "Rename a container" - operationId: "ContainerRename" - responses: - 204: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 409: - description: "name already in use" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "name" - in: "query" - required: true - description: "New name for the container" - type: "string" - tags: ["Container"] - /containers/{id}/pause: - post: - summary: "Pause a container" - description: | - Use the freezer cgroup to suspend all processes in a container. - - Traditionally, when suspending a process the `SIGSTOP` signal is used, - which is observable by the process being suspended. With the freezer - cgroup the process is unaware, and unable to capture, that it is being - suspended, and subsequently resumed. - operationId: "ContainerPause" - responses: - 204: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - tags: ["Container"] - /containers/{id}/unpause: - post: - summary: "Unpause a container" - description: "Resume a container which has been paused." - operationId: "ContainerUnpause" - responses: - 204: - description: "no error" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - tags: ["Container"] - /containers/{id}/attach: - post: - summary: "Attach to a container" - description: | - Attach to a container to read its output or send it input. You can attach - to the same container multiple times and you can reattach to containers - that have been detached. - - Either the `stream` or `logs` parameter must be `true` for this endpoint - to do anything. - - See the [documentation for the `docker attach` command](https://docs.docker.com/engine/reference/commandline/attach/) - for more details. - - ### Hijacking - - This endpoint hijacks the HTTP connection to transport `stdin`, `stdout`, - and `stderr` on the same socket. - - This is the response from the daemon for an attach request: - - ``` - HTTP/1.1 200 OK - Content-Type: application/vnd.docker.raw-stream - - [STREAM] - ``` - - After the headers and two new lines, the TCP connection can now be used - for raw, bidirectional communication between the client and server. - - To hint potential proxies about connection hijacking, the Docker client - can also optionally send connection upgrade headers. - - For example, the client sends this request to upgrade the connection: - - ``` - POST /containers/16253994b7c4/attach?stream=1&stdout=1 HTTP/1.1 - Upgrade: tcp - Connection: Upgrade - ``` - - The Docker daemon will respond with a `101 UPGRADED` response, and will - similarly follow with the raw stream: - - ``` - HTTP/1.1 101 UPGRADED - Content-Type: application/vnd.docker.raw-stream - Connection: Upgrade - Upgrade: tcp - - [STREAM] - ``` - - ### Stream format - - When the TTY setting is disabled in [`POST /containers/create`](#operation/ContainerCreate), - the HTTP Content-Type header is set to application/vnd.docker.multiplexed-stream - and the stream over the hijacked connected is multiplexed to separate out - `stdout` and `stderr`. The stream consists of a series of frames, each - containing a header and a payload. - - The header contains the information which the stream writes (`stdout` or - `stderr`). It also contains the size of the associated frame encoded in - the last four bytes (`uint32`). - - It is encoded on the first eight bytes like this: - - ```go - header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4} - ``` - - `STREAM_TYPE` can be: - - - 0: `stdin` (is written on `stdout`) - - 1: `stdout` - - 2: `stderr` - - `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of the `uint32` size - encoded as big endian. - - Following the header is the payload, which is the specified number of - bytes of `STREAM_TYPE`. - - The simplest way to implement this protocol is the following: - - 1. Read 8 bytes. - 2. Choose `stdout` or `stderr` depending on the first byte. - 3. Extract the frame size from the last four bytes. - 4. Read the extracted size and output it on the correct output. - 5. Goto 1. - - ### Stream format when using a TTY - - When the TTY setting is enabled in [`POST /containers/create`](#operation/ContainerCreate), - the stream is not multiplexed. The data exchanged over the hijacked - connection is simply the raw data from the process PTY and client's - `stdin`. - - operationId: "ContainerAttach" - produces: - - "application/vnd.docker.raw-stream" - - "application/vnd.docker.multiplexed-stream" - responses: - 101: - description: "no error, hints proxy about hijacking" - 200: - description: "no error, no upgrade header found" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "detachKeys" - in: "query" - description: | - Override the key sequence for detaching a container.Format is a single - character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, - `@`, `^`, `[`, `,` or `_`. - type: "string" - - name: "logs" - in: "query" - description: | - Replay previous logs from the container. - - This is useful for attaching to a container that has started and you - want to output everything since the container started. - - If `stream` is also enabled, once all the previous output has been - returned, it will seamlessly transition into streaming current - output. - type: "boolean" - default: false - - name: "stream" - in: "query" - description: | - Stream attached streams from the time the request was made onwards. - type: "boolean" - default: false - - name: "stdin" - in: "query" - description: "Attach to `stdin`" - type: "boolean" - default: false - - name: "stdout" - in: "query" - description: "Attach to `stdout`" - type: "boolean" - default: false - - name: "stderr" - in: "query" - description: "Attach to `stderr`" - type: "boolean" - default: false - tags: ["Container"] - /containers/{id}/attach/ws: - get: - summary: "Attach to a container via a websocket" - operationId: "ContainerAttachWebsocket" - responses: - 101: - description: "no error, hints proxy about hijacking" - 200: - description: "no error, no upgrade header found" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "detachKeys" - in: "query" - description: | - Override the key sequence for detaching a container.Format is a single - character `[a-Z]` or `ctrl-` where `` is one of: `a-z`, - `@`, `^`, `[`, `,`, or `_`. - type: "string" - - name: "logs" - in: "query" - description: "Return logs" - type: "boolean" - default: false - - name: "stream" - in: "query" - description: "Return stream" - type: "boolean" - default: false - - name: "stdin" - in: "query" - description: "Attach to `stdin`" - type: "boolean" - default: false - - name: "stdout" - in: "query" - description: "Attach to `stdout`" - type: "boolean" - default: false - - name: "stderr" - in: "query" - description: "Attach to `stderr`" - type: "boolean" - default: false - tags: ["Container"] - /containers/{id}/wait: - post: - summary: "Wait for a container" - description: "Block until a container stops, then returns the exit code." - operationId: "ContainerWait" - produces: ["application/json"] - responses: - 200: - description: "The container has exit." - schema: - $ref: "#/definitions/ContainerWaitResponse" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "condition" - in: "query" - description: | - Wait until a container state reaches the given condition. - - Defaults to `not-running` if omitted or empty. - type: "string" - enum: - - "not-running" - - "next-exit" - - "removed" - default: "not-running" - tags: ["Container"] - /containers/{id}: - delete: - summary: "Remove a container" - operationId: "ContainerDelete" - responses: - 204: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 409: - description: "conflict" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: | - You cannot remove a running container: c2ada9df5af8. Stop the - container before attempting removal or force remove - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "v" - in: "query" - description: "Remove anonymous volumes associated with the container." - type: "boolean" - default: false - - name: "force" - in: "query" - description: "If the container is running, kill it before removing it." - type: "boolean" - default: false - - name: "link" - in: "query" - description: "Remove the specified link associated with the container." - type: "boolean" - default: false - tags: ["Container"] - /containers/{id}/archive: - head: - summary: "Get information about files in a container" - description: | - A response header `X-Docker-Container-Path-Stat` is returned, containing - a base64 - encoded JSON object with some filesystem header information - about the path. - operationId: "ContainerArchiveInfo" - responses: - 200: - description: "no error" - headers: - X-Docker-Container-Path-Stat: - type: "string" - description: | - A base64 - encoded JSON object with some filesystem header - information about the path - 400: - description: "Bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "Container or path does not exist" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "path" - in: "query" - required: true - description: "Resource in the container’s filesystem to archive." - type: "string" - tags: ["Container"] - get: - summary: "Get an archive of a filesystem resource in a container" - description: "Get a tar archive of a resource in the filesystem of container id." - operationId: "ContainerArchive" - produces: ["application/x-tar"] - responses: - 200: - description: "no error" - 400: - description: "Bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "Container or path does not exist" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "path" - in: "query" - required: true - description: "Resource in the container’s filesystem to archive." - type: "string" - tags: ["Container"] - put: - summary: "Extract an archive of files or folders to a directory in a container" - description: | - Upload a tar archive to be extracted to a path in the filesystem of container id. - `path` parameter is asserted to be a directory. If it exists as a file, 400 error - will be returned with message "not a directory". - operationId: "PutContainerArchive" - consumes: ["application/x-tar", "application/octet-stream"] - responses: - 200: - description: "The content was extracted successfully" - 400: - description: "Bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "not a directory" - 403: - description: "Permission denied, the volume or container rootfs is marked as read-only." - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "No such container or path does not exist inside the container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the container" - type: "string" - - name: "path" - in: "query" - required: true - description: "Path to a directory in the container to extract the archive’s contents into. " - type: "string" - - name: "noOverwriteDirNonDir" - in: "query" - description: | - If `1`, `true`, or `True` then it will be an error if unpacking the - given content would cause an existing directory to be replaced with - a non-directory and vice versa. - type: "string" - - name: "copyUIDGID" - in: "query" - description: | - If `1`, `true`, then it will copy UID/GID maps to the dest file or - dir - type: "string" - - name: "inputStream" - in: "body" - required: true - description: | - The input stream must be a tar archive compressed with one of the - following algorithms: `identity` (no compression), `gzip`, `bzip2`, - or `xz`. - schema: - type: "string" - format: "binary" - tags: ["Container"] - /containers/prune: - post: - summary: "Delete stopped containers" - produces: - - "application/json" - operationId: "ContainerPrune" - parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the prune list, encoded as JSON (a `map[string][]string`). - - Available filters: - - `until=` Prune containers created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. - - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune containers with (or without, in case `label!=...` is used) the specified labels. - type: "string" - responses: - 200: - description: "No error" - schema: - type: "object" - title: "ContainerPruneResponse" - properties: - ContainersDeleted: - description: "Container IDs that were deleted" - type: "array" - items: - type: "string" - SpaceReclaimed: - description: "Disk space reclaimed in bytes" - type: "integer" - format: "int64" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Container"] - /images/json: - get: - summary: "List Images" - description: "Returns a list of images on the server. Note that it uses a different, smaller representation of an image than inspecting a single image." - operationId: "ImageList" - produces: - - "application/json" - responses: - 200: - description: "Summary image data for the images matching the query" - schema: - type: "array" - items: - $ref: "#/definitions/ImageSummary" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "all" - in: "query" - description: "Show all images. Only images from a final layer (no children) are shown by default." - type: "boolean" - default: false - - name: "filters" - in: "query" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the images list. - - Available filters: - - - `before`=(`[:]`, `` or ``) - - `dangling=true` - - `label=key` or `label="key=value"` of an image label - - `reference`=(`[:]`) - - `since`=(`[:]`, `` or ``) - - `until=` - type: "string" - - name: "shared-size" - in: "query" - description: "Compute and show shared size as a `SharedSize` field on each image." - type: "boolean" - default: false - - name: "digests" - in: "query" - description: "Show digest information as a `RepoDigests` field on each image." - type: "boolean" - default: false - - name: "manifests" - in: "query" - description: "Include `Manifests` in the image summary." - type: "boolean" - default: false - tags: ["Image"] - /build: - post: - summary: "Build an image" - description: | - Build an image from a tar archive with a `Dockerfile` in it. - - The `Dockerfile` specifies how the image is built from the tar archive. It is typically in the archive's root, but can be at a different path or have a different name by specifying the `dockerfile` parameter. [See the `Dockerfile` reference for more information](https://docs.docker.com/engine/reference/builder/). - - The Docker daemon performs a preliminary validation of the `Dockerfile` before starting the build, and returns an error if the syntax is incorrect. After that, each instruction is run one-by-one until the ID of the new image is output. - - The build is canceled if the client drops the connection by quitting or being killed. - operationId: "ImageBuild" - consumes: - - "application/octet-stream" - produces: - - "application/json" - parameters: - - name: "inputStream" - in: "body" - description: "A tar archive compressed with one of the following algorithms: identity (no compression), gzip, bzip2, xz." - schema: - type: "string" - format: "binary" - - name: "dockerfile" - in: "query" - description: "Path within the build context to the `Dockerfile`. This is ignored if `remote` is specified and points to an external `Dockerfile`." - type: "string" - default: "Dockerfile" - - name: "t" - in: "query" - description: "A name and optional tag to apply to the image in the `name:tag` format. If you omit the tag the default `latest` value is assumed. You can provide several `t` parameters." - type: "string" - - name: "extrahosts" - in: "query" - description: "Extra hosts to add to /etc/hosts" - type: "string" - - name: "remote" - in: "query" - description: "A Git repository URI or HTTP/HTTPS context URI. If the URI points to a single text file, the file’s contents are placed into a file called `Dockerfile` and the image is built from that file. If the URI points to a tarball, the file is downloaded by the daemon and the contents therein used as the context for the build. If the URI points to a tarball and the `dockerfile` parameter is also specified, there must be a file with the corresponding path inside the tarball." - type: "string" - - name: "q" - in: "query" - description: "Suppress verbose build output." - type: "boolean" - default: false - - name: "nocache" - in: "query" - description: "Do not use the cache when building the image." - type: "boolean" - default: false - - name: "cachefrom" - in: "query" - description: "JSON array of images used for build cache resolution." - type: "string" - - name: "pull" - in: "query" - description: "Attempt to pull the image even if an older image exists locally." - type: "string" - - name: "rm" - in: "query" - description: "Remove intermediate containers after a successful build." - type: "boolean" - default: true - - name: "forcerm" - in: "query" - description: "Always remove intermediate containers, even upon failure." - type: "boolean" - default: false - - name: "memory" - in: "query" - description: "Set memory limit for build." - type: "integer" - - name: "memswap" - in: "query" - description: "Total memory (memory + swap). Set as `-1` to disable swap." - type: "integer" - - name: "cpushares" - in: "query" - description: "CPU shares (relative weight)." - type: "integer" - - name: "cpusetcpus" - in: "query" - description: "CPUs in which to allow execution (e.g., `0-3`, `0,1`)." - type: "string" - - name: "cpuperiod" - in: "query" - description: "The length of a CPU period in microseconds." - type: "integer" - - name: "cpuquota" - in: "query" - description: "Microseconds of CPU time that the container can get in a CPU period." - type: "integer" - - name: "buildargs" - in: "query" - description: > - JSON map of string pairs for build-time variables. Users pass these values at build-time. Docker - uses the buildargs as the environment context for commands run via the `Dockerfile` RUN - instruction, or for variable expansion in other `Dockerfile` instructions. This is not meant for - passing secret values. - - - For example, the build arg `FOO=bar` would become `{"FOO":"bar"}` in JSON. This would result in the - query parameter `buildargs={"FOO":"bar"}`. Note that `{"FOO":"bar"}` should be URI component encoded. - - - [Read more about the buildargs instruction.](https://docs.docker.com/engine/reference/builder/#arg) - type: "string" - - name: "shmsize" - in: "query" - description: "Size of `/dev/shm` in bytes. The size must be greater than 0. If omitted the system uses 64MB." - type: "integer" - - name: "squash" - in: "query" - description: "Squash the resulting images layers into a single layer. *(Experimental release only.)*" - type: "boolean" - - name: "labels" - in: "query" - description: "Arbitrary key/value labels to set on the image, as a JSON map of string pairs." - type: "string" - - name: "networkmode" - in: "query" - description: | - Sets the networking mode for the run commands during build. Supported - standard values are: `bridge`, `host`, `none`, and `container:`. - Any other value is taken as a custom network's name or ID to which this - container should connect to. - type: "string" - - name: "Content-type" - in: "header" - type: "string" - enum: - - "application/x-tar" - default: "application/x-tar" - - name: "X-Registry-Config" - in: "header" - description: | - This is a base64-encoded JSON object with auth configurations for multiple registries that a build may refer to. - - The key is a registry URL, and the value is an auth configuration object, [as described in the authentication section](#section/Authentication). For example: - - ``` - { - "docker.example.com": { - "username": "janedoe", - "password": "hunter2" - }, - "https://index.docker.io/v1/": { - "username": "mobydock", - "password": "conta1n3rize14" - } - } - ``` - - Only the registry domain name (and port if not the default 443) are required. However, for legacy reasons, the Docker Hub registry must be specified with both a `https://` prefix and a `/v1/` suffix even though Docker will prefer to use the v2 registry API. - type: "string" - - name: "platform" - in: "query" - description: "Platform in the format os[/arch[/variant]]" - type: "string" - default: "" - - name: "target" - in: "query" - description: "Target build stage" - type: "string" - default: "" - - name: "outputs" - in: "query" - description: "BuildKit output configuration" - type: "string" - default: "" - - name: "version" - in: "query" - type: "string" - default: "1" - enum: ["1", "2"] - description: | - Version of the builder backend to use. - - - `1` is the first generation classic (deprecated) builder in the Docker daemon (default) - - `2` is [BuildKit](https://github.com/moby/buildkit) - responses: - 200: - description: "no error" - 400: - description: "Bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Image"] - /build/prune: - post: - summary: "Delete builder cache" - produces: - - "application/json" - operationId: "BuildPrune" - parameters: - - name: "keep-storage" - in: "query" - description: | - Amount of disk space in bytes to keep for cache - - > **Deprecated**: This parameter is deprecated and has been renamed to "reserved-space". - > It is kept for backward compatibility and will be removed in API v1.49. - type: "integer" - format: "int64" - - name: "reserved-space" - in: "query" - description: "Amount of disk space in bytes to keep for cache" - type: "integer" - format: "int64" - - name: "max-used-space" - in: "query" - description: "Maximum amount of disk space allowed to keep for cache" - type: "integer" - format: "int64" - - name: "min-free-space" - in: "query" - description: "Target amount of free disk space after pruning" - type: "integer" - format: "int64" - - name: "all" - in: "query" - type: "boolean" - description: "Remove all types of build cache" - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the list of build cache objects. - - Available filters: - - - `until=` remove cache older than ``. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon's local time. - - `id=` - - `parent=` - - `type=` - - `description=` - - `inuse` - - `shared` - - `private` - responses: - 200: - description: "No error" - schema: - type: "object" - title: "BuildPruneResponse" - properties: - CachesDeleted: - type: "array" - items: - description: "ID of build cache object" - type: "string" - SpaceReclaimed: - description: "Disk space reclaimed in bytes" - type: "integer" - format: "int64" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Image"] - /images/create: - post: - summary: "Create an image" - description: "Pull or import an image." - operationId: "ImageCreate" - consumes: - - "text/plain" - - "application/octet-stream" - produces: - - "application/json" - responses: - 200: - description: "no error" - 404: - description: "repository does not exist or no read access" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "fromImage" - in: "query" - description: | - Name of the image to pull. If the name includes a tag or digest, specific behavior applies: - - - If only `fromImage` includes a tag, that tag is used. - - If both `fromImage` and `tag` are provided, `tag` takes precedence. - - If `fromImage` includes a digest, the image is pulled by digest, and `tag` is ignored. - - If neither a tag nor digest is specified, all tags are pulled. - type: "string" - - name: "fromSrc" - in: "query" - description: "Source to import. The value may be a URL from which the image can be retrieved or `-` to read the image from the request body. This parameter may only be used when importing an image." - type: "string" - - name: "repo" - in: "query" - description: "Repository name given to an image when it is imported. The repo may include a tag. This parameter may only be used when importing an image." - type: "string" - - name: "tag" - in: "query" - description: "Tag or digest. If empty when pulling an image, this causes all tags for the given image to be pulled." - type: "string" - - name: "message" - in: "query" - description: "Set commit message for imported image." - type: "string" - - name: "inputImage" - in: "body" - description: "Image content if the value `-` has been specified in fromSrc query parameter" - schema: - type: "string" - required: false - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - - name: "changes" - in: "query" - description: | - Apply `Dockerfile` instructions to the image that is created, - for example: `changes=ENV DEBUG=true`. - Note that `ENV DEBUG=true` should be URI component encoded. - - Supported `Dockerfile` instructions: - `CMD`|`ENTRYPOINT`|`ENV`|`EXPOSE`|`ONBUILD`|`USER`|`VOLUME`|`WORKDIR` - type: "array" - items: - type: "string" - - name: "platform" - in: "query" - description: | - Platform in the format os[/arch[/variant]]. - - When used in combination with the `fromImage` option, the daemon checks - if the given image is present in the local image cache with the given - OS and Architecture, and otherwise attempts to pull the image. If the - option is not set, the host's native OS and Architecture are used. - If the given image does not exist in the local image cache, the daemon - attempts to pull the image with the host's native OS and Architecture. - If the given image does exists in the local image cache, but its OS or - architecture does not match, a warning is produced. - - When used with the `fromSrc` option to import an image from an archive, - this option sets the platform information for the imported image. If - the option is not set, the host's native OS and Architecture are used - for the imported image. - type: "string" - default: "" - tags: ["Image"] - /images/{name}/json: - get: - summary: "Inspect an image" - description: "Return low-level information about an image." - operationId: "ImageInspect" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - $ref: "#/definitions/ImageInspect" - 404: - description: "No such image" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such image: someimage (tag: latest)" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or id" - type: "string" - required: true - - name: "manifests" - in: "query" - description: "Include Manifests in the image summary." - type: "boolean" - default: false - required: false - tags: ["Image"] - /images/{name}/history: - get: - summary: "Get the history of an image" - description: "Return parent layers of an image." - operationId: "ImageHistory" - produces: ["application/json"] - responses: - 200: - description: "List of image layers" - schema: - type: "array" - items: - type: "object" - x-go-name: HistoryResponseItem - title: "HistoryResponseItem" - description: "individual image layer information in response to ImageHistory operation" - required: [Id, Created, CreatedBy, Tags, Size, Comment] - properties: - Id: - type: "string" - x-nullable: false - Created: - type: "integer" - format: "int64" - x-nullable: false - CreatedBy: - type: "string" - x-nullable: false - Tags: - type: "array" - items: - type: "string" - Size: - type: "integer" - format: "int64" - x-nullable: false - Comment: - type: "string" - x-nullable: false - examples: - application/json: - - Id: "3db9c44f45209632d6050b35958829c3a2aa256d81b9a7be45b362ff85c54710" - Created: 1398108230 - CreatedBy: "/bin/sh -c #(nop) ADD file:eb15dbd63394e063b805a3c32ca7bf0266ef64676d5a6fab4801f2e81e2a5148 in /" - Tags: - - "ubuntu:lucid" - - "ubuntu:10.04" - Size: 182964289 - Comment: "" - - Id: "6cfa4d1f33fb861d4d114f43b25abd0ac737509268065cdfd69d544a59c85ab8" - Created: 1398108222 - CreatedBy: "/bin/sh -c #(nop) MAINTAINER Tianon Gravi - mkimage-debootstrap.sh -i iproute,iputils-ping,ubuntu-minimal -t lucid.tar.xz lucid http://archive.ubuntu.com/ubuntu/" - Tags: [] - Size: 0 - Comment: "" - - Id: "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158" - Created: 1371157430 - CreatedBy: "" - Tags: - - "scratch12:latest" - - "scratch:latest" - Size: 0 - Comment: "Imported from -" - 404: - description: "No such image" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or ID" - type: "string" - required: true - - name: "platform" - type: "string" - in: "query" - description: | - JSON-encoded OCI platform to select the platform-variant. - If omitted, it defaults to any locally available platform, - prioritizing the daemon's host platform. - - If the daemon provides a multi-platform image store, this selects - the platform-variant to show the history for. If the image is - a single-platform image, or if the multi-platform image does not - provide a variant matching the given platform, an error is returned. - - Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` - tags: ["Image"] - /images/{name}/push: - post: - summary: "Push an image" - description: | - Push an image to a registry. - - If you wish to push an image on to a private registry, that image must - already have a tag which references the registry. For example, - `registry.example.com/myimage:latest`. - - The push is cancelled if the HTTP connection is closed. - operationId: "ImagePush" - consumes: - - "application/octet-stream" - responses: - 200: - description: "No error" - 404: - description: "No such image" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - Name of the image to push. For example, `registry.example.com/myimage`. - The image must be present in the local image store with the same name. - - The name should be provided without tag; if a tag is provided, it - is ignored. For example, `registry.example.com/myimage:latest` is - considered equivalent to `registry.example.com/myimage`. - - Use the `tag` parameter to specify the tag to push. - type: "string" - required: true - - name: "tag" - in: "query" - description: | - Tag of the image to push. For example, `latest`. If no tag is provided, - all tags of the given image that are present in the local image store - are pushed. - type: "string" - - name: "platform" - type: "string" - in: "query" - description: | - JSON-encoded OCI platform to select the platform-variant to push. - If not provided, all available variants will attempt to be pushed. - - If the daemon provides a multi-platform image store, this selects - the platform-variant to push to the registry. If the image is - a single-platform image, or if the multi-platform image does not - provide a variant matching the given platform, an error is returned. - - Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - required: true - tags: ["Image"] - /images/{name}/tag: - post: - summary: "Tag an image" - description: "Tag an image so that it becomes part of a repository." - operationId: "ImageTag" - responses: - 201: - description: "No error" - 400: - description: "Bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "No such image" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "Conflict" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or ID to tag." - type: "string" - required: true - - name: "repo" - in: "query" - description: "The repository to tag in. For example, `someuser/someimage`." - type: "string" - - name: "tag" - in: "query" - description: "The name of the new tag." - type: "string" - tags: ["Image"] - /images/{name}: - delete: - summary: "Remove an image" - description: | - Remove an image, along with any untagged parent images that were - referenced by that image. - - Images can't be removed if they have descendant images, are being - used by a running container or are being used by a build. - operationId: "ImageDelete" - produces: ["application/json"] - responses: - 200: - description: "The image was deleted successfully" - schema: - type: "array" - items: - $ref: "#/definitions/ImageDeleteResponseItem" - examples: - application/json: - - Untagged: "3e2f21a89f" - - Deleted: "3e2f21a89f" - - Deleted: "53b4f83ac9" - 404: - description: "No such image" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "Conflict" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or ID" - type: "string" - required: true - - name: "force" - in: "query" - description: "Remove the image even if it is being used by stopped containers or has other tags" - type: "boolean" - default: false - - name: "noprune" - in: "query" - description: "Do not delete untagged parent images" - type: "boolean" - default: false - - name: "platforms" - in: "query" - description: | - Select platform-specific content to delete. - Multiple values are accepted. - Each platform is a OCI platform encoded as a JSON string. - type: "array" - items: - # This should be OCIPlatform - # but $ref is not supported for array in query in Swagger 2.0 - # $ref: "#/definitions/OCIPlatform" - type: "string" - tags: ["Image"] - /images/search: - get: - summary: "Search images" - description: "Search for an image on Docker Hub." - operationId: "ImageSearch" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - type: "array" - items: - type: "object" - title: "ImageSearchResponseItem" - properties: - description: - type: "string" - is_official: - type: "boolean" - is_automated: - description: | - Whether this repository has automated builds enabled. - -


- - > **Deprecated**: This field is deprecated and will always be "false". - type: "boolean" - example: false - name: - type: "string" - star_count: - type: "integer" - examples: - application/json: - - description: "A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!" - is_official: true - is_automated: false - name: "alpine" - star_count: 10093 - - description: "Busybox base image." - is_official: true - is_automated: false - name: "Busybox base image." - star_count: 3037 - - description: "The PostgreSQL object-relational database system provides reliability and data integrity." - is_official: true - is_automated: false - name: "postgres" - star_count: 12408 - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "term" - in: "query" - description: "Term to search" - type: "string" - required: true - - name: "limit" - in: "query" - description: "Maximum number of results to return" - type: "integer" - - name: "filters" - in: "query" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters: - - - `is-official=(true|false)` - - `stars=` Matches images that has at least 'number' stars. - type: "string" - tags: ["Image"] - /images/prune: - post: - summary: "Delete unused images" - produces: - - "application/json" - operationId: "ImagePrune" - parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the prune list, encoded as JSON (a `map[string][]string`). Available filters: - - - `dangling=` When set to `true` (or `1`), prune only - unused *and* untagged images. When set to `false` - (or `0`), all unused images are pruned. - - `until=` Prune images created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. - - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune images with (or without, in case `label!=...` is used) the specified labels. - type: "string" - responses: - 200: - description: "No error" - schema: - type: "object" - title: "ImagePruneResponse" - properties: - ImagesDeleted: - description: "Images that were deleted" - type: "array" - items: - $ref: "#/definitions/ImageDeleteResponseItem" - SpaceReclaimed: - description: "Disk space reclaimed in bytes" - type: "integer" - format: "int64" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Image"] - /auth: - post: - summary: "Check auth configuration" - description: | - Validate credentials for a registry and, if available, get an identity - token for accessing the registry without password. - operationId: "SystemAuth" - consumes: ["application/json"] - produces: ["application/json"] - responses: - 200: - description: "An identity token was generated successfully." - schema: - type: "object" - title: "SystemAuthResponse" - required: [Status] - properties: - Status: - description: "The status of the authentication" - type: "string" - x-nullable: false - IdentityToken: - description: "An opaque token used to authenticate a user after a successful login" - type: "string" - x-nullable: false - examples: - application/json: - Status: "Login Succeeded" - IdentityToken: "9cbaf023786cd7..." - 204: - description: "No error" - 401: - description: "Auth error" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "authConfig" - in: "body" - description: "Authentication to check" - schema: - $ref: "#/definitions/AuthConfig" - tags: ["System"] - /info: - get: - summary: "Get system information" - operationId: "SystemInfo" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - $ref: "#/definitions/SystemInfo" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["System"] - /version: - get: - summary: "Get version" - description: "Returns the version of Docker that is running and various information about the system that Docker is running on." - operationId: "SystemVersion" - produces: ["application/json"] - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/SystemVersion" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["System"] - /_ping: - get: - summary: "Ping" - description: "This is a dummy endpoint you can use to test if the server is accessible." - operationId: "SystemPing" - produces: ["text/plain"] - responses: - 200: - description: "no error" - schema: - type: "string" - example: "OK" - headers: - Api-Version: - type: "string" - description: "Max API Version the server supports" - Builder-Version: - type: "string" - description: | - Default version of docker image builder - - The default on Linux is version "2" (BuildKit), but the daemon - can be configured to recommend version "1" (classic Builder). - Windows does not yet support BuildKit for native Windows images, - and uses "1" (classic builder) as a default. - - This value is a recommendation as advertised by the daemon, and - it is up to the client to choose which builder to use. - default: "2" - Docker-Experimental: - type: "boolean" - description: "If the server is running with experimental mode enabled" - Swarm: - type: "string" - enum: ["inactive", "pending", "error", "locked", "active/worker", "active/manager"] - description: | - Contains information about Swarm status of the daemon, - and if the daemon is acting as a manager or worker node. - default: "inactive" - Cache-Control: - type: "string" - default: "no-cache, no-store, must-revalidate" - Pragma: - type: "string" - default: "no-cache" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - headers: - Cache-Control: - type: "string" - default: "no-cache, no-store, must-revalidate" - Pragma: - type: "string" - default: "no-cache" - tags: ["System"] - head: - summary: "Ping" - description: "This is a dummy endpoint you can use to test if the server is accessible." - operationId: "SystemPingHead" - produces: ["text/plain"] - responses: - 200: - description: "no error" - schema: - type: "string" - example: "(empty)" - headers: - Api-Version: - type: "string" - description: "Max API Version the server supports" - Builder-Version: - type: "string" - description: "Default version of docker image builder" - Docker-Experimental: - type: "boolean" - description: "If the server is running with experimental mode enabled" - Swarm: - type: "string" - enum: ["inactive", "pending", "error", "locked", "active/worker", "active/manager"] - description: | - Contains information about Swarm status of the daemon, - and if the daemon is acting as a manager or worker node. - default: "inactive" - Cache-Control: - type: "string" - default: "no-cache, no-store, must-revalidate" - Pragma: - type: "string" - default: "no-cache" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["System"] - /commit: - post: - summary: "Create a new image from a container" - operationId: "ImageCommit" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "no error" - schema: - $ref: "#/definitions/IDResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "containerConfig" - in: "body" - description: "The container configuration" - schema: - $ref: "#/definitions/ContainerConfig" - - name: "container" - in: "query" - description: "The ID or name of the container to commit" - type: "string" - - name: "repo" - in: "query" - description: "Repository name for the created image" - type: "string" - - name: "tag" - in: "query" - description: "Tag name for the create image" - type: "string" - - name: "comment" - in: "query" - description: "Commit message" - type: "string" - - name: "author" - in: "query" - description: "Author of the image (e.g., `John Hannibal Smith `)" - type: "string" - - name: "pause" - in: "query" - description: "Whether to pause the container before committing" - type: "boolean" - default: true - - name: "changes" - in: "query" - description: "`Dockerfile` instructions to apply while committing" - type: "string" - tags: ["Image"] - /events: - get: - summary: "Monitor events" - description: | - Stream real-time events from the server. - - Various objects within Docker report events when something happens to them. - - Containers report these events: `attach`, `commit`, `copy`, `create`, `destroy`, `detach`, `die`, `exec_create`, `exec_detach`, `exec_start`, `exec_die`, `export`, `health_status`, `kill`, `oom`, `pause`, `rename`, `resize`, `restart`, `start`, `stop`, `top`, `unpause`, `update`, and `prune` - - Images report these events: `create`, `delete`, `import`, `load`, `pull`, `push`, `save`, `tag`, `untag`, and `prune` - - Volumes report these events: `create`, `mount`, `unmount`, `destroy`, and `prune` - - Networks report these events: `create`, `connect`, `disconnect`, `destroy`, `update`, `remove`, and `prune` - - The Docker daemon reports these events: `reload` - - Services report these events: `create`, `update`, and `remove` - - Nodes report these events: `create`, `update`, and `remove` - - Secrets report these events: `create`, `update`, and `remove` - - Configs report these events: `create`, `update`, and `remove` - - The Builder reports `prune` events - - operationId: "SystemEvents" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/EventMessage" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "since" - in: "query" - description: "Show events created since this timestamp then stream new events." - type: "string" - - name: "until" - in: "query" - description: "Show events created until this timestamp then stop streaming." - type: "string" - - name: "filters" - in: "query" - description: | - A JSON encoded value of filters (a `map[string][]string`) to process on the event list. Available filters: - - - `config=` config name or ID - - `container=` container name or ID - - `daemon=` daemon name or ID - - `event=` event type - - `image=` image name or ID - - `label=` image or container label - - `network=` network name or ID - - `node=` node ID - - `plugin`= plugin name or ID - - `scope`= local or swarm - - `secret=` secret name or ID - - `service=` service name or ID - - `type=` object to filter by, one of `container`, `image`, `volume`, `network`, `daemon`, `plugin`, `node`, `service`, `secret` or `config` - - `volume=` volume name - type: "string" - tags: ["System"] - /system/df: - get: - summary: "Get data usage information" - operationId: "SystemDataUsage" - responses: - 200: - description: "no error" - schema: - type: "object" - title: "SystemDataUsageResponse" - properties: - LayersSize: - type: "integer" - format: "int64" - Images: - type: "array" - items: - $ref: "#/definitions/ImageSummary" - Containers: - type: "array" - items: - $ref: "#/definitions/ContainerSummary" - Volumes: - type: "array" - items: - $ref: "#/definitions/Volume" - BuildCache: - type: "array" - items: - $ref: "#/definitions/BuildCache" - example: - LayersSize: 1092588 - Images: - - - Id: "sha256:2b8fd9751c4c0f5dd266fcae00707e67a2545ef34f9a29354585f93dac906749" - ParentId: "" - RepoTags: - - "busybox:latest" - RepoDigests: - - "busybox@sha256:a59906e33509d14c036c8678d687bd4eec81ed7c4b8ce907b888c607f6a1e0e6" - Created: 1466724217 - Size: 1092588 - SharedSize: 0 - Labels: {} - Containers: 1 - Containers: - - - Id: "e575172ed11dc01bfce087fb27bee502db149e1a0fad7c296ad300bbff178148" - Names: - - "/top" - Image: "busybox" - ImageID: "sha256:2b8fd9751c4c0f5dd266fcae00707e67a2545ef34f9a29354585f93dac906749" - Command: "top" - Created: 1472592424 - Ports: [] - SizeRootFs: 1092588 - Labels: {} - State: "exited" - Status: "Exited (0) 56 minutes ago" - HostConfig: - NetworkMode: "default" - NetworkSettings: - Networks: - bridge: - IPAMConfig: null - Links: null - Aliases: null - NetworkID: "d687bc59335f0e5c9ee8193e5612e8aee000c8c62ea170cfb99c098f95899d92" - EndpointID: "8ed5115aeaad9abb174f68dcf135b49f11daf597678315231a32ca28441dec6a" - Gateway: "172.18.0.1" - IPAddress: "172.18.0.2" - IPPrefixLen: 16 - IPv6Gateway: "" - GlobalIPv6Address: "" - GlobalIPv6PrefixLen: 0 - MacAddress: "02:42:ac:12:00:02" - Mounts: [] - Volumes: - - - Name: "my-volume" - Driver: "local" - Mountpoint: "/var/lib/docker/volumes/my-volume/_data" - Labels: null - Scope: "local" - Options: null - UsageData: - Size: 10920104 - RefCount: 2 - BuildCache: - - - ID: "hw53o5aio51xtltp5xjp8v7fx" - Parents: [] - Type: "regular" - Description: "pulled from docker.io/library/debian@sha256:234cb88d3020898631af0ccbbcca9a66ae7306ecd30c9720690858c1b007d2a0" - InUse: false - Shared: true - Size: 0 - CreatedAt: "2021-06-28T13:31:01.474619385Z" - LastUsedAt: "2021-07-07T22:02:32.738075951Z" - UsageCount: 26 - - - ID: "ndlpt0hhvkqcdfkputsk4cq9c" - Parents: ["ndlpt0hhvkqcdfkputsk4cq9c"] - Type: "regular" - Description: "mount / from exec /bin/sh -c echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' > /etc/apt/apt.conf.d/keep-cache" - InUse: false - Shared: true - Size: 51 - CreatedAt: "2021-06-28T13:31:03.002625487Z" - LastUsedAt: "2021-07-07T22:02:32.773909517Z" - UsageCount: 26 - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "type" - in: "query" - description: | - Object types, for which to compute and return data. - type: "array" - collectionFormat: multi - items: - type: "string" - enum: ["container", "image", "volume", "build-cache"] - tags: ["System"] - /images/{name}/get: - get: - summary: "Export an image" - description: | - Get a tarball containing all images and metadata for a repository. - - If `name` is a specific name and tag (e.g. `ubuntu:latest`), then only that image (and its parents) are returned. If `name` is an image ID, similarly only that image (and its parents) are returned, but with the exclusion of the `repositories` file in the tarball, as there were no image names referenced. - - ### Image tarball format - - An image tarball contains [Content as defined in the OCI Image Layout Specification](https://github.com/opencontainers/image-spec/blob/v1.1.1/image-layout.md#content). - - Additionally, includes the manifest.json file associated with a backwards compatible docker save format. - - If the tarball defines a repository, the tarball should also include a `repositories` file at the root that contains a list of repository and tag names mapped to layer IDs. - - ```json - { - "hello-world": { - "latest": "565a9d68a73f6706862bfe8409a7f659776d4d60a8d096eb4a3cbce6999cc2a1" - } - } - ``` - operationId: "ImageGet" - produces: - - "application/x-tar" - responses: - 200: - description: "no error" - schema: - type: "string" - format: "binary" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or ID" - type: "string" - required: true - - name: "platform" - type: "string" - in: "query" - description: | - JSON encoded OCI platform describing a platform which will be used - to select a platform-specific image to be saved if the image is - multi-platform. - If not provided, the full multi-platform image will be saved. - - Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` - tags: ["Image"] - /images/get: - get: - summary: "Export several images" - description: | - Get a tarball containing all images and metadata for several image - repositories. - - For each value of the `names` parameter: if it is a specific name and - tag (e.g. `ubuntu:latest`), then only that image (and its parents) are - returned; if it is an image ID, similarly only that image (and its parents) - are returned and there would be no names referenced in the 'repositories' - file for this image ID. - - For details on the format, see the [export image endpoint](#operation/ImageGet). - operationId: "ImageGetAll" - produces: - - "application/x-tar" - responses: - 200: - description: "no error" - schema: - type: "string" - format: "binary" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "names" - in: "query" - description: "Image names to filter by" - type: "array" - items: - type: "string" - - name: "platform" - type: "string" - in: "query" - description: | - JSON encoded OCI platform describing a platform which will be used - to select a platform-specific image to be saved if the image is - multi-platform. - If not provided, the full multi-platform image will be saved. - - Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` - tags: ["Image"] - /images/load: - post: - summary: "Import images" - description: | - Load a set of images and tags into a repository. - - For details on the format, see the [export image endpoint](#operation/ImageGet). - operationId: "ImageLoad" - consumes: - - "application/x-tar" - produces: - - "application/json" - responses: - 200: - description: "no error" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "imagesTarball" - in: "body" - description: "Tar archive containing images" - schema: - type: "string" - format: "binary" - - name: "quiet" - in: "query" - description: "Suppress progress details during load." - type: "boolean" - default: false - - name: "platform" - type: "string" - in: "query" - description: | - JSON encoded OCI platform describing a platform which will be used - to select a platform-specific image to be load if the image is - multi-platform. - If not provided, the full multi-platform image will be loaded. - - Example: `{"os": "linux", "architecture": "arm", "variant": "v5"}` - tags: ["Image"] - /containers/{id}/exec: - post: - summary: "Create an exec instance" - description: "Run a command inside a running container." - operationId: "ContainerExec" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "no error" - schema: - $ref: "#/definitions/IDResponse" - 404: - description: "no such container" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such container: c2ada9df5af8" - 409: - description: "container is paused" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "execConfig" - in: "body" - description: "Exec configuration" - schema: - type: "object" - title: "ExecConfig" - properties: - AttachStdin: - type: "boolean" - description: "Attach to `stdin` of the exec command." - AttachStdout: - type: "boolean" - description: "Attach to `stdout` of the exec command." - AttachStderr: - type: "boolean" - description: "Attach to `stderr` of the exec command." - ConsoleSize: - type: "array" - description: "Initial console size, as an `[height, width]` array." - x-nullable: true - minItems: 2 - maxItems: 2 - items: - type: "integer" - minimum: 0 - example: [80, 64] - DetachKeys: - type: "string" - description: | - Override the key sequence for detaching a container. Format is - a single character `[a-Z]` or `ctrl-` where `` - is one of: `a-z`, `@`, `^`, `[`, `,` or `_`. - Tty: - type: "boolean" - description: "Allocate a pseudo-TTY." - Env: - description: | - A list of environment variables in the form `["VAR=value", ...]`. - type: "array" - items: - type: "string" - Cmd: - type: "array" - description: "Command to run, as a string or array of strings." - items: - type: "string" - Privileged: - type: "boolean" - description: "Runs the exec process with extended privileges." - default: false - User: - type: "string" - description: | - The user, and optionally, group to run the exec process inside - the container. Format is one of: `user`, `user:group`, `uid`, - or `uid:gid`. - WorkingDir: - type: "string" - description: | - The working directory for the exec process inside the container. - example: - AttachStdin: false - AttachStdout: true - AttachStderr: true - DetachKeys: "ctrl-p,ctrl-q" - Tty: false - Cmd: - - "date" - Env: - - "FOO=bar" - - "BAZ=quux" - required: true - - name: "id" - in: "path" - description: "ID or name of container" - type: "string" - required: true - tags: ["Exec"] - /exec/{id}/start: - post: - summary: "Start an exec instance" - description: | - Starts a previously set up exec instance. If detach is true, this endpoint - returns immediately after starting the command. Otherwise, it sets up an - interactive session with the command. - operationId: "ExecStart" - consumes: - - "application/json" - produces: - - "application/vnd.docker.raw-stream" - - "application/vnd.docker.multiplexed-stream" - responses: - 200: - description: "No error" - 404: - description: "No such exec instance" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "Container is stopped or paused" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "execStartConfig" - in: "body" - schema: - type: "object" - title: "ExecStartConfig" - properties: - Detach: - type: "boolean" - description: "Detach from the command." - example: false - Tty: - type: "boolean" - description: "Allocate a pseudo-TTY." - example: true - ConsoleSize: - type: "array" - description: "Initial console size, as an `[height, width]` array." - x-nullable: true - minItems: 2 - maxItems: 2 - items: - type: "integer" - minimum: 0 - example: [80, 64] - - name: "id" - in: "path" - description: "Exec instance ID" - required: true - type: "string" - tags: ["Exec"] - /exec/{id}/resize: - post: - summary: "Resize an exec instance" - description: | - Resize the TTY session used by an exec instance. This endpoint only works - if `tty` was specified as part of creating and starting the exec instance. - operationId: "ExecResize" - responses: - 200: - description: "No error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "No such exec instance" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Exec instance ID" - required: true - type: "string" - - name: "h" - in: "query" - required: true - description: "Height of the TTY session in characters" - type: "integer" - - name: "w" - in: "query" - required: true - description: "Width of the TTY session in characters" - type: "integer" - tags: ["Exec"] - /exec/{id}/json: - get: - summary: "Inspect an exec instance" - description: "Return low-level information about an exec instance." - operationId: "ExecInspect" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - type: "object" - title: "ExecInspectResponse" - properties: - CanRemove: - type: "boolean" - DetachKeys: - type: "string" - ID: - type: "string" - Running: - type: "boolean" - ExitCode: - type: "integer" - ProcessConfig: - $ref: "#/definitions/ProcessConfig" - OpenStdin: - type: "boolean" - OpenStderr: - type: "boolean" - OpenStdout: - type: "boolean" - ContainerID: - type: "string" - Pid: - type: "integer" - description: "The system process ID for the exec process." - examples: - application/json: - CanRemove: false - ContainerID: "b53ee82b53a40c7dca428523e34f741f3abc51d9f297a14ff874bf761b995126" - DetachKeys: "" - ExitCode: 2 - ID: "f33bbfb39f5b142420f4759b2348913bd4a8d1a6d7fd56499cb41a1bb91d7b3b" - OpenStderr: true - OpenStdin: true - OpenStdout: true - ProcessConfig: - arguments: - - "-c" - - "exit 2" - entrypoint: "sh" - privileged: false - tty: true - user: "1000" - Running: false - Pid: 42000 - 404: - description: "No such exec instance" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Exec instance ID" - required: true - type: "string" - tags: ["Exec"] - - /volumes: - get: - summary: "List volumes" - operationId: "VolumeList" - produces: ["application/json"] - responses: - 200: - description: "Summary volume data that matches the query" - schema: - $ref: "#/definitions/VolumeListResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - description: | - JSON encoded value of the filters (a `map[string][]string`) to - process on the volumes list. Available filters: - - - `dangling=` When set to `true` (or `1`), returns all - volumes that are not in use by a container. When set to `false` - (or `0`), only volumes that are in use by one or more - containers are returned. - - `driver=` Matches volumes based on their driver. - - `label=` or `label=:` Matches volumes based on - the presence of a `label` alone or a `label` and a value. - - `name=` Matches all or part of a volume name. - type: "string" - format: "json" - tags: ["Volume"] - - /volumes/create: - post: - summary: "Create a volume" - operationId: "VolumeCreate" - consumes: ["application/json"] - produces: ["application/json"] - responses: - 201: - description: "The volume was created successfully" - schema: - $ref: "#/definitions/Volume" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "volumeConfig" - in: "body" - required: true - description: "Volume configuration" - schema: - $ref: "#/definitions/VolumeCreateOptions" - tags: ["Volume"] - - /volumes/{name}: - get: - summary: "Inspect a volume" - operationId: "VolumeInspect" - produces: ["application/json"] - responses: - 200: - description: "No error" - schema: - $ref: "#/definitions/Volume" - 404: - description: "No such volume" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - required: true - description: "Volume name or ID" - type: "string" - tags: ["Volume"] - - put: - summary: | - "Update a volume. Valid only for Swarm cluster volumes" - operationId: "VolumeUpdate" - consumes: ["application/json"] - produces: ["application/json"] - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such volume" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "The name or ID of the volume" - type: "string" - required: true - - name: "body" - in: "body" - schema: - # though the schema for is an object that contains only a - # ClusterVolumeSpec, wrapping the ClusterVolumeSpec in this object - # means that if, later on, we support things like changing the - # labels, we can do so without duplicating that information to the - # ClusterVolumeSpec. - type: "object" - description: "Volume configuration" - properties: - Spec: - $ref: "#/definitions/ClusterVolumeSpec" - description: | - The spec of the volume to update. Currently, only Availability may - change. All other fields must remain unchanged. - - name: "version" - in: "query" - description: | - The version number of the volume being updated. This is required to - avoid conflicting writes. Found in the volume's `ClusterVolume` - field. - type: "integer" - format: "int64" - required: true - tags: ["Volume"] - - delete: - summary: "Remove a volume" - description: "Instruct the driver to remove the volume." - operationId: "VolumeDelete" - responses: - 204: - description: "The volume was removed" - 404: - description: "No such volume or volume driver" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "Volume is in use and cannot be removed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - required: true - description: "Volume name or ID" - type: "string" - - name: "force" - in: "query" - description: "Force the removal of the volume" - type: "boolean" - default: false - tags: ["Volume"] - - /volumes/prune: - post: - summary: "Delete unused volumes" - produces: - - "application/json" - operationId: "VolumePrune" - parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the prune list, encoded as JSON (a `map[string][]string`). - - Available filters: - - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune volumes with (or without, in case `label!=...` is used) the specified labels. - - `all` (`all=true`) - Consider all (local) volumes for pruning and not just anonymous volumes. - type: "string" - responses: - 200: - description: "No error" - schema: - type: "object" - title: "VolumePruneResponse" - properties: - VolumesDeleted: - description: "Volumes that were deleted" - type: "array" - items: - type: "string" - SpaceReclaimed: - description: "Disk space reclaimed in bytes" - type: "integer" - format: "int64" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Volume"] - /networks: - get: - summary: "List networks" - description: | - Returns a list of networks. For details on the format, see the - [network inspect endpoint](#operation/NetworkInspect). - - Note that it uses a different, smaller representation of a network than - inspecting a single network. For example, the list of containers attached - to the network is not propagated in API versions 1.28 and up. - operationId: "NetworkList" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - type: "array" - items: - $ref: "#/definitions/Network" - examples: - application/json: - - Name: "bridge" - Id: "f2de39df4171b0dc801e8002d1d999b77256983dfc63041c0f34030aa3977566" - Created: "2016-10-19T06:21:00.416543526Z" - Scope: "local" - Driver: "bridge" - EnableIPv4: true - EnableIPv6: false - Internal: false - Attachable: false - Ingress: false - IPAM: - Driver: "default" - Config: - - - Subnet: "172.17.0.0/16" - Options: - com.docker.network.bridge.default_bridge: "true" - com.docker.network.bridge.enable_icc: "true" - com.docker.network.bridge.enable_ip_masquerade: "true" - com.docker.network.bridge.host_binding_ipv4: "0.0.0.0" - com.docker.network.bridge.name: "docker0" - com.docker.network.driver.mtu: "1500" - - Name: "none" - Id: "e086a3893b05ab69242d3c44e49483a3bbbd3a26b46baa8f61ab797c1088d794" - Created: "0001-01-01T00:00:00Z" - Scope: "local" - Driver: "null" - EnableIPv4: false - EnableIPv6: false - Internal: false - Attachable: false - Ingress: false - IPAM: - Driver: "default" - Config: [] - Containers: {} - Options: {} - - Name: "host" - Id: "13e871235c677f196c4e1ecebb9dc733b9b2d2ab589e30c539efeda84a24215e" - Created: "0001-01-01T00:00:00Z" - Scope: "local" - Driver: "host" - EnableIPv4: false - EnableIPv6: false - Internal: false - Attachable: false - Ingress: false - IPAM: - Driver: "default" - Config: [] - Containers: {} - Options: {} - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - description: | - JSON encoded value of the filters (a `map[string][]string`) to process - on the networks list. - - Available filters: - - - `dangling=` When set to `true` (or `1`), returns all - networks that are not in use by a container. When set to `false` - (or `0`), only networks that are in use by one or more - containers are returned. - - `driver=` Matches a network's driver. - - `id=` Matches all or part of a network ID. - - `label=` or `label==` of a network label. - - `name=` Matches all or part of a network name. - - `scope=["swarm"|"global"|"local"]` Filters networks by scope (`swarm`, `global`, or `local`). - - `type=["custom"|"builtin"]` Filters networks by type. The `custom` keyword returns all user-defined networks. - type: "string" - tags: ["Network"] - - /networks/{id}: - get: - summary: "Inspect a network" - operationId: "NetworkInspect" - produces: - - "application/json" - responses: - 200: - description: "No error" - schema: - $ref: "#/definitions/Network" - 404: - description: "Network not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Network ID or name" - required: true - type: "string" - - name: "verbose" - in: "query" - description: "Detailed inspect output for troubleshooting" - type: "boolean" - default: false - - name: "scope" - in: "query" - description: "Filter the network by scope (swarm, global, or local)" - type: "string" - tags: ["Network"] - - delete: - summary: "Remove a network" - operationId: "NetworkDelete" - responses: - 204: - description: "No error" - 403: - description: "operation not supported for pre-defined networks" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such network" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Network ID or name" - required: true - type: "string" - tags: ["Network"] - - /networks/create: - post: - summary: "Create a network" - operationId: "NetworkCreate" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "Network created successfully" - schema: - $ref: "#/definitions/NetworkCreateResponse" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: | - Forbidden operation. This happens when trying to create a network named after a pre-defined network, - or when trying to create an overlay network on a daemon which is not part of a Swarm cluster. - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "plugin not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "networkConfig" - in: "body" - description: "Network configuration" - required: true - schema: - type: "object" - title: "NetworkCreateRequest" - required: ["Name"] - properties: - Name: - description: "The network's name." - type: "string" - example: "my_network" - Driver: - description: "Name of the network driver plugin to use." - type: "string" - default: "bridge" - example: "bridge" - Scope: - description: | - The level at which the network exists (e.g. `swarm` for cluster-wide - or `local` for machine level). - type: "string" - Internal: - description: "Restrict external access to the network." - type: "boolean" - Attachable: - description: | - Globally scoped network is manually attachable by regular - containers from workers in swarm mode. - type: "boolean" - example: true - Ingress: - description: | - Ingress network is the network which provides the routing-mesh - in swarm mode. - type: "boolean" - example: false - ConfigOnly: - description: | - Creates a config-only network. Config-only networks are placeholder - networks for network configurations to be used by other networks. - Config-only networks cannot be used directly to run containers - or services. - type: "boolean" - default: false - example: false - ConfigFrom: - description: | - Specifies the source which will provide the configuration for - this network. The specified network must be an existing - config-only network; see ConfigOnly. - $ref: "#/definitions/ConfigReference" - IPAM: - description: "Optional custom IP scheme for the network." - $ref: "#/definitions/IPAM" - EnableIPv4: - description: "Enable IPv4 on the network." - type: "boolean" - example: true - EnableIPv6: - description: "Enable IPv6 on the network." - type: "boolean" - example: true - Options: - description: "Network specific options to be used by the drivers." - type: "object" - additionalProperties: - type: "string" - example: - com.docker.network.bridge.default_bridge: "true" - com.docker.network.bridge.enable_icc: "true" - com.docker.network.bridge.enable_ip_masquerade: "true" - com.docker.network.bridge.host_binding_ipv4: "0.0.0.0" - com.docker.network.bridge.name: "docker0" - com.docker.network.driver.mtu: "1500" - Labels: - description: "User-defined key/value metadata." - type: "object" - additionalProperties: - type: "string" - example: - com.example.some-label: "some-value" - com.example.some-other-label: "some-other-value" - tags: ["Network"] - - /networks/{id}/connect: - post: - summary: "Connect a container to a network" - description: "The network must be either a local-scoped network or a swarm-scoped network with the `attachable` option set. A network cannot be re-attached to a running container" - operationId: "NetworkConnect" - consumes: - - "application/json" - responses: - 200: - description: "No error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "Operation forbidden" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "Network or container not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Network ID or name" - required: true - type: "string" - - name: "container" - in: "body" - required: true - schema: - type: "object" - title: "NetworkConnectRequest" - properties: - Container: - type: "string" - description: "The ID or name of the container to connect to the network." - EndpointConfig: - $ref: "#/definitions/EndpointSettings" - example: - Container: "3613f73ba0e4" - EndpointConfig: - IPAMConfig: - IPv4Address: "172.24.56.89" - IPv6Address: "2001:db8::5689" - MacAddress: "02:42:ac:12:05:02" - Priority: 100 - tags: ["Network"] - - /networks/{id}/disconnect: - post: - summary: "Disconnect a container from a network" - operationId: "NetworkDisconnect" - consumes: - - "application/json" - responses: - 200: - description: "No error" - 403: - description: "Operation not supported for swarm scoped networks" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "Network or container not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "Network ID or name" - required: true - type: "string" - - name: "container" - in: "body" - required: true - schema: - type: "object" - title: "NetworkDisconnectRequest" - properties: - Container: - type: "string" - description: | - The ID or name of the container to disconnect from the network. - Force: - type: "boolean" - description: | - Force the container to disconnect from the network. - tags: ["Network"] - /networks/prune: - post: - summary: "Delete unused networks" - produces: - - "application/json" - operationId: "NetworkPrune" - parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the prune list, encoded as JSON (a `map[string][]string`). - - Available filters: - - `until=` Prune networks created before this timestamp. The `` can be Unix timestamps, date formatted timestamps, or Go duration strings (e.g. `10m`, `1h30m`) computed relative to the daemon machine’s time. - - `label` (`label=`, `label==`, `label!=`, or `label!==`) Prune networks with (or without, in case `label!=...` is used) the specified labels. - type: "string" - responses: - 200: - description: "No error" - schema: - type: "object" - title: "NetworkPruneResponse" - properties: - NetworksDeleted: - description: "Networks that were deleted" - type: "array" - items: - type: "string" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Network"] - /plugins: - get: - summary: "List plugins" - operationId: "PluginList" - description: "Returns information about installed plugins." - produces: ["application/json"] - responses: - 200: - description: "No error" - schema: - type: "array" - items: - $ref: "#/definitions/Plugin" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the plugin list. - - Available filters: - - - `capability=` - - `enable=|` - tags: ["Plugin"] - - /plugins/privileges: - get: - summary: "Get plugin privileges" - operationId: "GetPluginPrivileges" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/PluginPrivilege" - example: - - Name: "network" - Description: "" - Value: - - "host" - - Name: "mount" - Description: "" - Value: - - "/data" - - Name: "device" - Description: "" - Value: - - "/dev/cpu_dma_latency" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "remote" - in: "query" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - tags: - - "Plugin" - - /plugins/pull: - post: - summary: "Install a plugin" - operationId: "PluginPull" - description: | - Pulls and installs a plugin. After the plugin is installed, it can be - enabled using the [`POST /plugins/{name}/enable` endpoint](#operation/PostPluginsEnable). - produces: - - "application/json" - responses: - 204: - description: "no error" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "remote" - in: "query" - description: | - Remote reference for plugin to install. - - The `:latest` tag is optional, and is used as the default if omitted. - required: true - type: "string" - - name: "name" - in: "query" - description: | - Local name for the pulled plugin. - - The `:latest` tag is optional, and is used as the default if omitted. - required: false - type: "string" - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration to use when pulling a plugin - from a registry. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - - name: "body" - in: "body" - schema: - type: "array" - items: - $ref: "#/definitions/PluginPrivilege" - example: - - Name: "network" - Description: "" - Value: - - "host" - - Name: "mount" - Description: "" - Value: - - "/data" - - Name: "device" - Description: "" - Value: - - "/dev/cpu_dma_latency" - tags: ["Plugin"] - /plugins/{name}/json: - get: - summary: "Inspect a plugin" - operationId: "PluginInspect" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Plugin" - 404: - description: "plugin is not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - tags: ["Plugin"] - /plugins/{name}: - delete: - summary: "Remove a plugin" - operationId: "PluginDelete" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Plugin" - 404: - description: "plugin is not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "force" - in: "query" - description: | - Disable the plugin before removing. This may result in issues if the - plugin is in use by a container. - type: "boolean" - default: false - tags: ["Plugin"] - /plugins/{name}/enable: - post: - summary: "Enable a plugin" - operationId: "PluginEnable" - responses: - 200: - description: "no error" - 404: - description: "plugin is not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "timeout" - in: "query" - description: "Set the HTTP client timeout (in seconds)" - type: "integer" - default: 0 - tags: ["Plugin"] - /plugins/{name}/disable: - post: - summary: "Disable a plugin" - operationId: "PluginDisable" - responses: - 200: - description: "no error" - 404: - description: "plugin is not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "force" - in: "query" - description: | - Force disable a plugin even if still in use. - required: false - type: "boolean" - tags: ["Plugin"] - /plugins/{name}/upgrade: - post: - summary: "Upgrade a plugin" - operationId: "PluginUpgrade" - responses: - 204: - description: "no error" - 404: - description: "plugin not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "remote" - in: "query" - description: | - Remote reference to upgrade to. - - The `:latest` tag is optional, and is used as the default if omitted. - required: true - type: "string" - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration to use when pulling a plugin - from a registry. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - - name: "body" - in: "body" - schema: - type: "array" - items: - $ref: "#/definitions/PluginPrivilege" - example: - - Name: "network" - Description: "" - Value: - - "host" - - Name: "mount" - Description: "" - Value: - - "/data" - - Name: "device" - Description: "" - Value: - - "/dev/cpu_dma_latency" - tags: ["Plugin"] - /plugins/create: - post: - summary: "Create a plugin" - operationId: "PluginCreate" - consumes: - - "application/x-tar" - responses: - 204: - description: "no error" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "query" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "tarContext" - in: "body" - description: "Path to tar containing plugin rootfs and manifest" - schema: - type: "string" - format: "binary" - tags: ["Plugin"] - /plugins/{name}/push: - post: - summary: "Push a plugin" - operationId: "PluginPush" - description: | - Push a plugin to the registry. - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - responses: - 200: - description: "no error" - 404: - description: "plugin not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Plugin"] - /plugins/{name}/set: - post: - summary: "Configure a plugin" - operationId: "PluginSet" - consumes: - - "application/json" - parameters: - - name: "name" - in: "path" - description: | - The name of the plugin. The `:latest` tag is optional, and is the - default if omitted. - required: true - type: "string" - - name: "body" - in: "body" - schema: - type: "array" - items: - type: "string" - example: ["DEBUG=1"] - responses: - 204: - description: "No error" - 404: - description: "Plugin not installed" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Plugin"] - /nodes: - get: - summary: "List nodes" - operationId: "NodeList" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/Node" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - description: | - Filters to process on the nodes list, encoded as JSON (a `map[string][]string`). - - Available filters: - - `id=` - - `label=` - - `membership=`(`accepted`|`pending`)` - - `name=` - - `node.label=` - - `role=`(`manager`|`worker`)` - type: "string" - tags: ["Node"] - /nodes/{id}: - get: - summary: "Inspect a node" - operationId: "NodeInspect" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Node" - 404: - description: "no such node" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "The ID or name of the node" - type: "string" - required: true - tags: ["Node"] - delete: - summary: "Delete a node" - operationId: "NodeDelete" - responses: - 200: - description: "no error" - 404: - description: "no such node" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "The ID or name of the node" - type: "string" - required: true - - name: "force" - in: "query" - description: "Force remove a node from the swarm" - default: false - type: "boolean" - tags: ["Node"] - /nodes/{id}/update: - post: - summary: "Update a node" - operationId: "NodeUpdate" - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such node" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "The ID of the node" - type: "string" - required: true - - name: "body" - in: "body" - schema: - $ref: "#/definitions/NodeSpec" - - name: "version" - in: "query" - description: | - The version number of the node object being updated. This is required - to avoid conflicting writes. - type: "integer" - format: "int64" - required: true - tags: ["Node"] - /swarm: - get: - summary: "Inspect swarm" - operationId: "SwarmInspect" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Swarm" - 404: - description: "no such swarm" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Swarm"] - /swarm/init: - post: - summary: "Initialize a new swarm" - operationId: "SwarmInit" - produces: - - "application/json" - - "text/plain" - responses: - 200: - description: "no error" - schema: - description: "The node ID" - type: "string" - example: "7v2t30z9blmxuhnyo6s4cpenp" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is already part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - required: true - schema: - type: "object" - title: "SwarmInitRequest" - properties: - ListenAddr: - description: | - Listen address used for inter-manager communication, as well - as determining the networking interface used for the VXLAN - Tunnel Endpoint (VTEP). This can either be an address/port - combination in the form `192.168.1.1:4567`, or an interface - followed by a port number, like `eth0:4567`. If the port number - is omitted, the default swarm listening port is used. - type: "string" - AdvertiseAddr: - description: | - Externally reachable address advertised to other nodes. This - can either be an address/port combination in the form - `192.168.1.1:4567`, or an interface followed by a port number, - like `eth0:4567`. If the port number is omitted, the port - number from the listen address is used. If `AdvertiseAddr` is - not specified, it will be automatically detected when possible. - type: "string" - DataPathAddr: - description: | - Address or interface to use for data path traffic (format: - ``), for example, `192.168.1.1`, or an interface, - like `eth0`. If `DataPathAddr` is unspecified, the same address - as `AdvertiseAddr` is used. - - The `DataPathAddr` specifies the address that global scope - network drivers will publish towards other nodes in order to - reach the containers running on this node. Using this parameter - it is possible to separate the container data traffic from the - management traffic of the cluster. - type: "string" - DataPathPort: - description: | - DataPathPort specifies the data path port number for data traffic. - Acceptable port range is 1024 to 49151. - if no port is set or is set to 0, default port 4789 will be used. - type: "integer" - format: "uint32" - DefaultAddrPool: - description: | - Default Address Pool specifies default subnet pools for global - scope networks. - type: "array" - items: - type: "string" - example: ["10.10.0.0/16", "20.20.0.0/16"] - ForceNewCluster: - description: "Force creation of a new swarm." - type: "boolean" - SubnetSize: - description: | - SubnetSize specifies the subnet size of the networks created - from the default subnet pool. - type: "integer" - format: "uint32" - Spec: - $ref: "#/definitions/SwarmSpec" - example: - ListenAddr: "0.0.0.0:2377" - AdvertiseAddr: "192.168.1.1:2377" - DataPathPort: 4789 - DefaultAddrPool: ["10.10.0.0/8", "20.20.0.0/8"] - SubnetSize: 24 - ForceNewCluster: false - Spec: - Orchestration: {} - Raft: {} - Dispatcher: {} - CAConfig: {} - EncryptionConfig: - AutoLockManagers: false - tags: ["Swarm"] - /swarm/join: - post: - summary: "Join an existing swarm" - operationId: "SwarmJoin" - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is already part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - required: true - schema: - type: "object" - title: "SwarmJoinRequest" - properties: - ListenAddr: - description: | - Listen address used for inter-manager communication if the node - gets promoted to manager, as well as determining the networking - interface used for the VXLAN Tunnel Endpoint (VTEP). - type: "string" - AdvertiseAddr: - description: | - Externally reachable address advertised to other nodes. This - can either be an address/port combination in the form - `192.168.1.1:4567`, or an interface followed by a port number, - like `eth0:4567`. If the port number is omitted, the port - number from the listen address is used. If `AdvertiseAddr` is - not specified, it will be automatically detected when possible. - type: "string" - DataPathAddr: - description: | - Address or interface to use for data path traffic (format: - ``), for example, `192.168.1.1`, or an interface, - like `eth0`. If `DataPathAddr` is unspecified, the same address - as `AdvertiseAddr` is used. - - The `DataPathAddr` specifies the address that global scope - network drivers will publish towards other nodes in order to - reach the containers running on this node. Using this parameter - it is possible to separate the container data traffic from the - management traffic of the cluster. - - type: "string" - RemoteAddrs: - description: | - Addresses of manager nodes already participating in the swarm. - type: "array" - items: - type: "string" - JoinToken: - description: "Secret token for joining this swarm." - type: "string" - example: - ListenAddr: "0.0.0.0:2377" - AdvertiseAddr: "192.168.1.1:2377" - DataPathAddr: "192.168.1.1" - RemoteAddrs: - - "node1:2377" - JoinToken: "SWMTKN-1-3pu6hszjas19xyp7ghgosyx9k8atbfcr8p2is99znpy26u2lkl-7p73s1dx5in4tatdymyhg9hu2" - tags: ["Swarm"] - /swarm/leave: - post: - summary: "Leave a swarm" - operationId: "SwarmLeave" - responses: - 200: - description: "no error" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "force" - description: | - Force leave swarm, even if this is the last manager or that it will - break the cluster. - in: "query" - type: "boolean" - default: false - tags: ["Swarm"] - /swarm/update: - post: - summary: "Update a swarm" - operationId: "SwarmUpdate" - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - required: true - schema: - $ref: "#/definitions/SwarmSpec" - - name: "version" - in: "query" - description: | - The version number of the swarm object being updated. This is - required to avoid conflicting writes. - type: "integer" - format: "int64" - required: true - - name: "rotateWorkerToken" - in: "query" - description: "Rotate the worker join token." - type: "boolean" - default: false - - name: "rotateManagerToken" - in: "query" - description: "Rotate the manager join token." - type: "boolean" - default: false - - name: "rotateManagerUnlockKey" - in: "query" - description: "Rotate the manager unlock key." - type: "boolean" - default: false - tags: ["Swarm"] - /swarm/unlockkey: - get: - summary: "Get the unlock key" - operationId: "SwarmUnlockkey" - consumes: - - "application/json" - responses: - 200: - description: "no error" - schema: - type: "object" - title: "UnlockKeyResponse" - properties: - UnlockKey: - description: "The swarm's unlock key." - type: "string" - example: - UnlockKey: "SWMKEY-1-7c37Cc8654o6p38HnroywCi19pllOnGtbdZEgtKxZu8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Swarm"] - /swarm/unlock: - post: - summary: "Unlock a locked manager" - operationId: "SwarmUnlock" - consumes: - - "application/json" - produces: - - "application/json" - parameters: - - name: "body" - in: "body" - required: true - schema: - type: "object" - title: "SwarmUnlockRequest" - properties: - UnlockKey: - description: "The swarm's unlock key." - type: "string" - example: - UnlockKey: "SWMKEY-1-7c37Cc8654o6p38HnroywCi19pllOnGtbdZEgtKxZu8" - responses: - 200: - description: "no error" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Swarm"] - /services: - get: - summary: "List services" - operationId: "ServiceList" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/Service" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the services list. - - Available filters: - - - `id=` - - `label=` - - `mode=["replicated"|"global"]` - - `name=` - - name: "status" - in: "query" - type: "boolean" - description: | - Include service status, with count of running and desired tasks. - tags: ["Service"] - /services/create: - post: - summary: "Create a service" - operationId: "ServiceCreate" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "no error" - schema: - $ref: "#/definitions/ServiceCreateResponse" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 403: - description: "network is not eligible for services" - schema: - $ref: "#/definitions/ErrorResponse" - 409: - description: "name conflicts with an existing service" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - required: true - schema: - allOf: - - $ref: "#/definitions/ServiceSpec" - - type: "object" - example: - Name: "web" - TaskTemplate: - ContainerSpec: - Image: "nginx:alpine" - Mounts: - - - ReadOnly: true - Source: "web-data" - Target: "/usr/share/nginx/html" - Type: "volume" - VolumeOptions: - DriverConfig: {} - Labels: - com.example.something: "something-value" - Hosts: ["10.10.10.10 host1", "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789 host2"] - User: "33" - DNSConfig: - Nameservers: ["8.8.8.8"] - Search: ["example.org"] - Options: ["timeout:3"] - Secrets: - - - File: - Name: "www.example.org.key" - UID: "33" - GID: "33" - Mode: 384 - SecretID: "fpjqlhnwb19zds35k8wn80lq9" - SecretName: "example_org_domain_key" - OomScoreAdj: 0 - LogDriver: - Name: "json-file" - Options: - max-file: "3" - max-size: "10M" - Placement: {} - Resources: - Limits: - MemoryBytes: 104857600 - Reservations: {} - RestartPolicy: - Condition: "on-failure" - Delay: 10000000000 - MaxAttempts: 10 - Mode: - Replicated: - Replicas: 4 - UpdateConfig: - Parallelism: 2 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - RollbackConfig: - Parallelism: 1 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - EndpointSpec: - Ports: - - - Protocol: "tcp" - PublishedPort: 8080 - TargetPort: 80 - Labels: - foo: "bar" - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration for pulling from private - registries. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - tags: ["Service"] - /services/{id}: - get: - summary: "Inspect a service" - operationId: "ServiceInspect" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Service" - 404: - description: "no such service" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "ID or name of service." - required: true - type: "string" - - name: "insertDefaults" - in: "query" - description: "Fill empty fields with default values." - type: "boolean" - default: false - tags: ["Service"] - delete: - summary: "Delete a service" - operationId: "ServiceDelete" - responses: - 200: - description: "no error" - 404: - description: "no such service" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "ID or name of service." - required: true - type: "string" - tags: ["Service"] - /services/{id}/update: - post: - summary: "Update a service" - operationId: "ServiceUpdate" - consumes: ["application/json"] - produces: ["application/json"] - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/ServiceUpdateResponse" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such service" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "ID or name of service." - required: true - type: "string" - - name: "body" - in: "body" - required: true - schema: - allOf: - - $ref: "#/definitions/ServiceSpec" - - type: "object" - example: - Name: "top" - TaskTemplate: - ContainerSpec: - Image: "busybox" - Args: - - "top" - OomScoreAdj: 0 - Resources: - Limits: {} - Reservations: {} - RestartPolicy: - Condition: "any" - MaxAttempts: 0 - Placement: {} - ForceUpdate: 0 - Mode: - Replicated: - Replicas: 1 - UpdateConfig: - Parallelism: 2 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - RollbackConfig: - Parallelism: 1 - Delay: 1000000000 - FailureAction: "pause" - Monitor: 15000000000 - MaxFailureRatio: 0.15 - EndpointSpec: - Mode: "vip" - - - name: "version" - in: "query" - description: | - The version number of the service object being updated. This is - required to avoid conflicting writes. - This version number should be the value as currently set on the - service *before* the update. You can find the current version by - calling `GET /services/{id}` - required: true - type: "integer" - - name: "registryAuthFrom" - in: "query" - description: | - If the `X-Registry-Auth` header is not specified, this parameter - indicates where to find registry authorization credentials. - type: "string" - enum: ["spec", "previous-spec"] - default: "spec" - - name: "rollback" - in: "query" - description: | - Set to this parameter to `previous` to cause a server-side rollback - to the previous service spec. The supplied spec will be ignored in - this case. - type: "string" - - name: "X-Registry-Auth" - in: "header" - description: | - A base64url-encoded auth configuration for pulling from private - registries. - - Refer to the [authentication section](#section/Authentication) for - details. - type: "string" - - tags: ["Service"] - /services/{id}/logs: - get: - summary: "Get service logs" - description: | - Get `stdout` and `stderr` logs from a service. See also - [`/containers/{id}/logs`](#operation/ContainerLogs). - - **Note**: This endpoint works only for services with the `local`, - `json-file` or `journald` logging drivers. - produces: - - "application/vnd.docker.raw-stream" - - "application/vnd.docker.multiplexed-stream" - operationId: "ServiceLogs" - responses: - 200: - description: "logs returned as a stream in response body" - schema: - type: "string" - format: "binary" - 404: - description: "no such service" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such service: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID or name of the service" - type: "string" - - name: "details" - in: "query" - description: "Show service context and extra details provided to logs." - type: "boolean" - default: false - - name: "follow" - in: "query" - description: "Keep connection after returning logs." - type: "boolean" - default: false - - name: "stdout" - in: "query" - description: "Return logs from `stdout`" - type: "boolean" - default: false - - name: "stderr" - in: "query" - description: "Return logs from `stderr`" - type: "boolean" - default: false - - name: "since" - in: "query" - description: "Only return logs since this time, as a UNIX timestamp" - type: "integer" - default: 0 - - name: "timestamps" - in: "query" - description: "Add timestamps to every log line" - type: "boolean" - default: false - - name: "tail" - in: "query" - description: | - Only return this number of log lines from the end of the logs. - Specify as an integer or `all` to output all log lines. - type: "string" - default: "all" - tags: ["Service"] - /tasks: - get: - summary: "List tasks" - operationId: "TaskList" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/Task" - example: - - ID: "0kzzo1i0y4jz6027t0k7aezc7" - Version: - Index: 71 - CreatedAt: "2016-06-07T21:07:31.171892745Z" - UpdatedAt: "2016-06-07T21:07:31.376370513Z" - Spec: - ContainerSpec: - Image: "redis" - Resources: - Limits: {} - Reservations: {} - RestartPolicy: - Condition: "any" - MaxAttempts: 0 - Placement: {} - ServiceID: "9mnpnzenvg8p8tdbtq4wvbkcz" - Slot: 1 - NodeID: "60gvrl6tm78dmak4yl7srz94v" - Status: - Timestamp: "2016-06-07T21:07:31.290032978Z" - State: "running" - Message: "started" - ContainerStatus: - ContainerID: "e5d62702a1b48d01c3e02ca1e0212a250801fa8d67caca0b6f35919ebc12f035" - PID: 677 - DesiredState: "running" - NetworksAttachments: - - Network: - ID: "4qvuz4ko70xaltuqbt8956gd1" - Version: - Index: 18 - CreatedAt: "2016-06-07T20:31:11.912919752Z" - UpdatedAt: "2016-06-07T21:07:29.955277358Z" - Spec: - Name: "ingress" - Labels: - com.docker.swarm.internal: "true" - DriverConfiguration: {} - IPAMOptions: - Driver: {} - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - DriverState: - Name: "overlay" - Options: - com.docker.network.driver.overlay.vxlanid_list: "256" - IPAMOptions: - Driver: - Name: "default" - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - Addresses: - - "10.255.0.10/16" - - ID: "1yljwbmlr8er2waf8orvqpwms" - Version: - Index: 30 - CreatedAt: "2016-06-07T21:07:30.019104782Z" - UpdatedAt: "2016-06-07T21:07:30.231958098Z" - Name: "hopeful_cori" - Spec: - ContainerSpec: - Image: "redis" - Resources: - Limits: {} - Reservations: {} - RestartPolicy: - Condition: "any" - MaxAttempts: 0 - Placement: {} - ServiceID: "9mnpnzenvg8p8tdbtq4wvbkcz" - Slot: 1 - NodeID: "60gvrl6tm78dmak4yl7srz94v" - Status: - Timestamp: "2016-06-07T21:07:30.202183143Z" - State: "shutdown" - Message: "shutdown" - ContainerStatus: - ContainerID: "1cf8d63d18e79668b0004a4be4c6ee58cddfad2dae29506d8781581d0688a213" - DesiredState: "shutdown" - NetworksAttachments: - - Network: - ID: "4qvuz4ko70xaltuqbt8956gd1" - Version: - Index: 18 - CreatedAt: "2016-06-07T20:31:11.912919752Z" - UpdatedAt: "2016-06-07T21:07:29.955277358Z" - Spec: - Name: "ingress" - Labels: - com.docker.swarm.internal: "true" - DriverConfiguration: {} - IPAMOptions: - Driver: {} - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - DriverState: - Name: "overlay" - Options: - com.docker.network.driver.overlay.vxlanid_list: "256" - IPAMOptions: - Driver: - Name: "default" - Configs: - - Subnet: "10.255.0.0/16" - Gateway: "10.255.0.1" - Addresses: - - "10.255.0.5/16" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the tasks list. - - Available filters: - - - `desired-state=(running | shutdown | accepted)` - - `id=` - - `label=key` or `label="key=value"` - - `name=` - - `node=` - - `service=` - tags: ["Task"] - /tasks/{id}: - get: - summary: "Inspect a task" - operationId: "TaskInspect" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Task" - 404: - description: "no such task" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "ID of the task" - required: true - type: "string" - tags: ["Task"] - /tasks/{id}/logs: - get: - summary: "Get task logs" - description: | - Get `stdout` and `stderr` logs from a task. - See also [`/containers/{id}/logs`](#operation/ContainerLogs). - - **Note**: This endpoint works only for services with the `local`, - `json-file` or `journald` logging drivers. - operationId: "TaskLogs" - produces: - - "application/vnd.docker.raw-stream" - - "application/vnd.docker.multiplexed-stream" - responses: - 200: - description: "logs returned as a stream in response body" - schema: - type: "string" - format: "binary" - 404: - description: "no such task" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such task: c2ada9df5af8" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - description: "ID of the task" - type: "string" - - name: "details" - in: "query" - description: "Show task context and extra details provided to logs." - type: "boolean" - default: false - - name: "follow" - in: "query" - description: "Keep connection after returning logs." - type: "boolean" - default: false - - name: "stdout" - in: "query" - description: "Return logs from `stdout`" - type: "boolean" - default: false - - name: "stderr" - in: "query" - description: "Return logs from `stderr`" - type: "boolean" - default: false - - name: "since" - in: "query" - description: "Only return logs since this time, as a UNIX timestamp" - type: "integer" - default: 0 - - name: "timestamps" - in: "query" - description: "Add timestamps to every log line" - type: "boolean" - default: false - - name: "tail" - in: "query" - description: | - Only return this number of log lines from the end of the logs. - Specify as an integer or `all` to output all log lines. - type: "string" - default: "all" - tags: ["Task"] - /secrets: - get: - summary: "List secrets" - operationId: "SecretList" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/Secret" - example: - - ID: "blt1owaxmitz71s9v5zh81zun" - Version: - Index: 85 - CreatedAt: "2017-07-20T13:55:28.678958722Z" - UpdatedAt: "2017-07-20T13:55:28.678958722Z" - Spec: - Name: "mysql-passwd" - Labels: - some.label: "some.value" - Driver: - Name: "secret-bucket" - Options: - OptionA: "value for driver option A" - OptionB: "value for driver option B" - - ID: "ktnbjxoalbkvbvedmg1urrz8h" - Version: - Index: 11 - CreatedAt: "2016-11-05T01:20:17.327670065Z" - UpdatedAt: "2016-11-05T01:20:17.327670065Z" - Spec: - Name: "app-dev.crt" - Labels: - foo: "bar" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the secrets list. - - Available filters: - - - `id=` - - `label= or label==value` - - `name=` - - `names=` - tags: ["Secret"] - /secrets/create: - post: - summary: "Create a secret" - operationId: "SecretCreate" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "no error" - schema: - $ref: "#/definitions/IDResponse" - 409: - description: "name conflicts with an existing object" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - schema: - allOf: - - $ref: "#/definitions/SecretSpec" - - type: "object" - example: - Name: "app-key.crt" - Labels: - foo: "bar" - Data: "VEhJUyBJUyBOT1QgQSBSRUFMIENFUlRJRklDQVRFCg==" - Driver: - Name: "secret-bucket" - Options: - OptionA: "value for driver option A" - OptionB: "value for driver option B" - tags: ["Secret"] - /secrets/{id}: - get: - summary: "Inspect a secret" - operationId: "SecretInspect" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Secret" - examples: - application/json: - ID: "ktnbjxoalbkvbvedmg1urrz8h" - Version: - Index: 11 - CreatedAt: "2016-11-05T01:20:17.327670065Z" - UpdatedAt: "2016-11-05T01:20:17.327670065Z" - Spec: - Name: "app-dev.crt" - Labels: - foo: "bar" - Driver: - Name: "secret-bucket" - Options: - OptionA: "value for driver option A" - OptionB: "value for driver option B" - - 404: - description: "secret not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - type: "string" - description: "ID of the secret" - tags: ["Secret"] - delete: - summary: "Delete a secret" - operationId: "SecretDelete" - produces: - - "application/json" - responses: - 204: - description: "no error" - 404: - description: "secret not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - type: "string" - description: "ID of the secret" - tags: ["Secret"] - /secrets/{id}/update: - post: - summary: "Update a Secret" - operationId: "SecretUpdate" - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such secret" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "The ID or name of the secret" - type: "string" - required: true - - name: "body" - in: "body" - schema: - $ref: "#/definitions/SecretSpec" - description: | - The spec of the secret to update. Currently, only the Labels field - can be updated. All other fields must remain unchanged from the - [SecretInspect endpoint](#operation/SecretInspect) response values. - - name: "version" - in: "query" - description: | - The version number of the secret object being updated. This is - required to avoid conflicting writes. - type: "integer" - format: "int64" - required: true - tags: ["Secret"] - /configs: - get: - summary: "List configs" - operationId: "ConfigList" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - type: "array" - items: - $ref: "#/definitions/Config" - example: - - ID: "ktnbjxoalbkvbvedmg1urrz8h" - Version: - Index: 11 - CreatedAt: "2016-11-05T01:20:17.327670065Z" - UpdatedAt: "2016-11-05T01:20:17.327670065Z" - Spec: - Name: "server.conf" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "filters" - in: "query" - type: "string" - description: | - A JSON encoded value of the filters (a `map[string][]string`) to - process on the configs list. - - Available filters: - - - `id=` - - `label= or label==value` - - `name=` - - `names=` - tags: ["Config"] - /configs/create: - post: - summary: "Create a config" - operationId: "ConfigCreate" - consumes: - - "application/json" - produces: - - "application/json" - responses: - 201: - description: "no error" - schema: - $ref: "#/definitions/IDResponse" - 409: - description: "name conflicts with an existing object" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "body" - in: "body" - schema: - allOf: - - $ref: "#/definitions/ConfigSpec" - - type: "object" - example: - Name: "server.conf" - Labels: - foo: "bar" - Data: "VEhJUyBJUyBOT1QgQSBSRUFMIENFUlRJRklDQVRFCg==" - tags: ["Config"] - /configs/{id}: - get: - summary: "Inspect a config" - operationId: "ConfigInspect" - produces: - - "application/json" - responses: - 200: - description: "no error" - schema: - $ref: "#/definitions/Config" - examples: - application/json: - ID: "ktnbjxoalbkvbvedmg1urrz8h" - Version: - Index: 11 - CreatedAt: "2016-11-05T01:20:17.327670065Z" - UpdatedAt: "2016-11-05T01:20:17.327670065Z" - Spec: - Name: "app-dev.crt" - 404: - description: "config not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - type: "string" - description: "ID of the config" - tags: ["Config"] - delete: - summary: "Delete a config" - operationId: "ConfigDelete" - produces: - - "application/json" - responses: - 204: - description: "no error" - 404: - description: "config not found" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - required: true - type: "string" - description: "ID of the config" - tags: ["Config"] - /configs/{id}/update: - post: - summary: "Update a Config" - operationId: "ConfigUpdate" - responses: - 200: - description: "no error" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 404: - description: "no such config" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - 503: - description: "node is not part of a swarm" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "id" - in: "path" - description: "The ID or name of the config" - type: "string" - required: true - - name: "body" - in: "body" - schema: - $ref: "#/definitions/ConfigSpec" - description: | - The spec of the config to update. Currently, only the Labels field - can be updated. All other fields must remain unchanged from the - [ConfigInspect endpoint](#operation/ConfigInspect) response values. - - name: "version" - in: "query" - description: | - The version number of the config object being updated. This is - required to avoid conflicting writes. - type: "integer" - format: "int64" - required: true - tags: ["Config"] - /distribution/{name}/json: - get: - summary: "Get image information from the registry" - description: | - Return image digest and platform information by contacting the registry. - operationId: "DistributionInspect" - produces: - - "application/json" - responses: - 200: - description: "descriptor and platform information" - schema: - $ref: "#/definitions/DistributionInspect" - 401: - description: "Failed authentication or no image found" - schema: - $ref: "#/definitions/ErrorResponse" - examples: - application/json: - message: "No such image: someimage (tag: latest)" - 500: - description: "Server error" - schema: - $ref: "#/definitions/ErrorResponse" - parameters: - - name: "name" - in: "path" - description: "Image name or id" - type: "string" - required: true - tags: ["Distribution"] - /session: - post: - summary: "Initialize interactive session" - description: | - Start a new interactive session with a server. Session allows server to - call back to the client for advanced capabilities. - - ### Hijacking - - This endpoint hijacks the HTTP connection to HTTP2 transport that allows - the client to expose gPRC services on that connection. - - For example, the client sends this request to upgrade the connection: - - ``` - POST /session HTTP/1.1 - Upgrade: h2c - Connection: Upgrade - ``` - - The Docker daemon responds with a `101 UPGRADED` response follow with - the raw stream: - - ``` - HTTP/1.1 101 UPGRADED - Connection: Upgrade - Upgrade: h2c - ``` - operationId: "Session" - produces: - - "application/vnd.docker.raw-stream" - responses: - 101: - description: "no error, hijacking successful" - 400: - description: "bad parameter" - schema: - $ref: "#/definitions/ErrorResponse" - 500: - description: "server error" - schema: - $ref: "#/definitions/ErrorResponse" - tags: ["Session"] diff --git a/vendor/github.com/docker/docker/api/types/build/build.go b/vendor/github.com/docker/docker/api/types/build/build.go deleted file mode 100644 index c43a0e21ea74..000000000000 --- a/vendor/github.com/docker/docker/api/types/build/build.go +++ /dev/null @@ -1,91 +0,0 @@ -package build - -import ( - "io" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/registry" -) - -// BuilderVersion sets the version of underlying builder to use -type BuilderVersion string - -const ( - // BuilderV1 is the first generation builder in docker daemon - BuilderV1 BuilderVersion = "1" - // BuilderBuildKit is builder based on moby/buildkit project - BuilderBuildKit BuilderVersion = "2" -) - -// Result contains the image id of a successful build. -type Result struct { - ID string -} - -// ImageBuildOptions holds the information -// necessary to build images. -type ImageBuildOptions struct { - Tags []string - SuppressOutput bool - RemoteContext string - NoCache bool - Remove bool - ForceRemove bool - PullParent bool - Isolation container.Isolation - CPUSetCPUs string - CPUSetMems string - CPUShares int64 - CPUQuota int64 - CPUPeriod int64 - Memory int64 - MemorySwap int64 - CgroupParent string - NetworkMode string - ShmSize int64 - Dockerfile string - Ulimits []*container.Ulimit - // BuildArgs needs to be a *string instead of just a string so that - // we can tell the difference between "" (empty string) and no value - // at all (nil). See the parsing of buildArgs in - // api/server/router/build/build_routes.go for even more info. - BuildArgs map[string]*string - AuthConfigs map[string]registry.AuthConfig - Context io.Reader - Labels map[string]string - // squash the resulting image's layers to the parent - // preserves the original image and creates a new one from the parent with all - // the changes applied to a single layer - Squash bool - // CacheFrom specifies images that are used for matching cache. Images - // specified here do not need to have a valid parent chain to match cache. - CacheFrom []string - SecurityOpt []string - ExtraHosts []string // List of extra hosts - Target string - SessionID string - Platform string - // Version specifies the version of the underlying builder to use - Version BuilderVersion - // BuildID is an optional identifier that can be passed together with the - // build request. The same identifier can be used to gracefully cancel the - // build with the cancel request. - BuildID string - // Outputs defines configurations for exporting build results. Only supported - // in BuildKit mode - Outputs []ImageBuildOutput -} - -// ImageBuildOutput defines configuration for exporting a build result -type ImageBuildOutput struct { - Type string - Attrs map[string]string -} - -// ImageBuildResponse holds information -// returned by a server after building -// an image. -type ImageBuildResponse struct { - Body io.ReadCloser - OSType string -} diff --git a/vendor/github.com/docker/docker/api/types/build/cache.go b/vendor/github.com/docker/docker/api/types/build/cache.go deleted file mode 100644 index 42c840457364..000000000000 --- a/vendor/github.com/docker/docker/api/types/build/cache.go +++ /dev/null @@ -1,52 +0,0 @@ -package build - -import ( - "time" - - "github.com/docker/docker/api/types/filters" -) - -// CacheRecord contains information about a build cache record. -type CacheRecord struct { - // ID is the unique ID of the build cache record. - ID string - // Parent is the ID of the parent build cache record. - // - // Deprecated: deprecated in API v1.42 and up, as it was deprecated in BuildKit; use Parents instead. - Parent string `json:"Parent,omitempty"` - // Parents is the list of parent build cache record IDs. - Parents []string `json:" Parents,omitempty"` - // Type is the cache record type. - Type string - // Description is a description of the build-step that produced the build cache. - Description string - // InUse indicates if the build cache is in use. - InUse bool - // Shared indicates if the build cache is shared. - Shared bool - // Size is the amount of disk space used by the build cache (in bytes). - Size int64 - // CreatedAt is the date and time at which the build cache was created. - CreatedAt time.Time - // LastUsedAt is the date and time at which the build cache was last used. - LastUsedAt *time.Time - UsageCount int -} - -// CachePruneOptions hold parameters to prune the build cache. -type CachePruneOptions struct { - All bool - ReservedSpace int64 - MaxUsedSpace int64 - MinFreeSpace int64 - Filters filters.Args - - KeepStorage int64 // Deprecated: deprecated in API 1.48. -} - -// CachePruneReport contains the response for Engine API: -// POST "/build/prune" -type CachePruneReport struct { - CachesDeleted []string - SpaceReclaimed uint64 -} diff --git a/vendor/github.com/docker/docker/api/types/build/disk_usage.go b/vendor/github.com/docker/docker/api/types/build/disk_usage.go deleted file mode 100644 index e969b6d615f2..000000000000 --- a/vendor/github.com/docker/docker/api/types/build/disk_usage.go +++ /dev/null @@ -1,8 +0,0 @@ -package build - -// CacheDiskUsage contains disk usage for the build cache. -type CacheDiskUsage struct { - TotalSize int64 - Reclaimable int64 - Items []*CacheRecord -} diff --git a/vendor/github.com/docker/docker/api/types/checkpoint/options.go b/vendor/github.com/docker/docker/api/types/checkpoint/options.go deleted file mode 100644 index 9477458c241e..000000000000 --- a/vendor/github.com/docker/docker/api/types/checkpoint/options.go +++ /dev/null @@ -1,19 +0,0 @@ -package checkpoint - -// CreateOptions holds parameters to create a checkpoint from a container. -type CreateOptions struct { - CheckpointID string - CheckpointDir string - Exit bool -} - -// ListOptions holds parameters to list checkpoints for a container. -type ListOptions struct { - CheckpointDir string -} - -// DeleteOptions holds parameters to delete a checkpoint from a container. -type DeleteOptions struct { - CheckpointID string - CheckpointDir string -} diff --git a/vendor/github.com/docker/docker/api/types/client.go b/vendor/github.com/docker/docker/api/types/client.go deleted file mode 100644 index 42fe03ecca89..000000000000 --- a/vendor/github.com/docker/docker/api/types/client.go +++ /dev/null @@ -1,85 +0,0 @@ -package types - -import ( - "bufio" - "context" - "net" -) - -// NewHijackedResponse initializes a [HijackedResponse] type. -func NewHijackedResponse(conn net.Conn, mediaType string) HijackedResponse { - return HijackedResponse{Conn: conn, Reader: bufio.NewReader(conn), mediaType: mediaType} -} - -// HijackedResponse holds connection information for a hijacked request. -type HijackedResponse struct { - mediaType string - Conn net.Conn - Reader *bufio.Reader -} - -// Close closes the hijacked connection and reader. -func (h *HijackedResponse) Close() { - h.Conn.Close() -} - -// MediaType let client know if HijackedResponse hold a raw or multiplexed stream. -// returns false if HTTP Content-Type is not relevant, and container must be inspected -func (h *HijackedResponse) MediaType() (string, bool) { - if h.mediaType == "" { - return "", false - } - return h.mediaType, true -} - -// CloseWriter is an interface that implements structs -// that close input streams to prevent from writing. -type CloseWriter interface { - CloseWrite() error -} - -// CloseWrite closes a readWriter for writing. -func (h *HijackedResponse) CloseWrite() error { - if conn, ok := h.Conn.(CloseWriter); ok { - return conn.CloseWrite() - } - return nil -} - -// PluginRemoveOptions holds parameters to remove plugins. -type PluginRemoveOptions struct { - Force bool -} - -// PluginEnableOptions holds parameters to enable plugins. -type PluginEnableOptions struct { - Timeout int -} - -// PluginDisableOptions holds parameters to disable plugins. -type PluginDisableOptions struct { - Force bool -} - -// PluginInstallOptions holds parameters to install a plugin. -type PluginInstallOptions struct { - Disabled bool - AcceptAllPermissions bool - RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry - RemoteRef string // RemoteRef is the plugin name on the registry - - // PrivilegeFunc is a function that clients can supply to retry operations - // after getting an authorization error. This function returns the registry - // authentication header value in base64 encoded format, or an error if the - // privilege request fails. - // - // For details, refer to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. - PrivilegeFunc func(context.Context) (string, error) - AcceptPermissionsFunc func(context.Context, PluginPrivileges) (bool, error) - Args []string -} - -// PluginCreateOptions hold all options to plugin create. -type PluginCreateOptions struct { - RepoName string -} diff --git a/vendor/github.com/docker/docker/api/types/container/config.go b/vendor/github.com/docker/docker/api/types/container/config.go deleted file mode 100644 index 0555416540bf..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/config.go +++ /dev/null @@ -1,73 +0,0 @@ -package container - -import ( - "time" - - "github.com/docker/docker/api/types/strslice" - "github.com/docker/go-connections/nat" - dockerspec "github.com/moby/docker-image-spec/specs-go/v1" -) - -// MinimumDuration puts a minimum on user configured duration. -// This is to prevent API error on time unit. For example, API may -// set 3 as healthcheck interval with intention of 3 seconds, but -// Docker interprets it as 3 nanoseconds. -const MinimumDuration = 1 * time.Millisecond - -// StopOptions holds the options to stop or restart a container. -type StopOptions struct { - // Signal (optional) is the signal to send to the container to (gracefully) - // stop it before forcibly terminating the container with SIGKILL after the - // timeout expires. If not value is set, the default (SIGTERM) is used. - Signal string `json:",omitempty"` - - // Timeout (optional) is the timeout (in seconds) to wait for the container - // to stop gracefully before forcibly terminating it with SIGKILL. - // - // - Use nil to use the default timeout (10 seconds). - // - Use '-1' to wait indefinitely. - // - Use '0' to not wait for the container to exit gracefully, and - // immediately proceeds to forcibly terminating the container. - // - Other positive values are used as timeout (in seconds). - Timeout *int `json:",omitempty"` -} - -// HealthConfig holds configuration settings for the HEALTHCHECK feature. -type HealthConfig = dockerspec.HealthcheckConfig - -// Config contains the configuration data about a container. -// It should hold only portable information about the container. -// Here, "portable" means "independent from the host we are running on". -// Non-portable information *should* appear in HostConfig. -// All fields added to this struct must be marked `omitempty` to keep getting -// predictable hashes from the old `v1Compatibility` configuration. -type Config struct { - Hostname string // Hostname - Domainname string // Domainname - User string // User that will run the command(s) inside the container, also support user:group - AttachStdin bool // Attach the standard input, makes possible user interaction - AttachStdout bool // Attach the standard output - AttachStderr bool // Attach the standard error - ExposedPorts nat.PortSet `json:",omitempty"` // List of exposed ports - Tty bool // Attach standard streams to a tty, including stdin if it is not closed. - OpenStdin bool // Open stdin - StdinOnce bool // If true, close stdin after the 1 attached client disconnects. - Env []string // List of environment variable to set in the container - Cmd strslice.StrSlice // Command to run when starting the container - Healthcheck *HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy - ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (meaning treat as a command line) (Windows specific). - Image string // Name of the image as it was passed by the operator (e.g. could be symbolic) - Volumes map[string]struct{} // List of volumes (mounts) used for the container - WorkingDir string // Current directory (PWD) in the command will be launched - Entrypoint strslice.StrSlice // Entrypoint to run when starting the container - NetworkDisabled bool `json:",omitempty"` // Is network disabled - // Mac Address of the container. - // - // Deprecated: this field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead. - MacAddress string `json:",omitempty"` - OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile - Labels map[string]string // List of labels set to this container - StopSignal string `json:",omitempty"` // Signal to stop a container - StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container - Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT -} diff --git a/vendor/github.com/docker/docker/api/types/container/container.go b/vendor/github.com/docker/docker/api/types/container/container.go deleted file mode 100644 index a191ca8bdb73..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/container.go +++ /dev/null @@ -1,188 +0,0 @@ -package container - -import ( - "io" - "os" - "time" - - "github.com/docker/docker/api/types/mount" - "github.com/docker/docker/api/types/storage" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ContainerUpdateOKBody OK response to ContainerUpdate operation -// -// Deprecated: use [UpdateResponse]. This alias will be removed in the next release. -type ContainerUpdateOKBody = UpdateResponse - -// ContainerTopOKBody OK response to ContainerTop operation -// -// Deprecated: use [TopResponse]. This alias will be removed in the next release. -type ContainerTopOKBody = TopResponse - -// PruneReport contains the response for Engine API: -// POST "/containers/prune" -type PruneReport struct { - ContainersDeleted []string - SpaceReclaimed uint64 -} - -// PathStat is used to encode the header from -// GET "/containers/{name:.*}/archive" -// "Name" is the file or directory name. -type PathStat struct { - Name string `json:"name"` - Size int64 `json:"size"` - Mode os.FileMode `json:"mode"` - Mtime time.Time `json:"mtime"` - LinkTarget string `json:"linkTarget"` -} - -// CopyToContainerOptions holds information -// about files to copy into a container -type CopyToContainerOptions struct { - AllowOverwriteDirWithFile bool - CopyUIDGID bool -} - -// StatsResponseReader wraps an io.ReadCloser to read (a stream of) stats -// for a container, as produced by the GET "/stats" endpoint. -// -// The OSType field is set to the server's platform to allow -// platform-specific handling of the response. -// -// TODO(thaJeztah): remove this wrapper, and make OSType part of [StatsResponse]. -type StatsResponseReader struct { - Body io.ReadCloser `json:"body"` - OSType string `json:"ostype"` -} - -// MountPoint represents a mount point configuration inside the container. -// This is used for reporting the mountpoints in use by a container. -type MountPoint struct { - // Type is the type of mount, see `Type` definitions in - // github.com/docker/docker/api/types/mount.Type - Type mount.Type `json:",omitempty"` - - // Name is the name reference to the underlying data defined by `Source` - // e.g., the volume name. - Name string `json:",omitempty"` - - // Source is the source location of the mount. - // - // For volumes, this contains the storage location of the volume (within - // `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains - // the source (host) part of the bind-mount. For `tmpfs` mount points, this - // field is empty. - Source string - - // Destination is the path relative to the container root (`/`) where the - // Source is mounted inside the container. - Destination string - - // Driver is the volume driver used to create the volume (if it is a volume). - Driver string `json:",omitempty"` - - // Mode is a comma separated list of options supplied by the user when - // creating the bind/volume mount. - // - // The default is platform-specific (`"z"` on Linux, empty on Windows). - Mode string - - // RW indicates whether the mount is mounted writable (read-write). - RW bool - - // Propagation describes how mounts are propagated from the host into the - // mount point, and vice-versa. Refer to the Linux kernel documentation - // for details: - // https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt - // - // This field is not used on Windows. - Propagation mount.Propagation -} - -// State stores container's running state -// it's part of ContainerJSONBase and returned by "inspect" command -type State struct { - Status ContainerState // String representation of the container state. Can be one of "created", "running", "paused", "restarting", "removing", "exited", or "dead" - Running bool - Paused bool - Restarting bool - OOMKilled bool - Dead bool - Pid int - ExitCode int - Error string - StartedAt string - FinishedAt string - Health *Health `json:",omitempty"` -} - -// Summary contains response of Engine API: -// GET "/containers/json" -type Summary struct { - ID string `json:"Id"` - Names []string - Image string - ImageID string - ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"` - Command string - Created int64 - Ports []Port - SizeRw int64 `json:",omitempty"` - SizeRootFs int64 `json:",omitempty"` - Labels map[string]string - State ContainerState - Status string - HostConfig struct { - NetworkMode string `json:",omitempty"` - Annotations map[string]string `json:",omitempty"` - } - NetworkSettings *NetworkSettingsSummary - Mounts []MountPoint -} - -// ContainerJSONBase contains response of Engine API GET "/containers/{name:.*}/json" -// for API version 1.18 and older. -// -// TODO(thaJeztah): combine ContainerJSONBase and InspectResponse into a single struct. -// The split between ContainerJSONBase (ContainerJSONBase) and InspectResponse (InspectResponse) -// was done in commit 6deaa58ba5f051039643cedceee97c8695e2af74 (https://github.com/moby/moby/pull/13675). -// ContainerJSONBase contained all fields for API < 1.19, and InspectResponse -// held fields that were added in API 1.19 and up. Given that the minimum -// supported API version is now 1.24, we no longer use the separate type. -type ContainerJSONBase struct { - ID string `json:"Id"` - Created string - Path string - Args []string - State *State - Image string - ResolvConfPath string - HostnamePath string - HostsPath string - LogPath string - Name string - RestartCount int - Driver string - Platform string - MountLabel string - ProcessLabel string - AppArmorProfile string - ExecIDs []string - HostConfig *HostConfig - GraphDriver storage.DriverData - SizeRw *int64 `json:",omitempty"` - SizeRootFs *int64 `json:",omitempty"` -} - -// InspectResponse is the response for the GET "/containers/{name:.*}/json" -// endpoint. -type InspectResponse struct { - *ContainerJSONBase - Mounts []MountPoint - Config *Config - NetworkSettings *NetworkSettings - // ImageManifestDescriptor is the descriptor of a platform-specific manifest of the image used to create the container. - ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/container/create_request.go b/vendor/github.com/docker/docker/api/types/container/create_request.go deleted file mode 100644 index e98dd6ad449b..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/create_request.go +++ /dev/null @@ -1,13 +0,0 @@ -package container - -import "github.com/docker/docker/api/types/network" - -// CreateRequest is the request message sent to the server for container -// create calls. It is a config wrapper that holds the container [Config] -// (portable) and the corresponding [HostConfig] (non-portable) and -// [network.NetworkingConfig]. -type CreateRequest struct { - *Config - HostConfig *HostConfig `json:"HostConfig,omitempty"` - NetworkingConfig *network.NetworkingConfig `json:"NetworkingConfig,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/container/create_response.go b/vendor/github.com/docker/docker/api/types/container/create_response.go deleted file mode 100644 index aa0e7f7d0789..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/create_response.go +++ /dev/null @@ -1,19 +0,0 @@ -package container - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// CreateResponse ContainerCreateResponse -// -// OK response to ContainerCreate operation -// swagger:model CreateResponse -type CreateResponse struct { - - // The ID of the created container - // Required: true - ID string `json:"Id"` - - // Warnings encountered when creating the container - // Required: true - Warnings []string `json:"Warnings"` -} diff --git a/vendor/github.com/docker/docker/api/types/container/disk_usage.go b/vendor/github.com/docker/docker/api/types/container/disk_usage.go deleted file mode 100644 index 05b6cbe9c709..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/disk_usage.go +++ /dev/null @@ -1,8 +0,0 @@ -package container - -// DiskUsage contains disk usage for containers. -type DiskUsage struct { - TotalSize int64 - Reclaimable int64 - Items []*Summary -} diff --git a/vendor/github.com/docker/docker/api/types/container/exec.go b/vendor/github.com/docker/docker/api/types/container/exec.go deleted file mode 100644 index e455cd27b278..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/exec.go +++ /dev/null @@ -1,53 +0,0 @@ -package container - -import "github.com/docker/docker/api/types/common" - -// ExecCreateResponse is the response for a successful exec-create request. -// It holds the ID of the exec that was created. -// -// TODO(thaJeztah): make this a distinct type. -type ExecCreateResponse = common.IDResponse - -// ExecOptions is a small subset of the Config struct that holds the configuration -// for the exec feature of docker. -type ExecOptions struct { - User string // User that will run the command - Privileged bool // Is the container in privileged mode - Tty bool // Attach standard streams to a tty. - ConsoleSize *[2]uint `json:",omitempty"` // Initial console size [height, width] - AttachStdin bool // Attach the standard input, makes possible user interaction - AttachStderr bool // Attach the standard error - AttachStdout bool // Attach the standard output - DetachKeys string // Escape keys for detach - Env []string // Environment variables - WorkingDir string // Working directory - Cmd []string // Execution commands and args - - // Deprecated: the Detach field is not used, and will be removed in a future release. - Detach bool -} - -// ExecStartOptions is a temp struct used by execStart -// Config fields is part of ExecConfig in runconfig package -type ExecStartOptions struct { - // ExecStart will first check if it's detached - Detach bool - // Check if there's a tty - Tty bool - // Terminal size [height, width], unused if Tty == false - ConsoleSize *[2]uint `json:",omitempty"` -} - -// ExecAttachOptions is a temp struct used by execAttach. -// -// TODO(thaJeztah): make this a separate type; ContainerExecAttach does not use the Detach option, and cannot run detached. -type ExecAttachOptions = ExecStartOptions - -// ExecInspect holds information returned by exec inspect. -type ExecInspect struct { - ExecID string `json:"ID"` - ContainerID string - Running bool - ExitCode int - Pid int -} diff --git a/vendor/github.com/docker/docker/api/types/container/network_settings.go b/vendor/github.com/docker/docker/api/types/container/network_settings.go deleted file mode 100644 index afec0e54323e..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/network_settings.go +++ /dev/null @@ -1,56 +0,0 @@ -package container - -import ( - "github.com/docker/docker/api/types/network" - "github.com/docker/go-connections/nat" -) - -// NetworkSettings exposes the network settings in the api -type NetworkSettings struct { - NetworkSettingsBase - DefaultNetworkSettings - Networks map[string]*network.EndpointSettings -} - -// NetworkSettingsBase holds networking state for a container when inspecting it. -type NetworkSettingsBase struct { - Bridge string // Bridge contains the name of the default bridge interface iff it was set through the daemon --bridge flag. - SandboxID string // SandboxID uniquely represents a container's network stack - SandboxKey string // SandboxKey identifies the sandbox - Ports nat.PortMap // Ports is a collection of PortBinding indexed by Port - - // HairpinMode specifies if hairpin NAT should be enabled on the virtual interface - // - // Deprecated: This field is never set and will be removed in a future release. - HairpinMode bool - // LinkLocalIPv6Address is an IPv6 unicast address using the link-local prefix - // - // Deprecated: This field is never set and will be removed in a future release. - LinkLocalIPv6Address string - // LinkLocalIPv6PrefixLen is the prefix length of an IPv6 unicast address - // - // Deprecated: This field is never set and will be removed in a future release. - LinkLocalIPv6PrefixLen int - SecondaryIPAddresses []network.Address // Deprecated: This field is never set and will be removed in a future release. - SecondaryIPv6Addresses []network.Address // Deprecated: This field is never set and will be removed in a future release. -} - -// DefaultNetworkSettings holds network information -// during the 2 release deprecation period. -// It will be removed in Docker 1.11. -type DefaultNetworkSettings struct { - EndpointID string // EndpointID uniquely represents a service endpoint in a Sandbox - Gateway string // Gateway holds the gateway address for the network - GlobalIPv6Address string // GlobalIPv6Address holds network's global IPv6 address - GlobalIPv6PrefixLen int // GlobalIPv6PrefixLen represents mask length of network's global IPv6 address - IPAddress string // IPAddress holds the IPv4 address for the network - IPPrefixLen int // IPPrefixLen represents mask length of network's IPv4 address - IPv6Gateway string // IPv6Gateway holds gateway address specific for IPv6 - MacAddress string // MacAddress holds the MAC address for the network -} - -// NetworkSettingsSummary provides a summary of container's networks -// in /containers/json -type NetworkSettingsSummary struct { - Networks map[string]*network.EndpointSettings -} diff --git a/vendor/github.com/docker/docker/api/types/container/options.go b/vendor/github.com/docker/docker/api/types/container/options.go deleted file mode 100644 index 7a2300576923..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/options.go +++ /dev/null @@ -1,67 +0,0 @@ -package container - -import "github.com/docker/docker/api/types/filters" - -// ResizeOptions holds parameters to resize a TTY. -// It can be used to resize container TTYs and -// exec process TTYs too. -type ResizeOptions struct { - Height uint - Width uint -} - -// AttachOptions holds parameters to attach to a container. -type AttachOptions struct { - Stream bool - Stdin bool - Stdout bool - Stderr bool - DetachKeys string - Logs bool -} - -// CommitOptions holds parameters to commit changes into a container. -type CommitOptions struct { - Reference string - Comment string - Author string - Changes []string - Pause bool - Config *Config -} - -// RemoveOptions holds parameters to remove containers. -type RemoveOptions struct { - RemoveVolumes bool - RemoveLinks bool - Force bool -} - -// StartOptions holds parameters to start containers. -type StartOptions struct { - CheckpointID string - CheckpointDir string -} - -// ListOptions holds parameters to list containers with. -type ListOptions struct { - Size bool - All bool - Latest bool - Since string - Before string - Limit int - Filters filters.Args -} - -// LogsOptions holds parameters to filter logs with. -type LogsOptions struct { - ShowStdout bool - ShowStderr bool - Since string - Until string - Timestamps bool - Follow bool - Tail string - Details bool -} diff --git a/vendor/github.com/docker/docker/api/types/container/port.go b/vendor/github.com/docker/docker/api/types/container/port.go deleted file mode 100644 index 895043cfe94f..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/port.go +++ /dev/null @@ -1,23 +0,0 @@ -package container - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// Port An open port on a container -// swagger:model Port -type Port struct { - - // Host IP address that the container's port is mapped to - IP string `json:"IP,omitempty"` - - // Port on the container - // Required: true - PrivatePort uint16 `json:"PrivatePort"` - - // Port exposed on the host - PublicPort uint16 `json:"PublicPort,omitempty"` - - // type - // Required: true - Type string `json:"Type"` -} diff --git a/vendor/github.com/docker/docker/api/types/container/state.go b/vendor/github.com/docker/docker/api/types/container/state.go deleted file mode 100644 index 78d5c4fe85c7..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/state.go +++ /dev/null @@ -1,64 +0,0 @@ -package container - -import ( - "fmt" - "strings" -) - -// ContainerState is a string representation of the container's current state. -// -// It currently is an alias for string, but may become a distinct type in the future. -type ContainerState = string - -const ( - StateCreated ContainerState = "created" // StateCreated indicates the container is created, but not (yet) started. - StateRunning ContainerState = "running" // StateRunning indicates that the container is running. - StatePaused ContainerState = "paused" // StatePaused indicates that the container's current state is paused. - StateRestarting ContainerState = "restarting" // StateRestarting indicates that the container is currently restarting. - StateRemoving ContainerState = "removing" // StateRemoving indicates that the container is being removed. - StateExited ContainerState = "exited" // StateExited indicates that the container exited. - StateDead ContainerState = "dead" // StateDead indicates that the container failed to be deleted. Containers in this state are attempted to be cleaned up when the daemon restarts. -) - -var validStates = []ContainerState{ - StateCreated, StateRunning, StatePaused, StateRestarting, StateRemoving, StateExited, StateDead, -} - -// ValidateContainerState checks if the provided string is a valid -// container [ContainerState]. -func ValidateContainerState(s ContainerState) error { - switch s { - case StateCreated, StateRunning, StatePaused, StateRestarting, StateRemoving, StateExited, StateDead: - return nil - default: - return errInvalidParameter{error: fmt.Errorf("invalid value for state (%s): must be one of %s", s, strings.Join(validStates, ", "))} - } -} - -// StateStatus is used to return container wait results. -// Implements exec.ExitCode interface. -// This type is needed as State include a sync.Mutex field which make -// copying it unsafe. -type StateStatus struct { - exitCode int - err error -} - -// ExitCode returns current exitcode for the state. -func (s StateStatus) ExitCode() int { - return s.exitCode -} - -// Err returns current error for the state. Returns nil if the container had -// exited on its own. -func (s StateStatus) Err() error { - return s.err -} - -// NewStateStatus returns a new StateStatus with the given exit code and error. -func NewStateStatus(exitCode int, err error) StateStatus { - return StateStatus{ - exitCode: exitCode, - err: err, - } -} diff --git a/vendor/github.com/docker/docker/api/types/container/stats.go b/vendor/github.com/docker/docker/api/types/container/stats.go deleted file mode 100644 index 3bfeb4849f91..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/stats.go +++ /dev/null @@ -1,177 +0,0 @@ -package container - -import "time" - -// ThrottlingData stores CPU throttling stats of one running container. -// Not used on Windows. -type ThrottlingData struct { - // Number of periods with throttling active - Periods uint64 `json:"periods"` - // Number of periods when the container hits its throttling limit. - ThrottledPeriods uint64 `json:"throttled_periods"` - // Aggregate time the container was throttled for in nanoseconds. - ThrottledTime uint64 `json:"throttled_time"` -} - -// CPUUsage stores All CPU stats aggregated since container inception. -type CPUUsage struct { - // Total CPU time consumed. - // Units: nanoseconds (Linux) - // Units: 100's of nanoseconds (Windows) - TotalUsage uint64 `json:"total_usage"` - - // Total CPU time consumed per core (Linux). Not used on Windows. - // Units: nanoseconds. - PercpuUsage []uint64 `json:"percpu_usage,omitempty"` - - // Time spent by tasks of the cgroup in kernel mode (Linux). - // Time spent by all container processes in kernel mode (Windows). - // Units: nanoseconds (Linux). - // Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers. - UsageInKernelmode uint64 `json:"usage_in_kernelmode"` - - // Time spent by tasks of the cgroup in user mode (Linux). - // Time spent by all container processes in user mode (Windows). - // Units: nanoseconds (Linux). - // Units: 100's of nanoseconds (Windows). Not populated for Hyper-V Containers - UsageInUsermode uint64 `json:"usage_in_usermode"` -} - -// CPUStats aggregates and wraps all CPU related info of container -type CPUStats struct { - // CPU Usage. Linux and Windows. - CPUUsage CPUUsage `json:"cpu_usage"` - - // System Usage. Linux only. - SystemUsage uint64 `json:"system_cpu_usage,omitempty"` - - // Online CPUs. Linux only. - OnlineCPUs uint32 `json:"online_cpus,omitempty"` - - // Throttling Data. Linux only. - ThrottlingData ThrottlingData `json:"throttling_data,omitempty"` -} - -// MemoryStats aggregates all memory stats since container inception on Linux. -// Windows returns stats for commit and private working set only. -type MemoryStats struct { - // Linux Memory Stats - - // current res_counter usage for memory - Usage uint64 `json:"usage,omitempty"` - // maximum usage ever recorded. - MaxUsage uint64 `json:"max_usage,omitempty"` - // TODO(vishh): Export these as stronger types. - // all the stats exported via memory.stat. - Stats map[string]uint64 `json:"stats,omitempty"` - // number of times memory usage hits limits. - Failcnt uint64 `json:"failcnt,omitempty"` - Limit uint64 `json:"limit,omitempty"` - - // Windows Memory Stats - // See https://technet.microsoft.com/en-us/magazine/ff382715.aspx - - // committed bytes - Commit uint64 `json:"commitbytes,omitempty"` - // peak committed bytes - CommitPeak uint64 `json:"commitpeakbytes,omitempty"` - // private working set - PrivateWorkingSet uint64 `json:"privateworkingset,omitempty"` -} - -// BlkioStatEntry is one small entity to store a piece of Blkio stats -// Not used on Windows. -type BlkioStatEntry struct { - Major uint64 `json:"major"` - Minor uint64 `json:"minor"` - Op string `json:"op"` - Value uint64 `json:"value"` -} - -// BlkioStats stores All IO service stats for data read and write. -// This is a Linux specific structure as the differences between expressing -// block I/O on Windows and Linux are sufficiently significant to make -// little sense attempting to morph into a combined structure. -type BlkioStats struct { - // number of bytes transferred to and from the block device - IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive"` - IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive"` - IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive"` - IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive"` - IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive"` - IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive"` - IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive"` - SectorsRecursive []BlkioStatEntry `json:"sectors_recursive"` -} - -// StorageStats is the disk I/O stats for read/write on Windows. -type StorageStats struct { - ReadCountNormalized uint64 `json:"read_count_normalized,omitempty"` - ReadSizeBytes uint64 `json:"read_size_bytes,omitempty"` - WriteCountNormalized uint64 `json:"write_count_normalized,omitempty"` - WriteSizeBytes uint64 `json:"write_size_bytes,omitempty"` -} - -// NetworkStats aggregates the network stats of one container -type NetworkStats struct { - // Bytes received. Windows and Linux. - RxBytes uint64 `json:"rx_bytes"` - // Packets received. Windows and Linux. - RxPackets uint64 `json:"rx_packets"` - // Received errors. Not used on Windows. Note that we don't `omitempty` this - // field as it is expected in the >=v1.21 API stats structure. - RxErrors uint64 `json:"rx_errors"` - // Incoming packets dropped. Windows and Linux. - RxDropped uint64 `json:"rx_dropped"` - // Bytes sent. Windows and Linux. - TxBytes uint64 `json:"tx_bytes"` - // Packets sent. Windows and Linux. - TxPackets uint64 `json:"tx_packets"` - // Sent errors. Not used on Windows. Note that we don't `omitempty` this - // field as it is expected in the >=v1.21 API stats structure. - TxErrors uint64 `json:"tx_errors"` - // Outgoing packets dropped. Windows and Linux. - TxDropped uint64 `json:"tx_dropped"` - // Endpoint ID. Not used on Linux. - EndpointID string `json:"endpoint_id,omitempty"` - // Instance ID. Not used on Linux. - InstanceID string `json:"instance_id,omitempty"` -} - -// PidsStats contains the stats of a container's pids -type PidsStats struct { - // Current is the number of pids in the cgroup - Current uint64 `json:"current,omitempty"` - // Limit is the hard limit on the number of pids in the cgroup. - // A "Limit" of 0 means that there is no limit. - Limit uint64 `json:"limit,omitempty"` -} - -// Stats is Ultimate struct aggregating all types of stats of one container -// -// Deprecated: use [StatsResponse] instead. This type will be removed in the next release. -type Stats = StatsResponse - -// StatsResponse aggregates all types of stats of one container. -type StatsResponse struct { - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - - // Common stats - Read time.Time `json:"read"` - PreRead time.Time `json:"preread"` - - // Linux specific stats, not populated on Windows. - PidsStats PidsStats `json:"pids_stats,omitempty"` - BlkioStats BlkioStats `json:"blkio_stats,omitempty"` - - // Windows specific stats, not populated on Linux. - NumProcs uint32 `json:"num_procs"` - StorageStats StorageStats `json:"storage_stats,omitempty"` - - // Shared stats - CPUStats CPUStats `json:"cpu_stats,omitempty"` - PreCPUStats CPUStats `json:"precpu_stats,omitempty"` // "Pre"="Previous" - MemoryStats MemoryStats `json:"memory_stats,omitempty"` - Networks map[string]NetworkStats `json:"networks,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/container/top_response.go b/vendor/github.com/docker/docker/api/types/container/top_response.go deleted file mode 100644 index b4bae5ef036b..000000000000 --- a/vendor/github.com/docker/docker/api/types/container/top_response.go +++ /dev/null @@ -1,18 +0,0 @@ -package container - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// TopResponse ContainerTopResponse -// -// Container "top" response. -// swagger:model TopResponse -type TopResponse struct { - - // Each process running in the container, where each process - // is an array of values corresponding to the titles. - Processes [][]string `json:"Processes"` - - // The ps column titles - Titles []string `json:"Titles"` -} diff --git a/vendor/github.com/docker/docker/api/types/error_response.go b/vendor/github.com/docker/docker/api/types/error_response.go deleted file mode 100644 index dc942d9d9efa..000000000000 --- a/vendor/github.com/docker/docker/api/types/error_response.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// ErrorResponse Represents an error. -// swagger:model ErrorResponse -type ErrorResponse struct { - - // The error message. - // Required: true - Message string `json:"message"` -} diff --git a/vendor/github.com/docker/docker/api/types/events/events.go b/vendor/github.com/docker/docker/api/types/events/events.go deleted file mode 100644 index 952c0ff2429c..000000000000 --- a/vendor/github.com/docker/docker/api/types/events/events.go +++ /dev/null @@ -1,139 +0,0 @@ -package events - -import "github.com/docker/docker/api/types/filters" - -// Type is used for event-types. -type Type string - -// List of known event types. -const ( - BuilderEventType Type = "builder" // BuilderEventType is the event type that the builder generates. - ConfigEventType Type = "config" // ConfigEventType is the event type that configs generate. - ContainerEventType Type = "container" // ContainerEventType is the event type that containers generate. - DaemonEventType Type = "daemon" // DaemonEventType is the event type that daemon generate. - ImageEventType Type = "image" // ImageEventType is the event type that images generate. - NetworkEventType Type = "network" // NetworkEventType is the event type that networks generate. - NodeEventType Type = "node" // NodeEventType is the event type that nodes generate. - PluginEventType Type = "plugin" // PluginEventType is the event type that plugins generate. - SecretEventType Type = "secret" // SecretEventType is the event type that secrets generate. - ServiceEventType Type = "service" // ServiceEventType is the event type that services generate. - VolumeEventType Type = "volume" // VolumeEventType is the event type that volumes generate. -) - -// Action is used for event-actions. -type Action string - -const ( - ActionCreate Action = "create" - ActionStart Action = "start" - ActionRestart Action = "restart" - ActionStop Action = "stop" - ActionCheckpoint Action = "checkpoint" - ActionPause Action = "pause" - ActionUnPause Action = "unpause" - ActionAttach Action = "attach" - ActionDetach Action = "detach" - ActionResize Action = "resize" - ActionUpdate Action = "update" - ActionRename Action = "rename" - ActionKill Action = "kill" - ActionDie Action = "die" - ActionOOM Action = "oom" - ActionDestroy Action = "destroy" - ActionRemove Action = "remove" - ActionCommit Action = "commit" - ActionTop Action = "top" - ActionCopy Action = "copy" - ActionArchivePath Action = "archive-path" - ActionExtractToDir Action = "extract-to-dir" - ActionExport Action = "export" - ActionImport Action = "import" - ActionSave Action = "save" - ActionLoad Action = "load" - ActionTag Action = "tag" - ActionUnTag Action = "untag" - ActionPush Action = "push" - ActionPull Action = "pull" - ActionPrune Action = "prune" - ActionDelete Action = "delete" - ActionEnable Action = "enable" - ActionDisable Action = "disable" - ActionConnect Action = "connect" - ActionDisconnect Action = "disconnect" - ActionReload Action = "reload" - ActionMount Action = "mount" - ActionUnmount Action = "unmount" - - // ActionExecCreate is the prefix used for exec_create events. These - // event-actions are commonly followed by a colon and space (": "), - // and the command that's defined for the exec, for example: - // - // exec_create: /bin/sh -c 'echo hello' - // - // This is far from ideal; it's a compromise to allow filtering and - // to preserve backward-compatibility. - ActionExecCreate Action = "exec_create" - // ActionExecStart is the prefix used for exec_create events. These - // event-actions are commonly followed by a colon and space (": "), - // and the command that's defined for the exec, for example: - // - // exec_start: /bin/sh -c 'echo hello' - // - // This is far from ideal; it's a compromise to allow filtering and - // to preserve backward-compatibility. - ActionExecStart Action = "exec_start" - ActionExecDie Action = "exec_die" - ActionExecDetach Action = "exec_detach" - - // ActionHealthStatus is the prefix to use for health_status events. - // - // Health-status events can either have a pre-defined status, in which - // case the "health_status" action is followed by a colon, or can be - // "free-form", in which case they're followed by the output of the - // health-check output. - // - // This is far form ideal, and a compromise to allow filtering, and - // to preserve backward-compatibility. - ActionHealthStatus Action = "health_status" - ActionHealthStatusRunning Action = "health_status: running" - ActionHealthStatusHealthy Action = "health_status: healthy" - ActionHealthStatusUnhealthy Action = "health_status: unhealthy" -) - -// Actor describes something that generates events, -// like a container, or a network, or a volume. -// It has a defined name and a set of attributes. -// The container attributes are its labels, other actors -// can generate these attributes from other properties. -type Actor struct { - ID string - Attributes map[string]string -} - -// Message represents the information an event contains -type Message struct { - // Deprecated: use Action instead. - // Information from JSONMessage. - // With data only in container events. - Status string `json:"status,omitempty"` - // Deprecated: use Actor.ID instead. - ID string `json:"id,omitempty"` - // Deprecated: use Actor.Attributes["image"] instead. - From string `json:"from,omitempty"` - - Type Type - Action Action - Actor Actor - // Engine events are local scope. Cluster events are swarm scope. - Scope string `json:"scope,omitempty"` - - Time int64 `json:"time,omitempty"` - TimeNano int64 `json:"timeNano,omitempty"` -} - -// ListOptions holds parameters to filter events with. -type ListOptions struct { - Since string - Until string - Filters filters.Args -} diff --git a/vendor/github.com/docker/docker/api/types/filters/errors.go b/vendor/github.com/docker/docker/api/types/filters/errors.go deleted file mode 100644 index b8a690d67ab5..000000000000 --- a/vendor/github.com/docker/docker/api/types/filters/errors.go +++ /dev/null @@ -1,24 +0,0 @@ -package filters - -import "fmt" - -// invalidFilter indicates that the provided filter or its value is invalid -type invalidFilter struct { - Filter string - Value []string -} - -func (e invalidFilter) Error() string { - msg := "invalid filter" - if e.Filter != "" { - msg += " '" + e.Filter - if e.Value != nil { - msg = fmt.Sprintf("%s=%s", msg, e.Value) - } - msg += "'" - } - return msg -} - -// InvalidParameter marks this error as ErrInvalidParameter -func (e invalidFilter) InvalidParameter() {} diff --git a/vendor/github.com/docker/docker/api/types/filters/parse.go b/vendor/github.com/docker/docker/api/types/filters/parse.go deleted file mode 100644 index 86f4bdb28e17..000000000000 --- a/vendor/github.com/docker/docker/api/types/filters/parse.go +++ /dev/null @@ -1,336 +0,0 @@ -/* -Package filters provides tools for encoding a mapping of keys to a set of -multiple values. -*/ -package filters - -import ( - "encoding/json" - "regexp" - "strings" - - "github.com/docker/docker/api/types/versions" -) - -// Args stores a mapping of keys to a set of multiple values. -type Args struct { - fields map[string]map[string]bool -} - -// KeyValuePair are used to initialize a new Args -type KeyValuePair struct { - Key string - Value string -} - -// Arg creates a new KeyValuePair for initializing Args -func Arg(key, value string) KeyValuePair { - return KeyValuePair{Key: key, Value: value} -} - -// NewArgs returns a new Args populated with the initial args -func NewArgs(initialArgs ...KeyValuePair) Args { - args := Args{fields: map[string]map[string]bool{}} - for _, arg := range initialArgs { - args.Add(arg.Key, arg.Value) - } - return args -} - -// Keys returns all the keys in list of Args -func (args Args) Keys() []string { - keys := make([]string, 0, len(args.fields)) - for k := range args.fields { - keys = append(keys, k) - } - return keys -} - -// MarshalJSON returns a JSON byte representation of the Args -func (args Args) MarshalJSON() ([]byte, error) { - if len(args.fields) == 0 { - return []byte("{}"), nil - } - return json.Marshal(args.fields) -} - -// ToJSON returns the Args as a JSON encoded string -func ToJSON(a Args) (string, error) { - if a.Len() == 0 { - return "", nil - } - buf, err := json.Marshal(a) - return string(buf), err -} - -// ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22 -// then the encoded format will use an older legacy format where the values are a -// list of strings, instead of a set. -// -// Deprecated: do not use in any new code; use ToJSON instead -func ToParamWithVersion(version string, a Args) (string, error) { - if a.Len() == 0 { - return "", nil - } - - if version != "" && versions.LessThan(version, "1.22") { - buf, err := json.Marshal(convertArgsToSlice(a.fields)) - return string(buf), err - } - - return ToJSON(a) -} - -// FromJSON decodes a JSON encoded string into Args -func FromJSON(p string) (Args, error) { - args := NewArgs() - - if p == "" { - return args, nil - } - - raw := []byte(p) - err := json.Unmarshal(raw, &args) - if err == nil { - return args, nil - } - - // Fallback to parsing arguments in the legacy slice format - deprecated := map[string][]string{} - if legacyErr := json.Unmarshal(raw, &deprecated); legacyErr != nil { - return args, &invalidFilter{} - } - - args.fields = deprecatedArgs(deprecated) - return args, nil -} - -// UnmarshalJSON populates the Args from JSON encode bytes -func (args Args) UnmarshalJSON(raw []byte) error { - return json.Unmarshal(raw, &args.fields) -} - -// Get returns the list of values associated with the key -func (args Args) Get(key string) []string { - values := args.fields[key] - if values == nil { - return make([]string, 0) - } - slice := make([]string, 0, len(values)) - for key := range values { - slice = append(slice, key) - } - return slice -} - -// Add a new value to the set of values -func (args Args) Add(key, value string) { - if _, ok := args.fields[key]; ok { - args.fields[key][value] = true - } else { - args.fields[key] = map[string]bool{value: true} - } -} - -// Del removes a value from the set -func (args Args) Del(key, value string) { - if _, ok := args.fields[key]; ok { - delete(args.fields[key], value) - if len(args.fields[key]) == 0 { - delete(args.fields, key) - } - } -} - -// Len returns the number of keys in the mapping -func (args Args) Len() int { - return len(args.fields) -} - -// MatchKVList returns true if all the pairs in sources exist as key=value -// pairs in the mapping at key, or if there are no values at key. -func (args Args) MatchKVList(key string, sources map[string]string) bool { - fieldValues := args.fields[key] - - // do not filter if there is no filter set or cannot determine filter - if len(fieldValues) == 0 { - return true - } - - if len(sources) == 0 { - return false - } - - for value := range fieldValues { - testK, testV, hasValue := strings.Cut(value, "=") - - v, ok := sources[testK] - if !ok { - return false - } - if hasValue && testV != v { - return false - } - } - - return true -} - -// Match returns true if any of the values at key match the source string -func (args Args) Match(field, source string) bool { - if args.ExactMatch(field, source) { - return true - } - - fieldValues := args.fields[field] - for name2match := range fieldValues { - match, err := regexp.MatchString(name2match, source) - if err != nil { - continue - } - if match { - return true - } - } - return false -} - -// GetBoolOrDefault returns a boolean value of the key if the key is present -// and is interpretable as a boolean value. Otherwise the default value is returned. -// Error is not nil only if the filter values are not valid boolean or are conflicting. -func (args Args) GetBoolOrDefault(key string, defaultValue bool) (bool, error) { - fieldValues, ok := args.fields[key] - if !ok { - return defaultValue, nil - } - - if len(fieldValues) == 0 { - return defaultValue, &invalidFilter{key, nil} - } - - isFalse := fieldValues["0"] || fieldValues["false"] - isTrue := fieldValues["1"] || fieldValues["true"] - if isFalse == isTrue { - // Either no or conflicting truthy/falsy value were provided - return defaultValue, &invalidFilter{key, args.Get(key)} - } - return isTrue, nil -} - -// ExactMatch returns true if the source matches exactly one of the values. -func (args Args) ExactMatch(key, source string) bool { - fieldValues, ok := args.fields[key] - // do not filter if there is no filter set or cannot determine filter - if !ok || len(fieldValues) == 0 { - return true - } - - // try to match full name value to avoid O(N) regular expression matching - return fieldValues[source] -} - -// UniqueExactMatch returns true if there is only one value and the source -// matches exactly the value. -func (args Args) UniqueExactMatch(key, source string) bool { - fieldValues := args.fields[key] - // do not filter if there is no filter set or cannot determine filter - if len(fieldValues) == 0 { - return true - } - if len(args.fields[key]) != 1 { - return false - } - - // try to match full name value to avoid O(N) regular expression matching - return fieldValues[source] -} - -// FuzzyMatch returns true if the source matches exactly one value, or the -// source has one of the values as a prefix. -func (args Args) FuzzyMatch(key, source string) bool { - if args.ExactMatch(key, source) { - return true - } - - fieldValues := args.fields[key] - for prefix := range fieldValues { - if strings.HasPrefix(source, prefix) { - return true - } - } - return false -} - -// Contains returns true if the key exists in the mapping -func (args Args) Contains(field string) bool { - _, ok := args.fields[field] - return ok -} - -// Validate compared the set of accepted keys against the keys in the mapping. -// An error is returned if any mapping keys are not in the accepted set. -func (args Args) Validate(accepted map[string]bool) error { - for name := range args.fields { - if !accepted[name] { - return &invalidFilter{name, nil} - } - } - return nil -} - -// WalkValues iterates over the list of values for a key in the mapping and calls -// op() for each value. If op returns an error the iteration stops and the -// error is returned. -func (args Args) WalkValues(field string, op func(value string) error) error { - if _, ok := args.fields[field]; !ok { - return nil - } - for v := range args.fields[field] { - if err := op(v); err != nil { - return err - } - } - return nil -} - -// Clone returns a copy of args. -func (args Args) Clone() (newArgs Args) { - newArgs.fields = make(map[string]map[string]bool, len(args.fields)) - for k, m := range args.fields { - var mm map[string]bool - if m != nil { - mm = make(map[string]bool, len(m)) - for kk, v := range m { - mm[kk] = v - } - } - newArgs.fields[k] = mm - } - return newArgs -} - -func deprecatedArgs(d map[string][]string) map[string]map[string]bool { - m := map[string]map[string]bool{} - for k, v := range d { - values := map[string]bool{} - for _, vv := range v { - values[vv] = true - } - m[k] = values - } - return m -} - -func convertArgsToSlice(f map[string]map[string]bool) map[string][]string { - m := map[string][]string{} - for k, v := range f { - values := []string{} - for kk := range v { - if v[kk] { - values = append(values, kk) - } - } - m[k] = values - } - return m -} diff --git a/vendor/github.com/docker/docker/api/types/image/disk_usage.go b/vendor/github.com/docker/docker/api/types/image/disk_usage.go deleted file mode 100644 index b29d925cac48..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/disk_usage.go +++ /dev/null @@ -1,8 +0,0 @@ -package image - -// DiskUsage contains disk usage for images. -type DiskUsage struct { - TotalSize int64 - Reclaimable int64 - Items []*Summary -} diff --git a/vendor/github.com/docker/docker/api/types/image/image.go b/vendor/github.com/docker/docker/api/types/image/image.go deleted file mode 100644 index abb7ffd8052e..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/image.go +++ /dev/null @@ -1,47 +0,0 @@ -package image - -import ( - "io" - "time" -) - -// Metadata contains engine-local data about the image. -type Metadata struct { - // LastTagTime is the date and time at which the image was last tagged. - LastTagTime time.Time `json:",omitempty"` -} - -// PruneReport contains the response for Engine API: -// POST "/images/prune" -type PruneReport struct { - ImagesDeleted []DeleteResponse - SpaceReclaimed uint64 -} - -// LoadResponse returns information to the client about a load process. -// -// TODO(thaJeztah): remove this type, and just use an io.ReadCloser -// -// This type was added in https://github.com/moby/moby/pull/18878, related -// to https://github.com/moby/moby/issues/19177; -// -// Make docker load to output json when the response content type is json -// Swarm hijacks the response from docker load and returns JSON rather -// than plain text like the Engine does. This makes the API library to return -// information to figure that out. -// -// However the "load" endpoint unconditionally returns JSON; -// https://github.com/moby/moby/blob/7b9d2ef6e5518a3d3f3cc418459f8df786cfbbd1/api/server/router/image/image_routes.go#L248-L255 -// -// PR https://github.com/moby/moby/pull/21959 made the response-type depend -// on whether "quiet" was set, but this logic got changed in a follow-up -// https://github.com/moby/moby/pull/25557, which made the JSON response-type -// unconditionally, but the output produced depend on whether"quiet" was set. -// -// We should deprecated the "quiet" option, as it's really a client -// responsibility. -type LoadResponse struct { - // Body must be closed to avoid a resource leak - Body io.ReadCloser - JSON bool -} diff --git a/vendor/github.com/docker/docker/api/types/image/image_history.go b/vendor/github.com/docker/docker/api/types/image/image_history.go deleted file mode 100644 index a6cdab84d8a9..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/image_history.go +++ /dev/null @@ -1,36 +0,0 @@ -package image - -// ---------------------------------------------------------------------------- -// Code generated by `swagger generate operation`. DO NOT EDIT. -// -// See hack/generate-swagger-api.sh -// ---------------------------------------------------------------------------- - -// HistoryResponseItem individual image layer information in response to ImageHistory operation -// swagger:model HistoryResponseItem -type HistoryResponseItem struct { - - // comment - // Required: true - Comment string `json:"Comment"` - - // created - // Required: true - Created int64 `json:"Created"` - - // created by - // Required: true - CreatedBy string `json:"CreatedBy"` - - // Id - // Required: true - ID string `json:"Id"` - - // size - // Required: true - Size int64 `json:"Size"` - - // tags - // Required: true - Tags []string `json:"Tags"` -} diff --git a/vendor/github.com/docker/docker/api/types/image/image_inspect.go b/vendor/github.com/docker/docker/api/types/image/image_inspect.go deleted file mode 100644 index 3bdb474287c0..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/image_inspect.go +++ /dev/null @@ -1,142 +0,0 @@ -package image - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/storage" - dockerspec "github.com/moby/docker-image-spec/specs-go/v1" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// RootFS returns Image's RootFS description including the layer IDs. -type RootFS struct { - Type string `json:",omitempty"` - Layers []string `json:",omitempty"` -} - -// InspectResponse contains response of Engine API: -// GET "/images/{name:.*}/json" -type InspectResponse struct { - // ID is the content-addressable ID of an image. - // - // This identifier is a content-addressable digest calculated from the - // image's configuration (which includes the digests of layers used by - // the image). - // - // Note that this digest differs from the `RepoDigests` below, which - // holds digests of image manifests that reference the image. - ID string `json:"Id"` - - // RepoTags is a list of image names/tags in the local image cache that - // reference this image. - // - // Multiple image tags can refer to the same image, and this list may be - // empty if no tags reference the image, in which case the image is - // "untagged", in which case it can still be referenced by its ID. - RepoTags []string - - // RepoDigests is a list of content-addressable digests of locally available - // image manifests that the image is referenced from. Multiple manifests can - // refer to the same image. - // - // These digests are usually only available if the image was either pulled - // from a registry, or if the image was pushed to a registry, which is when - // the manifest is generated and its digest calculated. - RepoDigests []string - - // Parent is the ID of the parent image. - // - // Depending on how the image was created, this field may be empty and - // is only set for images that were built/created locally. This field - // is empty if the image was pulled from an image registry. - Parent string - - // Comment is an optional message that can be set when committing or - // importing the image. - Comment string - - // Created is the date and time at which the image was created, formatted in - // RFC 3339 nano-seconds (time.RFC3339Nano). - // - // This information is only available if present in the image, - // and omitted otherwise. - Created string `json:",omitempty"` - - // Container is the ID of the container that was used to create the image. - // - // Depending on how the image was created, this field may be empty. - // - // Deprecated: this field is omitted in API v1.45, but kept for backward compatibility. - Container string `json:",omitempty"` - - // ContainerConfig is an optional field containing the configuration of the - // container that was last committed when creating the image. - // - // Previous versions of Docker builder used this field to store build cache, - // and it is not in active use anymore. - // - // Deprecated: this field is omitted in API v1.45, but kept for backward compatibility. - ContainerConfig *container.Config `json:",omitempty"` - - // DockerVersion is the version of Docker that was used to build the image. - // - // Depending on how the image was created, this field may be empty. - DockerVersion string - - // Author is the name of the author that was specified when committing the - // image, or as specified through MAINTAINER (deprecated) in the Dockerfile. - Author string - Config *dockerspec.DockerOCIImageConfig - - // Architecture is the hardware CPU architecture that the image runs on. - Architecture string - - // Variant is the CPU architecture variant (presently ARM-only). - Variant string `json:",omitempty"` - - // OS is the Operating System the image is built to run on. - Os string - - // OsVersion is the version of the Operating System the image is built to - // run on (especially for Windows). - OsVersion string `json:",omitempty"` - - // Size is the total size of the image including all layers it is composed of. - Size int64 - - // VirtualSize is the total size of the image including all layers it is - // composed of. - // - // Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - VirtualSize int64 `json:"VirtualSize,omitempty"` - - // GraphDriver holds information about the storage driver used to store the - // container's and image's filesystem. - GraphDriver storage.DriverData - - // RootFS contains information about the image's RootFS, including the - // layer IDs. - RootFS RootFS - - // Metadata of the image in the local cache. - // - // This information is local to the daemon, and not part of the image itself. - Metadata Metadata - - // Descriptor is the OCI descriptor of the image target. - // It's only set if the daemon provides a multi-platform image store. - // - // WARNING: This is experimental and may change at any time without any backward - // compatibility. - Descriptor *ocispec.Descriptor `json:"Descriptor,omitempty"` - - // Manifests is a list of image manifests available in this image. It - // provides a more detailed view of the platform-specific image manifests or - // other image-attached data like build attestations. - // - // Only available if the daemon provides a multi-platform image store, the client - // requests manifests AND does not request a specific platform. - // - // WARNING: This is experimental and may change at any time without any backward - // compatibility. - Manifests []ManifestSummary `json:"Manifests,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/image/opts.go b/vendor/github.com/docker/docker/api/types/image/opts.go deleted file mode 100644 index 9e33a42fa63b..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/opts.go +++ /dev/null @@ -1,124 +0,0 @@ -package image - -import ( - "context" - "io" - - "github.com/docker/docker/api/types/filters" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ImportSource holds source information for ImageImport -type ImportSource struct { - Source io.Reader // Source is the data to send to the server to create this image from. You must set SourceName to "-" to leverage this. - SourceName string // SourceName is the name of the image to pull. Set to "-" to leverage the Source attribute. -} - -// ImportOptions holds information to import images from the client host. -type ImportOptions struct { - Tag string // Tag is the name to tag this image with. This attribute is deprecated. - Message string // Message is the message to tag the image with - Changes []string // Changes are the raw changes to apply to this image - Platform string // Platform is the target platform of the image -} - -// CreateOptions holds information to create images. -type CreateOptions struct { - RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry. - Platform string // Platform is the target platform of the image if it needs to be pulled from the registry. -} - -// PullOptions holds information to pull images. -type PullOptions struct { - All bool - RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry - - // PrivilegeFunc is a function that clients can supply to retry operations - // after getting an authorization error. This function returns the registry - // authentication header value in base64 encoded format, or an error if the - // privilege request fails. - // - // For details, refer to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. - PrivilegeFunc func(context.Context) (string, error) - Platform string -} - -// PushOptions holds information to push images. -type PushOptions struct { - All bool - RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry - - // PrivilegeFunc is a function that clients can supply to retry operations - // after getting an authorization error. This function returns the registry - // authentication header value in base64 encoded format, or an error if the - // privilege request fails. - // - // For details, refer to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. - PrivilegeFunc func(context.Context) (string, error) - - // Platform is an optional field that selects a specific platform to push - // when the image is a multi-platform image. - // Using this will only push a single platform-specific manifest. - Platform *ocispec.Platform `json:",omitempty"` -} - -// ListOptions holds parameters to list images with. -type ListOptions struct { - // All controls whether all images in the graph are filtered, or just - // the heads. - All bool - - // Filters is a JSON-encoded set of filter arguments. - Filters filters.Args - - // SharedSize indicates whether the shared size of images should be computed. - SharedSize bool - - // ContainerCount indicates whether container count should be computed. - // - // Deprecated: This field has been unused and is no longer required and will be removed in a future version. - ContainerCount bool - - // Manifests indicates whether the image manifests should be returned. - Manifests bool -} - -// RemoveOptions holds parameters to remove images. -type RemoveOptions struct { - Platforms []ocispec.Platform - Force bool - PruneChildren bool -} - -// HistoryOptions holds parameters to get image history. -type HistoryOptions struct { - // Platform from the manifest list to use for history. - Platform *ocispec.Platform -} - -// LoadOptions holds parameters to load images. -type LoadOptions struct { - // Quiet suppresses progress output - Quiet bool - - // Platforms selects the platforms to load if the image is a - // multi-platform image and has multiple variants. - Platforms []ocispec.Platform -} - -type InspectOptions struct { - // Manifests returns the image manifests. - Manifests bool - - // Platform selects the specific platform of a multi-platform image to inspect. - // - // This option is only available for API version 1.49 and up. - Platform *ocispec.Platform -} - -// SaveOptions holds parameters to save images. -type SaveOptions struct { - // Platforms selects the platforms to save if the image is a - // multi-platform image and has multiple variants. - Platforms []ocispec.Platform -} diff --git a/vendor/github.com/docker/docker/api/types/image/summary.go b/vendor/github.com/docker/docker/api/types/image/summary.go deleted file mode 100644 index c5ae6ab9ca11..000000000000 --- a/vendor/github.com/docker/docker/api/types/image/summary.go +++ /dev/null @@ -1,101 +0,0 @@ -package image - -import ocispec "github.com/opencontainers/image-spec/specs-go/v1" - -type Summary struct { - - // Number of containers using this image. Includes both stopped and running - // containers. - // - // This size is not calculated by default, and depends on which API endpoint - // is used. `-1` indicates that the value has not been set / calculated. - // - // Required: true - Containers int64 `json:"Containers"` - - // Date and time at which the image was created as a Unix timestamp - // (number of seconds since EPOCH). - // - // Required: true - Created int64 `json:"Created"` - - // ID is the content-addressable ID of an image. - // - // This identifier is a content-addressable digest calculated from the - // image's configuration (which includes the digests of layers used by - // the image). - // - // Note that this digest differs from the `RepoDigests` below, which - // holds digests of image manifests that reference the image. - // - // Required: true - ID string `json:"Id"` - - // User-defined key/value metadata. - // Required: true - Labels map[string]string `json:"Labels"` - - // ID of the parent image. - // - // Depending on how the image was created, this field may be empty and - // is only set for images that were built/created locally. This field - // is empty if the image was pulled from an image registry. - // - // Required: true - ParentID string `json:"ParentId"` - - // Descriptor is the OCI descriptor of the image target. - // It's only set if the daemon provides a multi-platform image store. - // - // WARNING: This is experimental and may change at any time without any backward - // compatibility. - Descriptor *ocispec.Descriptor `json:"Descriptor,omitempty"` - - // Manifests is a list of image manifests available in this image. It - // provides a more detailed view of the platform-specific image manifests or - // other image-attached data like build attestations. - // - // WARNING: This is experimental and may change at any time without any backward - // compatibility. - Manifests []ManifestSummary `json:"Manifests,omitempty"` - - // List of content-addressable digests of locally available image manifests - // that the image is referenced from. Multiple manifests can refer to the - // same image. - // - // These digests are usually only available if the image was either pulled - // from a registry, or if the image was pushed to a registry, which is when - // the manifest is generated and its digest calculated. - // - // Required: true - RepoDigests []string `json:"RepoDigests"` - - // List of image names/tags in the local image cache that reference this - // image. - // - // Multiple image tags can refer to the same image, and this list may be - // empty if no tags reference the image, in which case the image is - // "untagged", in which case it can still be referenced by its ID. - // - // Required: true - RepoTags []string `json:"RepoTags"` - - // Total size of image layers that are shared between this image and other - // images. - // - // This size is not calculated by default. `-1` indicates that the value - // has not been set / calculated. - // - // Required: true - SharedSize int64 `json:"SharedSize"` - - // Total size of the image including all layers it is composed of. - // - // Required: true - Size int64 `json:"Size"` - - // Total size of the image including all layers it is composed of. - // - // Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - VirtualSize int64 `json:"VirtualSize,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/network/create_response.go b/vendor/github.com/docker/docker/api/types/network/create_response.go deleted file mode 100644 index c32b35bff522..000000000000 --- a/vendor/github.com/docker/docker/api/types/network/create_response.go +++ /dev/null @@ -1,19 +0,0 @@ -package network - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// CreateResponse NetworkCreateResponse -// -// OK response to NetworkCreate operation -// swagger:model CreateResponse -type CreateResponse struct { - - // The ID of the created network. - // Required: true - ID string `json:"Id"` - - // Warnings encountered when creating the container - // Required: true - Warning string `json:"Warning"` -} diff --git a/vendor/github.com/docker/docker/api/types/network/endpoint.go b/vendor/github.com/docker/docker/api/types/network/endpoint.go deleted file mode 100644 index 167ac70ab56a..000000000000 --- a/vendor/github.com/docker/docker/api/types/network/endpoint.go +++ /dev/null @@ -1,153 +0,0 @@ -package network - -import ( - "errors" - "fmt" - "net" - - "github.com/docker/docker/internal/multierror" -) - -// EndpointSettings stores the network endpoint details -type EndpointSettings struct { - // Configurations - IPAMConfig *EndpointIPAMConfig - Links []string - Aliases []string // Aliases holds the list of extra, user-specified DNS names for this endpoint. - // MacAddress may be used to specify a MAC address when the container is created. - // Once the container is running, it becomes operational data (it may contain a - // generated address). - MacAddress string - DriverOpts map[string]string - - // GwPriority determines which endpoint will provide the default gateway - // for the container. The endpoint with the highest priority will be used. - // If multiple endpoints have the same priority, they are lexicographically - // sorted based on their network name, and the one that sorts first is picked. - GwPriority int - // Operational data - NetworkID string - EndpointID string - Gateway string - IPAddress string - IPPrefixLen int - IPv6Gateway string - GlobalIPv6Address string - GlobalIPv6PrefixLen int - // DNSNames holds all the (non fully qualified) DNS names associated to this endpoint. First entry is used to - // generate PTR records. - DNSNames []string -} - -// Copy makes a deep copy of `EndpointSettings` -func (es *EndpointSettings) Copy() *EndpointSettings { - epCopy := *es - if es.IPAMConfig != nil { - epCopy.IPAMConfig = es.IPAMConfig.Copy() - } - - if es.Links != nil { - links := make([]string, 0, len(es.Links)) - epCopy.Links = append(links, es.Links...) - } - - if es.Aliases != nil { - aliases := make([]string, 0, len(es.Aliases)) - epCopy.Aliases = append(aliases, es.Aliases...) - } - - if len(es.DNSNames) > 0 { - epCopy.DNSNames = make([]string, len(es.DNSNames)) - copy(epCopy.DNSNames, es.DNSNames) - } - - return &epCopy -} - -// EndpointIPAMConfig represents IPAM configurations for the endpoint -type EndpointIPAMConfig struct { - IPv4Address string `json:",omitempty"` - IPv6Address string `json:",omitempty"` - LinkLocalIPs []string `json:",omitempty"` -} - -// Copy makes a copy of the endpoint ipam config -func (cfg *EndpointIPAMConfig) Copy() *EndpointIPAMConfig { - cfgCopy := *cfg - cfgCopy.LinkLocalIPs = make([]string, 0, len(cfg.LinkLocalIPs)) - cfgCopy.LinkLocalIPs = append(cfgCopy.LinkLocalIPs, cfg.LinkLocalIPs...) - return &cfgCopy -} - -// NetworkSubnet describes a user-defined subnet for a specific network. It's only used to validate if an -// EndpointIPAMConfig is valid for a specific network. -type NetworkSubnet interface { - // Contains checks whether the NetworkSubnet contains [addr]. - Contains(addr net.IP) bool - // IsStatic checks whether the subnet was statically allocated (ie. user-defined). - IsStatic() bool -} - -// IsInRange checks whether static IP addresses are valid in a specific network. -func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets []NetworkSubnet) error { - var errs []error - - if err := validateEndpointIPAddress(cfg.IPv4Address, v4Subnets); err != nil { - errs = append(errs, err) - } - if err := validateEndpointIPAddress(cfg.IPv6Address, v6Subnets); err != nil { - errs = append(errs, err) - } - - return multierror.Join(errs...) -} - -func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error { - if epAddr == "" { - return nil - } - - var staticSubnet bool - parsedAddr := net.ParseIP(epAddr) - for _, subnet := range ipamSubnets { - if subnet.IsStatic() { - staticSubnet = true - if subnet.Contains(parsedAddr) { - return nil - } - } - } - - if staticSubnet { - return fmt.Errorf("no configured subnet or ip-range contain the IP address %s", epAddr) - } - - return errors.New("user specified IP address is supported only when connecting to networks with user configured subnets") -} - -// Validate checks whether cfg is valid. -func (cfg *EndpointIPAMConfig) Validate() error { - if cfg == nil { - return nil - } - - var errs []error - - if cfg.IPv4Address != "" { - if addr := net.ParseIP(cfg.IPv4Address); addr == nil || addr.To4() == nil || addr.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid IPv4 address: %s", cfg.IPv4Address)) - } - } - if cfg.IPv6Address != "" { - if addr := net.ParseIP(cfg.IPv6Address); addr == nil || addr.To4() != nil || addr.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid IPv6 address: %s", cfg.IPv6Address)) - } - } - for _, addr := range cfg.LinkLocalIPs { - if parsed := net.ParseIP(addr); parsed == nil || parsed.IsUnspecified() { - errs = append(errs, fmt.Errorf("invalid link-local IP address: %s", addr)) - } - } - - return multierror.Join(errs...) -} diff --git a/vendor/github.com/docker/docker/api/types/network/ipam.go b/vendor/github.com/docker/docker/api/types/network/ipam.go deleted file mode 100644 index f319e1402b08..000000000000 --- a/vendor/github.com/docker/docker/api/types/network/ipam.go +++ /dev/null @@ -1,134 +0,0 @@ -package network - -import ( - "errors" - "fmt" - "net/netip" - - "github.com/docker/docker/internal/multierror" -) - -// IPAM represents IP Address Management -type IPAM struct { - Driver string - Options map[string]string // Per network IPAM driver options - Config []IPAMConfig -} - -// IPAMConfig represents IPAM configurations -type IPAMConfig struct { - Subnet string `json:",omitempty"` - IPRange string `json:",omitempty"` - Gateway string `json:",omitempty"` - AuxAddress map[string]string `json:"AuxiliaryAddresses,omitempty"` -} - -type ipFamily string - -const ( - ip4 ipFamily = "IPv4" - ip6 ipFamily = "IPv6" -) - -// ValidateIPAM checks whether the network's IPAM passed as argument is valid. It returns a joinError of the list of -// errors found. -func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error { - if ipam == nil { - return nil - } - - var errs []error - for _, cfg := range ipam.Config { - subnet, err := netip.ParsePrefix(cfg.Subnet) - if err != nil { - errs = append(errs, fmt.Errorf("invalid subnet %s: invalid CIDR block notation", cfg.Subnet)) - continue - } - subnetFamily := ip4 - if subnet.Addr().Is6() { - subnetFamily = ip6 - } - - if !enableIPv6 && subnetFamily == ip6 { - continue - } - - if subnet != subnet.Masked() { - errs = append(errs, fmt.Errorf("invalid subnet %s: it should be %s", subnet, subnet.Masked())) - } - - if ipRangeErrs := validateIPRange(cfg.IPRange, subnet, subnetFamily); len(ipRangeErrs) > 0 { - errs = append(errs, ipRangeErrs...) - } - - if err := validateAddress(cfg.Gateway, subnet, subnetFamily); err != nil { - errs = append(errs, fmt.Errorf("invalid gateway %s: %w", cfg.Gateway, err)) - } - - for auxName, aux := range cfg.AuxAddress { - if err := validateAddress(aux, subnet, subnetFamily); err != nil { - errs = append(errs, fmt.Errorf("invalid auxiliary address %s: %w", auxName, err)) - } - } - } - - if err := multierror.Join(errs...); err != nil { - return fmt.Errorf("invalid network config:\n%w", err) - } - - return nil -} - -func validateIPRange(ipRange string, subnet netip.Prefix, subnetFamily ipFamily) []error { - if ipRange == "" { - return nil - } - prefix, err := netip.ParsePrefix(ipRange) - if err != nil { - return []error{fmt.Errorf("invalid ip-range %s: invalid CIDR block notation", ipRange)} - } - family := ip4 - if prefix.Addr().Is6() { - family = ip6 - } - - if family != subnetFamily { - return []error{fmt.Errorf("invalid ip-range %s: parent subnet is an %s block", ipRange, subnetFamily)} - } - - var errs []error - if prefix.Bits() < subnet.Bits() { - errs = append(errs, fmt.Errorf("invalid ip-range %s: CIDR block is bigger than its parent subnet %s", ipRange, subnet)) - } - if prefix != prefix.Masked() { - errs = append(errs, fmt.Errorf("invalid ip-range %s: it should be %s", prefix, prefix.Masked())) - } - if !subnet.Overlaps(prefix) { - errs = append(errs, fmt.Errorf("invalid ip-range %s: parent subnet %s doesn't contain ip-range", ipRange, subnet)) - } - - return errs -} - -func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) error { - if address == "" { - return nil - } - addr, err := netip.ParseAddr(address) - if err != nil { - return errors.New("invalid address") - } - family := ip4 - if addr.Is6() { - family = ip6 - } - - if family != subnetFamily { - return fmt.Errorf("parent subnet is an %s block", subnetFamily) - } - if !subnet.Contains(addr) { - return fmt.Errorf("parent subnet %s doesn't contain this address", subnet) - } - - return nil -} diff --git a/vendor/github.com/docker/docker/api/types/network/network.go b/vendor/github.com/docker/docker/api/types/network/network.go deleted file mode 100644 index 4a0cb479848d..000000000000 --- a/vendor/github.com/docker/docker/api/types/network/network.go +++ /dev/null @@ -1,168 +0,0 @@ -package network - -import ( - "time" - - "github.com/docker/docker/api/types/filters" -) - -const ( - // NetworkDefault is a platform-independent alias to choose the platform-specific default network stack. - NetworkDefault = "default" - // NetworkHost is the name of the predefined network used when the NetworkMode host is selected (only available on Linux) - NetworkHost = "host" - // NetworkNone is the name of the predefined network used when the NetworkMode none is selected (available on both Linux and Windows) - NetworkNone = "none" - // NetworkBridge is the name of the default network on Linux - NetworkBridge = "bridge" - // NetworkNat is the name of the default network on Windows - NetworkNat = "nat" -) - -// CreateRequest is the request message sent to the server for network create call. -type CreateRequest struct { - CreateOptions - Name string // Name is the requested name of the network. - - // Deprecated: CheckDuplicate is deprecated since API v1.44, but it defaults to true when sent by the client - // package to older daemons. - CheckDuplicate *bool `json:",omitempty"` -} - -// CreateOptions holds options to create a network. -type CreateOptions struct { - Driver string // Driver is the driver-name used to create the network (e.g. `bridge`, `overlay`) - Scope string // Scope describes the level at which the network exists (e.g. `swarm` for cluster-wide or `local` for machine level). - EnableIPv4 *bool `json:",omitempty"` // EnableIPv4 represents whether to enable IPv4. - EnableIPv6 *bool `json:",omitempty"` // EnableIPv6 represents whether to enable IPv6. - IPAM *IPAM // IPAM is the network's IP Address Management. - Internal bool // Internal represents if the network is used internal only. - Attachable bool // Attachable represents if the global scope is manually attachable by regular containers from workers in swarm mode. - Ingress bool // Ingress indicates the network is providing the routing-mesh for the swarm cluster. - ConfigOnly bool // ConfigOnly creates a config-only network. Config-only networks are place-holder networks for network configurations to be used by other networks. ConfigOnly networks cannot be used directly to run containers or services. - ConfigFrom *ConfigReference // ConfigFrom specifies the source which will provide the configuration for this network. The specified network must be a config-only network; see [CreateOptions.ConfigOnly]. - Options map[string]string // Options specifies the network-specific options to use for when creating the network. - Labels map[string]string // Labels holds metadata specific to the network being created. -} - -// ListOptions holds parameters to filter the list of networks with. -type ListOptions struct { - Filters filters.Args -} - -// InspectOptions holds parameters to inspect network. -type InspectOptions struct { - Scope string - Verbose bool -} - -// ConnectOptions represents the data to be used to connect a container to the -// network. -type ConnectOptions struct { - Container string - EndpointConfig *EndpointSettings `json:",omitempty"` -} - -// DisconnectOptions represents the data to be used to disconnect a container -// from the network. -type DisconnectOptions struct { - Container string - Force bool -} - -// Inspect is the body of the "get network" http response message. -type Inspect struct { - Name string // Name is the name of the network - ID string `json:"Id"` // ID uniquely identifies a network on a single machine - Created time.Time // Created is the time the network created - Scope string // Scope describes the level at which the network exists (e.g. `swarm` for cluster-wide or `local` for machine level) - Driver string // Driver is the Driver name used to create the network (e.g. `bridge`, `overlay`) - EnableIPv4 bool // EnableIPv4 represents whether IPv4 is enabled - EnableIPv6 bool // EnableIPv6 represents whether IPv6 is enabled - IPAM IPAM // IPAM is the network's IP Address Management - Internal bool // Internal represents if the network is used internal only - Attachable bool // Attachable represents if the global scope is manually attachable by regular containers from workers in swarm mode. - Ingress bool // Ingress indicates the network is providing the routing-mesh for the swarm cluster. - ConfigFrom ConfigReference // ConfigFrom specifies the source which will provide the configuration for this network. - ConfigOnly bool // ConfigOnly networks are place-holder networks for network configurations to be used by other networks. ConfigOnly networks cannot be used directly to run containers or services. - Containers map[string]EndpointResource // Containers contains endpoints belonging to the network - Options map[string]string // Options holds the network specific options to use for when creating the network - Labels map[string]string // Labels holds metadata specific to the network being created - Peers []PeerInfo `json:",omitempty"` // List of peer nodes for an overlay network - Services map[string]ServiceInfo `json:",omitempty"` -} - -// Summary is used as response when listing networks. It currently is an alias -// for [Inspect], but may diverge in the future, as not all information may -// be included when listing networks. -type Summary = Inspect - -// Address represents an IP address -type Address struct { - Addr string - PrefixLen int -} - -// PeerInfo represents one peer of an overlay network -type PeerInfo struct { - Name string - IP string -} - -// Task carries the information about one backend task -type Task struct { - Name string - EndpointID string - EndpointIP string - Info map[string]string -} - -// ServiceInfo represents service parameters with the list of service's tasks -type ServiceInfo struct { - VIP string - Ports []string - LocalLBIndex int - Tasks []Task -} - -// EndpointResource contains network resources allocated and used for a -// container in a network. -type EndpointResource struct { - Name string - EndpointID string - MacAddress string - IPv4Address string - IPv6Address string -} - -// NetworkingConfig represents the container's networking configuration for each of its interfaces -// Carries the networking configs specified in the `docker run` and `docker network connect` commands -type NetworkingConfig struct { - EndpointsConfig map[string]*EndpointSettings // Endpoint configs for each connecting network -} - -// ConfigReference specifies the source which provides a network's configuration -type ConfigReference struct { - Network string -} - -var acceptedFilters = map[string]bool{ - "dangling": true, - "driver": true, - "id": true, - "label": true, - "name": true, - "scope": true, - "type": true, -} - -// ValidateFilters validates the list of filter args with the available filters. -func ValidateFilters(filter filters.Args) error { - return filter.Validate(acceptedFilters) -} - -// PruneReport contains the response for Engine API: -// POST "/networks/prune" -type PruneReport struct { - NetworksDeleted []string -} diff --git a/vendor/github.com/docker/docker/api/types/plugin.go b/vendor/github.com/docker/docker/api/types/plugin.go deleted file mode 100644 index abae48b9ab01..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin.go +++ /dev/null @@ -1,203 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// Plugin A plugin for the Engine API -// swagger:model Plugin -type Plugin struct { - - // config - // Required: true - Config PluginConfig `json:"Config"` - - // True if the plugin is running. False if the plugin is not running, only installed. - // Required: true - Enabled bool `json:"Enabled"` - - // Id - ID string `json:"Id,omitempty"` - - // name - // Required: true - Name string `json:"Name"` - - // plugin remote reference used to push/pull the plugin - PluginReference string `json:"PluginReference,omitempty"` - - // settings - // Required: true - Settings PluginSettings `json:"Settings"` -} - -// PluginConfig The config of a plugin. -// swagger:model PluginConfig -type PluginConfig struct { - - // args - // Required: true - Args PluginConfigArgs `json:"Args"` - - // description - // Required: true - Description string `json:"Description"` - - // Docker Version used to create the plugin - DockerVersion string `json:"DockerVersion,omitempty"` - - // documentation - // Required: true - Documentation string `json:"Documentation"` - - // entrypoint - // Required: true - Entrypoint []string `json:"Entrypoint"` - - // env - // Required: true - Env []PluginEnv `json:"Env"` - - // interface - // Required: true - Interface PluginConfigInterface `json:"Interface"` - - // ipc host - // Required: true - IpcHost bool `json:"IpcHost"` - - // linux - // Required: true - Linux PluginConfigLinux `json:"Linux"` - - // mounts - // Required: true - Mounts []PluginMount `json:"Mounts"` - - // network - // Required: true - Network PluginConfigNetwork `json:"Network"` - - // pid host - // Required: true - PidHost bool `json:"PidHost"` - - // propagated mount - // Required: true - PropagatedMount string `json:"PropagatedMount"` - - // user - User PluginConfigUser `json:"User,omitempty"` - - // work dir - // Required: true - WorkDir string `json:"WorkDir"` - - // rootfs - Rootfs *PluginConfigRootfs `json:"rootfs,omitempty"` -} - -// PluginConfigArgs plugin config args -// swagger:model PluginConfigArgs -type PluginConfigArgs struct { - - // description - // Required: true - Description string `json:"Description"` - - // name - // Required: true - Name string `json:"Name"` - - // settable - // Required: true - Settable []string `json:"Settable"` - - // value - // Required: true - Value []string `json:"Value"` -} - -// PluginConfigInterface The interface between Docker and the plugin -// swagger:model PluginConfigInterface -type PluginConfigInterface struct { - - // Protocol to use for clients connecting to the plugin. - ProtocolScheme string `json:"ProtocolScheme,omitempty"` - - // socket - // Required: true - Socket string `json:"Socket"` - - // types - // Required: true - Types []PluginInterfaceType `json:"Types"` -} - -// PluginConfigLinux plugin config linux -// swagger:model PluginConfigLinux -type PluginConfigLinux struct { - - // allow all devices - // Required: true - AllowAllDevices bool `json:"AllowAllDevices"` - - // capabilities - // Required: true - Capabilities []string `json:"Capabilities"` - - // devices - // Required: true - Devices []PluginDevice `json:"Devices"` -} - -// PluginConfigNetwork plugin config network -// swagger:model PluginConfigNetwork -type PluginConfigNetwork struct { - - // type - // Required: true - Type string `json:"Type"` -} - -// PluginConfigRootfs plugin config rootfs -// swagger:model PluginConfigRootfs -type PluginConfigRootfs struct { - - // diff ids - DiffIds []string `json:"diff_ids"` - - // type - Type string `json:"type,omitempty"` -} - -// PluginConfigUser plugin config user -// swagger:model PluginConfigUser -type PluginConfigUser struct { - - // g ID - GID uint32 `json:"GID,omitempty"` - - // UID - UID uint32 `json:"UID,omitempty"` -} - -// PluginSettings Settings that can be modified by users. -// swagger:model PluginSettings -type PluginSettings struct { - - // args - // Required: true - Args []string `json:"Args"` - - // devices - // Required: true - Devices []PluginDevice `json:"Devices"` - - // env - // Required: true - Env []string `json:"Env"` - - // mounts - // Required: true - Mounts []PluginMount `json:"Mounts"` -} diff --git a/vendor/github.com/docker/docker/api/types/plugin_device.go b/vendor/github.com/docker/docker/api/types/plugin_device.go deleted file mode 100644 index 569901067559..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin_device.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// PluginDevice plugin device -// swagger:model PluginDevice -type PluginDevice struct { - - // description - // Required: true - Description string `json:"Description"` - - // name - // Required: true - Name string `json:"Name"` - - // path - // Required: true - Path *string `json:"Path"` - - // settable - // Required: true - Settable []string `json:"Settable"` -} diff --git a/vendor/github.com/docker/docker/api/types/plugin_env.go b/vendor/github.com/docker/docker/api/types/plugin_env.go deleted file mode 100644 index 32962dc2ebea..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin_env.go +++ /dev/null @@ -1,25 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// PluginEnv plugin env -// swagger:model PluginEnv -type PluginEnv struct { - - // description - // Required: true - Description string `json:"Description"` - - // name - // Required: true - Name string `json:"Name"` - - // settable - // Required: true - Settable []string `json:"Settable"` - - // value - // Required: true - Value *string `json:"Value"` -} diff --git a/vendor/github.com/docker/docker/api/types/plugin_interface_type.go b/vendor/github.com/docker/docker/api/types/plugin_interface_type.go deleted file mode 100644 index c82f204e8708..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin_interface_type.go +++ /dev/null @@ -1,21 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// PluginInterfaceType plugin interface type -// swagger:model PluginInterfaceType -type PluginInterfaceType struct { - - // capability - // Required: true - Capability string `json:"Capability"` - - // prefix - // Required: true - Prefix string `json:"Prefix"` - - // version - // Required: true - Version string `json:"Version"` -} diff --git a/vendor/github.com/docker/docker/api/types/plugin_mount.go b/vendor/github.com/docker/docker/api/types/plugin_mount.go deleted file mode 100644 index 5c031cf8b5cc..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin_mount.go +++ /dev/null @@ -1,37 +0,0 @@ -package types - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// PluginMount plugin mount -// swagger:model PluginMount -type PluginMount struct { - - // description - // Required: true - Description string `json:"Description"` - - // destination - // Required: true - Destination string `json:"Destination"` - - // name - // Required: true - Name string `json:"Name"` - - // options - // Required: true - Options []string `json:"Options"` - - // settable - // Required: true - Settable []string `json:"Settable"` - - // source - // Required: true - Source *string `json:"Source"` - - // type - // Required: true - Type string `json:"Type"` -} diff --git a/vendor/github.com/docker/docker/api/types/plugin_responses.go b/vendor/github.com/docker/docker/api/types/plugin_responses.go deleted file mode 100644 index 18f743fcde3a..000000000000 --- a/vendor/github.com/docker/docker/api/types/plugin_responses.go +++ /dev/null @@ -1,71 +0,0 @@ -package types - -import ( - "encoding/json" - "fmt" - "sort" -) - -// PluginsListResponse contains the response for the Engine API -type PluginsListResponse []*Plugin - -// UnmarshalJSON implements json.Unmarshaler for PluginInterfaceType -func (t *PluginInterfaceType) UnmarshalJSON(p []byte) error { - versionIndex := len(p) - prefixIndex := 0 - if len(p) < 2 || p[0] != '"' || p[len(p)-1] != '"' { - return fmt.Errorf("%q is not a plugin interface type", p) - } - p = p[1 : len(p)-1] -loop: - for i, b := range p { - switch b { - case '.': - prefixIndex = i - case '/': - versionIndex = i - break loop - } - } - t.Prefix = string(p[:prefixIndex]) - t.Capability = string(p[prefixIndex+1 : versionIndex]) - if versionIndex < len(p) { - t.Version = string(p[versionIndex+1:]) - } - return nil -} - -// MarshalJSON implements json.Marshaler for PluginInterfaceType -func (t *PluginInterfaceType) MarshalJSON() ([]byte, error) { - return json.Marshal(t.String()) -} - -// String implements fmt.Stringer for PluginInterfaceType -func (t PluginInterfaceType) String() string { - return fmt.Sprintf("%s.%s/%s", t.Prefix, t.Capability, t.Version) -} - -// PluginPrivilege describes a permission the user has to accept -// upon installing a plugin. -type PluginPrivilege struct { - Name string - Description string - Value []string -} - -// PluginPrivileges is a list of PluginPrivilege -type PluginPrivileges []PluginPrivilege - -func (s PluginPrivileges) Len() int { - return len(s) -} - -func (s PluginPrivileges) Less(i, j int) bool { - return s[i].Name < s[j].Name -} - -func (s PluginPrivileges) Swap(i, j int) { - sort.Strings(s[i].Value) - sort.Strings(s[j].Value) - s[i], s[j] = s[j], s[i] -} diff --git a/vendor/github.com/docker/docker/api/types/registry/authconfig.go b/vendor/github.com/docker/docker/api/types/registry/authconfig.go deleted file mode 100644 index 70f732007211..000000000000 --- a/vendor/github.com/docker/docker/api/types/registry/authconfig.go +++ /dev/null @@ -1,110 +0,0 @@ -package registry - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "strings" -) - -// AuthHeader is the name of the header used to send encoded registry -// authorization credentials for registry operations (push/pull). -const AuthHeader = "X-Registry-Auth" - -// RequestAuthConfig is a function interface that clients can supply -// to retry operations after getting an authorization error. -// -// The function must return the [AuthHeader] value ([AuthConfig]), encoded -// in base64url format ([RFC4648, section 5]), which can be decoded by -// [DecodeAuthConfig]. -// -// It must return an error if the privilege request fails. -// -// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 -type RequestAuthConfig func(context.Context) (string, error) - -// AuthConfig contains authorization information for connecting to a Registry. -type AuthConfig struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Auth string `json:"auth,omitempty"` - - // Email is an optional value associated with the username. - // This field is deprecated and will be removed in a later - // version of docker. - Email string `json:"email,omitempty"` - - ServerAddress string `json:"serveraddress,omitempty"` - - // IdentityToken is used to authenticate the user and get - // an access token for the registry. - IdentityToken string `json:"identitytoken,omitempty"` - - // RegistryToken is a bearer token to be sent to a registry - RegistryToken string `json:"registrytoken,omitempty"` -} - -// EncodeAuthConfig serializes the auth configuration as a base64url encoded -// ([RFC4648, section 5]) JSON string for sending through the X-Registry-Auth header. -// -// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 -func EncodeAuthConfig(authConfig AuthConfig) (string, error) { - buf, err := json.Marshal(authConfig) - if err != nil { - return "", errInvalidParameter{err} - } - return base64.URLEncoding.EncodeToString(buf), nil -} - -// DecodeAuthConfig decodes base64url encoded ([RFC4648, section 5]) JSON -// authentication information as sent through the X-Registry-Auth header. -// -// This function always returns an [AuthConfig], even if an error occurs. It is up -// to the caller to decide if authentication is required, and if the error can -// be ignored. -// -// [RFC4648, section 5]: https://tools.ietf.org/html/rfc4648#section-5 -func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) { - if authEncoded == "" { - return &AuthConfig{}, nil - } - - authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded)) - return decodeAuthConfigFromReader(authJSON) -} - -// DecodeAuthConfigBody decodes authentication information as sent as JSON in the -// body of a request. This function is to provide backward compatibility with old -// clients and API versions. Current clients and API versions expect authentication -// to be provided through the X-Registry-Auth header. -// -// Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an -// error occurs. It is up to the caller to decide if authentication is required, -// and if the error can be ignored. -func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) { - return decodeAuthConfigFromReader(rdr) -} - -func decodeAuthConfigFromReader(rdr io.Reader) (*AuthConfig, error) { - authConfig := &AuthConfig{} - if err := json.NewDecoder(rdr).Decode(authConfig); err != nil { - // always return an (empty) AuthConfig to increase compatibility with - // the existing API. - return &AuthConfig{}, invalid(err) - } - return authConfig, nil -} - -func invalid(err error) error { - return errInvalidParameter{fmt.Errorf("invalid X-Registry-Auth header: %w", err)} -} - -type errInvalidParameter struct{ error } - -func (errInvalidParameter) InvalidParameter() {} - -func (e errInvalidParameter) Cause() error { return e.error } - -func (e errInvalidParameter) Unwrap() error { return e.error } diff --git a/vendor/github.com/docker/docker/api/types/registry/authenticate.go b/vendor/github.com/docker/docker/api/types/registry/authenticate.go deleted file mode 100644 index 42cac4430a62..000000000000 --- a/vendor/github.com/docker/docker/api/types/registry/authenticate.go +++ /dev/null @@ -1,21 +0,0 @@ -package registry - -// ---------------------------------------------------------------------------- -// DO NOT EDIT THIS FILE -// This file was generated by `swagger generate operation` -// -// See hack/generate-swagger-api.sh -// ---------------------------------------------------------------------------- - -// AuthenticateOKBody authenticate o k body -// swagger:model AuthenticateOKBody -type AuthenticateOKBody struct { - - // An opaque token used to authenticate a user after a successful login - // Required: true - IdentityToken string `json:"IdentityToken"` - - // The status of the authentication - // Required: true - Status string `json:"Status"` -} diff --git a/vendor/github.com/docker/docker/api/types/registry/registry.go b/vendor/github.com/docker/docker/api/types/registry/registry.go deleted file mode 100644 index 9319c964cdbd..000000000000 --- a/vendor/github.com/docker/docker/api/types/registry/registry.go +++ /dev/null @@ -1,122 +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.23 - -package registry - -import ( - "encoding/json" - "net" - - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ServiceConfig stores daemon registry services configuration. -type ServiceConfig struct { - AllowNondistributableArtifactsCIDRs []*NetIPNet `json:"AllowNondistributableArtifactsCIDRs,omitempty"` // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. - AllowNondistributableArtifactsHostnames []string `json:"AllowNondistributableArtifactsHostnames,omitempty"` // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. - - InsecureRegistryCIDRs []*NetIPNet `json:"InsecureRegistryCIDRs"` - IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"` - Mirrors []string - - // ExtraFields is for internal use to include deprecated fields on older API versions. - ExtraFields map[string]any `json:"-"` -} - -// MarshalJSON implements a custom marshaler to include legacy fields -// in API responses. -func (sc *ServiceConfig) MarshalJSON() ([]byte, error) { - type tmp ServiceConfig - base, err := json.Marshal((*tmp)(sc)) - if err != nil { - return nil, err - } - var merged map[string]any - _ = json.Unmarshal(base, &merged) - - for k, v := range sc.ExtraFields { - merged[k] = v - } - return json.Marshal(merged) -} - -// NetIPNet is the net.IPNet type, which can be marshalled and -// unmarshalled to JSON -type NetIPNet net.IPNet - -// String returns the CIDR notation of ipnet -func (ipnet *NetIPNet) String() string { - return (*net.IPNet)(ipnet).String() -} - -// MarshalJSON returns the JSON representation of the IPNet -func (ipnet *NetIPNet) MarshalJSON() ([]byte, error) { - return json.Marshal((*net.IPNet)(ipnet).String()) -} - -// UnmarshalJSON sets the IPNet from a byte array of JSON -func (ipnet *NetIPNet) UnmarshalJSON(b []byte) error { - var ipnetStr string - if err := json.Unmarshal(b, &ipnetStr); err != nil { - return err - } - _, cidr, err := net.ParseCIDR(ipnetStr) - if err != nil { - return err - } - *ipnet = NetIPNet(*cidr) - return nil -} - -// IndexInfo contains information about a registry -// -// RepositoryInfo Examples: -// -// { -// "Index" : { -// "Name" : "docker.io", -// "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"], -// "Secure" : true, -// "Official" : true, -// }, -// "RemoteName" : "library/debian", -// "LocalName" : "debian", -// "CanonicalName" : "docker.io/debian" -// "Official" : true, -// } -// -// { -// "Index" : { -// "Name" : "127.0.0.1:5000", -// "Mirrors" : [], -// "Secure" : false, -// "Official" : false, -// }, -// "RemoteName" : "user/repo", -// "LocalName" : "127.0.0.1:5000/user/repo", -// "CanonicalName" : "127.0.0.1:5000/user/repo", -// "Official" : false, -// } -type IndexInfo struct { - // Name is the name of the registry, such as "docker.io" - Name string - // Mirrors is a list of mirrors, expressed as URIs - Mirrors []string - // Secure is set to false if the registry is part of the list of - // insecure registries. Insecure registries accept HTTP and/or accept - // HTTPS with certificates from unknown CAs. - Secure bool - // Official indicates whether this is an official registry - Official bool -} - -// DistributionInspect describes the result obtained from contacting the -// registry to retrieve image metadata -type DistributionInspect struct { - // Descriptor contains information about the manifest, including - // the content addressable digest - Descriptor ocispec.Descriptor - // Platforms contains the list of platforms supported by the image, - // obtained by parsing the manifest - Platforms []ocispec.Platform -} diff --git a/vendor/github.com/docker/docker/api/types/registry/search.go b/vendor/github.com/docker/docker/api/types/registry/search.go deleted file mode 100644 index 994ca4c6f96f..000000000000 --- a/vendor/github.com/docker/docker/api/types/registry/search.go +++ /dev/null @@ -1,48 +0,0 @@ -package registry - -import ( - "context" - - "github.com/docker/docker/api/types/filters" -) - -// SearchOptions holds parameters to search images with. -type SearchOptions struct { - RegistryAuth string - - // PrivilegeFunc is a function that clients can supply to retry operations - // after getting an authorization error. This function returns the registry - // authentication header value in base64 encoded format, or an error if the - // privilege request fails. - // - // For details, refer to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. - PrivilegeFunc func(context.Context) (string, error) - Filters filters.Args - Limit int -} - -// SearchResult describes a search result returned from a registry -type SearchResult struct { - // StarCount indicates the number of stars this repository has - StarCount int `json:"star_count"` - // IsOfficial is true if the result is from an official repository. - IsOfficial bool `json:"is_official"` - // Name is the name of the repository - Name string `json:"name"` - // IsAutomated indicates whether the result is automated. - // - // Deprecated: the "is_automated" field is deprecated and will always be "false". - IsAutomated bool `json:"is_automated"` - // Description is a textual description of the repository - Description string `json:"description"` -} - -// SearchResults lists a collection search results returned from a registry -type SearchResults struct { - // Query contains the query string that generated the search results - Query string `json:"query"` - // NumResults indicates the number of results the query returned - NumResults int `json:"num_results"` - // Results is a slice containing the actual results for the search - Results []SearchResult `json:"results"` -} diff --git a/vendor/github.com/docker/docker/api/types/storage/driver_data.go b/vendor/github.com/docker/docker/api/types/storage/driver_data.go deleted file mode 100644 index 009e21309507..000000000000 --- a/vendor/github.com/docker/docker/api/types/storage/driver_data.go +++ /dev/null @@ -1,23 +0,0 @@ -package storage - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// DriverData Information about the storage driver used to store the container's and -// image's filesystem. -// -// swagger:model DriverData -type DriverData struct { - - // Low-level storage metadata, provided as key/value pairs. - // - // This information is driver-specific, and depends on the storage-driver - // in use, and should be used for informational purposes only. - // - // Required: true - Data map[string]string `json:"Data"` - - // Name of the storage driver. - // Required: true - Name string `json:"Name"` -} diff --git a/vendor/github.com/docker/docker/api/types/strslice/strslice.go b/vendor/github.com/docker/docker/api/types/strslice/strslice.go deleted file mode 100644 index bad493fb89fd..000000000000 --- a/vendor/github.com/docker/docker/api/types/strslice/strslice.go +++ /dev/null @@ -1,30 +0,0 @@ -package strslice - -import "encoding/json" - -// StrSlice represents a string or an array of strings. -// We need to override the json decoder to accept both options. -type StrSlice []string - -// UnmarshalJSON decodes the byte slice whether it's a string or an array of -// strings. This method is needed to implement json.Unmarshaler. -func (e *StrSlice) UnmarshalJSON(b []byte) error { - if len(b) == 0 { - // With no input, we preserve the existing value by returning nil and - // leaving the target alone. This allows defining default values for - // the type. - return nil - } - - p := make([]string, 0, 1) - if err := json.Unmarshal(b, &p); err != nil { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - p = append(p, s) - } - - *e = p - return nil -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/config.go b/vendor/github.com/docker/docker/api/types/swarm/config.go deleted file mode 100644 index 80a6ffdb9aaf..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/config.go +++ /dev/null @@ -1,62 +0,0 @@ -package swarm - -import ( - "os" - - "github.com/docker/docker/api/types/filters" -) - -// Config represents a config. -type Config struct { - ID string - Meta - Spec ConfigSpec -} - -// ConfigSpec represents a config specification from a config in swarm -type ConfigSpec struct { - Annotations - - // Data is the data to store as a config. - // - // The maximum allowed size is 1000KB, as defined in [MaxConfigSize]. - // - // [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize - Data []byte `json:",omitempty"` - - // Templating controls whether and how to evaluate the config payload as - // a template. If it is not set, no templating is used. - Templating *Driver `json:",omitempty"` -} - -// ConfigReferenceFileTarget is a file target in a config reference -type ConfigReferenceFileTarget struct { - Name string - UID string - GID string - Mode os.FileMode -} - -// ConfigReferenceRuntimeTarget is a target for a config specifying that it -// isn't mounted into the container but instead has some other purpose. -type ConfigReferenceRuntimeTarget struct{} - -// ConfigReference is a reference to a config in swarm -type ConfigReference struct { - File *ConfigReferenceFileTarget `json:",omitempty"` - Runtime *ConfigReferenceRuntimeTarget `json:",omitempty"` - ConfigID string - ConfigName string -} - -// ConfigCreateResponse contains the information returned to a client -// on the creation of a new config. -type ConfigCreateResponse struct { - // ID is the id of the created config. - ID string -} - -// ConfigListOptions holds parameters to list configs -type ConfigListOptions struct { - Filters filters.Args -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/container.go b/vendor/github.com/docker/docker/api/types/swarm/container.go deleted file mode 100644 index f9416bacca6b..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/container.go +++ /dev/null @@ -1,119 +0,0 @@ -package swarm - -import ( - "time" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" -) - -// DNSConfig specifies DNS related configurations in resolver configuration file (resolv.conf) -// Detailed documentation is available in: -// http://man7.org/linux/man-pages/man5/resolv.conf.5.html -// `nameserver`, `search`, `options` have been supported. -// TODO: `domain` is not supported yet. -type DNSConfig struct { - // Nameservers specifies the IP addresses of the name servers - Nameservers []string `json:",omitempty"` - // Search specifies the search list for host-name lookup - Search []string `json:",omitempty"` - // Options allows certain internal resolver variables to be modified - Options []string `json:",omitempty"` -} - -// SELinuxContext contains the SELinux labels of the container. -type SELinuxContext struct { - Disable bool - - User string - Role string - Type string - Level string -} - -// SeccompMode is the type used for the enumeration of possible seccomp modes -// in SeccompOpts -type SeccompMode string - -const ( - SeccompModeDefault SeccompMode = "default" - SeccompModeUnconfined SeccompMode = "unconfined" - SeccompModeCustom SeccompMode = "custom" -) - -// SeccompOpts defines the options for configuring seccomp on a swarm-managed -// container. -type SeccompOpts struct { - // Mode is the SeccompMode used for the container. - Mode SeccompMode `json:",omitempty"` - // Profile is the custom seccomp profile as a json object to be used with - // the container. Mode should be set to SeccompModeCustom when using a - // custom profile in this manner. - Profile []byte `json:",omitempty"` -} - -// AppArmorMode is type used for the enumeration of possible AppArmor modes in -// AppArmorOpts -type AppArmorMode string - -const ( - AppArmorModeDefault AppArmorMode = "default" - AppArmorModeDisabled AppArmorMode = "disabled" -) - -// AppArmorOpts defines the options for configuring AppArmor on a swarm-managed -// container. Currently, custom AppArmor profiles are not supported. -type AppArmorOpts struct { - Mode AppArmorMode `json:",omitempty"` -} - -// CredentialSpec for managed service account (Windows only) -type CredentialSpec struct { - Config string - File string - Registry string -} - -// Privileges defines the security options for the container. -type Privileges struct { - CredentialSpec *CredentialSpec - SELinuxContext *SELinuxContext - Seccomp *SeccompOpts `json:",omitempty"` - AppArmor *AppArmorOpts `json:",omitempty"` - NoNewPrivileges bool -} - -// ContainerSpec represents the spec of a container. -type ContainerSpec struct { - Image string `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Command []string `json:",omitempty"` - Args []string `json:",omitempty"` - Hostname string `json:",omitempty"` - Env []string `json:",omitempty"` - Dir string `json:",omitempty"` - User string `json:",omitempty"` - Groups []string `json:",omitempty"` - Privileges *Privileges `json:",omitempty"` - Init *bool `json:",omitempty"` - StopSignal string `json:",omitempty"` - TTY bool `json:",omitempty"` - OpenStdin bool `json:",omitempty"` - ReadOnly bool `json:",omitempty"` - Mounts []mount.Mount `json:",omitempty"` - StopGracePeriod *time.Duration `json:",omitempty"` - Healthcheck *container.HealthConfig `json:",omitempty"` - // The format of extra hosts on swarmkit is specified in: - // http://man7.org/linux/man-pages/man5/hosts.5.html - // IP_address canonical_hostname [aliases...] - Hosts []string `json:",omitempty"` - DNSConfig *DNSConfig `json:",omitempty"` - Secrets []*SecretReference `json:",omitempty"` - Configs []*ConfigReference `json:",omitempty"` - Isolation container.Isolation `json:",omitempty"` - Sysctls map[string]string `json:",omitempty"` - CapabilityAdd []string `json:",omitempty"` - CapabilityDrop []string `json:",omitempty"` - Ulimits []*container.Ulimit `json:",omitempty"` - OomScoreAdj int64 `json:",omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/network.go b/vendor/github.com/docker/docker/api/types/swarm/network.go deleted file mode 100644 index 4b8807233993..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/network.go +++ /dev/null @@ -1,121 +0,0 @@ -package swarm - -import ( - "github.com/docker/docker/api/types/network" -) - -// Endpoint represents an endpoint. -type Endpoint struct { - Spec EndpointSpec `json:",omitempty"` - Ports []PortConfig `json:",omitempty"` - VirtualIPs []EndpointVirtualIP `json:",omitempty"` -} - -// EndpointSpec represents the spec of an endpoint. -type EndpointSpec struct { - Mode ResolutionMode `json:",omitempty"` - Ports []PortConfig `json:",omitempty"` -} - -// ResolutionMode represents a resolution mode. -type ResolutionMode string - -const ( - // ResolutionModeVIP VIP - ResolutionModeVIP ResolutionMode = "vip" - // ResolutionModeDNSRR DNSRR - ResolutionModeDNSRR ResolutionMode = "dnsrr" -) - -// PortConfig represents the config of a port. -type PortConfig struct { - Name string `json:",omitempty"` - Protocol PortConfigProtocol `json:",omitempty"` - // TargetPort is the port inside the container - TargetPort uint32 `json:",omitempty"` - // PublishedPort is the port on the swarm hosts - PublishedPort uint32 `json:",omitempty"` - // PublishMode is the mode in which port is published - PublishMode PortConfigPublishMode `json:",omitempty"` -} - -// PortConfigPublishMode represents the mode in which the port is to -// be published. -type PortConfigPublishMode string - -const ( - // PortConfigPublishModeIngress is used for ports published - // for ingress load balancing using routing mesh. - PortConfigPublishModeIngress PortConfigPublishMode = "ingress" - // PortConfigPublishModeHost is used for ports published - // for direct host level access on the host where the task is running. - PortConfigPublishModeHost PortConfigPublishMode = "host" -) - -// PortConfigProtocol represents the protocol of a port. -type PortConfigProtocol string - -const ( - // TODO(stevvooe): These should be used generally, not just for PortConfig. - - // PortConfigProtocolTCP TCP - PortConfigProtocolTCP PortConfigProtocol = "tcp" - // PortConfigProtocolUDP UDP - PortConfigProtocolUDP PortConfigProtocol = "udp" - // PortConfigProtocolSCTP SCTP - PortConfigProtocolSCTP PortConfigProtocol = "sctp" -) - -// EndpointVirtualIP represents the virtual ip of a port. -type EndpointVirtualIP struct { - NetworkID string `json:",omitempty"` - Addr string `json:",omitempty"` -} - -// Network represents a network. -type Network struct { - ID string - Meta - Spec NetworkSpec `json:",omitempty"` - DriverState Driver `json:",omitempty"` - IPAMOptions *IPAMOptions `json:",omitempty"` -} - -// NetworkSpec represents the spec of a network. -type NetworkSpec struct { - Annotations - DriverConfiguration *Driver `json:",omitempty"` - IPv6Enabled bool `json:",omitempty"` - Internal bool `json:",omitempty"` - Attachable bool `json:",omitempty"` - Ingress bool `json:",omitempty"` - IPAMOptions *IPAMOptions `json:",omitempty"` - ConfigFrom *network.ConfigReference `json:",omitempty"` - Scope string `json:",omitempty"` -} - -// NetworkAttachmentConfig represents the configuration of a network attachment. -type NetworkAttachmentConfig struct { - Target string `json:",omitempty"` - Aliases []string `json:",omitempty"` - DriverOpts map[string]string `json:",omitempty"` -} - -// NetworkAttachment represents a network attachment. -type NetworkAttachment struct { - Network Network `json:",omitempty"` - Addresses []string `json:",omitempty"` -} - -// IPAMOptions represents ipam options. -type IPAMOptions struct { - Driver Driver `json:",omitempty"` - Configs []IPAMConfig `json:",omitempty"` -} - -// IPAMConfig represents ipam configuration. -type IPAMConfig struct { - Subnet string `json:",omitempty"` - Range string `json:",omitempty"` - Gateway string `json:",omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime.go b/vendor/github.com/docker/docker/api/types/swarm/runtime.go deleted file mode 100644 index 8a28320f7b85..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime.go +++ /dev/null @@ -1,27 +0,0 @@ -package swarm - -// RuntimeType is the type of runtime used for the TaskSpec -type RuntimeType string - -// RuntimeURL is the proto type url -type RuntimeURL string - -const ( - // RuntimeContainer is the container based runtime - RuntimeContainer RuntimeType = "container" - // RuntimePlugin is the plugin based runtime - RuntimePlugin RuntimeType = "plugin" - // RuntimeNetworkAttachment is the network attachment runtime - RuntimeNetworkAttachment RuntimeType = "attachment" - - // RuntimeURLContainer is the proto url for the container type - RuntimeURLContainer RuntimeURL = "types.docker.com/RuntimeContainer" - // RuntimeURLPlugin is the proto url for the plugin type - RuntimeURLPlugin RuntimeURL = "types.docker.com/RuntimePlugin" -) - -// NetworkAttachmentSpec represents the runtime spec type for network -// attachment tasks -type NetworkAttachmentSpec struct { - ContainerID string -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go b/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go deleted file mode 100644 index 90e572cf9c90..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:generate protoc --gogofaster_out=import_path=github.com/docker/docker/api/types/swarm/runtime:. plugin.proto - -package runtime diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go b/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go deleted file mode 100644 index 32aaf0d51990..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go +++ /dev/null @@ -1,808 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: plugin.proto - -package runtime - -import ( - fmt "fmt" - proto "github.com/gogo/protobuf/proto" - io "io" - math "math" - math_bits "math/bits" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package - -// PluginSpec defines the base payload which clients can specify for creating -// a service with the plugin runtime. -type PluginSpec struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Remote string `protobuf:"bytes,2,opt,name=remote,proto3" json:"remote,omitempty"` - Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges,proto3" json:"privileges,omitempty"` - Disabled bool `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"` - Env []string `protobuf:"bytes,5,rep,name=env,proto3" json:"env,omitempty"` -} - -func (m *PluginSpec) Reset() { *m = PluginSpec{} } -func (m *PluginSpec) String() string { return proto.CompactTextString(m) } -func (*PluginSpec) ProtoMessage() {} -func (*PluginSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_22a625af4bc1cc87, []int{0} -} -func (m *PluginSpec) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PluginSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_PluginSpec.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *PluginSpec) XXX_Merge(src proto.Message) { - xxx_messageInfo_PluginSpec.Merge(m, src) -} -func (m *PluginSpec) XXX_Size() int { - return m.Size() -} -func (m *PluginSpec) XXX_DiscardUnknown() { - xxx_messageInfo_PluginSpec.DiscardUnknown(m) -} - -var xxx_messageInfo_PluginSpec proto.InternalMessageInfo - -func (m *PluginSpec) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *PluginSpec) GetRemote() string { - if m != nil { - return m.Remote - } - return "" -} - -func (m *PluginSpec) GetPrivileges() []*PluginPrivilege { - if m != nil { - return m.Privileges - } - return nil -} - -func (m *PluginSpec) GetDisabled() bool { - if m != nil { - return m.Disabled - } - return false -} - -func (m *PluginSpec) GetEnv() []string { - if m != nil { - return m.Env - } - return nil -} - -// PluginPrivilege describes a permission the user has to accept -// upon installing a plugin. -type PluginPrivilege struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - Value []string `protobuf:"bytes,3,rep,name=value,proto3" json:"value,omitempty"` -} - -func (m *PluginPrivilege) Reset() { *m = PluginPrivilege{} } -func (m *PluginPrivilege) String() string { return proto.CompactTextString(m) } -func (*PluginPrivilege) ProtoMessage() {} -func (*PluginPrivilege) Descriptor() ([]byte, []int) { - return fileDescriptor_22a625af4bc1cc87, []int{1} -} -func (m *PluginPrivilege) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PluginPrivilege) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_PluginPrivilege.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *PluginPrivilege) XXX_Merge(src proto.Message) { - xxx_messageInfo_PluginPrivilege.Merge(m, src) -} -func (m *PluginPrivilege) XXX_Size() int { - return m.Size() -} -func (m *PluginPrivilege) XXX_DiscardUnknown() { - xxx_messageInfo_PluginPrivilege.DiscardUnknown(m) -} - -var xxx_messageInfo_PluginPrivilege proto.InternalMessageInfo - -func (m *PluginPrivilege) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *PluginPrivilege) GetDescription() string { - if m != nil { - return m.Description - } - return "" -} - -func (m *PluginPrivilege) GetValue() []string { - if m != nil { - return m.Value - } - return nil -} - -func init() { - proto.RegisterType((*PluginSpec)(nil), "PluginSpec") - proto.RegisterType((*PluginPrivilege)(nil), "PluginPrivilege") -} - -func init() { proto.RegisterFile("plugin.proto", fileDescriptor_22a625af4bc1cc87) } - -var fileDescriptor_22a625af4bc1cc87 = []byte{ - // 225 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xc8, 0x29, 0x4d, - 0xcf, 0xcc, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x9a, 0xc1, 0xc8, 0xc5, 0x15, 0x00, 0x16, - 0x08, 0x2e, 0x48, 0x4d, 0x16, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d, 0x95, 0x60, 0x54, 0x60, - 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x85, 0xc4, 0xb8, 0xd8, 0x8a, 0x52, 0x73, 0xf3, 0x4b, 0x52, 0x25, - 0x98, 0xc0, 0xa2, 0x50, 0x9e, 0x90, 0x01, 0x17, 0x57, 0x41, 0x51, 0x66, 0x59, 0x66, 0x4e, 0x6a, - 0x7a, 0x6a, 0xb1, 0x04, 0xb3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x80, 0x1e, 0xc4, 0xb0, 0x00, 0x98, - 0x44, 0x10, 0x92, 0x1a, 0x21, 0x29, 0x2e, 0x8e, 0x94, 0xcc, 0xe2, 0xc4, 0xa4, 0x9c, 0xd4, 0x14, - 0x09, 0x16, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0x39, 0x35, 0xaf, 0x4c, - 0x82, 0x55, 0x81, 0x59, 0x83, 0x33, 0x08, 0xc4, 0x54, 0x8a, 0xe5, 0xe2, 0x47, 0x33, 0x0c, 0xab, - 0xf3, 0x14, 0xb8, 0xb8, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, 0x0b, 0x4a, 0x32, 0xf3, 0xf3, 0xa0, - 0x6e, 0x44, 0x16, 0x12, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x05, 0xbb, 0x91, 0x33, - 0x08, 0xc2, 0x71, 0x92, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, - 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x24, 0x36, - 0x70, 0xd0, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x37, 0xea, 0xe2, 0xca, 0x2a, 0x01, 0x00, - 0x00, -} - -func (m *PluginSpec) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *PluginSpec) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *PluginSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Env) > 0 { - for iNdEx := len(m.Env) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Env[iNdEx]) - copy(dAtA[i:], m.Env[iNdEx]) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Env[iNdEx]))) - i-- - dAtA[i] = 0x2a - } - } - if m.Disabled { - i-- - if m.Disabled { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x20 - } - if len(m.Privileges) > 0 { - for iNdEx := len(m.Privileges) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Privileges[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintPlugin(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x1a - } - } - if len(m.Remote) > 0 { - i -= len(m.Remote) - copy(dAtA[i:], m.Remote) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Remote))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *PluginPrivilege) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *PluginPrivilege) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *PluginPrivilege) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Value) > 0 { - for iNdEx := len(m.Value) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Value[iNdEx]) - copy(dAtA[i:], m.Value[iNdEx]) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Value[iNdEx]))) - i-- - dAtA[i] = 0x1a - } - } - if len(m.Description) > 0 { - i -= len(m.Description) - copy(dAtA[i:], m.Description) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Description))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarintPlugin(dAtA []byte, offset int, v uint64) int { - offset -= sovPlugin(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *PluginSpec) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - l = len(m.Remote) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - if len(m.Privileges) > 0 { - for _, e := range m.Privileges { - l = e.Size() - n += 1 + l + sovPlugin(uint64(l)) - } - } - if m.Disabled { - n += 2 - } - if len(m.Env) > 0 { - for _, s := range m.Env { - l = len(s) - n += 1 + l + sovPlugin(uint64(l)) - } - } - return n -} - -func (m *PluginPrivilege) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - l = len(m.Description) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - if len(m.Value) > 0 { - for _, s := range m.Value { - l = len(s) - n += 1 + l + sovPlugin(uint64(l)) - } - } - return n -} - -func sovPlugin(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozPlugin(x uint64) (n int) { - return sovPlugin(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *PluginSpec) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: PluginSpec: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: PluginSpec: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Remote", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Remote = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Privileges", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Privileges = append(m.Privileges, &PluginPrivilege{}) - if err := m.Privileges[len(m.Privileges)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Disabled", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Disabled = bool(v != 0) - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Env", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Env = append(m.Env, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipPlugin(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthPlugin - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *PluginPrivilege) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: PluginPrivilege: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: PluginPrivilege: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Description = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = append(m.Value, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipPlugin(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthPlugin - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipPlugin(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLengthPlugin - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroupPlugin - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLengthPlugin - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowPlugin = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroupPlugin = fmt.Errorf("proto: unexpected end of group") -) diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto b/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto deleted file mode 100644 index e311b36ba2cf..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -// PluginSpec defines the base payload which clients can specify for creating -// a service with the plugin runtime. -message PluginSpec { - string name = 1; - string remote = 2; - repeated PluginPrivilege privileges = 3; - bool disabled = 4; - repeated string env = 5; -} - -// PluginPrivilege describes a permission the user has to accept -// upon installing a plugin. -message PluginPrivilege { - string name = 1; - string description = 2; - repeated string value = 3; -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/service.go b/vendor/github.com/docker/docker/api/types/swarm/service.go deleted file mode 100644 index 56c660c1f0c4..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/service.go +++ /dev/null @@ -1,272 +0,0 @@ -package swarm - -import ( - "time" - - "github.com/docker/docker/api/types/filters" -) - -// Service represents a service. -type Service struct { - ID string - Meta - Spec ServiceSpec `json:",omitempty"` - PreviousSpec *ServiceSpec `json:",omitempty"` - Endpoint Endpoint `json:",omitempty"` - UpdateStatus *UpdateStatus `json:",omitempty"` - - // ServiceStatus is an optional, extra field indicating the number of - // desired and running tasks. It is provided primarily as a shortcut to - // calculating these values client-side, which otherwise would require - // listing all tasks for a service, an operation that could be - // computation and network expensive. - ServiceStatus *ServiceStatus `json:",omitempty"` - - // JobStatus is the status of a Service which is in one of ReplicatedJob or - // GlobalJob modes. It is absent on Replicated and Global services. - JobStatus *JobStatus `json:",omitempty"` -} - -// ServiceSpec represents the spec of a service. -type ServiceSpec struct { - Annotations - - // TaskTemplate defines how the service should construct new tasks when - // orchestrating this service. - TaskTemplate TaskSpec `json:",omitempty"` - Mode ServiceMode `json:",omitempty"` - UpdateConfig *UpdateConfig `json:",omitempty"` - RollbackConfig *UpdateConfig `json:",omitempty"` - - // Networks specifies which networks the service should attach to. - // - // Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead. - Networks []NetworkAttachmentConfig `json:",omitempty"` - EndpointSpec *EndpointSpec `json:",omitempty"` -} - -// ServiceMode represents the mode of a service. -type ServiceMode struct { - Replicated *ReplicatedService `json:",omitempty"` - Global *GlobalService `json:",omitempty"` - ReplicatedJob *ReplicatedJob `json:",omitempty"` - GlobalJob *GlobalJob `json:",omitempty"` -} - -// UpdateState is the state of a service update. -type UpdateState string - -const ( - // UpdateStateUpdating is the updating state. - UpdateStateUpdating UpdateState = "updating" - // UpdateStatePaused is the paused state. - UpdateStatePaused UpdateState = "paused" - // UpdateStateCompleted is the completed state. - UpdateStateCompleted UpdateState = "completed" - // UpdateStateRollbackStarted is the state with a rollback in progress. - UpdateStateRollbackStarted UpdateState = "rollback_started" - // UpdateStateRollbackPaused is the state with a rollback in progress. - UpdateStateRollbackPaused UpdateState = "rollback_paused" - // UpdateStateRollbackCompleted is the state with a rollback in progress. - UpdateStateRollbackCompleted UpdateState = "rollback_completed" -) - -// UpdateStatus reports the status of a service update. -type UpdateStatus struct { - State UpdateState `json:",omitempty"` - StartedAt *time.Time `json:",omitempty"` - CompletedAt *time.Time `json:",omitempty"` - Message string `json:",omitempty"` -} - -// ReplicatedService is a kind of ServiceMode. -type ReplicatedService struct { - Replicas *uint64 `json:",omitempty"` -} - -// GlobalService is a kind of ServiceMode. -type GlobalService struct{} - -// ReplicatedJob is the a type of Service which executes a defined Tasks -// in parallel until the specified number of Tasks have succeeded. -type ReplicatedJob struct { - // MaxConcurrent indicates the maximum number of Tasks that should be - // executing simultaneously for this job at any given time. There may be - // fewer Tasks that MaxConcurrent executing simultaneously; for example, if - // there are fewer than MaxConcurrent tasks needed to reach - // TotalCompletions. - // - // If this field is empty, it will default to a max concurrency of 1. - MaxConcurrent *uint64 `json:",omitempty"` - - // TotalCompletions is the total number of Tasks desired to run to - // completion. - // - // If this field is empty, the value of MaxConcurrent will be used. - TotalCompletions *uint64 `json:",omitempty"` -} - -// GlobalJob is the type of a Service which executes a Task on every Node -// matching the Service's placement constraints. These tasks run to completion -// and then exit. -// -// This type is deliberately empty. -type GlobalJob struct{} - -const ( - // UpdateFailureActionPause PAUSE - UpdateFailureActionPause = "pause" - // UpdateFailureActionContinue CONTINUE - UpdateFailureActionContinue = "continue" - // UpdateFailureActionRollback ROLLBACK - UpdateFailureActionRollback = "rollback" - - // UpdateOrderStopFirst STOP_FIRST - UpdateOrderStopFirst = "stop-first" - // UpdateOrderStartFirst START_FIRST - UpdateOrderStartFirst = "start-first" -) - -// UpdateConfig represents the update configuration. -type UpdateConfig struct { - // Maximum number of tasks to be updated in one iteration. - // 0 means unlimited parallelism. - Parallelism uint64 - - // Amount of time between updates. - Delay time.Duration `json:",omitempty"` - - // FailureAction is the action to take when an update failures. - FailureAction string `json:",omitempty"` - - // Monitor indicates how long to monitor a task for failure after it is - // created. If the task fails by ending up in one of the states - // REJECTED, COMPLETED, or FAILED, within Monitor from its creation, - // this counts as a failure. If it fails after Monitor, it does not - // count as a failure. If Monitor is unspecified, a default value will - // be used. - Monitor time.Duration `json:",omitempty"` - - // MaxFailureRatio is the fraction of tasks that may fail during - // an update before the failure action is invoked. Any task created by - // the current update which ends up in one of the states REJECTED, - // COMPLETED or FAILED within Monitor from its creation counts as a - // failure. The number of failures is divided by the number of tasks - // being updated, and if this fraction is greater than - // MaxFailureRatio, the failure action is invoked. - // - // If the failure action is CONTINUE, there is no effect. - // If the failure action is PAUSE, no more tasks will be updated until - // another update is started. - MaxFailureRatio float32 - - // Order indicates the order of operations when rolling out an updated - // task. Either the old task is shut down before the new task is - // started, or the new task is started before the old task is shut down. - Order string -} - -// ServiceStatus represents the number of running tasks in a service and the -// number of tasks desired to be running. -type ServiceStatus struct { - // RunningTasks is the number of tasks for the service actually in the - // Running state - RunningTasks uint64 - - // DesiredTasks is the number of tasks desired to be running by the - // service. For replicated services, this is the replica count. For global - // services, this is computed by taking the number of tasks with desired - // state of not-Shutdown. - DesiredTasks uint64 - - // CompletedTasks is the number of tasks in the state Completed, if this - // service is in ReplicatedJob or GlobalJob mode. This field must be - // cross-referenced with the service type, because the default value of 0 - // may mean that a service is not in a job mode, or it may mean that the - // job has yet to complete any tasks. - CompletedTasks uint64 -} - -// JobStatus is the status of a job-type service. -type JobStatus struct { - // JobIteration is a value increased each time a Job is executed, - // successfully or otherwise. "Executed", in this case, means the job as a - // whole has been started, not that an individual Task has been launched. A - // job is "Executed" when its ServiceSpec is updated. JobIteration can be - // used to disambiguate Tasks belonging to different executions of a job. - // - // Though JobIteration will increase with each subsequent execution, it may - // not necessarily increase by 1, and so JobIteration should not be used to - // keep track of the number of times a job has been executed. - JobIteration Version - - // LastExecution is the time that the job was last executed, as observed by - // Swarm manager. - LastExecution time.Time `json:",omitempty"` -} - -// ServiceCreateOptions contains the options to use when creating a service. -type ServiceCreateOptions struct { - // EncodedRegistryAuth is the encoded registry authorization credentials to - // use when updating the service. - // - // This field follows the format of the X-Registry-Auth header. - EncodedRegistryAuth string - - // QueryRegistry indicates whether the service update requires - // contacting a registry. A registry may be contacted to retrieve - // the image digest and manifest, which in turn can be used to update - // platform or other information about the service. - QueryRegistry bool -} - -// Values for RegistryAuthFrom in ServiceUpdateOptions -const ( - RegistryAuthFromSpec = "spec" - RegistryAuthFromPreviousSpec = "previous-spec" -) - -// ServiceUpdateOptions contains the options to be used for updating services. -type ServiceUpdateOptions struct { - // EncodedRegistryAuth is the encoded registry authorization credentials to - // use when updating the service. - // - // This field follows the format of the X-Registry-Auth header. - EncodedRegistryAuth string - - // TODO(stevvooe): Consider moving the version parameter of ServiceUpdate - // into this field. While it does open API users up to racy writes, most - // users may not need that level of consistency in practice. - - // RegistryAuthFrom specifies where to find the registry authorization - // credentials if they are not given in EncodedRegistryAuth. Valid - // values are "spec" and "previous-spec". - RegistryAuthFrom string - - // Rollback indicates whether a server-side rollback should be - // performed. When this is set, the provided spec will be ignored. - // The valid values are "previous" and "none". An empty value is the - // same as "none". - Rollback string - - // QueryRegistry indicates whether the service update requires - // contacting a registry. A registry may be contacted to retrieve - // the image digest and manifest, which in turn can be used to update - // platform or other information about the service. - QueryRegistry bool -} - -// ServiceListOptions holds parameters to list services with. -type ServiceListOptions struct { - Filters filters.Args - - // Status indicates whether the server should include the service task - // count of running and desired tasks. - Status bool -} - -// ServiceInspectOptions holds parameters related to the "service inspect" -// operation. -type ServiceInspectOptions struct { - InsertDefaults bool -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/service_create_response.go b/vendor/github.com/docker/docker/api/types/swarm/service_create_response.go deleted file mode 100644 index 9a268ff1b93a..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/service_create_response.go +++ /dev/null @@ -1,20 +0,0 @@ -package swarm - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// ServiceCreateResponse contains the information returned to a client on the -// creation of a new service. -// -// swagger:model ServiceCreateResponse -type ServiceCreateResponse struct { - - // The ID of the created service. - ID string `json:"ID,omitempty"` - - // Optional warning message. - // - // FIXME(thaJeztah): this should have "omitempty" in the generated type. - // - Warnings []string `json:"Warnings"` -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/service_update_response.go b/vendor/github.com/docker/docker/api/types/swarm/service_update_response.go deleted file mode 100644 index 0417467dae39..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/service_update_response.go +++ /dev/null @@ -1,12 +0,0 @@ -package swarm - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// ServiceUpdateResponse service update response -// swagger:model ServiceUpdateResponse -type ServiceUpdateResponse struct { - - // Optional warning messages - Warnings []string `json:"Warnings"` -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/task.go b/vendor/github.com/docker/docker/api/types/swarm/task.go deleted file mode 100644 index 4dc95e8b1dde..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/task.go +++ /dev/null @@ -1,231 +0,0 @@ -package swarm - -import ( - "time" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm/runtime" -) - -// TaskState represents the state of a task. -type TaskState string - -const ( - // TaskStateNew NEW - TaskStateNew TaskState = "new" - // TaskStateAllocated ALLOCATED - TaskStateAllocated TaskState = "allocated" - // TaskStatePending PENDING - TaskStatePending TaskState = "pending" - // TaskStateAssigned ASSIGNED - TaskStateAssigned TaskState = "assigned" - // TaskStateAccepted ACCEPTED - TaskStateAccepted TaskState = "accepted" - // TaskStatePreparing PREPARING - TaskStatePreparing TaskState = "preparing" - // TaskStateReady READY - TaskStateReady TaskState = "ready" - // TaskStateStarting STARTING - TaskStateStarting TaskState = "starting" - // TaskStateRunning RUNNING - TaskStateRunning TaskState = "running" - // TaskStateComplete COMPLETE - TaskStateComplete TaskState = "complete" - // TaskStateShutdown SHUTDOWN - TaskStateShutdown TaskState = "shutdown" - // TaskStateFailed FAILED - TaskStateFailed TaskState = "failed" - // TaskStateRejected REJECTED - TaskStateRejected TaskState = "rejected" - // TaskStateRemove REMOVE - TaskStateRemove TaskState = "remove" - // TaskStateOrphaned ORPHANED - TaskStateOrphaned TaskState = "orphaned" -) - -// Task represents a task. -type Task struct { - ID string - Meta - Annotations - - Spec TaskSpec `json:",omitempty"` - ServiceID string `json:",omitempty"` - Slot int `json:",omitempty"` - NodeID string `json:",omitempty"` - Status TaskStatus `json:",omitempty"` - DesiredState TaskState `json:",omitempty"` - NetworksAttachments []NetworkAttachment `json:",omitempty"` - GenericResources []GenericResource `json:",omitempty"` - - // JobIteration is the JobIteration of the Service that this Task was - // spawned from, if the Service is a ReplicatedJob or GlobalJob. This is - // used to determine which Tasks belong to which run of the job. This field - // is absent if the Service mode is Replicated or Global. - JobIteration *Version `json:",omitempty"` - - // Volumes is the list of VolumeAttachments for this task. It specifies - // which particular volumes are to be used by this particular task, and - // fulfilling what mounts in the spec. - Volumes []VolumeAttachment -} - -// TaskSpec represents the spec of a task. -type TaskSpec struct { - // ContainerSpec, NetworkAttachmentSpec, and PluginSpec are mutually exclusive. - // PluginSpec is only used when the `Runtime` field is set to `plugin` - // NetworkAttachmentSpec is used if the `Runtime` field is set to - // `attachment`. - ContainerSpec *ContainerSpec `json:",omitempty"` - PluginSpec *runtime.PluginSpec `json:",omitempty"` - NetworkAttachmentSpec *NetworkAttachmentSpec `json:",omitempty"` - - Resources *ResourceRequirements `json:",omitempty"` - RestartPolicy *RestartPolicy `json:",omitempty"` - Placement *Placement `json:",omitempty"` - Networks []NetworkAttachmentConfig `json:",omitempty"` - - // LogDriver specifies the LogDriver to use for tasks created from this - // spec. If not present, the one on cluster default on swarm.Spec will be - // used, finally falling back to the engine default if not specified. - LogDriver *Driver `json:",omitempty"` - - // ForceUpdate is a counter that triggers an update even if no relevant - // parameters have been changed. - ForceUpdate uint64 - - Runtime RuntimeType `json:",omitempty"` -} - -// Resources represents resources (CPU/Memory) which can be advertised by a -// node and requested to be reserved for a task. -type Resources struct { - NanoCPUs int64 `json:",omitempty"` - MemoryBytes int64 `json:",omitempty"` - GenericResources []GenericResource `json:",omitempty"` -} - -// Limit describes limits on resources which can be requested by a task. -type Limit struct { - NanoCPUs int64 `json:",omitempty"` - MemoryBytes int64 `json:",omitempty"` - Pids int64 `json:",omitempty"` -} - -// GenericResource represents a "user defined" resource which can -// be either an integer (e.g: SSD=3) or a string (e.g: SSD=sda1) -type GenericResource struct { - NamedResourceSpec *NamedGenericResource `json:",omitempty"` - DiscreteResourceSpec *DiscreteGenericResource `json:",omitempty"` -} - -// NamedGenericResource represents a "user defined" resource which is defined -// as a string. -// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...) -// Value is used to identify the resource (GPU="UUID-1", FPGA="/dev/sdb5", ...) -type NamedGenericResource struct { - Kind string `json:",omitempty"` - Value string `json:",omitempty"` -} - -// DiscreteGenericResource represents a "user defined" resource which is defined -// as an integer -// "Kind" is used to describe the Kind of a resource (e.g: "GPU", "FPGA", "SSD", ...) -// Value is used to count the resource (SSD=5, HDD=3, ...) -type DiscreteGenericResource struct { - Kind string `json:",omitempty"` - Value int64 `json:",omitempty"` -} - -// ResourceRequirements represents resources requirements. -type ResourceRequirements struct { - Limits *Limit `json:",omitempty"` - Reservations *Resources `json:",omitempty"` -} - -// Placement represents orchestration parameters. -type Placement struct { - Constraints []string `json:",omitempty"` - Preferences []PlacementPreference `json:",omitempty"` - MaxReplicas uint64 `json:",omitempty"` - - // Platforms stores all the platforms that the image can run on. - // This field is used in the platform filter for scheduling. If empty, - // then the platform filter is off, meaning there are no scheduling restrictions. - Platforms []Platform `json:",omitempty"` -} - -// PlacementPreference provides a way to make the scheduler aware of factors -// such as topology. -type PlacementPreference struct { - Spread *SpreadOver -} - -// SpreadOver is a scheduling preference that instructs the scheduler to spread -// tasks evenly over groups of nodes identified by labels. -type SpreadOver struct { - // label descriptor, such as engine.labels.az - SpreadDescriptor string -} - -// RestartPolicy represents the restart policy. -type RestartPolicy struct { - Condition RestartPolicyCondition `json:",omitempty"` - Delay *time.Duration `json:",omitempty"` - MaxAttempts *uint64 `json:",omitempty"` - Window *time.Duration `json:",omitempty"` -} - -// RestartPolicyCondition represents when to restart. -type RestartPolicyCondition string - -const ( - // RestartPolicyConditionNone NONE - RestartPolicyConditionNone RestartPolicyCondition = "none" - // RestartPolicyConditionOnFailure ON_FAILURE - RestartPolicyConditionOnFailure RestartPolicyCondition = "on-failure" - // RestartPolicyConditionAny ANY - RestartPolicyConditionAny RestartPolicyCondition = "any" -) - -// TaskStatus represents the status of a task. -type TaskStatus struct { - Timestamp time.Time `json:",omitempty"` - State TaskState `json:",omitempty"` - Message string `json:",omitempty"` - Err string `json:",omitempty"` - ContainerStatus *ContainerStatus `json:",omitempty"` - PortStatus PortStatus `json:",omitempty"` -} - -// ContainerStatus represents the status of a container. -type ContainerStatus struct { - ContainerID string - PID int - ExitCode int -} - -// PortStatus represents the port status of a task's host ports whose -// service has published host ports -type PortStatus struct { - Ports []PortConfig `json:",omitempty"` -} - -// VolumeAttachment contains the associating a Volume to a Task. -type VolumeAttachment struct { - // ID is the Swarmkit ID of the Volume. This is not the CSI VolumeId. - ID string `json:",omitempty"` - - // Source, together with Target, indicates the Mount, as specified in the - // ContainerSpec, that this volume fulfills. - Source string `json:",omitempty"` - - // Target, together with Source, indicates the Mount, as specified - // in the ContainerSpec, that this volume fulfills. - Target string `json:",omitempty"` -} - -// TaskListOptions holds parameters to list tasks with. -type TaskListOptions struct { - Filters filters.Args -} diff --git a/vendor/github.com/docker/docker/api/types/system/disk_usage.go b/vendor/github.com/docker/docker/api/types/system/disk_usage.go deleted file mode 100644 index 99078cf196d0..000000000000 --- a/vendor/github.com/docker/docker/api/types/system/disk_usage.go +++ /dev/null @@ -1,17 +0,0 @@ -package system - -import ( - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/volume" -) - -// DiskUsage contains response of Engine API for API 1.49 and greater: -// GET "/system/df" -type DiskUsage struct { - Images *image.DiskUsage - Containers *container.DiskUsage - Volumes *volume.DiskUsage - BuildCache *build.CacheDiskUsage -} diff --git a/vendor/github.com/docker/docker/api/types/system/info.go b/vendor/github.com/docker/docker/api/types/system/info.go deleted file mode 100644 index 047639ed91e2..000000000000 --- a/vendor/github.com/docker/docker/api/types/system/info.go +++ /dev/null @@ -1,170 +0,0 @@ -package system - -import ( - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/swarm" -) - -// Info contains response of Engine API: -// GET "/info" -type Info struct { - ID string - Containers int - ContainersRunning int - ContainersPaused int - ContainersStopped int - Images int - Driver string - DriverStatus [][2]string - SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API - Plugins PluginsInfo - MemoryLimit bool - SwapLimit bool - KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes - KernelMemoryTCP bool `json:",omitempty"` // KernelMemoryTCP is not supported on cgroups v2. - CPUCfsPeriod bool `json:"CpuCfsPeriod"` - CPUCfsQuota bool `json:"CpuCfsQuota"` - CPUShares bool - CPUSet bool - PidsLimit bool - IPv4Forwarding bool - Debug bool - NFd int - OomKillDisable bool - NGoroutines int - SystemTime string - LoggingDriver string - CgroupDriver string - CgroupVersion string `json:",omitempty"` - NEventsListener int - KernelVersion string - OperatingSystem string - OSVersion string - OSType string - Architecture string - IndexServerAddress string - RegistryConfig *registry.ServiceConfig - NCPU int - MemTotal int64 - GenericResources []swarm.GenericResource - DockerRootDir string - HTTPProxy string `json:"HttpProxy"` - HTTPSProxy string `json:"HttpsProxy"` - NoProxy string - Name string - Labels []string - ExperimentalBuild bool - ServerVersion string - Runtimes map[string]RuntimeWithStatus - DefaultRuntime string - Swarm swarm.Info - // LiveRestoreEnabled determines whether containers should be kept - // running when the daemon is shutdown or upon daemon start if - // running containers are detected - LiveRestoreEnabled bool - Isolation container.Isolation - InitBinary string - ContainerdCommit Commit - RuncCommit Commit - InitCommit Commit - SecurityOptions []string - ProductLicense string `json:",omitempty"` - DefaultAddressPools []NetworkAddressPool `json:",omitempty"` - FirewallBackend *FirewallInfo `json:"FirewallBackend,omitempty"` - CDISpecDirs []string - DiscoveredDevices []DeviceInfo `json:",omitempty"` - - Containerd *ContainerdInfo `json:",omitempty"` - - // Warnings contains a slice of warnings that occurred while collecting - // system information. These warnings are intended to be informational - // messages for the user, and are not intended to be parsed / used for - // other purposes, as they do not have a fixed format. - Warnings []string -} - -// ContainerdInfo holds information about the containerd instance used by the daemon. -type ContainerdInfo struct { - // Address is the path to the containerd socket. - Address string `json:",omitempty"` - // Namespaces is the containerd namespaces used by the daemon. - Namespaces ContainerdNamespaces -} - -// ContainerdNamespaces reflects the containerd namespaces used by the daemon. -// -// These namespaces can be configured in the daemon configuration, and are -// considered to be used exclusively by the daemon, -// -// As these namespaces are considered to be exclusively accessed -// by the daemon, it is not recommended to change these values, -// or to change them to a value that is used by other systems, -// such as cri-containerd. -type ContainerdNamespaces struct { - // Containers holds the default containerd namespace used for - // containers managed by the daemon. - // - // The default namespace for containers is "moby", but will be - // suffixed with the `.` of the remapped `root` if - // user-namespaces are enabled and the containerd image-store - // is used. - Containers string - - // Plugins holds the default containerd namespace used for - // plugins managed by the daemon. - // - // The default namespace for plugins is "moby", but will be - // suffixed with the `.` of the remapped `root` if - // user-namespaces are enabled and the containerd image-store - // is used. - Plugins string -} - -// PluginsInfo is a temp struct holding Plugins name -// registered with docker daemon. It is used by [Info] struct -type PluginsInfo struct { - // List of Volume plugins registered - Volume []string - // List of Network plugins registered - Network []string - // List of Authorization plugins registered - Authorization []string - // List of Log plugins registered - Log []string -} - -// Commit holds the Git-commit (SHA1) that a binary was built from, as reported -// in the version-string of external tools, such as containerd, or runC. -type Commit struct { - // ID is the actual commit ID or version of external tool. - ID string - - // Expected is the commit ID of external tool expected by dockerd as set at build time. - // - // Deprecated: this field is no longer used in API v1.49, but kept for backward-compatibility with older API versions. - Expected string `json:",omitempty"` -} - -// NetworkAddressPool is a temp struct used by [Info] struct. -type NetworkAddressPool struct { - Base string - Size int -} - -// FirewallInfo describes the firewall backend. -type FirewallInfo struct { - // Driver is the name of the firewall backend driver. - Driver string `json:"Driver"` - // Info is a list of label/value pairs, containing information related to the firewall. - Info [][2]string `json:"Info,omitempty"` -} - -// DeviceInfo represents a discoverable device from a device driver. -type DeviceInfo struct { - // Source indicates the origin device driver. - Source string `json:"Source"` - // ID is the unique identifier for the device. - // Example: CDI FQDN like "vendor.com/gpu=0", or other driver-specific device ID - ID string `json:"ID"` -} diff --git a/vendor/github.com/docker/docker/api/types/system/runtime.go b/vendor/github.com/docker/docker/api/types/system/runtime.go deleted file mode 100644 index d077295a0d31..000000000000 --- a/vendor/github.com/docker/docker/api/types/system/runtime.go +++ /dev/null @@ -1,20 +0,0 @@ -package system - -// Runtime describes an OCI runtime -type Runtime struct { - // "Legacy" runtime configuration for runc-compatible runtimes. - - Path string `json:"path,omitempty"` - Args []string `json:"runtimeArgs,omitempty"` - - // Shimv2 runtime configuration. Mutually exclusive with the legacy config above. - - Type string `json:"runtimeType,omitempty"` - Options map[string]interface{} `json:"options,omitempty"` -} - -// RuntimeWithStatus extends [Runtime] to hold [RuntimeStatus]. -type RuntimeWithStatus struct { - Runtime - Status map[string]string `json:"status,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/system/security_opts.go b/vendor/github.com/docker/docker/api/types/system/security_opts.go deleted file mode 100644 index edff3eb1acc8..000000000000 --- a/vendor/github.com/docker/docker/api/types/system/security_opts.go +++ /dev/null @@ -1,48 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "strings" -) - -// SecurityOpt contains the name and options of a security option -type SecurityOpt struct { - Name string - Options []KeyValue -} - -// DecodeSecurityOptions decodes a security options string slice to a -// type-safe [SecurityOpt]. -func DecodeSecurityOptions(opts []string) ([]SecurityOpt, error) { - so := []SecurityOpt{} - for _, opt := range opts { - // support output from a < 1.13 docker daemon - if !strings.Contains(opt, "=") { - so = append(so, SecurityOpt{Name: opt}) - continue - } - secopt := SecurityOpt{} - for _, s := range strings.Split(opt, ",") { - k, v, ok := strings.Cut(s, "=") - if !ok { - return nil, fmt.Errorf("invalid security option %q", s) - } - if k == "" || v == "" { - return nil, errors.New("invalid empty security option") - } - if k == "name" { - secopt.Name = v - continue - } - secopt.Options = append(secopt.Options, KeyValue{Key: k, Value: v}) - } - so = append(so, secopt) - } - return so, nil -} - -// KeyValue holds a key/value pair. -type KeyValue struct { - Key, Value string -} diff --git a/vendor/github.com/docker/docker/api/types/time/timestamp.go b/vendor/github.com/docker/docker/api/types/time/timestamp.go deleted file mode 100644 index 0e1df38a43f8..000000000000 --- a/vendor/github.com/docker/docker/api/types/time/timestamp.go +++ /dev/null @@ -1,131 +0,0 @@ -package time - -import ( - "fmt" - "math" - "strconv" - "strings" - "time" -) - -// These are additional predefined layouts for use in Time.Format and Time.Parse -// with --since and --until parameters for `docker logs` and `docker events` -const ( - rFC3339Local = "2006-01-02T15:04:05" // RFC3339 with local timezone - rFC3339NanoLocal = "2006-01-02T15:04:05.999999999" // RFC3339Nano with local timezone - dateWithZone = "2006-01-02Z07:00" // RFC3339 with time at 00:00:00 - dateLocal = "2006-01-02" // RFC3339 with local timezone and time at 00:00:00 -) - -// GetTimestamp tries to parse given string as golang duration, -// then RFC3339 time and finally as a Unix timestamp. If -// any of these were successful, it returns a Unix timestamp -// as string otherwise returns the given value back. -// In case of duration input, the returned timestamp is computed -// as the given reference time minus the amount of the duration. -func GetTimestamp(value string, reference time.Time) (string, error) { - if d, err := time.ParseDuration(value); value != "0" && err == nil { - return strconv.FormatInt(reference.Add(-d).Unix(), 10), nil - } - - var format string - // if the string has a Z or a + or three dashes use parse otherwise use parseinlocation - parseInLocation := !strings.ContainsAny(value, "zZ+") && strings.Count(value, "-") != 3 - - if strings.Contains(value, ".") { - if parseInLocation { - format = rFC3339NanoLocal - } else { - format = time.RFC3339Nano - } - } else if strings.Contains(value, "T") { - // we want the number of colons in the T portion of the timestamp - tcolons := strings.Count(value, ":") - // if parseInLocation is off and we have a +/- zone offset (not Z) then - // there will be an extra colon in the input for the tz offset subtract that - // colon from the tcolons count - if !parseInLocation && !strings.ContainsAny(value, "zZ") && tcolons > 0 { - tcolons-- - } - if parseInLocation { - switch tcolons { - case 0: - format = "2006-01-02T15" - case 1: - format = "2006-01-02T15:04" - default: - format = rFC3339Local - } - } else { - switch tcolons { - case 0: - format = "2006-01-02T15Z07:00" - case 1: - format = "2006-01-02T15:04Z07:00" - default: - format = time.RFC3339 - } - } - } else if parseInLocation { - format = dateLocal - } else { - format = dateWithZone - } - - var t time.Time - var err error - - if parseInLocation { - t, err = time.ParseInLocation(format, value, time.FixedZone(reference.Zone())) - } else { - t, err = time.Parse(format, value) - } - - if err != nil { - // if there is a `-` then it's an RFC3339 like timestamp - if strings.Contains(value, "-") { - return "", err // was probably an RFC3339 like timestamp but the parser failed with an error - } - if _, _, err := parseTimestamp(value); err != nil { - return "", fmt.Errorf("failed to parse value as time or duration: %q", value) - } - return value, nil // unix timestamp in and out case (meaning: the value passed at the command line is already in the right format for passing to the server) - } - - return fmt.Sprintf("%d.%09d", t.Unix(), int64(t.Nanosecond())), nil -} - -// ParseTimestamps returns seconds and nanoseconds from a timestamp that has -// the format ("%d.%09d", time.Unix(), int64(time.Nanosecond())). -// If the incoming nanosecond portion is longer than 9 digits it is truncated. -// The expectation is that the seconds and nanoseconds will be used to create a -// time variable. For example: -// -// seconds, nanoseconds, _ := ParseTimestamp("1136073600.000000001",0) -// since := time.Unix(seconds, nanoseconds) -// -// returns seconds as defaultSeconds if value == "" -func ParseTimestamps(value string, defaultSeconds int64) (seconds int64, nanoseconds int64, _ error) { - if value == "" { - return defaultSeconds, 0, nil - } - return parseTimestamp(value) -} - -func parseTimestamp(value string) (seconds int64, nanoseconds int64, _ error) { - s, n, ok := strings.Cut(value, ".") - sec, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return sec, 0, err - } - if !ok { - return sec, 0, nil - } - nsec, err := strconv.ParseInt(n, 10, 64) - if err != nil { - return sec, nsec, err - } - // should already be in nanoseconds but just in case convert n to nanoseconds - nsec = int64(float64(nsec) * math.Pow(float64(10), float64(9-len(n)))) - return sec, nsec, nil -} diff --git a/vendor/github.com/docker/docker/api/types/types.go b/vendor/github.com/docker/docker/api/types/types.go deleted file mode 100644 index 8bbadeb208e3..000000000000 --- a/vendor/github.com/docker/docker/api/types/types.go +++ /dev/null @@ -1,103 +0,0 @@ -package types - -import ( - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/volume" -) - -const ( - // MediaTypeRawStream is vendor specific MIME-Type set for raw TTY streams - MediaTypeRawStream = "application/vnd.docker.raw-stream" - - // MediaTypeMultiplexedStream is vendor specific MIME-Type set for stdin/stdout/stderr multiplexed streams - MediaTypeMultiplexedStream = "application/vnd.docker.multiplexed-stream" -) - -// Ping contains response of Engine API: -// GET "/_ping" -type Ping struct { - APIVersion string - OSType string - Experimental bool - BuilderVersion build.BuilderVersion - - // SwarmStatus provides information about the current swarm status of the - // engine, obtained from the "Swarm" header in the API response. - // - // It can be a nil struct if the API version does not provide this header - // 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 - // endpoint. - SwarmStatus *swarm.Status -} - -// ComponentVersion describes the version information for a specific component. -type ComponentVersion struct { - Name string - Version string - Details map[string]string `json:",omitempty"` -} - -// Version contains response of Engine API: -// GET "/version" -type Version struct { - Platform struct{ Name string } `json:",omitempty"` - Components []ComponentVersion `json:",omitempty"` - - // The following fields are deprecated, they relate to the Engine component and are kept for backwards compatibility - - Version string - APIVersion string `json:"ApiVersion"` - MinAPIVersion string `json:"MinAPIVersion,omitempty"` - GitCommit string - GoVersion string - Os string - Arch string - KernelVersion string `json:",omitempty"` - Experimental bool `json:",omitempty"` - BuildTime string `json:",omitempty"` -} - -// DiskUsageObject represents an object type used for disk usage query filtering. -type DiskUsageObject string - -const ( - // ContainerObject represents a container DiskUsageObject. - ContainerObject DiskUsageObject = "container" - // ImageObject represents an image DiskUsageObject. - ImageObject DiskUsageObject = "image" - // VolumeObject represents a volume DiskUsageObject. - VolumeObject DiskUsageObject = "volume" - // BuildCacheObject represents a build-cache DiskUsageObject. - BuildCacheObject DiskUsageObject = "build-cache" -) - -// DiskUsageOptions holds parameters for system disk usage query. -type DiskUsageOptions struct { - // Types specifies what object types to include in the response. If empty, - // all object types are returned. - Types []DiskUsageObject -} - -// DiskUsage contains response of Engine API: -// GET "/system/df" -type DiskUsage struct { - LayersSize int64 - Images []*image.Summary - Containers []*container.Summary - Volumes []*volume.Volume - BuildCache []*build.CacheRecord - BuilderSize int64 `json:",omitempty"` // Deprecated: deprecated in API 1.38, and no longer used since API 1.40. -} - -// PushResult contains the tag, manifest digest, and manifest size from the -// push. It's used to signal this information to the trust code in the client -// so it can sign the manifest if necessary. -type PushResult struct { - Tag string - Digest string - Size int -} diff --git a/vendor/github.com/docker/docker/api/types/types_deprecated.go b/vendor/github.com/docker/docker/api/types/types_deprecated.go deleted file mode 100644 index 8456a45607e2..000000000000 --- a/vendor/github.com/docker/docker/api/types/types_deprecated.go +++ /dev/null @@ -1,241 +0,0 @@ -package types - -import ( - "context" - - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/common" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/storage" - "github.com/docker/docker/api/types/swarm" -) - -// IDResponse Response to an API call that returns just an Id. -// -// Deprecated: use either [container.CommitResponse] or [container.ExecCreateResponse]. It will be removed in the next release. -type IDResponse = common.IDResponse - -// ContainerJSONBase contains response of Engine API GET "/containers/{name:.*}/json" -// for API version 1.18 and older. -// -// Deprecated: use [container.InspectResponse] or [container.ContainerJSONBase]. It will be removed in the next release. -type ContainerJSONBase = container.ContainerJSONBase - -// ContainerJSON is the response for the GET "/containers/{name:.*}/json" -// endpoint. -// -// Deprecated: use [container.InspectResponse]. It will be removed in the next release. -type ContainerJSON = container.InspectResponse - -// Container contains response of Engine API: -// GET "/containers/json" -// -// Deprecated: use [container.Summary]. -type Container = container.Summary - -// ContainerState stores container's running state -// -// Deprecated: use [container.State]. -type ContainerState = container.State - -// NetworkSettings exposes the network settings in the api. -// -// Deprecated: use [container.NetworkSettings]. -type NetworkSettings = container.NetworkSettings - -// NetworkSettingsBase holds networking state for a container when inspecting it. -// -// Deprecated: use [container.NetworkSettingsBase]. -type NetworkSettingsBase = container.NetworkSettingsBase - -// DefaultNetworkSettings holds network information -// during the 2 release deprecation period. -// It will be removed in Docker 1.11. -// -// Deprecated: use [container.DefaultNetworkSettings]. -type DefaultNetworkSettings = container.DefaultNetworkSettings - -// SummaryNetworkSettings provides a summary of container's networks -// in /containers/json. -// -// Deprecated: use [container.NetworkSettingsSummary]. -type SummaryNetworkSettings = container.NetworkSettingsSummary - -// Health states -const ( - NoHealthcheck = container.NoHealthcheck // Deprecated: use [container.NoHealthcheck]. - Starting = container.Starting // Deprecated: use [container.Starting]. - Healthy = container.Healthy // Deprecated: use [container.Healthy]. - Unhealthy = container.Unhealthy // Deprecated: use [container.Unhealthy]. -) - -// Health stores information about the container's healthcheck results. -// -// Deprecated: use [container.Health]. -type Health = container.Health - -// HealthcheckResult stores information about a single run of a healthcheck probe. -// -// Deprecated: use [container.HealthcheckResult]. -type HealthcheckResult = container.HealthcheckResult - -// MountPoint represents a mount point configuration inside the container. -// This is used for reporting the mountpoints in use by a container. -// -// Deprecated: use [container.MountPoint]. -type MountPoint = container.MountPoint - -// Port An open port on a container -// -// Deprecated: use [container.Port]. -type Port = container.Port - -// GraphDriverData Information about the storage driver used to store the container's and -// image's filesystem. -// -// Deprecated: use [storage.DriverData]. -type GraphDriverData = storage.DriverData - -// RootFS returns Image's RootFS description including the layer IDs. -// -// Deprecated: use [image.RootFS]. -type RootFS = image.RootFS - -// ImageInspect contains response of Engine API: -// GET "/images/{name:.*}/json" -// -// Deprecated: use [image.InspectResponse]. -type ImageInspect = image.InspectResponse - -// RequestPrivilegeFunc is a function interface that clients can supply to -// retry operations after getting an authorization error. -// This function returns the registry authentication header value in base64 -// format, or an error if the privilege request fails. -// -// Deprecated: moved to [github.com/docker/docker/api/types/registry.RequestAuthConfig]. -type RequestPrivilegeFunc func(context.Context) (string, error) - -// SecretCreateResponse contains the information returned to a client -// on the creation of a new secret. -// -// Deprecated: use [swarm.SecretCreateResponse]. -type SecretCreateResponse = swarm.SecretCreateResponse - -// SecretListOptions holds parameters to list secrets -// -// Deprecated: use [swarm.SecretListOptions]. -type SecretListOptions = swarm.SecretListOptions - -// ConfigCreateResponse contains the information returned to a client -// on the creation of a new config. -// -// Deprecated: use [swarm.ConfigCreateResponse]. -type ConfigCreateResponse = swarm.ConfigCreateResponse - -// ConfigListOptions holds parameters to list configs -// -// Deprecated: use [swarm.ConfigListOptions]. -type ConfigListOptions = swarm.ConfigListOptions - -// NodeListOptions holds parameters to list nodes with. -// -// Deprecated: use [swarm.NodeListOptions]. -type NodeListOptions = swarm.NodeListOptions - -// NodeRemoveOptions holds parameters to remove nodes with. -// -// Deprecated: use [swarm.NodeRemoveOptions]. -type NodeRemoveOptions = swarm.NodeRemoveOptions - -// TaskListOptions holds parameters to list tasks with. -// -// Deprecated: use [swarm.TaskListOptions]. -type TaskListOptions = swarm.TaskListOptions - -// ServiceCreateOptions contains the options to use when creating a service. -// -// Deprecated: use [swarm.ServiceCreateOptions]. -type ServiceCreateOptions = swarm.ServiceCreateOptions - -// ServiceUpdateOptions contains the options to be used for updating services. -// -// Deprecated: use [swarm.ServiceCreateOptions]. -type ServiceUpdateOptions = swarm.ServiceUpdateOptions - -const ( - RegistryAuthFromSpec = swarm.RegistryAuthFromSpec // Deprecated: use [swarm.RegistryAuthFromSpec]. - RegistryAuthFromPreviousSpec = swarm.RegistryAuthFromPreviousSpec // Deprecated: use [swarm.RegistryAuthFromPreviousSpec]. -) - -// ServiceListOptions holds parameters to list services with. -// -// Deprecated: use [swarm.ServiceListOptions]. -type ServiceListOptions = swarm.ServiceListOptions - -// ServiceInspectOptions holds parameters related to the "service inspect" -// operation. -// -// Deprecated: use [swarm.ServiceInspectOptions]. -type ServiceInspectOptions = swarm.ServiceInspectOptions - -// SwarmUnlockKeyResponse contains the response for Engine API: -// GET /swarm/unlockkey -// -// Deprecated: use [swarm.UnlockKeyResponse]. -type SwarmUnlockKeyResponse = swarm.UnlockKeyResponse - -// BuildCache contains information about a build cache record. -// -// Deprecated: deprecated in API 1.49. Use [build.CacheRecord] instead. -type BuildCache = build.CacheRecord - -// BuildCachePruneOptions hold parameters to prune the build cache -// -// Deprecated: use [build.CachePruneOptions]. -type BuildCachePruneOptions = build.CachePruneOptions - -// BuildCachePruneReport contains the response for Engine API: -// POST "/build/prune" -// -// Deprecated: use [build.CachePruneReport]. -type BuildCachePruneReport = build.CachePruneReport - -// BuildResult contains the image id of a successful build/ -// -// Deprecated: use [build.Result]. -type BuildResult = build.Result - -// ImageBuildOptions holds the information -// necessary to build images. -// -// Deprecated: use [build.ImageBuildOptions]. -type ImageBuildOptions = build.ImageBuildOptions - -// ImageBuildOutput defines configuration for exporting a build result -// -// Deprecated: use [build.ImageBuildOutput]. -type ImageBuildOutput = build.ImageBuildOutput - -// ImageBuildResponse holds information -// returned by a server after building -// an image. -// -// Deprecated: use [build.ImageBuildResponse]. -type ImageBuildResponse = build.ImageBuildResponse - -// BuilderVersion sets the version of underlying builder to use -// -// Deprecated: use [build.BuilderVersion]. -type BuilderVersion = build.BuilderVersion - -const ( - // BuilderV1 is the first generation builder in docker daemon - // - // Deprecated: use [build.BuilderV1]. - BuilderV1 = build.BuilderV1 - // BuilderBuildKit is builder based on moby/buildkit project - // - // Deprecated: use [build.BuilderBuildKit]. - BuilderBuildKit = build.BuilderBuildKit -) diff --git a/vendor/github.com/docker/docker/api/types/volume/create_options.go b/vendor/github.com/docker/docker/api/types/volume/create_options.go deleted file mode 100644 index 37c41a609690..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/create_options.go +++ /dev/null @@ -1,29 +0,0 @@ -package volume - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// CreateOptions VolumeConfig -// -// Volume configuration -// swagger:model CreateOptions -type CreateOptions struct { - - // cluster volume spec - ClusterVolumeSpec *ClusterVolumeSpec `json:"ClusterVolumeSpec,omitempty"` - - // Name of the volume driver to use. - Driver string `json:"Driver,omitempty"` - - // A mapping of driver options and values. These options are - // passed directly to the driver and are driver specific. - // - DriverOpts map[string]string `json:"DriverOpts,omitempty"` - - // User-defined key/value metadata. - Labels map[string]string `json:"Labels,omitempty"` - - // The new volume's name. If not specified, Docker generates a name. - // - Name string `json:"Name,omitempty"` -} diff --git a/vendor/github.com/docker/docker/api/types/volume/disk_usage.go b/vendor/github.com/docker/docker/api/types/volume/disk_usage.go deleted file mode 100644 index 3d716c6e00d9..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/disk_usage.go +++ /dev/null @@ -1,8 +0,0 @@ -package volume - -// DiskUsage contains disk usage for volumes. -type DiskUsage struct { - TotalSize int64 - Reclaimable int64 - Items []*Volume -} diff --git a/vendor/github.com/docker/docker/api/types/volume/list_response.go b/vendor/github.com/docker/docker/api/types/volume/list_response.go deleted file mode 100644 index ca5192a2a91e..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/list_response.go +++ /dev/null @@ -1,18 +0,0 @@ -package volume - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// ListResponse VolumeListResponse -// -// Volume list response -// swagger:model ListResponse -type ListResponse struct { - - // List of volumes - Volumes []*Volume `json:"Volumes"` - - // Warnings that occurred when fetching the list of volumes. - // - Warnings []string `json:"Warnings"` -} diff --git a/vendor/github.com/docker/docker/api/types/volume/options.go b/vendor/github.com/docker/docker/api/types/volume/options.go deleted file mode 100644 index 875524fbc2d4..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/options.go +++ /dev/null @@ -1,15 +0,0 @@ -package volume - -import "github.com/docker/docker/api/types/filters" - -// ListOptions holds parameters to list volumes. -type ListOptions struct { - Filters filters.Args -} - -// PruneReport contains the response for Engine API: -// POST "/volumes/prune" -type PruneReport struct { - VolumesDeleted []string - SpaceReclaimed uint64 -} diff --git a/vendor/github.com/docker/docker/api/types/volume/volume.go b/vendor/github.com/docker/docker/api/types/volume/volume.go deleted file mode 100644 index ea7d555e5b49..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/volume.go +++ /dev/null @@ -1,75 +0,0 @@ -package volume - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -// Volume volume -// swagger:model Volume -type Volume struct { - - // cluster volume - ClusterVolume *ClusterVolume `json:"ClusterVolume,omitempty"` - - // Date/Time the volume was created. - CreatedAt string `json:"CreatedAt,omitempty"` - - // Name of the volume driver used by the volume. - // Required: true - Driver string `json:"Driver"` - - // User-defined key/value metadata. - // Required: true - Labels map[string]string `json:"Labels"` - - // Mount path of the volume on the host. - // Required: true - Mountpoint string `json:"Mountpoint"` - - // Name of the volume. - // Required: true - Name string `json:"Name"` - - // The driver specific options used when creating the volume. - // - // Required: true - Options map[string]string `json:"Options"` - - // The level at which the volume exists. Either `global` for cluster-wide, - // or `local` for machine level. - // - // Required: true - Scope string `json:"Scope"` - - // Low-level details about the volume, provided by the volume driver. - // Details are returned as a map with key/value pairs: - // `{"key":"value","key2":"value2"}`. - // - // The `Status` field is optional, and is omitted if the volume driver - // does not support this feature. - // - Status map[string]interface{} `json:"Status,omitempty"` - - // usage data - UsageData *UsageData `json:"UsageData,omitempty"` -} - -// UsageData Usage details about the volume. This information is used by the -// `GET /system/df` endpoint, and omitted in other endpoints. -// -// swagger:model UsageData -type UsageData struct { - - // The number of containers referencing this volume. This field - // is set to `-1` if the reference-count is not available. - // - // Required: true - RefCount int64 `json:"RefCount"` - - // Amount of disk space used by the volume (in bytes). This information - // is only available for volumes created with the `"local"` volume - // driver. For volumes created with other volume drivers, this field - // is set to `-1` ("not available") - // - // Required: true - Size int64 `json:"Size"` -} diff --git a/vendor/github.com/docker/docker/api/types/volume/volume_update.go b/vendor/github.com/docker/docker/api/types/volume/volume_update.go deleted file mode 100644 index c26ed44c6cc2..000000000000 --- a/vendor/github.com/docker/docker/api/types/volume/volume_update.go +++ /dev/null @@ -1,7 +0,0 @@ -package volume - -// UpdateOptions is configuration to update a Volume with. -type UpdateOptions struct { - // Spec is the ClusterVolumeSpec to update the volume to. - Spec *ClusterVolumeSpec `json:"Spec,omitempty"` -} diff --git a/vendor/github.com/docker/docker/client/README.md b/vendor/github.com/docker/docker/client/README.md deleted file mode 100644 index f8af3ab903df..000000000000 --- a/vendor/github.com/docker/docker/client/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Go client for the Docker Engine API - -The `docker` command uses this package to communicate with the daemon. It can -also be used by your own Go applications to do anything the command-line -interface does – running containers, pulling images, managing swarms, etc. - -For example, to list all containers (the equivalent of `docker ps --all`): - -```go -package main - -import ( - "context" - "fmt" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" -) - -func main() { - apiClient, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - panic(err) - } - defer apiClient.Close() - - containers, err := apiClient.ContainerList(context.Background(), container.ListOptions{All: true}) - if err != nil { - panic(err) - } - - for _, ctr := range containers { - fmt.Printf("%s %s (status: %s)\n", ctr.ID, ctr.Image, ctr.Status) - } -} -``` - -[Full documentation is available on pkg.go.dev.](https://pkg.go.dev/github.com/docker/docker/client) diff --git a/vendor/github.com/docker/docker/client/build_cancel.go b/vendor/github.com/docker/docker/client/build_cancel.go deleted file mode 100644 index a5eeb8172274..000000000000 --- a/vendor/github.com/docker/docker/client/build_cancel.go +++ /dev/null @@ -1,16 +0,0 @@ -package client - -import ( - "context" - "net/url" -) - -// BuildCancel requests the daemon to cancel the ongoing build request. -func (cli *Client) BuildCancel(ctx context.Context, id string) error { - query := url.Values{} - query.Set("id", id) - - resp, err := cli.post(ctx, "/build/cancel", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/build_prune.go b/vendor/github.com/docker/docker/client/build_prune.go deleted file mode 100644 index 6f0f59e30dbc..000000000000 --- a/vendor/github.com/docker/docker/client/build_prune.go +++ /dev/null @@ -1,56 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/filters" - "github.com/pkg/errors" -) - -// BuildCachePrune requests the daemon to delete unused cache data -func (cli *Client) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) { - if err := cli.NewVersionError(ctx, "1.31", "build prune"); err != nil { - return nil, err - } - - query := url.Values{} - if opts.All { - query.Set("all", "1") - } - - if opts.KeepStorage != 0 { - query.Set("keep-storage", strconv.Itoa(int(opts.KeepStorage))) - } - if opts.ReservedSpace != 0 { - query.Set("reserved-space", strconv.Itoa(int(opts.ReservedSpace))) - } - if opts.MaxUsedSpace != 0 { - query.Set("max-used-space", strconv.Itoa(int(opts.MaxUsedSpace))) - } - if opts.MinFreeSpace != 0 { - query.Set("min-free-space", strconv.Itoa(int(opts.MinFreeSpace))) - } - f, err := filters.ToJSON(opts.Filters) - if err != nil { - return nil, errors.Wrap(err, "prune could not marshal filters option") - } - query.Set("filters", f) - - resp, err := cli.post(ctx, "/build/prune", query, nil, nil) - defer ensureReaderClosed(resp) - - if err != nil { - return nil, err - } - - report := build.CachePruneReport{} - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return nil, errors.Wrap(err, "error retrieving disk usage") - } - - return &report, nil -} diff --git a/vendor/github.com/docker/docker/client/checkpoint.go b/vendor/github.com/docker/docker/client/checkpoint.go deleted file mode 100644 index d020574c897c..000000000000 --- a/vendor/github.com/docker/docker/client/checkpoint.go +++ /dev/null @@ -1,18 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/checkpoint" -) - -// CheckpointAPIClient defines API client methods for the checkpoints. -// -// Experimental: checkpoint and restore is still an experimental feature, -// and only available if the daemon is running with experimental features -// enabled. -type CheckpointAPIClient interface { - CheckpointCreate(ctx context.Context, container string, options checkpoint.CreateOptions) error - CheckpointDelete(ctx context.Context, container string, options checkpoint.DeleteOptions) error - CheckpointList(ctx context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) -} diff --git a/vendor/github.com/docker/docker/client/checkpoint_create.go b/vendor/github.com/docker/docker/client/checkpoint_create.go deleted file mode 100644 index 961a5fe62f96..000000000000 --- a/vendor/github.com/docker/docker/client/checkpoint_create.go +++ /dev/null @@ -1,19 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/checkpoint" -) - -// CheckpointCreate creates a checkpoint from the given container with the given name -func (cli *Client) CheckpointCreate(ctx context.Context, containerID string, options checkpoint.CreateOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/checkpoints", nil, options, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/checkpoint_delete.go b/vendor/github.com/docker/docker/client/checkpoint_delete.go deleted file mode 100644 index 4c51b25f2495..000000000000 --- a/vendor/github.com/docker/docker/client/checkpoint_delete.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/checkpoint" -) - -// CheckpointDelete deletes the checkpoint with the given name from the given container -func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, options checkpoint.DeleteOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if options.CheckpointDir != "" { - query.Set("dir", options.CheckpointDir) - } - - resp, err := cli.delete(ctx, "/containers/"+containerID+"/checkpoints/"+options.CheckpointID, query, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/checkpoint_list.go b/vendor/github.com/docker/docker/client/checkpoint_list.go deleted file mode 100644 index 8164c7668b8d..000000000000 --- a/vendor/github.com/docker/docker/client/checkpoint_list.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/checkpoint" -) - -// CheckpointList returns the checkpoints of the given container in the docker host -func (cli *Client) CheckpointList(ctx context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) { - var checkpoints []checkpoint.Summary - - query := url.Values{} - if options.CheckpointDir != "" { - query.Set("dir", options.CheckpointDir) - } - - resp, err := cli.get(ctx, "/containers/"+container+"/checkpoints", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return checkpoints, err - } - - err = json.NewDecoder(resp.Body).Decode(&checkpoints) - return checkpoints, err -} diff --git a/vendor/github.com/docker/docker/client/client.go b/vendor/github.com/docker/docker/client/client.go deleted file mode 100644 index d6e014dddf81..000000000000 --- a/vendor/github.com/docker/docker/client/client.go +++ /dev/null @@ -1,474 +0,0 @@ -/* -Package client is a Go client for the Docker Engine API. - -For more information about the Engine API, see the documentation: -https://docs.docker.com/reference/api/engine/ - -# Usage - -You use the library by constructing a client object using [NewClientWithOpts] -and calling methods on it. The client can be configured from environment -variables by passing the [FromEnv] option, or configured manually by passing any -of the other available [Opts]. - -For example, to list running containers (the equivalent of "docker ps"): - - package main - - import ( - "context" - "fmt" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/client" - ) - - func main() { - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - panic(err) - } - - containers, err := cli.ContainerList(context.Background(), container.ListOptions{}) - if err != nil { - panic(err) - } - - for _, ctr := range containers { - fmt.Printf("%s %s\n", ctr.ID, ctr.Image) - } - } -*/ -package client - -import ( - "context" - "crypto/tls" - "net" - "net/http" - "net/url" - "path" - "strings" - "sync" - "sync/atomic" - "time" - - "github.com/docker/docker/api" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/versions" - "github.com/docker/go-connections/sockets" - "github.com/pkg/errors" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -// DummyHost is a hostname used for local communication. -// -// It acts as a valid formatted hostname for local connections (such as "unix://" -// or "npipe://") which do not require a hostname. It should never be resolved, -// but uses the special-purpose ".localhost" TLD (as defined in [RFC 2606, Section 2] -// and [RFC 6761, Section 6.3]). -// -// [RFC 7230, Section 5.4] defines that an empty header must be used for such -// cases: -// -// If the authority component is missing or undefined for the target URI, -// then a client MUST send a Host header field with an empty field-value. -// -// However, [Go stdlib] enforces the semantics of HTTP(S) over TCP, does not -// allow an empty header to be used, and requires req.URL.Scheme to be either -// "http" or "https". -// -// For further details, refer to: -// -// - https://github.com/docker/engine-api/issues/189 -// - https://github.com/golang/go/issues/13624 -// - https://github.com/golang/go/issues/61076 -// - https://github.com/moby/moby/issues/45935 -// -// [RFC 2606, Section 2]: https://www.rfc-editor.org/rfc/rfc2606.html#section-2 -// [RFC 6761, Section 6.3]: https://www.rfc-editor.org/rfc/rfc6761#section-6.3 -// [RFC 7230, Section 5.4]: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 -// [Go stdlib]: https://github.com/golang/go/blob/6244b1946bc2101b01955468f1be502dbadd6807/src/net/http/transport.go#L558-L569 -const DummyHost = "api.moby.localhost" - -// fallbackAPIVersion is the version to fallback to if API-version negotiation -// fails. This version is the highest version of the API before API-version -// negotiation was introduced. If negotiation fails (or no API version was -// included in the API response), we assume the API server uses the most -// recent version before negotiation was introduced. -const fallbackAPIVersion = "1.24" - -// Ensure that Client always implements APIClient. -var _ APIClient = &Client{} - -// Client is the API client that performs all operations -// against a docker server. -type Client struct { - // scheme sets the scheme for the client - scheme string - // host holds the server address to connect to - host string - // proto holds the client protocol i.e. unix. - proto string - // addr holds the client address. - addr string - // basePath holds the path to prepend to the requests. - basePath string - // client used to send and receive http requests. - client *http.Client - // version of the server to talk to. - version string - // userAgent is the User-Agent header to use for HTTP requests. It takes - // precedence over User-Agent headers set in customHTTPHeaders, and other - // header variables. When set to an empty string, the User-Agent header - // is removed, and no header is sent. - userAgent *string - // custom HTTP headers configured by users. - customHTTPHeaders map[string]string - // manualOverride is set to true when the version was set by users. - manualOverride bool - - // negotiateVersion indicates if the client should automatically negotiate - // the API version to use when making requests. API version negotiation is - // performed on the first request, after which negotiated is set to "true" - // so that subsequent requests do not re-negotiate. - negotiateVersion bool - - // negotiated indicates that API version negotiation took place - negotiated atomic.Bool - - // negotiateLock is used to single-flight the version negotiation process - negotiateLock sync.Mutex - - traceOpts []otelhttp.Option - - // When the client transport is an *http.Transport (default) we need to do some extra things (like closing idle connections). - // Store the original transport as the http.Client transport will be wrapped with tracing libs. - baseTransport *http.Transport -} - -// ErrRedirect is the error returned by checkRedirect when the request is non-GET. -var ErrRedirect = errors.New("unexpected redirect in response") - -// CheckRedirect specifies the policy for dealing with redirect responses. It -// can be set on [http.Client.CheckRedirect] to prevent HTTP redirects for -// non-GET requests. It returns an [ErrRedirect] for non-GET request, otherwise -// returns a [http.ErrUseLastResponse], which is special-cased by http.Client -// to use the last response. -// -// Go 1.8 changed behavior for HTTP redirects (specifically 301, 307, and 308) -// in the client. The client (and by extension API client) can be made to send -// a request like "POST /containers//start" where what would normally be in the -// name section of the URL is empty. This triggers an HTTP 301 from the daemon. -// -// In go 1.8 this 301 is converted to a GET request, and ends up getting -// a 404 from the daemon. This behavior change manifests in the client in that -// before, the 301 was not followed and the client did not generate an error, -// but now results in a message like "Error response from daemon: page not found". -func CheckRedirect(_ *http.Request, via []*http.Request) error { - if via[0].Method == http.MethodGet { - return http.ErrUseLastResponse - } - return ErrRedirect -} - -// NewClientWithOpts initializes a new API client with a default HTTPClient, and -// default API host and version. It also initializes the custom HTTP headers to -// add to each request. -// -// It takes an optional list of [Opt] functional arguments, which are applied in -// the order they're provided, which allows modifying the defaults when creating -// the client. For example, the following initializes a client that configures -// itself with values from environment variables ([FromEnv]), and has automatic -// API version negotiation enabled ([WithAPIVersionNegotiation]). -// -// cli, err := client.NewClientWithOpts( -// client.FromEnv, -// client.WithAPIVersionNegotiation(), -// ) -func NewClientWithOpts(ops ...Opt) (*Client, error) { - hostURL, err := ParseHostURL(DefaultDockerHost) - if err != nil { - return nil, err - } - - client, err := defaultHTTPClient(hostURL) - if err != nil { - return nil, err - } - c := &Client{ - host: DefaultDockerHost, - version: api.DefaultVersion, - client: client, - proto: hostURL.Scheme, - addr: hostURL.Host, - - traceOpts: []otelhttp.Option{ - otelhttp.WithSpanNameFormatter(func(_ string, req *http.Request) string { - return req.Method + " " + req.URL.Path - }), - }, - } - - for _, op := range ops { - if err := op(c); err != nil { - return nil, err - } - } - - if tr, ok := c.client.Transport.(*http.Transport); ok { - // Store the base transport before we wrap it in tracing libs below - // This is used, as an example, to close idle connections when the client is closed - c.baseTransport = tr - } - - if c.scheme == "" { - // TODO(stevvooe): This isn't really the right way to write clients in Go. - // `NewClient` should probably only take an `*http.Client` and work from there. - // Unfortunately, the model of having a host-ish/url-thingy as the connection - // string has us confusing protocol and transport layers. We continue doing - // this to avoid breaking existing clients but this should be addressed. - if c.tlsConfig() != nil { - c.scheme = "https" - } else { - c.scheme = "http" - } - } - - c.client.Transport = otelhttp.NewTransport(c.client.Transport, c.traceOpts...) - - return c, nil -} - -func (cli *Client) tlsConfig() *tls.Config { - if cli.baseTransport == nil { - return nil - } - return cli.baseTransport.TLSClientConfig -} - -func defaultHTTPClient(hostURL *url.URL) (*http.Client, error) { - transport := &http.Transport{} - // Necessary to prevent long-lived processes using the - // client from leaking connections due to idle connections - // not being released. - // TODO: see if we can also address this from the server side, - // or in go-connections. - // see: https://github.com/moby/moby/issues/45539 - transport.MaxIdleConns = 6 - transport.IdleConnTimeout = 30 * time.Second - err := sockets.ConfigureTransport(transport, hostURL.Scheme, hostURL.Host) - if err != nil { - return nil, err - } - return &http.Client{ - Transport: transport, - CheckRedirect: CheckRedirect, - }, nil -} - -// Close the transport used by the client -func (cli *Client) Close() error { - if cli.baseTransport != nil { - cli.baseTransport.CloseIdleConnections() - return nil - } - return nil -} - -// checkVersion manually triggers API version negotiation (if configured). -// This allows for version-dependent code to use the same version as will -// be negotiated when making the actual requests, and for which cases -// we cannot do the negotiation lazily. -func (cli *Client) checkVersion(ctx context.Context) error { - if !cli.manualOverride && cli.negotiateVersion && !cli.negotiated.Load() { - // Ensure exclusive write access to version and negotiated fields - cli.negotiateLock.Lock() - defer cli.negotiateLock.Unlock() - - // May have been set during last execution of critical zone - if cli.negotiated.Load() { - return nil - } - - ping, err := cli.Ping(ctx) - if err != nil { - return err - } - cli.negotiateAPIVersionPing(ping) - } - return nil -} - -// getAPIPath returns the versioned request path to call the API. -// It appends the query parameters to the path if they are not empty. -func (cli *Client) getAPIPath(ctx context.Context, p string, query url.Values) string { - var apiPath string - _ = cli.checkVersion(ctx) - if cli.version != "" { - apiPath = path.Join(cli.basePath, "/v"+strings.TrimPrefix(cli.version, "v"), p) - } else { - apiPath = path.Join(cli.basePath, p) - } - return (&url.URL{Path: apiPath, RawQuery: query.Encode()}).String() -} - -// ClientVersion returns the API version used by this client. -func (cli *Client) ClientVersion() string { - return cli.version -} - -// NegotiateAPIVersion queries the API and updates the version to match the API -// version. NegotiateAPIVersion downgrades the client's API version to match the -// APIVersion if the ping version is lower than the default version. If the API -// version reported by the server is higher than the maximum version supported -// by the client, it uses the client's maximum version. -// -// If a manual override is in place, either through the "DOCKER_API_VERSION" -// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized -// with a fixed version ([WithVersion]), no negotiation is performed. -// -// If the API server's ping response does not contain an API version, or if the -// client did not get a successful ping response, it assumes it is connected with -// an old daemon that does not support API version negotiation, in which case it -// downgrades to the latest version of the API before version negotiation was -// added (1.24). -func (cli *Client) NegotiateAPIVersion(ctx context.Context) { - if !cli.manualOverride { - // Avoid concurrent modification of version-related fields - cli.negotiateLock.Lock() - defer cli.negotiateLock.Unlock() - - ping, err := cli.Ping(ctx) - if err != nil { - // FIXME(thaJeztah): Ping returns an error when failing to connect to the API; we should not swallow the error here, and instead returning it. - return - } - cli.negotiateAPIVersionPing(ping) - } -} - -// NegotiateAPIVersionPing downgrades the client's API version to match the -// APIVersion in the ping response. If the API version in pingResponse is higher -// than the maximum version supported by the client, it uses the client's maximum -// version. -// -// If a manual override is in place, either through the "DOCKER_API_VERSION" -// ([EnvOverrideAPIVersion]) environment variable, or if the client is initialized -// with a fixed version ([WithVersion]), no negotiation is performed. -// -// If the API server's ping response does not contain an API version, we assume -// we are connected with an old daemon without API version negotiation support, -// and downgrade to the latest version of the API before version negotiation was -// added (1.24). -func (cli *Client) NegotiateAPIVersionPing(pingResponse types.Ping) { - if !cli.manualOverride { - // Avoid concurrent modification of version-related fields - cli.negotiateLock.Lock() - defer cli.negotiateLock.Unlock() - - cli.negotiateAPIVersionPing(pingResponse) - } -} - -// negotiateAPIVersionPing queries the API and updates the version to match the -// API version from the ping response. -func (cli *Client) negotiateAPIVersionPing(pingResponse types.Ping) { - // default to the latest version before versioning headers existed - if pingResponse.APIVersion == "" { - pingResponse.APIVersion = fallbackAPIVersion - } - - // if the client is not initialized with a version, start with the latest supported version - if cli.version == "" { - cli.version = api.DefaultVersion - } - - // if server version is lower than the client version, downgrade - if versions.LessThan(pingResponse.APIVersion, cli.version) { - cli.version = pingResponse.APIVersion - } - - // Store the results, so that automatic API version negotiation (if enabled) - // won't be performed on the next request. - if cli.negotiateVersion { - cli.negotiated.Store(true) - } -} - -// DaemonHost returns the host address used by the client -func (cli *Client) DaemonHost() string { - return cli.host -} - -// HTTPClient returns a copy of the HTTP client bound to the server -func (cli *Client) HTTPClient() *http.Client { - c := *cli.client - return &c -} - -// ParseHostURL parses a url string, validates the string is a host url, and -// returns the parsed URL -func ParseHostURL(host string) (*url.URL, error) { - proto, addr, ok := strings.Cut(host, "://") - if !ok || addr == "" { - return nil, errors.Errorf("unable to parse docker host `%s`", host) - } - - var basePath string - if proto == "tcp" { - parsed, err := url.Parse("tcp://" + addr) - if err != nil { - return nil, err - } - addr = parsed.Host - basePath = parsed.Path - } - return &url.URL{ - Scheme: proto, - Host: addr, - Path: basePath, - }, nil -} - -func (cli *Client) dialerFromTransport() func(context.Context, string, string) (net.Conn, error) { - if cli.baseTransport == nil || cli.baseTransport.DialContext == nil { - return nil - } - - if cli.baseTransport.TLSClientConfig != nil { - // When using a tls config we don't use the configured dialer but instead a fallback dialer... - // Note: It seems like this should use the normal dialer and wrap the returned net.Conn in a tls.Conn - // I honestly don't know why it doesn't do that, but it doesn't and such a change is entirely unrelated to the change in this commit. - return nil - } - return cli.baseTransport.DialContext -} - -// Dialer returns a dialer for a raw stream connection, with an HTTP/1.1 header, -// that can be used for proxying the daemon connection. It is used by -// ["docker dial-stdio"]. -// -// ["docker dial-stdio"]: https://github.com/docker/cli/pull/1014 -func (cli *Client) Dialer() func(context.Context) (net.Conn, error) { - return cli.dialer() -} - -func (cli *Client) dialer() func(context.Context) (net.Conn, error) { - return func(ctx context.Context) (net.Conn, error) { - if dialFn := cli.dialerFromTransport(); dialFn != nil { - return dialFn(ctx, cli.proto, cli.addr) - } - switch cli.proto { - case "unix": - return net.Dial(cli.proto, cli.addr) - case "npipe": - return sockets.DialPipe(cli.addr, 32*time.Second) - default: - if tlsConfig := cli.tlsConfig(); tlsConfig != nil { - return tls.Dial(cli.proto, cli.addr, tlsConfig) - } - return net.Dial(cli.proto, cli.addr) - } - } -} diff --git a/vendor/github.com/docker/docker/client/client_deprecated.go b/vendor/github.com/docker/docker/client/client_deprecated.go deleted file mode 100644 index 9e366ce20d16..000000000000 --- a/vendor/github.com/docker/docker/client/client_deprecated.go +++ /dev/null @@ -1,27 +0,0 @@ -package client - -import "net/http" - -// NewClient initializes a new API client for the given host and API version. -// It uses the given http client as transport. -// It also initializes the custom http headers to add to each request. -// -// It won't send any version information if the version number is empty. It is -// highly recommended that you set a version or your client may break if the -// server is upgraded. -// -// Deprecated: use [NewClientWithOpts] passing the [WithHost], [WithVersion], -// [WithHTTPClient] and [WithHTTPHeaders] options. We recommend enabling API -// version negotiation by passing the [WithAPIVersionNegotiation] option instead -// of WithVersion. -func NewClient(host string, version string, client *http.Client, httpHeaders map[string]string) (*Client, error) { - return NewClientWithOpts(WithHost(host), WithVersion(version), WithHTTPClient(client), WithHTTPHeaders(httpHeaders)) -} - -// NewEnvClient initializes a new API client based on environment variables. -// See FromEnv for a list of support environment variables. -// -// Deprecated: use [NewClientWithOpts] passing the [FromEnv] option. -func NewEnvClient() (*Client, error) { - return NewClientWithOpts(FromEnv) -} diff --git a/vendor/github.com/docker/docker/client/client_interfaces.go b/vendor/github.com/docker/docker/client/client_interfaces.go deleted file mode 100644 index df7aad430c7d..000000000000 --- a/vendor/github.com/docker/docker/client/client_interfaces.go +++ /dev/null @@ -1,237 +0,0 @@ -package client - -import ( - "context" - "io" - "net" - "net/http" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/system" - "github.com/docker/docker/api/types/volume" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// CommonAPIClient is the common methods between stable and experimental versions of APIClient. -// -// Deprecated: use [APIClient] instead. This type will be an alias for [APIClient] in the next release, and removed after. -type CommonAPIClient = stableAPIClient - -// APIClient is an interface that clients that talk with a docker server must implement. -type APIClient interface { - stableAPIClient - CheckpointAPIClient // CheckpointAPIClient is still experimental. -} - -type stableAPIClient interface { - ConfigAPIClient - ContainerAPIClient - DistributionAPIClient - ImageAPIClient - NetworkAPIClient - PluginAPIClient - SystemAPIClient - VolumeAPIClient - ClientVersion() string - DaemonHost() string - HTTPClient() *http.Client - ServerVersion(ctx context.Context) (types.Version, error) - NegotiateAPIVersion(ctx context.Context) - NegotiateAPIVersionPing(types.Ping) - HijackDialer - Dialer() func(context.Context) (net.Conn, error) - Close() error - SwarmManagementAPIClient -} - -// SwarmManagementAPIClient defines all methods for managing Swarm-specific -// objects. -type SwarmManagementAPIClient interface { - SwarmAPIClient - NodeAPIClient - ServiceAPIClient - SecretAPIClient - ConfigAPIClient -} - -// HijackDialer defines methods for a hijack dialer. -type HijackDialer interface { - DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) -} - -// ContainerAPIClient defines API client methods for the containers -type ContainerAPIClient interface { - ContainerAttach(ctx context.Context, container string, options container.AttachOptions) (types.HijackedResponse, error) - ContainerCommit(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error) - ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) - ContainerDiff(ctx context.Context, container string) ([]container.FilesystemChange, error) - ContainerExecAttach(ctx context.Context, execID string, options container.ExecAttachOptions) (types.HijackedResponse, error) - ContainerExecCreate(ctx context.Context, container string, options container.ExecOptions) (container.ExecCreateResponse, error) - ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) - ContainerExecResize(ctx context.Context, execID string, options container.ResizeOptions) error - ContainerExecStart(ctx context.Context, execID string, options container.ExecStartOptions) error - ContainerExport(ctx context.Context, container string) (io.ReadCloser, error) - ContainerInspect(ctx context.Context, container string) (container.InspectResponse, error) - ContainerInspectWithRaw(ctx context.Context, container string, getSize bool) (container.InspectResponse, []byte, error) - ContainerKill(ctx context.Context, container, signal string) error - ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) - ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) - ContainerPause(ctx context.Context, container string) error - ContainerRemove(ctx context.Context, container string, options container.RemoveOptions) error - ContainerRename(ctx context.Context, container, newContainerName string) error - ContainerResize(ctx context.Context, container string, options container.ResizeOptions) error - ContainerRestart(ctx context.Context, container string, options container.StopOptions) error - ContainerStatPath(ctx context.Context, container, path string) (container.PathStat, error) - ContainerStats(ctx context.Context, container string, stream bool) (container.StatsResponseReader, error) - ContainerStatsOneShot(ctx context.Context, container string) (container.StatsResponseReader, error) - ContainerStart(ctx context.Context, container string, options container.StartOptions) error - ContainerStop(ctx context.Context, container string, options container.StopOptions) error - ContainerTop(ctx context.Context, container string, arguments []string) (container.TopResponse, error) - ContainerUnpause(ctx context.Context, container string) error - ContainerUpdate(ctx context.Context, container string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) - ContainerWait(ctx context.Context, container string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) - CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, container.PathStat, error) - CopyToContainer(ctx context.Context, container, path string, content io.Reader, options container.CopyToContainerOptions) error - ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) -} - -// DistributionAPIClient defines API client methods for the registry -type DistributionAPIClient interface { - DistributionInspect(ctx context.Context, image, encodedRegistryAuth string) (registry.DistributionInspect, error) -} - -// ImageAPIClient defines API client methods for the images -type ImageAPIClient interface { - ImageBuild(ctx context.Context, context io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) - BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) - BuildCancel(ctx context.Context, id string) error - ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) - ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) - - ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) - ImagePull(ctx context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) - ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) - ImageRemove(ctx context.Context, image string, options image.RemoveOptions) ([]image.DeleteResponse, error) - ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error) - ImageTag(ctx context.Context, image, ref string) error - ImagesPrune(ctx context.Context, pruneFilter filters.Args) (image.PruneReport, error) - - ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error) - ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error) - ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (image.LoadResponse, error) - ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error) - - ImageAPIClientDeprecated -} - -// ImageAPIClientDeprecated defines deprecated methods of the ImageAPIClient. -type ImageAPIClientDeprecated interface { - // ImageInspectWithRaw returns the image information and its raw representation. - // - // Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option. - ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error) -} - -// NetworkAPIClient defines API client methods for the networks -type NetworkAPIClient interface { - NetworkConnect(ctx context.Context, network, container string, config *network.EndpointSettings) error - NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) - NetworkDisconnect(ctx context.Context, network, container string, force bool) error - NetworkInspect(ctx context.Context, network string, options network.InspectOptions) (network.Inspect, error) - NetworkInspectWithRaw(ctx context.Context, network string, options network.InspectOptions) (network.Inspect, []byte, error) - NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) - NetworkRemove(ctx context.Context, network string) error - NetworksPrune(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error) -} - -// NodeAPIClient defines API client methods for the nodes -type NodeAPIClient interface { - NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) - NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) - NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error - NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error -} - -// PluginAPIClient defines API client methods for the plugins -type PluginAPIClient interface { - PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) - PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error - PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error - PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error - PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) - PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) - PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) - PluginSet(ctx context.Context, name string, args []string) error - PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) - PluginCreate(ctx context.Context, createContext io.Reader, options types.PluginCreateOptions) error -} - -// ServiceAPIClient defines API client methods for the services -type ServiceAPIClient interface { - ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) - ServiceInspectWithRaw(ctx context.Context, serviceID string, options swarm.ServiceInspectOptions) (swarm.Service, []byte, error) - ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) - ServiceRemove(ctx context.Context, serviceID string) error - ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) - ServiceLogs(ctx context.Context, serviceID string, options container.LogsOptions) (io.ReadCloser, error) - TaskLogs(ctx context.Context, taskID string, options container.LogsOptions) (io.ReadCloser, error) - TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) - TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) -} - -// SwarmAPIClient defines API client methods for the swarm -type SwarmAPIClient interface { - SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) - SwarmJoin(ctx context.Context, req swarm.JoinRequest) error - SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) - SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error - SwarmLeave(ctx context.Context, force bool) error - SwarmInspect(ctx context.Context) (swarm.Swarm, error) - SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error -} - -// SystemAPIClient defines API client methods for the system -type SystemAPIClient interface { - Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) - Info(ctx context.Context) (system.Info, error) - RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) - DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) - Ping(ctx context.Context) (types.Ping, error) -} - -// VolumeAPIClient defines API client methods for the volumes -type VolumeAPIClient interface { - VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) - VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error) - VolumeInspectWithRaw(ctx context.Context, volumeID string) (volume.Volume, []byte, error) - VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) - VolumeRemove(ctx context.Context, volumeID string, force bool) error - VolumesPrune(ctx context.Context, pruneFilter filters.Args) (volume.PruneReport, error) - VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error -} - -// SecretAPIClient defines API client methods for secrets -type SecretAPIClient interface { - SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) - SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) - SecretRemove(ctx context.Context, id string) error - SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) - SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error -} - -// ConfigAPIClient defines API client methods for configs -type ConfigAPIClient interface { - ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) - ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) - ConfigRemove(ctx context.Context, id string) error - ConfigInspectWithRaw(ctx context.Context, name string) (swarm.Config, []byte, error) - ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error -} diff --git a/vendor/github.com/docker/docker/client/client_unix.go b/vendor/github.com/docker/docker/client/client_unix.go deleted file mode 100644 index e5b921b40642..000000000000 --- a/vendor/github.com/docker/docker/client/client_unix.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !windows - -package client - -// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST -// (EnvOverrideHost) environment variable is unset or empty. -const DefaultDockerHost = "unix:///var/run/docker.sock" diff --git a/vendor/github.com/docker/docker/client/client_windows.go b/vendor/github.com/docker/docker/client/client_windows.go deleted file mode 100644 index 19b954b2fd78..000000000000 --- a/vendor/github.com/docker/docker/client/client_windows.go +++ /dev/null @@ -1,5 +0,0 @@ -package client - -// DefaultDockerHost defines OS-specific default host if the DOCKER_HOST -// (EnvOverrideHost) environment variable is unset or empty. -const DefaultDockerHost = "npipe:////./pipe/docker_engine" diff --git a/vendor/github.com/docker/docker/client/config_create.go b/vendor/github.com/docker/docker/client/config_create.go deleted file mode 100644 index a39168e23be1..000000000000 --- a/vendor/github.com/docker/docker/client/config_create.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/swarm" -) - -// ConfigCreate creates a new config. -func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) { - var response swarm.ConfigCreateResponse - if err := cli.NewVersionError(ctx, "1.30", "config create"); err != nil { - return response, err - } - resp, err := cli.post(ctx, "/configs/create", nil, config, nil) - defer ensureReaderClosed(resp) - if err != nil { - return response, err - } - - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/config_inspect.go b/vendor/github.com/docker/docker/client/config_inspect.go deleted file mode 100644 index a9f0a8b05ee8..000000000000 --- a/vendor/github.com/docker/docker/client/config_inspect.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types/swarm" -) - -// ConfigInspectWithRaw returns the config information with raw data -func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) { - id, err := trimID("contig", id) - if err != nil { - return swarm.Config{}, nil, err - } - if err := cli.NewVersionError(ctx, "1.30", "config inspect"); err != nil { - return swarm.Config{}, nil, err - } - resp, err := cli.get(ctx, "/configs/"+id, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Config{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return swarm.Config{}, nil, err - } - - var config swarm.Config - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&config) - - return config, body, err -} diff --git a/vendor/github.com/docker/docker/client/config_list.go b/vendor/github.com/docker/docker/client/config_list.go deleted file mode 100644 index 6f8a1c21f0ca..000000000000 --- a/vendor/github.com/docker/docker/client/config_list.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" -) - -// ConfigList returns the list of configs. -func (cli *Client) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) { - if err := cli.NewVersionError(ctx, "1.30", "config list"); err != nil { - return nil, err - } - query := url.Values{} - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - resp, err := cli.get(ctx, "/configs", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var configs []swarm.Config - err = json.NewDecoder(resp.Body).Decode(&configs) - return configs, err -} diff --git a/vendor/github.com/docker/docker/client/config_remove.go b/vendor/github.com/docker/docker/client/config_remove.go deleted file mode 100644 index 99d33b1ce2ba..000000000000 --- a/vendor/github.com/docker/docker/client/config_remove.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -import "context" - -// ConfigRemove removes a config. -func (cli *Client) ConfigRemove(ctx context.Context, id string) error { - id, err := trimID("config", id) - if err != nil { - return err - } - if err := cli.NewVersionError(ctx, "1.30", "config remove"); err != nil { - return err - } - resp, err := cli.delete(ctx, "/configs/"+id, nil, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/config_update.go b/vendor/github.com/docker/docker/client/config_update.go deleted file mode 100644 index 9bc137f7f9d9..000000000000 --- a/vendor/github.com/docker/docker/client/config_update.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/swarm" -) - -// ConfigUpdate attempts to update a config -func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error { - id, err := trimID("config", id) - if err != nil { - return err - } - if err := cli.NewVersionError(ctx, "1.30", "config update"); err != nil { - return err - } - query := url.Values{} - query.Set("version", version.String()) - resp, err := cli.post(ctx, "/configs/"+id+"/update", query, config, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_attach.go b/vendor/github.com/docker/docker/client/container_attach.go deleted file mode 100644 index 1fb3493ebf9b..000000000000 --- a/vendor/github.com/docker/docker/client/container_attach.go +++ /dev/null @@ -1,65 +0,0 @@ -package client - -import ( - "context" - "net/http" - "net/url" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" -) - -// ContainerAttach attaches a connection to a container in the server. -// It returns a types.HijackedConnection with the hijacked connection -// and the a reader to get output. It's up to the called to close -// the hijacked connection by calling types.HijackedResponse.Close. -// -// The stream format on the response will be in one of two formats: -// -// If the container is using a TTY, there is only a single stream (stdout), and -// data is copied directly from the container output stream, no extra -// multiplexing or headers. -// -// If the container is *not* using a TTY, streams for stdout and stderr are -// multiplexed. -// The format of the multiplexed stream is as follows: -// -// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}[]byte{OUTPUT} -// -// STREAM_TYPE can be 1 for stdout and 2 for stderr -// -// SIZE1, SIZE2, SIZE3, and SIZE4 are four bytes of uint32 encoded as big endian. -// This is the size of OUTPUT. -// -// You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this -// stream. -func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return types.HijackedResponse{}, err - } - - query := url.Values{} - if options.Stream { - query.Set("stream", "1") - } - if options.Stdin { - query.Set("stdin", "1") - } - if options.Stdout { - query.Set("stdout", "1") - } - if options.Stderr { - query.Set("stderr", "1") - } - if options.DetachKeys != "" { - query.Set("detachKeys", options.DetachKeys) - } - if options.Logs { - query.Set("logs", "1") - } - - return cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{ - "Content-Type": {"text/plain"}, - }) -} diff --git a/vendor/github.com/docker/docker/client/container_commit.go b/vendor/github.com/docker/docker/client/container_commit.go deleted file mode 100644 index 2b5b9852fb0e..000000000000 --- a/vendor/github.com/docker/docker/client/container_commit.go +++ /dev/null @@ -1,60 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "errors" - "net/url" - - "github.com/distribution/reference" - "github.com/docker/docker/api/types/container" -) - -// ContainerCommit applies changes to a container and creates a new tagged image. -func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.CommitResponse{}, err - } - - var repository, tag string - if options.Reference != "" { - ref, err := reference.ParseNormalizedNamed(options.Reference) - if err != nil { - return container.CommitResponse{}, err - } - - if _, isCanonical := ref.(reference.Canonical); isCanonical { - return container.CommitResponse{}, errors.New("refusing to create a tag with a digest reference") - } - ref = reference.TagNameOnly(ref) - - if tagged, ok := ref.(reference.Tagged); ok { - tag = tagged.Tag() - } - repository = ref.Name() - } - - query := url.Values{} - query.Set("container", containerID) - query.Set("repo", repository) - query.Set("tag", tag) - query.Set("comment", options.Comment) - query.Set("author", options.Author) - for _, change := range options.Changes { - query.Add("changes", change) - } - if !options.Pause { - query.Set("pause", "0") - } - - var response container.CommitResponse - resp, err := cli.post(ctx, "/commit", query, options.Config, nil) - defer ensureReaderClosed(resp) - if err != nil { - return response, err - } - - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/container_copy.go b/vendor/github.com/docker/docker/client/container_copy.go deleted file mode 100644 index 7c4130dc7f1e..000000000000 --- a/vendor/github.com/docker/docker/client/container_copy.go +++ /dev/null @@ -1,104 +0,0 @@ -package client - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "path/filepath" - "strings" - - "github.com/docker/docker/api/types/container" -) - -// ContainerStatPath returns stat information about a path inside the container filesystem. -func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.PathStat{}, err - } - - query := url.Values{} - query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. - - resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.PathStat{}, err - } - return getContainerPathStatFromHeader(resp.Header) -} - -// CopyToContainer copies content into the container filesystem. -// Note that `content` must be a Reader for a TAR archive -func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options container.CopyToContainerOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API. - // Do not allow for an existing directory to be overwritten by a non-directory and vice versa. - if !options.AllowOverwriteDirWithFile { - query.Set("noOverwriteDirNonDir", "true") - } - - if options.CopyUIDGID { - query.Set("copyUIDGID", "true") - } - - response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil) - defer ensureReaderClosed(response) - if err != nil { - return err - } - - return nil -} - -// CopyFromContainer gets the content from the container and returns it as a Reader -// for a TAR archive to manipulate it in the host. It's up to the caller to close the reader. -func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return nil, container.PathStat{}, err - } - - query := make(url.Values, 1) - query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. - - resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) - if err != nil { - return nil, container.PathStat{}, err - } - - // In order to get the copy behavior right, we need to know information - // about both the source and the destination. The response headers include - // stat info about the source that we can use in deciding exactly how to - // copy it locally. Along with the stat info about the local destination, - // we have everything we need to handle the multiple possibilities there - // can be when copying a file/dir from one location to another file/dir. - stat, err := getContainerPathStatFromHeader(resp.Header) - if err != nil { - return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) - } - return resp.Body, stat, err -} - -func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) { - var stat container.PathStat - - encodedStat := header.Get("X-Docker-Container-Path-Stat") - statDecoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(encodedStat)) - - err := json.NewDecoder(statDecoder).Decode(&stat) - if err != nil { - err = fmt.Errorf("unable to decode container path stat header: %s", err) - } - - return stat, err -} diff --git a/vendor/github.com/docker/docker/client/container_create.go b/vendor/github.com/docker/docker/client/container_create.go deleted file mode 100644 index 0625cb125ccb..000000000000 --- a/vendor/github.com/docker/docker/client/container_create.go +++ /dev/null @@ -1,168 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "errors" - "net/url" - "path" - "sort" - "strings" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/versions" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ContainerCreate creates a new container based on the given configuration. -// It can be associated with a name, but it's not mandatory. -func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) { - var response container.CreateResponse - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return response, err - } - - if err := cli.NewVersionError(ctx, "1.25", "stop timeout"); config != nil && config.StopTimeout != nil && err != nil { - return response, err - } - if err := cli.NewVersionError(ctx, "1.41", "specify container image platform"); platform != nil && err != nil { - return response, err - } - if err := cli.NewVersionError(ctx, "1.44", "specify health-check start interval"); config != nil && config.Healthcheck != nil && config.Healthcheck.StartInterval != 0 && err != nil { - return response, err - } - if err := cli.NewVersionError(ctx, "1.44", "specify mac-address per network"); hasEndpointSpecificMacAddress(networkingConfig) && err != nil { - return response, err - } - - if hostConfig != nil { - if versions.LessThan(cli.ClientVersion(), "1.25") { - // When using API 1.24 and under, the client is responsible for removing the container - hostConfig.AutoRemove = false - } - if versions.GreaterThanOrEqualTo(cli.ClientVersion(), "1.42") || versions.LessThan(cli.ClientVersion(), "1.40") { - // KernelMemory was added in API 1.40, and deprecated in API 1.42 - hostConfig.KernelMemory = 0 - } - if platform != nil && platform.OS == "linux" && versions.LessThan(cli.ClientVersion(), "1.42") { - // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize - hostConfig.ConsoleSize = [2]uint{0, 0} - } - if versions.LessThan(cli.ClientVersion(), "1.44") { - for _, m := range hostConfig.Mounts { - if m.BindOptions != nil { - // ReadOnlyNonRecursive can be safely ignored when API < 1.44 - if m.BindOptions.ReadOnlyForceRecursive { - return response, errors.New("bind-recursive=readonly requires API v1.44 or later") - } - if m.BindOptions.NonRecursive && versions.LessThan(cli.ClientVersion(), "1.40") { - return response, errors.New("bind-recursive=disabled requires API v1.40 or later") - } - } - } - } - - hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd) - hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop) - } - - // Since API 1.44, the container-wide MacAddress is deprecated and will trigger a WARNING if it's specified. - if versions.GreaterThanOrEqualTo(cli.ClientVersion(), "1.44") { - config.MacAddress = "" //nolint:staticcheck // ignore SA1019: field is deprecated, but still used on API < v1.44. - } - - query := url.Values{} - if p := formatPlatform(platform); p != "" { - query.Set("platform", p) - } - - if containerName != "" { - query.Set("name", containerName) - } - - body := container.CreateRequest{ - Config: config, - HostConfig: hostConfig, - NetworkingConfig: networkingConfig, - } - - resp, err := cli.post(ctx, "/containers/create", query, body, nil) - defer ensureReaderClosed(resp) - if err != nil { - return response, err - } - - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} - -// formatPlatform returns a formatted string representing platform (e.g. linux/arm/v7). -// -// Similar to containerd's platforms.Format(), but does allow components to be -// omitted (e.g. pass "architecture" only, without "os": -// https://github.com/containerd/containerd/blob/v1.5.2/platforms/platforms.go#L243-L263 -func formatPlatform(platform *ocispec.Platform) string { - if platform == nil { - return "" - } - return path.Join(platform.OS, platform.Architecture, platform.Variant) -} - -// hasEndpointSpecificMacAddress checks whether one of the endpoint in networkingConfig has a MacAddress defined. -func hasEndpointSpecificMacAddress(networkingConfig *network.NetworkingConfig) bool { - if networkingConfig == nil { - return false - } - for _, endpoint := range networkingConfig.EndpointsConfig { - if endpoint.MacAddress != "" { - return true - } - } - return false -} - -// allCapabilities is a magic value for "all capabilities" -const allCapabilities = "ALL" - -// normalizeCapabilities normalizes capabilities to their canonical form, -// removes duplicates, and sorts the results. -// -// It is similar to [github.com/docker/docker/oci/caps.NormalizeLegacyCapabilities], -// but performs no validation based on supported capabilities. -func normalizeCapabilities(caps []string) []string { - var normalized []string - - unique := make(map[string]struct{}) - for _, c := range caps { - c = normalizeCap(c) - if _, ok := unique[c]; ok { - continue - } - unique[c] = struct{}{} - normalized = append(normalized, c) - } - - sort.Strings(normalized) - return normalized -} - -// normalizeCap normalizes a capability to its canonical format by upper-casing -// and adding a "CAP_" prefix (if not yet present). It also accepts the "ALL" -// magic-value. -func normalizeCap(cap string) string { - cap = strings.ToUpper(cap) - if cap == allCapabilities { - return cap - } - if !strings.HasPrefix(cap, "CAP_") { - cap = "CAP_" + cap - } - return cap -} diff --git a/vendor/github.com/docker/docker/client/container_diff.go b/vendor/github.com/docker/docker/client/container_diff.go deleted file mode 100644 index 3848e3117e1d..000000000000 --- a/vendor/github.com/docker/docker/client/container_diff.go +++ /dev/null @@ -1,30 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/container" -) - -// ContainerDiff shows differences in a container filesystem since it was started. -func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return nil, err - } - - resp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var changes []container.FilesystemChange - err = json.NewDecoder(resp.Body).Decode(&changes) - if err != nil { - return nil, err - } - return changes, err -} diff --git a/vendor/github.com/docker/docker/client/container_exec.go b/vendor/github.com/docker/docker/client/container_exec.go deleted file mode 100644 index 8abbf8924b67..000000000000 --- a/vendor/github.com/docker/docker/client/container_exec.go +++ /dev/null @@ -1,81 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/versions" -) - -// ContainerExecCreate creates a new exec configuration to run an exec process. -func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (container.ExecCreateResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.ExecCreateResponse{}, err - } - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return container.ExecCreateResponse{}, err - } - - if err := cli.NewVersionError(ctx, "1.25", "env"); len(options.Env) != 0 && err != nil { - return container.ExecCreateResponse{}, err - } - if versions.LessThan(cli.ClientVersion(), "1.42") { - options.ConsoleSize = nil - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/exec", nil, options, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.ExecCreateResponse{}, err - } - - var response container.ExecCreateResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} - -// ContainerExecStart starts an exec process already created in the docker host. -func (cli *Client) ContainerExecStart(ctx context.Context, execID string, config container.ExecStartOptions) error { - if versions.LessThan(cli.ClientVersion(), "1.42") { - config.ConsoleSize = nil - } - resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, config, nil) - ensureReaderClosed(resp) - return err -} - -// ContainerExecAttach attaches a connection to an exec process in the server. -// It returns a types.HijackedConnection with the hijacked connection -// and the a reader to get output. It's up to the called to close -// the hijacked connection by calling types.HijackedResponse.Close. -func (cli *Client) ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error) { - if versions.LessThan(cli.ClientVersion(), "1.42") { - config.ConsoleSize = nil - } - return cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, config, http.Header{ - "Content-Type": {"application/json"}, - }) -} - -// ContainerExecInspect returns information about a specific exec process on the docker host. -func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error) { - var response container.ExecInspect - resp, err := cli.get(ctx, "/exec/"+execID+"/json", nil, nil) - if err != nil { - return response, err - } - - err = json.NewDecoder(resp.Body).Decode(&response) - ensureReaderClosed(resp) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/container_export.go b/vendor/github.com/docker/docker/client/container_export.go deleted file mode 100644 index 3fc4d5704d2b..000000000000 --- a/vendor/github.com/docker/docker/client/container_export.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" -) - -// ContainerExport retrieves the raw contents of a container -// and returns them as an io.ReadCloser. It's up to the caller -// to close the stream. -func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return nil, err - } - - resp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil) - if err != nil { - return nil, err - } - - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/container_inspect.go b/vendor/github.com/docker/docker/client/container_inspect.go deleted file mode 100644 index 18ccdf23393a..000000000000 --- a/vendor/github.com/docker/docker/client/container_inspect.go +++ /dev/null @@ -1,57 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/url" - - "github.com/docker/docker/api/types/container" -) - -// ContainerInspect returns the container information. -func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.InspectResponse{}, err - } - - resp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.InspectResponse{}, err - } - - var response container.InspectResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} - -// ContainerInspectWithRaw returns the container information and its raw representation. -func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (container.InspectResponse, []byte, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.InspectResponse{}, nil, err - } - - query := url.Values{} - if getSize { - query.Set("size", "1") - } - resp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.InspectResponse{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return container.InspectResponse{}, nil, err - } - - var response container.InspectResponse - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&response) - return response, body, err -} diff --git a/vendor/github.com/docker/docker/client/container_kill.go b/vendor/github.com/docker/docker/client/container_kill.go deleted file mode 100644 index 251ae479a38f..000000000000 --- a/vendor/github.com/docker/docker/client/container_kill.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "context" - "net/url" -) - -// ContainerKill terminates the container process but does not remove the container from the docker host. -func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if signal != "" { - query.Set("signal", signal) - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/kill", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_list.go b/vendor/github.com/docker/docker/client/container_list.go deleted file mode 100644 index e17b14acff32..000000000000 --- a/vendor/github.com/docker/docker/client/container_list.go +++ /dev/null @@ -1,56 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" -) - -// ContainerList returns the list of containers in the docker host. -func (cli *Client) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) { - query := url.Values{} - - if options.All { - query.Set("all", "1") - } - - if options.Limit > 0 { - query.Set("limit", strconv.Itoa(options.Limit)) - } - - if options.Since != "" { - query.Set("since", options.Since) - } - - if options.Before != "" { - query.Set("before", options.Before) - } - - if options.Size { - query.Set("size", "1") - } - - if options.Filters.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - resp, err := cli.get(ctx, "/containers/json", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var containers []container.Summary - err = json.NewDecoder(resp.Body).Decode(&containers) - return containers, err -} diff --git a/vendor/github.com/docker/docker/client/container_logs.go b/vendor/github.com/docker/docker/client/container_logs.go deleted file mode 100644 index 3ea1f68d4212..000000000000 --- a/vendor/github.com/docker/docker/client/container_logs.go +++ /dev/null @@ -1,85 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" - "time" - - "github.com/docker/docker/api/types/container" - timetypes "github.com/docker/docker/api/types/time" - "github.com/pkg/errors" -) - -// ContainerLogs returns the logs generated by a container in an io.ReadCloser. -// It's up to the caller to close the stream. -// -// The stream format on the response will be in one of two formats: -// -// If the container is using a TTY, there is only a single stream (stdout), and -// data is copied directly from the container output stream, no extra -// multiplexing or headers. -// -// If the container is *not* using a TTY, streams for stdout and stderr are -// multiplexed. -// The format of the multiplexed stream is as follows: -// -// [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}[]byte{OUTPUT} -// -// STREAM_TYPE can be 1 for stdout and 2 for stderr -// -// SIZE1, SIZE2, SIZE3, and SIZE4 are four bytes of uint32 encoded as big endian. -// This is the size of OUTPUT. -// -// You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this -// stream. -func (cli *Client) ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return nil, err - } - - query := url.Values{} - if options.ShowStdout { - query.Set("stdout", "1") - } - - if options.ShowStderr { - query.Set("stderr", "1") - } - - if options.Since != "" { - ts, err := timetypes.GetTimestamp(options.Since, time.Now()) - if err != nil { - return nil, errors.Wrap(err, `invalid value for "since"`) - } - query.Set("since", ts) - } - - if options.Until != "" { - ts, err := timetypes.GetTimestamp(options.Until, time.Now()) - if err != nil { - return nil, errors.Wrap(err, `invalid value for "until"`) - } - query.Set("until", ts) - } - - if options.Timestamps { - query.Set("timestamps", "1") - } - - if options.Details { - query.Set("details", "1") - } - - if options.Follow { - query.Set("follow", "1") - } - query.Set("tail", options.Tail) - - resp, err := cli.get(ctx, "/containers/"+containerID+"/logs", query, nil) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/container_pause.go b/vendor/github.com/docker/docker/client/container_pause.go deleted file mode 100644 index 59b3e2d86584..000000000000 --- a/vendor/github.com/docker/docker/client/container_pause.go +++ /dev/null @@ -1,15 +0,0 @@ -package client - -import "context" - -// ContainerPause pauses the main process of a given container without terminating it. -func (cli *Client) ContainerPause(ctx context.Context, containerID string) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_prune.go b/vendor/github.com/docker/docker/client/container_prune.go deleted file mode 100644 index 84fb6bc2350a..000000000000 --- a/vendor/github.com/docker/docker/client/container_prune.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" -) - -// ContainersPrune requests the daemon to delete unused data -func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) { - if err := cli.NewVersionError(ctx, "1.25", "container prune"); err != nil { - return container.PruneReport{}, err - } - - query, err := getFiltersQuery(pruneFilters) - if err != nil { - return container.PruneReport{}, err - } - - resp, err := cli.post(ctx, "/containers/prune", query, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.PruneReport{}, err - } - - var report container.PruneReport - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return container.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err) - } - - return report, nil -} diff --git a/vendor/github.com/docker/docker/client/container_remove.go b/vendor/github.com/docker/docker/client/container_remove.go deleted file mode 100644 index b1a2ce6b831a..000000000000 --- a/vendor/github.com/docker/docker/client/container_remove.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/container" -) - -// ContainerRemove kills and removes a container from the docker host. -func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if options.RemoveVolumes { - query.Set("v", "1") - } - if options.RemoveLinks { - query.Set("link", "1") - } - - if options.Force { - query.Set("force", "1") - } - - resp, err := cli.delete(ctx, "/containers/"+containerID, query, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_rename.go b/vendor/github.com/docker/docker/client/container_rename.go deleted file mode 100644 index 4c030228cdd3..000000000000 --- a/vendor/github.com/docker/docker/client/container_rename.go +++ /dev/null @@ -1,20 +0,0 @@ -package client - -import ( - "context" - "net/url" -) - -// ContainerRename changes the name of a given container. -func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - query.Set("name", newContainerName) - resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_resize.go b/vendor/github.com/docker/docker/client/container_resize.go deleted file mode 100644 index 56b7368b753f..000000000000 --- a/vendor/github.com/docker/docker/client/container_resize.go +++ /dev/null @@ -1,38 +0,0 @@ -package client - -import ( - "context" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/container" -) - -// ContainerResize changes the size of the tty for a container. -func (cli *Client) ContainerResize(ctx context.Context, containerID string, options container.ResizeOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width) -} - -// ContainerExecResize changes the size of the tty for an exec process running inside a container. -func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options container.ResizeOptions) error { - execID, err := trimID("exec", execID) - if err != nil { - return err - } - return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width) -} - -func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error { - // FIXME(thaJeztah): the API / backend accepts uint32, but container.ResizeOptions uses uint. - query := url.Values{} - query.Set("h", strconv.FormatUint(uint64(height), 10)) - query.Set("w", strconv.FormatUint(uint64(width), 10)) - - resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_restart.go b/vendor/github.com/docker/docker/client/container_restart.go deleted file mode 100644 index 5af07bfc76f8..000000000000 --- a/vendor/github.com/docker/docker/client/container_restart.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -import ( - "context" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/versions" -) - -// ContainerRestart stops and starts a container again. -// It makes the daemon wait for the container to be up again for -// a specific amount of time, given the timeout. -func (cli *Client) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if options.Timeout != nil { - query.Set("t", strconv.Itoa(*options.Timeout)) - } - if options.Signal != "" { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if versions.GreaterThanOrEqualTo(cli.version, "1.42") { - query.Set("signal", options.Signal) - } - } - resp, err := cli.post(ctx, "/containers/"+containerID+"/restart", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_start.go b/vendor/github.com/docker/docker/client/container_start.go deleted file mode 100644 index c7206e320aee..000000000000 --- a/vendor/github.com/docker/docker/client/container_start.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/container" -) - -// ContainerStart sends a request to the docker daemon to start a container. -func (cli *Client) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if options.CheckpointID != "" { - query.Set("checkpoint", options.CheckpointID) - } - if options.CheckpointDir != "" { - query.Set("checkpoint-dir", options.CheckpointDir) - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/start", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_stats.go b/vendor/github.com/docker/docker/client/container_stats.go deleted file mode 100644 index 2244e0f4b94a..000000000000 --- a/vendor/github.com/docker/docker/client/container_stats.go +++ /dev/null @@ -1,56 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/container" -) - -// ContainerStats returns near realtime stats for a given container. -// It's up to the caller to close the io.ReadCloser returned. -func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.StatsResponseReader{}, err - } - - query := url.Values{} - query.Set("stream", "0") - if stream { - query.Set("stream", "1") - } - - resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) - if err != nil { - return container.StatsResponseReader{}, err - } - - return container.StatsResponseReader{ - Body: resp.Body, - OSType: getDockerOS(resp.Header.Get("Server")), - }, nil -} - -// ContainerStatsOneShot gets a single stat entry from a container. -// It differs from `ContainerStats` in that the API should not wait to prime the stats -func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (container.StatsResponseReader, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.StatsResponseReader{}, err - } - - query := url.Values{} - query.Set("stream", "0") - query.Set("one-shot", "1") - - resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) - if err != nil { - return container.StatsResponseReader{}, err - } - - return container.StatsResponseReader{ - Body: resp.Body, - OSType: getDockerOS(resp.Header.Get("Server")), - }, nil -} diff --git a/vendor/github.com/docker/docker/client/container_stop.go b/vendor/github.com/docker/docker/client/container_stop.go deleted file mode 100644 index 175b9c8bcc8f..000000000000 --- a/vendor/github.com/docker/docker/client/container_stop.go +++ /dev/null @@ -1,45 +0,0 @@ -package client - -import ( - "context" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/versions" -) - -// ContainerStop stops a container. In case the container fails to stop -// gracefully within a time frame specified by the timeout argument, -// it is forcefully terminated (killed). -// -// If the timeout is nil, the container's StopTimeout value is used, if set, -// otherwise the engine default. A negative timeout value can be specified, -// meaning no timeout, i.e. no forceful termination is performed. -func (cli *Client) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - query := url.Values{} - if options.Timeout != nil { - query.Set("t", strconv.Itoa(*options.Timeout)) - } - if options.Signal != "" { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if versions.GreaterThanOrEqualTo(cli.version, "1.42") { - query.Set("signal", options.Signal) - } - } - resp, err := cli.post(ctx, "/containers/"+containerID+"/stop", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_top.go b/vendor/github.com/docker/docker/client/container_top.go deleted file mode 100644 index 5770f9d469a1..000000000000 --- a/vendor/github.com/docker/docker/client/container_top.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - "strings" - - "github.com/docker/docker/api/types/container" -) - -// ContainerTop shows process information from within a container. -func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (container.TopResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.TopResponse{}, err - } - - query := url.Values{} - if len(arguments) > 0 { - query.Set("ps_args", strings.Join(arguments, " ")) - } - - resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.TopResponse{}, err - } - - var response container.TopResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/container_unpause.go b/vendor/github.com/docker/docker/client/container_unpause.go deleted file mode 100644 index c95f6e3abaef..000000000000 --- a/vendor/github.com/docker/docker/client/container_unpause.go +++ /dev/null @@ -1,15 +0,0 @@ -package client - -import "context" - -// ContainerUnpause resumes the process execution within a container -func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error { - containerID, err := trimID("container", containerID) - if err != nil { - return err - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/container_update.go b/vendor/github.com/docker/docker/client/container_update.go deleted file mode 100644 index 10e966d089e7..000000000000 --- a/vendor/github.com/docker/docker/client/container_update.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/container" -) - -// ContainerUpdate updates the resources of a container. -func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (container.UpdateResponse, error) { - containerID, err := trimID("container", containerID) - if err != nil { - return container.UpdateResponse{}, err - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil) - defer ensureReaderClosed(resp) - if err != nil { - return container.UpdateResponse{}, err - } - - var response container.UpdateResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/container_wait.go b/vendor/github.com/docker/docker/client/container_wait.go deleted file mode 100644 index 75c03a12fa67..000000000000 --- a/vendor/github.com/docker/docker/client/container_wait.go +++ /dev/null @@ -1,122 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "io" - "net/url" - - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/versions" -) - -const containerWaitErrorMsgLimit = 2 * 1024 /* Max: 2KiB */ - -// ContainerWait waits until the specified container is in a certain state -// indicated by the given condition, either "not-running" (default), -// "next-exit", or "removed". -// -// If this client's API version is before 1.30, condition is ignored and -// ContainerWait will return immediately with the two channels, as the server -// will wait as if the condition were "not-running". -// -// If this client's API version is at least 1.30, ContainerWait blocks until -// the request has been acknowledged by the server (with a response header), -// then returns two channels on which the caller can wait for the exit status -// of the container or an error if there was a problem either beginning the -// wait request or in getting the response. This allows the caller to -// synchronize ContainerWait with other calls, such as specifying a -// "next-exit" condition before issuing a ContainerStart request. -func (cli *Client) ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) { - resultC := make(chan container.WaitResponse) - errC := make(chan error, 1) - - containerID, err := trimID("container", containerID) - if err != nil { - errC <- err - return resultC, errC - } - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - errC <- err - return resultC, errC - } - if versions.LessThan(cli.ClientVersion(), "1.30") { - return cli.legacyContainerWait(ctx, containerID) - } - - query := url.Values{} - if condition != "" { - query.Set("condition", string(condition)) - } - - resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", query, nil, nil) - if err != nil { - defer ensureReaderClosed(resp) - errC <- err - return resultC, errC - } - - go func() { - defer ensureReaderClosed(resp) - - responseText := bytes.NewBuffer(nil) - stream := io.TeeReader(resp.Body, responseText) - - var res container.WaitResponse - if err := json.NewDecoder(stream).Decode(&res); err != nil { - // NOTE(nicks): The /wait API does not work well with HTTP proxies. - // At any time, the proxy could cut off the response stream. - // - // But because the HTTP status has already been written, the proxy's - // only option is to write a plaintext error message. - // - // If there's a JSON parsing error, read the real error message - // off the body and send it to the client. - if errors.As(err, new(*json.SyntaxError)) { - _, _ = io.ReadAll(io.LimitReader(stream, containerWaitErrorMsgLimit)) - errC <- errors.New(responseText.String()) - } else { - errC <- err - } - return - } - - resultC <- res - }() - - return resultC, errC -} - -// legacyContainerWait returns immediately and doesn't have an option to wait -// until the container is removed. -func (cli *Client) legacyContainerWait(ctx context.Context, containerID string) (<-chan container.WaitResponse, <-chan error) { - resultC := make(chan container.WaitResponse) - errC := make(chan error) - - go func() { - resp, err := cli.post(ctx, "/containers/"+containerID+"/wait", nil, nil, nil) - if err != nil { - errC <- err - return - } - defer ensureReaderClosed(resp) - - var res container.WaitResponse - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - errC <- err - return - } - - resultC <- res - }() - - return resultC, errC -} diff --git a/vendor/github.com/docker/docker/client/disk_usage.go b/vendor/github.com/docker/docker/client/disk_usage.go deleted file mode 100644 index 729e105715fd..000000000000 --- a/vendor/github.com/docker/docker/client/disk_usage.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - - "github.com/docker/docker/api/types" -) - -// DiskUsage requests the current data usage from the daemon -func (cli *Client) DiskUsage(ctx context.Context, options types.DiskUsageOptions) (types.DiskUsage, error) { - var query url.Values - if len(options.Types) > 0 { - query = url.Values{} - for _, t := range options.Types { - query.Add("type", string(t)) - } - } - - resp, err := cli.get(ctx, "/system/df", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return types.DiskUsage{}, err - } - - var du types.DiskUsage - if err := json.NewDecoder(resp.Body).Decode(&du); err != nil { - return types.DiskUsage{}, fmt.Errorf("Error retrieving disk usage: %v", err) - } - return du, nil -} diff --git a/vendor/github.com/docker/docker/client/distribution_inspect.go b/vendor/github.com/docker/docker/client/distribution_inspect.go deleted file mode 100644 index 693c4121a621..000000000000 --- a/vendor/github.com/docker/docker/client/distribution_inspect.go +++ /dev/null @@ -1,39 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/http" - "net/url" - - "github.com/docker/docker/api/types/registry" -) - -// DistributionInspect returns the image digest with the full manifest. -func (cli *Client) DistributionInspect(ctx context.Context, imageRef, encodedRegistryAuth string) (registry.DistributionInspect, error) { - if imageRef == "" { - return registry.DistributionInspect{}, objectNotFoundError{object: "distribution", id: imageRef} - } - - if err := cli.NewVersionError(ctx, "1.30", "distribution inspect"); err != nil { - return registry.DistributionInspect{}, err - } - - var headers http.Header - if encodedRegistryAuth != "" { - headers = http.Header{ - registry.AuthHeader: {encodedRegistryAuth}, - } - } - - // Contact the registry to retrieve digest and platform information - resp, err := cli.get(ctx, "/distribution/"+imageRef+"/json", url.Values{}, headers) - defer ensureReaderClosed(resp) - if err != nil { - return registry.DistributionInspect{}, err - } - - var distributionInspect registry.DistributionInspect - err = json.NewDecoder(resp.Body).Decode(&distributionInspect) - return distributionInspect, err -} diff --git a/vendor/github.com/docker/docker/client/envvars.go b/vendor/github.com/docker/docker/client/envvars.go deleted file mode 100644 index abe122d18e78..000000000000 --- a/vendor/github.com/docker/docker/client/envvars.go +++ /dev/null @@ -1,90 +0,0 @@ -package client - -const ( - // EnvOverrideHost is the name of the environment variable that can be used - // to override the default host to connect to (DefaultDockerHost). - // - // This env-var is read by FromEnv and WithHostFromEnv and when set to a - // non-empty value, takes precedence over the default host (which is platform - // specific), or any host already set. - EnvOverrideHost = "DOCKER_HOST" - - // EnvOverrideAPIVersion is the name of the environment variable that can - // be used to override the API version to use. Value should be - // formatted as MAJOR.MINOR, for example, "1.19". - // - // This env-var is read by FromEnv and WithVersionFromEnv and when set to a - // non-empty value, takes precedence over API version negotiation. - // - // This environment variable should be used for debugging purposes only, as - // it can set the client to use an incompatible (or invalid) API version. - EnvOverrideAPIVersion = "DOCKER_API_VERSION" - - // EnvOverrideCertPath is the name of the environment variable that can be - // used to specify the directory from which to load the TLS certificates - // (ca.pem, cert.pem, key.pem) from. These certificates are used to configure - // the Client for a TCP connection protected by TLS client authentication. - // - // TLS certificate verification is enabled by default if the Client is configured - // to use a TLS connection. Refer to EnvTLSVerify below to learn how to - // disable verification for testing purposes. - // - // WARNING: Access to the remote API is equivalent to root access to the - // host where the daemon runs. Do not expose the API without protection, - // and only if needed. Make sure you are familiar with the "daemon attack - // surface" (https://docs.docker.com/go/attack-surface/). - // - // For local access to the API, it is recommended to connect with the daemon - // using the default local socket connection (on Linux), or the named pipe - // (on Windows). - // - // If you need to access the API of a remote daemon, consider using an SSH - // (ssh://) connection, which is easier to set up, and requires no additional - // configuration if the host is accessible using ssh. - // - // If you cannot use the alternatives above, and you must expose the API over - // a TCP connection, refer to https://docs.docker.com/engine/security/protect-access/ - // to learn how to configure the daemon and client to use a TCP connection - // with TLS client authentication. Make sure you know the differences between - // a regular TLS connection and a TLS connection protected by TLS client - // authentication, and verify that the API cannot be accessed by other clients. - EnvOverrideCertPath = "DOCKER_CERT_PATH" - - // EnvTLSVerify is the name of the environment variable that can be used to - // enable or disable TLS certificate verification. When set to a non-empty - // value, TLS certificate verification is enabled, and the client is configured - // to use a TLS connection, using certificates from the default directories - // (within `~/.docker`); refer to EnvOverrideCertPath above for additional - // details. - // - // WARNING: Access to the remote API is equivalent to root access to the - // host where the daemon runs. Do not expose the API without protection, - // and only if needed. Make sure you are familiar with the "daemon attack - // surface" (https://docs.docker.com/go/attack-surface/). - // - // Before setting up your client and daemon to use a TCP connection with TLS - // client authentication, consider using one of the alternatives mentioned - // in EnvOverrideCertPath above. - // - // Disabling TLS certificate verification (for testing purposes) - // - // TLS certificate verification is enabled by default if the Client is configured - // to use a TLS connection, and it is highly recommended to keep verification - // enabled to prevent machine-in-the-middle attacks. Refer to the documentation - // at https://docs.docker.com/engine/security/protect-access/ and pages linked - // from that page to learn how to configure the daemon and client to use a - // TCP connection with TLS client authentication enabled. - // - // Set the "DOCKER_TLS_VERIFY" environment to an empty string ("") to - // disable TLS certificate verification. Disabling verification is insecure, - // so should only be done for testing purposes. From the Go documentation - // (https://pkg.go.dev/crypto/tls#Config): - // - // InsecureSkipVerify controls whether a client verifies the server's - // certificate chain and host name. If InsecureSkipVerify is true, crypto/tls - // accepts any certificate presented by the server and any host name in that - // certificate. In this mode, TLS is susceptible to machine-in-the-middle - // attacks unless custom verification is used. This should be used only for - // testing or in combination with VerifyConnection or VerifyPeerCertificate. - EnvTLSVerify = "DOCKER_TLS_VERIFY" -) diff --git a/vendor/github.com/docker/docker/client/errors.go b/vendor/github.com/docker/docker/client/errors.go deleted file mode 100644 index 9e3a2538f220..000000000000 --- a/vendor/github.com/docker/docker/client/errors.go +++ /dev/null @@ -1,129 +0,0 @@ -package client - -import ( - "context" - "errors" - "fmt" - "net/http" - - cerrdefs "github.com/containerd/errdefs" - "github.com/containerd/errdefs/pkg/errhttp" - "github.com/docker/docker/api/types/versions" -) - -// errConnectionFailed implements an error returned when connection failed. -type errConnectionFailed struct { - error -} - -// Error returns a string representation of an errConnectionFailed -func (e errConnectionFailed) Error() string { - return e.error.Error() -} - -func (e errConnectionFailed) Unwrap() error { - return e.error -} - -// IsErrConnectionFailed returns true if the error is caused by connection failed. -func IsErrConnectionFailed(err error) bool { - return errors.As(err, &errConnectionFailed{}) -} - -// ErrorConnectionFailed returns an error with host in the error message when connection to docker daemon failed. -// -// Deprecated: this function was only used internally, and will be removed in the next release. -func ErrorConnectionFailed(host string) error { - return connectionFailed(host) -} - -// connectionFailed returns an error with host in the error message when connection -// to docker daemon failed. -func connectionFailed(host string) error { - var err error - if host == "" { - err = errors.New("Cannot connect to the Docker daemon. Is the docker daemon running on this host?") - } else { - err = fmt.Errorf("Cannot connect to the Docker daemon at %s. Is the docker daemon running?", host) - } - return errConnectionFailed{error: err} -} - -// IsErrNotFound returns true if the error is a NotFound error, which is returned -// by the API when some object is not found. It is an alias for [cerrdefs.IsNotFound]. -// -// Deprecated: use [cerrdefs.IsNotFound] instead. -func IsErrNotFound(err error) bool { - return cerrdefs.IsNotFound(err) -} - -type objectNotFoundError struct { - object string - id string -} - -func (e objectNotFoundError) NotFound() {} - -func (e objectNotFoundError) Error() string { - return fmt.Sprintf("Error: No such %s: %s", e.object, e.id) -} - -// NewVersionError returns an error if the APIVersion required is less than the -// current supported version. -// -// It performs API-version negotiation if the Client is configured with this -// option, otherwise it assumes the latest API version is used. -func (cli *Client) NewVersionError(ctx context.Context, APIrequired, feature string) error { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if cli.version != "" && versions.LessThan(cli.version, APIrequired) { - return fmt.Errorf("%q requires API version %s, but the Docker daemon API version is %s", feature, APIrequired, cli.version) - } - return nil -} - -type httpError struct { - err error - errdef error -} - -func (e *httpError) Error() string { - return e.err.Error() -} - -func (e *httpError) Unwrap() error { - return e.err -} - -func (e *httpError) Is(target error) bool { - return errors.Is(e.errdef, target) -} - -// httpErrorFromStatusCode creates an errdef error, based on the provided HTTP status-code -func httpErrorFromStatusCode(err error, statusCode int) error { - if err == nil { - return nil - } - base := errhttp.ToNative(statusCode) - if base != nil { - return &httpError{err: err, errdef: base} - } - - switch { - case statusCode >= http.StatusOK && statusCode < http.StatusBadRequest: - // it's a client error - return err - case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError: - return &httpError{err: err, errdef: cerrdefs.ErrInvalidArgument} - case statusCode >= http.StatusInternalServerError && statusCode < 600: - return &httpError{err: err, errdef: cerrdefs.ErrInternal} - default: - return &httpError{err: err, errdef: cerrdefs.ErrUnknown} - } -} diff --git a/vendor/github.com/docker/docker/client/events.go b/vendor/github.com/docker/docker/client/events.go deleted file mode 100644 index 498fe4631404..000000000000 --- a/vendor/github.com/docker/docker/client/events.go +++ /dev/null @@ -1,100 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - "time" - - "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" - timetypes "github.com/docker/docker/api/types/time" -) - -// Events returns a stream of events in the daemon. It's up to the caller to close the stream -// by cancelling the context. Once the stream has been completely read an io.EOF error will -// be sent over the error channel. If an error is sent all processing will be stopped. It's up -// to the caller to reopen the stream in the event of an error by reinvoking this method. -func (cli *Client) Events(ctx context.Context, options events.ListOptions) (<-chan events.Message, <-chan error) { - messages := make(chan events.Message) - errs := make(chan error, 1) - - started := make(chan struct{}) - go func() { - defer close(errs) - - query, err := buildEventsQueryParams(cli.version, options) - if err != nil { - close(started) - errs <- err - return - } - - resp, err := cli.get(ctx, "/events", query, nil) - if err != nil { - close(started) - errs <- err - return - } - defer resp.Body.Close() - - decoder := json.NewDecoder(resp.Body) - - close(started) - for { - select { - case <-ctx.Done(): - errs <- ctx.Err() - return - default: - var event events.Message - if err := decoder.Decode(&event); err != nil { - errs <- err - return - } - - select { - case messages <- event: - case <-ctx.Done(): - errs <- ctx.Err() - return - } - } - } - }() - <-started - - return messages, errs -} - -func buildEventsQueryParams(cliVersion string, options events.ListOptions) (url.Values, error) { - query := url.Values{} - ref := time.Now() - - if options.Since != "" { - ts, err := timetypes.GetTimestamp(options.Since, ref) - if err != nil { - return nil, err - } - query.Set("since", ts) - } - - if options.Until != "" { - ts, err := timetypes.GetTimestamp(options.Until, ref) - if err != nil { - return nil, err - } - query.Set("until", ts) - } - - if options.Filters.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cliVersion, options.Filters) - if err != nil { - return nil, err - } - query.Set("filters", filterJSON) - } - - return query, nil -} diff --git a/vendor/github.com/docker/docker/client/hijack.go b/vendor/github.com/docker/docker/client/hijack.go deleted file mode 100644 index 01d121a62e71..000000000000 --- a/vendor/github.com/docker/docker/client/hijack.go +++ /dev/null @@ -1,139 +0,0 @@ -package client - -import ( - "bufio" - "context" - "fmt" - "net" - "net/http" - "net/url" - "time" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/versions" - "github.com/pkg/errors" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -// postHijacked sends a POST request and hijacks the connection. -func (cli *Client) postHijacked(ctx context.Context, path string, query url.Values, body interface{}, headers map[string][]string) (types.HijackedResponse, error) { - bodyEncoded, err := encodeData(body) - if err != nil { - return types.HijackedResponse{}, err - } - req, err := cli.buildRequest(ctx, http.MethodPost, cli.getAPIPath(ctx, path, query), bodyEncoded, headers) - if err != nil { - return types.HijackedResponse{}, err - } - conn, mediaType, err := setupHijackConn(cli.dialer(), req, "tcp") - if err != nil { - return types.HijackedResponse{}, err - } - - if versions.LessThan(cli.ClientVersion(), "1.42") { - // Prior to 1.42, Content-Type is always set to raw-stream and not relevant - mediaType = "" - } - - return types.NewHijackedResponse(conn, mediaType), nil -} - -// DialHijack returns a hijacked connection with negotiated protocol proto. -func (cli *Client) DialHijack(ctx context.Context, url, proto string, meta map[string][]string) (net.Conn, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody) - if err != nil { - return nil, err - } - req = cli.addHeaders(req, meta) - - conn, _, err := setupHijackConn(cli.Dialer(), req, proto) - return conn, err -} - -func setupHijackConn(dialer func(context.Context) (net.Conn, error), req *http.Request, proto string) (_ net.Conn, _ string, retErr error) { - ctx := req.Context() - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", proto) - - conn, err := dialer(ctx) - if err != nil { - return nil, "", errors.Wrap(err, "cannot connect to the Docker daemon. Is 'docker daemon' running on this host?") - } - defer func() { - if retErr != nil { - conn.Close() - } - }() - - // When we set up a TCP connection for hijack, there could be long periods - // of inactivity (a long running command with no output) that in certain - // network setups may cause ECONNTIMEOUT, leaving the client in an unknown - // state. Setting TCP KeepAlive on the socket connection will prohibit - // ECONNTIMEOUT unless the socket connection truly is broken - if tcpConn, ok := conn.(*net.TCPConn); ok { - _ = tcpConn.SetKeepAlive(true) - _ = tcpConn.SetKeepAlivePeriod(30 * time.Second) - } - - hc := &hijackedConn{conn, bufio.NewReader(conn)} - - // Server hijacks the connection, error 'connection closed' expected - resp, err := otelhttp.NewTransport(hc).RoundTrip(req) - if err != nil { - return nil, "", err - } - if resp.StatusCode != http.StatusSwitchingProtocols { - _ = resp.Body.Close() - return nil, "", fmt.Errorf("unable to upgrade to %s, received %d", proto, resp.StatusCode) - } - - if hc.r.Buffered() > 0 { - // If there is buffered content, wrap the connection. We return an - // object that implements CloseWrite if the underlying connection - // implements it. - if _, ok := hc.Conn.(types.CloseWriter); ok { - conn = &hijackedConnCloseWriter{hc} - } else { - conn = hc - } - } else { - hc.r.Reset(nil) - } - - return conn, resp.Header.Get("Content-Type"), nil -} - -// hijackedConn wraps a net.Conn and is returned by setupHijackConn in the case -// that a) there was already buffered data in the http layer when Hijack() was -// called, and b) the underlying net.Conn does *not* implement CloseWrite(). -// hijackedConn does not implement CloseWrite() either. -type hijackedConn struct { - net.Conn - r *bufio.Reader -} - -func (c *hijackedConn) RoundTrip(req *http.Request) (*http.Response, error) { - if err := req.Write(c.Conn); err != nil { - return nil, err - } - return http.ReadResponse(c.r, req) -} - -func (c *hijackedConn) Read(b []byte) (int, error) { - return c.r.Read(b) -} - -// hijackedConnCloseWriter is a hijackedConn which additionally implements -// CloseWrite(). It is returned by setupHijackConn in the case that a) there -// was already buffered data in the http layer when Hijack() was called, and b) -// the underlying net.Conn *does* implement CloseWrite(). -type hijackedConnCloseWriter struct { - *hijackedConn -} - -var _ types.CloseWriter = &hijackedConnCloseWriter{} - -func (c *hijackedConnCloseWriter) CloseWrite() error { - conn := c.Conn.(types.CloseWriter) - return conn.CloseWrite() -} diff --git a/vendor/github.com/docker/docker/client/image_create.go b/vendor/github.com/docker/docker/client/image_create.go deleted file mode 100644 index 1e044d7779d0..000000000000 --- a/vendor/github.com/docker/docker/client/image_create.go +++ /dev/null @@ -1,40 +0,0 @@ -package client - -import ( - "context" - "io" - "net/http" - "net/url" - "strings" - - "github.com/distribution/reference" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/registry" -) - -// ImageCreate creates a new image based on the parent options. -// It returns the JSON content in the response body. -func (cli *Client) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) { - ref, err := reference.ParseNormalizedNamed(parentReference) - if err != nil { - return nil, err - } - - query := url.Values{} - query.Set("fromImage", ref.Name()) - query.Set("tag", getAPITagFromNamedRef(ref)) - if options.Platform != "" { - query.Set("platform", strings.ToLower(options.Platform)) - } - resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) - if err != nil { - return nil, err - } - return resp.Body, nil -} - -func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/images/create", query, nil, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} diff --git a/vendor/github.com/docker/docker/client/image_history.go b/vendor/github.com/docker/docker/client/image_history.go deleted file mode 100644 index fce8b80e1819..000000000000 --- a/vendor/github.com/docker/docker/client/image_history.go +++ /dev/null @@ -1,56 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - - "github.com/docker/docker/api/types/image" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ImageHistoryWithPlatform sets the platform for the image history operation. -func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption { - return imageHistoryOptionFunc(func(opt *imageHistoryOpts) error { - if opt.apiOptions.Platform != nil { - return fmt.Errorf("platform already set to %s", *opt.apiOptions.Platform) - } - opt.apiOptions.Platform = &platform - return nil - }) -} - -// ImageHistory returns the changes in an image in history format. -func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) { - query := url.Values{} - - var opts imageHistoryOpts - for _, o := range historyOpts { - if err := o.Apply(&opts); err != nil { - return nil, err - } - } - - if opts.apiOptions.Platform != nil { - if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err - } - - p, err := encodePlatform(opts.apiOptions.Platform) - if err != nil { - return nil, err - } - query.Set("platform", p) - } - - resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var history []image.HistoryResponseItem - err = json.NewDecoder(resp.Body).Decode(&history) - return history, err -} diff --git a/vendor/github.com/docker/docker/client/image_history_opts.go b/vendor/github.com/docker/docker/client/image_history_opts.go deleted file mode 100644 index 6d3494dd0bfb..000000000000 --- a/vendor/github.com/docker/docker/client/image_history_opts.go +++ /dev/null @@ -1,19 +0,0 @@ -package client - -import ( - "github.com/docker/docker/api/types/image" -) - -// ImageHistoryOption is a type representing functional options for the image history operation. -type ImageHistoryOption interface { - Apply(*imageHistoryOpts) error -} -type imageHistoryOptionFunc func(opt *imageHistoryOpts) error - -func (f imageHistoryOptionFunc) Apply(o *imageHistoryOpts) error { - return f(o) -} - -type imageHistoryOpts struct { - apiOptions image.HistoryOptions -} diff --git a/vendor/github.com/docker/docker/client/image_import.go b/vendor/github.com/docker/docker/client/image_import.go deleted file mode 100644 index 5236dbc62a85..000000000000 --- a/vendor/github.com/docker/docker/client/image_import.go +++ /dev/null @@ -1,48 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" - "strings" - - "github.com/distribution/reference" - "github.com/docker/docker/api/types/image" -) - -// ImageImport creates a new image based on the source options. -// It returns the JSON content in the response body. -func (cli *Client) ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) { - if ref != "" { - // Check if the given image name can be resolved - if _, err := reference.ParseNormalizedNamed(ref); err != nil { - return nil, err - } - } - - query := url.Values{} - if source.SourceName != "" { - query.Set("fromSrc", source.SourceName) - } - if ref != "" { - query.Set("repo", ref) - } - if options.Tag != "" { - query.Set("tag", options.Tag) - } - if options.Message != "" { - query.Set("message", options.Message) - } - if options.Platform != "" { - query.Set("platform", strings.ToLower(options.Platform)) - } - for _, change := range options.Changes { - query.Add("changes", change) - } - - resp, err := cli.postRaw(ctx, "/images/create", query, source.Source, nil) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/image_inspect.go b/vendor/github.com/docker/docker/client/image_inspect.go deleted file mode 100644 index 4c35003129f0..000000000000 --- a/vendor/github.com/docker/docker/client/image_inspect.go +++ /dev/null @@ -1,76 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/url" - - "github.com/docker/docker/api/types/image" -) - -// ImageInspect returns the image information. -func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) { - if imageID == "" { - return image.InspectResponse{}, objectNotFoundError{object: "image", id: imageID} - } - - var opts imageInspectOpts - for _, opt := range inspectOpts { - if err := opt.Apply(&opts); err != nil { - return image.InspectResponse{}, fmt.Errorf("error applying image inspect option: %w", err) - } - } - - query := url.Values{} - if opts.apiOptions.Manifests { - if err := cli.NewVersionError(ctx, "1.48", "manifests"); err != nil { - return image.InspectResponse{}, err - } - query.Set("manifests", "1") - } - - if opts.apiOptions.Platform != nil { - if err := cli.NewVersionError(ctx, "1.49", "platform"); err != nil { - return image.InspectResponse{}, err - } - platform, err := encodePlatform(opts.apiOptions.Platform) - if err != nil { - return image.InspectResponse{}, err - } - query.Set("platform", platform) - } - - resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return image.InspectResponse{}, err - } - - buf := opts.raw - if buf == nil { - buf = &bytes.Buffer{} - } - - if _, err := io.Copy(buf, resp.Body); err != nil { - return image.InspectResponse{}, err - } - - var response image.InspectResponse - err = json.Unmarshal(buf.Bytes(), &response) - return response, err -} - -// ImageInspectWithRaw returns the image information and its raw representation. -// -// Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option. -func (cli *Client) ImageInspectWithRaw(ctx context.Context, imageID string) (image.InspectResponse, []byte, error) { - var buf bytes.Buffer - resp, err := cli.ImageInspect(ctx, imageID, ImageInspectWithRawResponse(&buf)) - if err != nil { - return image.InspectResponse{}, nil, err - } - return resp, buf.Bytes(), err -} diff --git a/vendor/github.com/docker/docker/client/image_inspect_opts.go b/vendor/github.com/docker/docker/client/image_inspect_opts.go deleted file mode 100644 index 655cbf0b7af0..000000000000 --- a/vendor/github.com/docker/docker/client/image_inspect_opts.go +++ /dev/null @@ -1,62 +0,0 @@ -package client - -import ( - "bytes" - - "github.com/docker/docker/api/types/image" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ImageInspectOption is a type representing functional options for the image inspect operation. -type ImageInspectOption interface { - Apply(*imageInspectOpts) error -} -type imageInspectOptionFunc func(opt *imageInspectOpts) error - -func (f imageInspectOptionFunc) Apply(o *imageInspectOpts) error { - return f(o) -} - -// ImageInspectWithRawResponse instructs the client to additionally store the -// raw inspect response in the provided buffer. -func ImageInspectWithRawResponse(raw *bytes.Buffer) ImageInspectOption { - return imageInspectOptionFunc(func(opts *imageInspectOpts) error { - opts.raw = raw - return nil - }) -} - -// ImageInspectWithManifests sets manifests API option for the image inspect operation. -// This option is only available for API version 1.48 and up. -// With this option set, the image inspect operation response will have the -// [image.InspectResponse.Manifests] field populated if the server is multi-platform capable. -func ImageInspectWithManifests(manifests bool) ImageInspectOption { - return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error { - clientOpts.apiOptions.Manifests = manifests - return nil - }) -} - -// ImageInspectWithPlatform sets platform API option for the image inspect operation. -// This option is only available for API version 1.49 and up. -// With this option set, the image inspect operation will return information for the -// specified platform variant of the multi-platform image. -func ImageInspectWithPlatform(platform *ocispec.Platform) ImageInspectOption { - return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error { - clientOpts.apiOptions.Platform = platform - return nil - }) -} - -// ImageInspectWithAPIOpts sets the API options for the image inspect operation. -func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption { - return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error { - clientOpts.apiOptions = opts - return nil - }) -} - -type imageInspectOpts struct { - raw *bytes.Buffer - apiOptions image.InspectOptions -} diff --git a/vendor/github.com/docker/docker/client/image_list.go b/vendor/github.com/docker/docker/client/image_list.go deleted file mode 100644 index ec0a2ad57060..000000000000 --- a/vendor/github.com/docker/docker/client/image_list.go +++ /dev/null @@ -1,67 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/versions" -) - -// ImageList returns a list of images in the docker host. -// -// Experimental: Setting the [options.Manifest] will populate -// [image.Summary.Manifests] with information about image manifests. -// This is experimental and might change in the future without any backward -// compatibility. -func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error) { - var images []image.Summary - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return images, err - } - - query := url.Values{} - - optionFilters := options.Filters - referenceFilters := optionFilters.Get("reference") - if versions.LessThan(cli.version, "1.25") && len(referenceFilters) > 0 { - query.Set("filter", referenceFilters[0]) - for _, filterValue := range referenceFilters { - optionFilters.Del("reference", filterValue) - } - } - if optionFilters.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cli.version, optionFilters) - if err != nil { - return images, err - } - query.Set("filters", filterJSON) - } - if options.All { - query.Set("all", "1") - } - if options.SharedSize && versions.GreaterThanOrEqualTo(cli.version, "1.42") { - query.Set("shared-size", "1") - } - if options.Manifests && versions.GreaterThanOrEqualTo(cli.version, "1.47") { - query.Set("manifests", "1") - } - - resp, err := cli.get(ctx, "/images/json", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return images, err - } - - err = json.NewDecoder(resp.Body).Decode(&images) - return images, err -} diff --git a/vendor/github.com/docker/docker/client/image_load.go b/vendor/github.com/docker/docker/client/image_load.go deleted file mode 100644 index 079002e943ea..000000000000 --- a/vendor/github.com/docker/docker/client/image_load.go +++ /dev/null @@ -1,54 +0,0 @@ -package client - -import ( - "context" - "io" - "net/http" - "net/url" - - "github.com/docker/docker/api/types/image" -) - -// ImageLoad loads an image in the docker host from the client host. -// It's up to the caller to close the io.ReadCloser in the -// ImageLoadResponse returned by this function. -// -// Platform is an optional parameter that specifies the platform to load from -// the provided multi-platform image. This is only has effect if the input image -// is a multi-platform image. -func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (image.LoadResponse, error) { - var opts imageLoadOpts - for _, opt := range loadOpts { - if err := opt.Apply(&opts); err != nil { - return image.LoadResponse{}, err - } - } - - query := url.Values{} - query.Set("quiet", "0") - if opts.apiOptions.Quiet { - query.Set("quiet", "1") - } - if len(opts.apiOptions.Platforms) > 0 { - if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return image.LoadResponse{}, err - } - - p, err := encodePlatforms(opts.apiOptions.Platforms...) - if err != nil { - return image.LoadResponse{}, err - } - query["platform"] = p - } - - resp, err := cli.postRaw(ctx, "/images/load", query, input, http.Header{ - "Content-Type": {"application/x-tar"}, - }) - if err != nil { - return image.LoadResponse{}, err - } - return image.LoadResponse{ - Body: resp.Body, - JSON: resp.Header.Get("Content-Type") == "application/json", - }, nil -} diff --git a/vendor/github.com/docker/docker/client/image_load_opts.go b/vendor/github.com/docker/docker/client/image_load_opts.go deleted file mode 100644 index ebcedd41ff5a..000000000000 --- a/vendor/github.com/docker/docker/client/image_load_opts.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -import ( - "fmt" - - "github.com/docker/docker/api/types/image" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -// ImageLoadOption is a type representing functional options for the image load operation. -type ImageLoadOption interface { - Apply(*imageLoadOpts) error -} -type imageLoadOptionFunc func(opt *imageLoadOpts) error - -func (f imageLoadOptionFunc) Apply(o *imageLoadOpts) error { - return f(o) -} - -type imageLoadOpts struct { - apiOptions image.LoadOptions -} - -// ImageLoadWithQuiet sets the quiet option for the image load operation. -func ImageLoadWithQuiet(quiet bool) ImageLoadOption { - return imageLoadOptionFunc(func(opt *imageLoadOpts) error { - opt.apiOptions.Quiet = quiet - return nil - }) -} - -// ImageLoadWithPlatforms sets the platforms to be loaded from the image. -func ImageLoadWithPlatforms(platforms ...ocispec.Platform) ImageLoadOption { - return imageLoadOptionFunc(func(opt *imageLoadOpts) error { - if opt.apiOptions.Platforms != nil { - return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms) - } - opt.apiOptions.Platforms = platforms - return nil - }) -} diff --git a/vendor/github.com/docker/docker/client/image_prune.go b/vendor/github.com/docker/docker/client/image_prune.go deleted file mode 100644 index 52e8bcf5512a..000000000000 --- a/vendor/github.com/docker/docker/client/image_prune.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/image" -) - -// ImagesPrune requests the daemon to delete unused data -func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (image.PruneReport, error) { - if err := cli.NewVersionError(ctx, "1.25", "image prune"); err != nil { - return image.PruneReport{}, err - } - - query, err := getFiltersQuery(pruneFilters) - if err != nil { - return image.PruneReport{}, err - } - - resp, err := cli.post(ctx, "/images/prune", query, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return image.PruneReport{}, err - } - - var report image.PruneReport - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return image.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err) - } - - return report, nil -} diff --git a/vendor/github.com/docker/docker/client/image_pull.go b/vendor/github.com/docker/docker/client/image_pull.go deleted file mode 100644 index ab7606b4563c..000000000000 --- a/vendor/github.com/docker/docker/client/image_pull.go +++ /dev/null @@ -1,64 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" - "strings" - - cerrdefs "github.com/containerd/errdefs" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/image" -) - -// ImagePull requests the docker host to pull an image from a remote registry. -// It executes the privileged function if the operation is unauthorized -// and it tries one more time. -// It's up to the caller to handle the io.ReadCloser and close it properly. -// -// FIXME(vdemeester): there is currently used in a few way in docker/docker -// - if not in trusted content, ref is used to pass the whole reference, and tag is empty -// - if in trusted content, ref is used to pass the reference name, and tag for the digest -func (cli *Client) ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) { - ref, err := reference.ParseNormalizedNamed(refStr) - if err != nil { - return nil, err - } - - query := url.Values{} - query.Set("fromImage", ref.Name()) - if !options.All { - query.Set("tag", getAPITagFromNamedRef(ref)) - } - if options.Platform != "" { - query.Set("platform", strings.ToLower(options.Platform)) - } - - resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) - if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { - newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) - if privilegeErr != nil { - return nil, privilegeErr - } - resp, err = cli.tryImageCreate(ctx, query, newAuthHeader) - } - if err != nil { - return nil, err - } - return resp.Body, nil -} - -// getAPITagFromNamedRef returns a tag from the specified reference. -// This function is necessary as long as the docker "server" api expects -// digests to be sent as tags and makes a distinction between the name -// and tag/digest part of a reference. -func getAPITagFromNamedRef(ref reference.Named) string { - if digested, ok := ref.(reference.Digested); ok { - return digested.Digest().String() - } - ref = reference.TagNameOnly(ref) - if tagged, ok := ref.(reference.Tagged); ok { - return tagged.Tag() - } - return "" -} diff --git a/vendor/github.com/docker/docker/client/image_push.go b/vendor/github.com/docker/docker/client/image_push.go deleted file mode 100644 index cbbe9a25d609..000000000000 --- a/vendor/github.com/docker/docker/client/image_push.go +++ /dev/null @@ -1,72 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - - cerrdefs "github.com/containerd/errdefs" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/registry" -) - -// ImagePush requests the docker host to push an image to a remote registry. -// It executes the privileged function if the operation is unauthorized -// and it tries one more time. -// It's up to the caller to handle the io.ReadCloser and close it properly. -func (cli *Client) ImagePush(ctx context.Context, image string, options image.PushOptions) (io.ReadCloser, error) { - ref, err := reference.ParseNormalizedNamed(image) - if err != nil { - return nil, err - } - - if _, isCanonical := ref.(reference.Canonical); isCanonical { - return nil, errors.New("cannot push a digest reference") - } - - query := url.Values{} - if !options.All { - ref = reference.TagNameOnly(ref) - if tagged, ok := ref.(reference.Tagged); ok { - query.Set("tag", tagged.Tag()) - } - } - - if options.Platform != nil { - if err := cli.NewVersionError(ctx, "1.46", "platform"); err != nil { - return nil, err - } - - p := *options.Platform - pJson, err := json.Marshal(p) - if err != nil { - return nil, fmt.Errorf("invalid platform: %v", err) - } - - query.Set("platform", string(pJson)) - } - - resp, err := cli.tryImagePush(ctx, ref.Name(), query, options.RegistryAuth) - if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { - newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) - if privilegeErr != nil { - return nil, privilegeErr - } - resp, err = cli.tryImagePush(ctx, ref.Name(), query, newAuthHeader) - } - if err != nil { - return nil, err - } - return resp.Body, nil -} - -func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/images/"+imageID+"/push", query, nil, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} diff --git a/vendor/github.com/docker/docker/client/image_save.go b/vendor/github.com/docker/docker/client/image_save.go deleted file mode 100644 index d2102becf14c..000000000000 --- a/vendor/github.com/docker/docker/client/image_save.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" -) - -// ImageSave retrieves one or more images from the docker host as an io.ReadCloser. -// -// Platforms is an optional parameter that specifies the platforms to save from the image. -// This is only has effect if the input image is a multi-platform image. -func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) { - var opts imageSaveOpts - for _, opt := range saveOpts { - if err := opt.Apply(&opts); err != nil { - return nil, err - } - } - - query := url.Values{ - "names": imageIDs, - } - - if len(opts.apiOptions.Platforms) > 0 { - if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { - return nil, err - } - p, err := encodePlatforms(opts.apiOptions.Platforms...) - if err != nil { - return nil, err - } - query["platform"] = p - } - - resp, err := cli.get(ctx, "/images/get", query, nil) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/image_save_opts.go b/vendor/github.com/docker/docker/client/image_save_opts.go deleted file mode 100644 index acd8f282b3d9..000000000000 --- a/vendor/github.com/docker/docker/client/image_save_opts.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "fmt" - - "github.com/docker/docker/api/types/image" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -type ImageSaveOption interface { - Apply(*imageSaveOpts) error -} - -type imageSaveOptionFunc func(opt *imageSaveOpts) error - -func (f imageSaveOptionFunc) Apply(o *imageSaveOpts) error { - return f(o) -} - -// ImageSaveWithPlatforms sets the platforms to be saved from the image. -func ImageSaveWithPlatforms(platforms ...ocispec.Platform) ImageSaveOption { - return imageSaveOptionFunc(func(opt *imageSaveOpts) error { - if opt.apiOptions.Platforms != nil { - return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms) - } - opt.apiOptions.Platforms = platforms - return nil - }) -} - -type imageSaveOpts struct { - apiOptions image.SaveOptions -} diff --git a/vendor/github.com/docker/docker/client/image_search.go b/vendor/github.com/docker/docker/client/image_search.go deleted file mode 100644 index 8f5343b9dae1..000000000000 --- a/vendor/github.com/docker/docker/client/image_search.go +++ /dev/null @@ -1,54 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/http" - "net/url" - "strconv" - - cerrdefs "github.com/containerd/errdefs" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/registry" -) - -// ImageSearch makes the docker host search by a term in a remote registry. -// The list of results is not sorted in any fashion. -func (cli *Client) ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error) { - var results []registry.SearchResult - query := url.Values{} - query.Set("term", term) - if options.Limit > 0 { - query.Set("limit", strconv.Itoa(options.Limit)) - } - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return results, err - } - query.Set("filters", filterJSON) - } - - resp, err := cli.tryImageSearch(ctx, query, options.RegistryAuth) - defer ensureReaderClosed(resp) - if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { - newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) - if privilegeErr != nil { - return results, privilegeErr - } - resp, err = cli.tryImageSearch(ctx, query, newAuthHeader) - } - if err != nil { - return results, err - } - - err = json.NewDecoder(resp.Body).Decode(&results) - return results, err -} - -func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) { - return cli.get(ctx, "/images/search", query, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} diff --git a/vendor/github.com/docker/docker/client/image_tag.go b/vendor/github.com/docker/docker/client/image_tag.go deleted file mode 100644 index 2bfafc51cdd5..000000000000 --- a/vendor/github.com/docker/docker/client/image_tag.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/distribution/reference" - "github.com/pkg/errors" -) - -// ImageTag tags an image in the docker host -func (cli *Client) ImageTag(ctx context.Context, source, target string) error { - if _, err := reference.ParseAnyReference(source); err != nil { - return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", source) - } - - ref, err := reference.ParseNormalizedNamed(target) - if err != nil { - return errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", target) - } - - if _, isCanonical := ref.(reference.Canonical); isCanonical { - return errors.New("refusing to create a tag with a digest reference") - } - - ref = reference.TagNameOnly(ref) - - query := url.Values{} - query.Set("repo", ref.Name()) - if tagged, ok := ref.(reference.Tagged); ok { - query.Set("tag", tagged.Tag()) - } - - resp, err := cli.post(ctx, "/images/"+source+"/tag", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/info.go b/vendor/github.com/docker/docker/client/info.go deleted file mode 100644 index ed85d7f8d9cf..000000000000 --- a/vendor/github.com/docker/docker/client/info.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - - "github.com/docker/docker/api/types/system" -) - -// Info returns information about the docker server. -func (cli *Client) Info(ctx context.Context) (system.Info, error) { - var info system.Info - resp, err := cli.get(ctx, "/info", url.Values{}, nil) - defer ensureReaderClosed(resp) - if err != nil { - return info, err - } - - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - return info, fmt.Errorf("Error reading remote info: %v", err) - } - - return info, nil -} diff --git a/vendor/github.com/docker/docker/client/login.go b/vendor/github.com/docker/docker/client/login.go deleted file mode 100644 index 2d7f179023c3..000000000000 --- a/vendor/github.com/docker/docker/client/login.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/registry" -) - -// RegistryLogin authenticates the docker server with a given docker registry. -// It returns unauthorizedError when the authentication fails. -func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) { - resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) - defer ensureReaderClosed(resp) - - if err != nil { - return registry.AuthenticateOKBody{}, err - } - - var response registry.AuthenticateOKBody - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/network_connect.go b/vendor/github.com/docker/docker/client/network_connect.go deleted file mode 100644 index f7526c5d8ee3..000000000000 --- a/vendor/github.com/docker/docker/client/network_connect.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/network" -) - -// NetworkConnect connects a container to an existent network in the docker host. -func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error { - networkID, err := trimID("network", networkID) - if err != nil { - return err - } - - containerID, err = trimID("container", containerID) - if err != nil { - return err - } - - nc := network.ConnectOptions{ - Container: containerID, - EndpointConfig: config, - } - resp, err := cli.post(ctx, "/networks/"+networkID+"/connect", nil, nc, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/network_create.go b/vendor/github.com/docker/docker/client/network_create.go deleted file mode 100644 index 6a7f2ea52e9e..000000000000 --- a/vendor/github.com/docker/docker/client/network_create.go +++ /dev/null @@ -1,40 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/network" - "github.com/docker/docker/api/types/versions" -) - -// NetworkCreate creates a new network in the docker host. -func (cli *Client) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return network.CreateResponse{}, err - } - - networkCreateRequest := network.CreateRequest{ - CreateOptions: options, - Name: name, - } - if versions.LessThan(cli.version, "1.44") { - enabled := true - networkCreateRequest.CheckDuplicate = &enabled //nolint:staticcheck // ignore SA1019: CheckDuplicate is deprecated since API v1.44. - } - - resp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil) - defer ensureReaderClosed(resp) - if err != nil { - return network.CreateResponse{}, err - } - - var response network.CreateResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/network_disconnect.go b/vendor/github.com/docker/docker/client/network_disconnect.go deleted file mode 100644 index 55f9b6a206d6..000000000000 --- a/vendor/github.com/docker/docker/client/network_disconnect.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/network" -) - -// NetworkDisconnect disconnects a container from an existent network in the docker host. -func (cli *Client) NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error { - networkID, err := trimID("network", networkID) - if err != nil { - return err - } - - containerID, err = trimID("container", containerID) - if err != nil { - return err - } - - nd := network.DisconnectOptions{ - Container: containerID, - Force: force, - } - resp, err := cli.post(ctx, "/networks/"+networkID+"/disconnect", nil, nd, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/network_inspect.go b/vendor/github.com/docker/docker/client/network_inspect.go deleted file mode 100644 index 734ec1021193..000000000000 --- a/vendor/github.com/docker/docker/client/network_inspect.go +++ /dev/null @@ -1,47 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - "net/url" - - "github.com/docker/docker/api/types/network" -) - -// NetworkInspect returns the information for a specific network configured in the docker host. -func (cli *Client) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { - networkResource, _, err := cli.NetworkInspectWithRaw(ctx, networkID, options) - return networkResource, err -} - -// NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation. -func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, []byte, error) { - networkID, err := trimID("network", networkID) - if err != nil { - return network.Inspect{}, nil, err - } - query := url.Values{} - if options.Verbose { - query.Set("verbose", "true") - } - if options.Scope != "" { - query.Set("scope", options.Scope) - } - - resp, err := cli.get(ctx, "/networks/"+networkID, query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return network.Inspect{}, nil, err - } - - raw, err := io.ReadAll(resp.Body) - if err != nil { - return network.Inspect{}, nil, err - } - - var nw network.Inspect - err = json.NewDecoder(bytes.NewReader(raw)).Decode(&nw) - return nw, raw, err -} diff --git a/vendor/github.com/docker/docker/client/network_list.go b/vendor/github.com/docker/docker/client/network_list.go deleted file mode 100644 index 8d93361966c2..000000000000 --- a/vendor/github.com/docker/docker/client/network_list.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" -) - -// NetworkList returns the list of networks configured in the docker host. -func (cli *Client) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) { - query := url.Values{} - if options.Filters.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - var networkResources []network.Summary - resp, err := cli.get(ctx, "/networks", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return networkResources, err - } - err = json.NewDecoder(resp.Body).Decode(&networkResources) - return networkResources, err -} diff --git a/vendor/github.com/docker/docker/client/network_prune.go b/vendor/github.com/docker/docker/client/network_prune.go deleted file mode 100644 index 7835fe90693f..000000000000 --- a/vendor/github.com/docker/docker/client/network_prune.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/network" -) - -// NetworksPrune requests the daemon to delete unused networks -func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args) (network.PruneReport, error) { - if err := cli.NewVersionError(ctx, "1.25", "network prune"); err != nil { - return network.PruneReport{}, err - } - - query, err := getFiltersQuery(pruneFilters) - if err != nil { - return network.PruneReport{}, err - } - - resp, err := cli.post(ctx, "/networks/prune", query, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return network.PruneReport{}, err - } - - var report network.PruneReport - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return network.PruneReport{}, fmt.Errorf("Error retrieving network prune report: %v", err) - } - - return report, nil -} diff --git a/vendor/github.com/docker/docker/client/network_remove.go b/vendor/github.com/docker/docker/client/network_remove.go deleted file mode 100644 index 9b164d3eae46..000000000000 --- a/vendor/github.com/docker/docker/client/network_remove.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import "context" - -// NetworkRemove removes an existent network from the docker host. -func (cli *Client) NetworkRemove(ctx context.Context, networkID string) error { - networkID, err := trimID("network", networkID) - if err != nil { - return err - } - resp, err := cli.delete(ctx, "/networks/"+networkID, nil, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/node_inspect.go b/vendor/github.com/docker/docker/client/node_inspect.go deleted file mode 100644 index dd1f1f8ab40b..000000000000 --- a/vendor/github.com/docker/docker/client/node_inspect.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types/swarm" -) - -// NodeInspectWithRaw returns the node information. -func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) { - nodeID, err := trimID("node", nodeID) - if err != nil { - return swarm.Node{}, nil, err - } - resp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Node{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return swarm.Node{}, nil, err - } - - var response swarm.Node - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&response) - return response, body, err -} diff --git a/vendor/github.com/docker/docker/client/node_list.go b/vendor/github.com/docker/docker/client/node_list.go deleted file mode 100644 index 3b393ffe38e3..000000000000 --- a/vendor/github.com/docker/docker/client/node_list.go +++ /dev/null @@ -1,34 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" -) - -// NodeList returns the list of nodes. -func (cli *Client) NodeList(ctx context.Context, options swarm.NodeListOptions) ([]swarm.Node, error) { - query := url.Values{} - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - resp, err := cli.get(ctx, "/nodes", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var nodes []swarm.Node - err = json.NewDecoder(resp.Body).Decode(&nodes) - return nodes, err -} diff --git a/vendor/github.com/docker/docker/client/node_remove.go b/vendor/github.com/docker/docker/client/node_remove.go deleted file mode 100644 index 644fe138108b..000000000000 --- a/vendor/github.com/docker/docker/client/node_remove.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/swarm" -) - -// NodeRemove removes a Node. -func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options swarm.NodeRemoveOptions) error { - nodeID, err := trimID("node", nodeID) - if err != nil { - return err - } - - query := url.Values{} - if options.Force { - query.Set("force", "1") - } - - resp, err := cli.delete(ctx, "/nodes/"+nodeID, query, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/node_update.go b/vendor/github.com/docker/docker/client/node_update.go deleted file mode 100644 index 62af964cf0dd..000000000000 --- a/vendor/github.com/docker/docker/client/node_update.go +++ /dev/null @@ -1,22 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/swarm" -) - -// NodeUpdate updates a Node. -func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { - nodeID, err := trimID("node", nodeID) - if err != nil { - return err - } - - query := url.Values{} - query.Set("version", version.String()) - resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/options.go b/vendor/github.com/docker/docker/client/options.go deleted file mode 100644 index 6f68fc2b8966..000000000000 --- a/vendor/github.com/docker/docker/client/options.go +++ /dev/null @@ -1,240 +0,0 @@ -package client - -import ( - "context" - "net" - "net/http" - "os" - "path/filepath" - "strings" - "time" - - "github.com/docker/go-connections/sockets" - "github.com/docker/go-connections/tlsconfig" - "github.com/pkg/errors" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel/trace" -) - -// Opt is a configuration option to initialize a [Client]. -type Opt func(*Client) error - -// FromEnv configures the client with values from environment variables. It -// is the equivalent of using the [WithTLSClientConfigFromEnv], [WithHostFromEnv], -// and [WithVersionFromEnv] options. -// -// FromEnv uses the following environment variables: -// -// - DOCKER_HOST ([EnvOverrideHost]) to set the URL to the docker server. -// - DOCKER_API_VERSION ([EnvOverrideAPIVersion]) to set the version of the -// API to use, leave empty for latest. -// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from -// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem'). -// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification -// (off by default). -func FromEnv(c *Client) error { - ops := []Opt{ - WithTLSClientConfigFromEnv(), - WithHostFromEnv(), - WithVersionFromEnv(), - } - for _, op := range ops { - if err := op(c); err != nil { - return err - } - } - return nil -} - -// WithDialContext applies the dialer to the client transport. This can be -// used to set the Timeout and KeepAlive settings of the client. It returns -// an error if the client does not have a [http.Transport] configured. -func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) Opt { - return func(c *Client) error { - if transport, ok := c.client.Transport.(*http.Transport); ok { - transport.DialContext = dialContext - return nil - } - return errors.Errorf("cannot apply dialer to transport: %T", c.client.Transport) - } -} - -// WithHost overrides the client host with the specified one. -func WithHost(host string) Opt { - return func(c *Client) error { - hostURL, err := ParseHostURL(host) - if err != nil { - return err - } - c.host = host - c.proto = hostURL.Scheme - c.addr = hostURL.Host - c.basePath = hostURL.Path - if transport, ok := c.client.Transport.(*http.Transport); ok { - return sockets.ConfigureTransport(transport, c.proto, c.addr) - } - return errors.Errorf("cannot apply host to transport: %T", c.client.Transport) - } -} - -// WithHostFromEnv overrides the client host with the host specified in the -// DOCKER_HOST ([EnvOverrideHost]) environment variable. If DOCKER_HOST is not set, -// or set to an empty value, the host is not modified. -func WithHostFromEnv() Opt { - return func(c *Client) error { - if host := os.Getenv(EnvOverrideHost); host != "" { - return WithHost(host)(c) - } - return nil - } -} - -// WithHTTPClient overrides the client's HTTP client with the specified one. -func WithHTTPClient(client *http.Client) Opt { - return func(c *Client) error { - if client != nil { - c.client = client - } - return nil - } -} - -// WithTimeout configures the time limit for requests made by the HTTP client. -func WithTimeout(timeout time.Duration) Opt { - return func(c *Client) error { - c.client.Timeout = timeout - return nil - } -} - -// WithUserAgent configures the User-Agent header to use for HTTP requests. -// It overrides any User-Agent set in headers. When set to an empty string, -// the User-Agent header is removed, and no header is sent. -func WithUserAgent(ua string) Opt { - return func(c *Client) error { - c.userAgent = &ua - return nil - } -} - -// WithHTTPHeaders appends custom HTTP headers to the client's default headers. -// It does not allow for built-in headers (such as "User-Agent", if set) to -// be overridden. Also see [WithUserAgent]. -func WithHTTPHeaders(headers map[string]string) Opt { - return func(c *Client) error { - c.customHTTPHeaders = headers - return nil - } -} - -// WithScheme overrides the client scheme with the specified one. -func WithScheme(scheme string) Opt { - return func(c *Client) error { - c.scheme = scheme - return nil - } -} - -// WithTLSClientConfig applies a TLS config to the client transport. -func WithTLSClientConfig(cacertPath, certPath, keyPath string) Opt { - return func(c *Client) error { - transport, ok := c.client.Transport.(*http.Transport) - if !ok { - return errors.Errorf("cannot apply tls config to transport: %T", c.client.Transport) - } - config, err := tlsconfig.Client(tlsconfig.Options{ - CAFile: cacertPath, - CertFile: certPath, - KeyFile: keyPath, - ExclusiveRootPools: true, - }) - if err != nil { - return errors.Wrap(err, "failed to create tls config") - } - transport.TLSClientConfig = config - return nil - } -} - -// WithTLSClientConfigFromEnv configures the client's TLS settings with the -// settings in the DOCKER_CERT_PATH ([EnvOverrideCertPath]) and DOCKER_TLS_VERIFY -// ([EnvTLSVerify]) environment variables. If DOCKER_CERT_PATH is not set or empty, -// TLS configuration is not modified. -// -// WithTLSClientConfigFromEnv uses the following environment variables: -// -// - DOCKER_CERT_PATH ([EnvOverrideCertPath]) to specify the directory from -// which to load the TLS certificates ("ca.pem", "cert.pem", "key.pem"). -// - DOCKER_TLS_VERIFY ([EnvTLSVerify]) to enable or disable TLS verification -// (off by default). -func WithTLSClientConfigFromEnv() Opt { - return func(c *Client) error { - dockerCertPath := os.Getenv(EnvOverrideCertPath) - if dockerCertPath == "" { - return nil - } - tlsc, err := tlsconfig.Client(tlsconfig.Options{ - CAFile: filepath.Join(dockerCertPath, "ca.pem"), - CertFile: filepath.Join(dockerCertPath, "cert.pem"), - KeyFile: filepath.Join(dockerCertPath, "key.pem"), - InsecureSkipVerify: os.Getenv(EnvTLSVerify) == "", - }) - if err != nil { - return err - } - - c.client = &http.Client{ - Transport: &http.Transport{TLSClientConfig: tlsc}, - CheckRedirect: CheckRedirect, - } - return nil - } -} - -// WithVersion overrides the client version with the specified one. If an empty -// version is provided, the value is ignored to allow version negotiation -// (see [WithAPIVersionNegotiation]). -func WithVersion(version string) Opt { - return func(c *Client) error { - if v := strings.TrimPrefix(version, "v"); v != "" { - c.version = v - c.manualOverride = true - } - return nil - } -} - -// WithVersionFromEnv overrides the client version with the version specified in -// the DOCKER_API_VERSION ([EnvOverrideAPIVersion]) environment variable. -// If DOCKER_API_VERSION is not set, or set to an empty value, the version -// is not modified. -func WithVersionFromEnv() Opt { - return func(c *Client) error { - return WithVersion(os.Getenv(EnvOverrideAPIVersion))(c) - } -} - -// WithAPIVersionNegotiation enables automatic API version negotiation for the client. -// With this option enabled, the client automatically negotiates the API version -// to use when making requests. API version negotiation is performed on the first -// request; subsequent requests do not re-negotiate. -func WithAPIVersionNegotiation() Opt { - return func(c *Client) error { - c.negotiateVersion = true - return nil - } -} - -// WithTraceProvider sets the trace provider for the client. -// If this is not set then the global trace provider will be used. -func WithTraceProvider(provider trace.TracerProvider) Opt { - return WithTraceOptions(otelhttp.WithTracerProvider(provider)) -} - -// WithTraceOptions sets tracing span options for the client. -func WithTraceOptions(opts ...otelhttp.Option) Opt { - return func(c *Client) error { - c.traceOpts = append(c.traceOpts, opts...) - return nil - } -} diff --git a/vendor/github.com/docker/docker/client/ping.go b/vendor/github.com/docker/docker/client/ping.go deleted file mode 100644 index 385fdf0524ea..000000000000 --- a/vendor/github.com/docker/docker/client/ping.go +++ /dev/null @@ -1,81 +0,0 @@ -package client - -import ( - "context" - "net/http" - "path" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/swarm" -) - -// Ping pings the server and returns the value of the "Docker-Experimental", -// "Builder-Version", "OS-Type" & "API-Version" headers. It attempts to use -// a HEAD request on the endpoint, but falls back to GET if HEAD is not supported -// by the daemon. It ignores internal server errors returned by the API, which -// may be returned if the daemon is in an unhealthy state, but returns errors -// for other non-success status codes, failing to connect to the API, or failing -// to parse the API response. -func (cli *Client) Ping(ctx context.Context) (types.Ping, error) { - var ping types.Ping - - // Using cli.buildRequest() + cli.doRequest() instead of cli.sendRequest() - // because ping requests are used during API version negotiation, so we want - // to hit the non-versioned /_ping endpoint, not /v1.xx/_ping - req, err := cli.buildRequest(ctx, http.MethodHead, path.Join(cli.basePath, "/_ping"), nil, nil) - if err != nil { - return ping, err - } - resp, err := cli.doRequest(req) - if err != nil { - if IsErrConnectionFailed(err) { - return ping, err - } - // We managed to connect, but got some error; continue and try GET request. - } else { - defer ensureReaderClosed(resp) - switch resp.StatusCode { - case http.StatusOK, http.StatusInternalServerError: - // Server handled the request, so parse the response - return parsePingResponse(cli, resp) - } - } - - // HEAD failed; fallback to GET. - req.Method = http.MethodGet - resp, err = cli.doRequest(req) - defer ensureReaderClosed(resp) - if err != nil { - return ping, err - } - return parsePingResponse(cli, resp) -} - -func parsePingResponse(cli *Client, resp *http.Response) (types.Ping, error) { - if resp == nil { - return types.Ping{}, nil - } - - var ping types.Ping - if resp.Header == nil { - return ping, cli.checkResponseErr(resp) - } - ping.APIVersion = resp.Header.Get("Api-Version") - ping.OSType = resp.Header.Get("Ostype") - if resp.Header.Get("Docker-Experimental") == "true" { - ping.Experimental = true - } - if bv := resp.Header.Get("Builder-Version"); bv != "" { - ping.BuilderVersion = build.BuilderVersion(bv) - } - if si := resp.Header.Get("Swarm"); si != "" { - state, role, _ := strings.Cut(si, "/") - ping.SwarmStatus = &swarm.Status{ - NodeState: swarm.LocalNodeState(state), - ControlAvailable: role == "manager", - } - } - return ping, cli.checkResponseErr(resp) -} diff --git a/vendor/github.com/docker/docker/client/plugin_create.go b/vendor/github.com/docker/docker/client/plugin_create.go deleted file mode 100644 index eaba7ee6539a..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_create.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "context" - "io" - "net/http" - "net/url" - - "github.com/docker/docker/api/types" -) - -// PluginCreate creates a plugin -func (cli *Client) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error { - headers := http.Header(make(map[string][]string)) - headers.Set("Content-Type", "application/x-tar") - - query := url.Values{} - query.Set("name", createOptions.RepoName) - - resp, err := cli.postRaw(ctx, "/plugins/create", query, createContext, headers) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/plugin_disable.go b/vendor/github.com/docker/docker/client/plugin_disable.go deleted file mode 100644 index 4049b1b6ace0..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_disable.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types" -) - -// PluginDisable disables a plugin -func (cli *Client) PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error { - name, err := trimID("plugin", name) - if err != nil { - return err - } - query := url.Values{} - if options.Force { - query.Set("force", "1") - } - resp, err := cli.post(ctx, "/plugins/"+name+"/disable", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/plugin_enable.go b/vendor/github.com/docker/docker/client/plugin_enable.go deleted file mode 100644 index 611856935dba..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_enable.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "context" - "net/url" - "strconv" - - "github.com/docker/docker/api/types" -) - -// PluginEnable enables a plugin -func (cli *Client) PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error { - name, err := trimID("plugin", name) - if err != nil { - return err - } - query := url.Values{} - query.Set("timeout", strconv.Itoa(options.Timeout)) - - resp, err := cli.post(ctx, "/plugins/"+name+"/enable", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/plugin_inspect.go b/vendor/github.com/docker/docker/client/plugin_inspect.go deleted file mode 100644 index eaedeb8a6740..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_inspect.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types" -) - -// PluginInspectWithRaw inspects an existing plugin -func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) { - name, err := trimID("plugin", name) - if err != nil { - return nil, nil, err - } - resp, err := cli.get(ctx, "/plugins/"+name+"/json", nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, nil, err - } - var p types.Plugin - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&p) - return &p, body, err -} diff --git a/vendor/github.com/docker/docker/client/plugin_install.go b/vendor/github.com/docker/docker/client/plugin_install.go deleted file mode 100644 index 5fd2ff2182ba..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_install.go +++ /dev/null @@ -1,117 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/url" - - cerrdefs "github.com/containerd/errdefs" - "github.com/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// PluginInstall installs a plugin -func (cli *Client) PluginInstall(ctx context.Context, name string, options types.PluginInstallOptions) (_ io.ReadCloser, retErr error) { - query := url.Values{} - if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { - return nil, errors.Wrap(err, "invalid remote reference") - } - query.Set("remote", options.RemoteRef) - - privileges, err := cli.checkPluginPermissions(ctx, query, options) - if err != nil { - return nil, err - } - - // set name for plugin pull, if empty should default to remote reference - query.Set("name", name) - - resp, err := cli.tryPluginPull(ctx, query, privileges, options.RegistryAuth) - if err != nil { - return nil, err - } - - name = resp.Header.Get("Docker-Plugin-Name") - - pr, pw := io.Pipe() - go func() { // todo: the client should probably be designed more around the actual api - _, err := io.Copy(pw, resp.Body) - if err != nil { - _ = pw.CloseWithError(err) - return - } - defer func() { - if retErr != nil { - delResp, _ := cli.delete(ctx, "/plugins/"+name, nil, nil) - ensureReaderClosed(delResp) - } - }() - if len(options.Args) > 0 { - if err := cli.PluginSet(ctx, name, options.Args); err != nil { - _ = pw.CloseWithError(err) - return - } - } - - if options.Disabled { - _ = pw.Close() - return - } - - enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) - _ = pw.CloseWithError(enableErr) - }() - return pr, nil -} - -func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) { - return cli.get(ctx, "/plugins/privileges", query, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} - -func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/plugins/pull", query, privileges, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} - -func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values, options types.PluginInstallOptions) (types.PluginPrivileges, error) { - resp, err := cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - if cerrdefs.IsUnauthorized(err) && options.PrivilegeFunc != nil { - // todo: do inspect before to check existing name before checking privileges - newAuthHeader, privilegeErr := options.PrivilegeFunc(ctx) - if privilegeErr != nil { - ensureReaderClosed(resp) - return nil, privilegeErr - } - options.RegistryAuth = newAuthHeader - resp, err = cli.tryPluginPrivileges(ctx, query, options.RegistryAuth) - } - if err != nil { - ensureReaderClosed(resp) - return nil, err - } - - var privileges types.PluginPrivileges - if err := json.NewDecoder(resp.Body).Decode(&privileges); err != nil { - ensureReaderClosed(resp) - return nil, err - } - ensureReaderClosed(resp) - - if !options.AcceptAllPermissions && options.AcceptPermissionsFunc != nil && len(privileges) > 0 { - accept, err := options.AcceptPermissionsFunc(ctx, privileges) - if err != nil { - return nil, err - } - if !accept { - return nil, errors.Errorf("permission denied while installing plugin %s", options.RemoteRef) - } - } - return privileges, nil -} diff --git a/vendor/github.com/docker/docker/client/plugin_list.go b/vendor/github.com/docker/docker/client/plugin_list.go deleted file mode 100644 index f314e17f1e1c..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_list.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" -) - -// PluginList returns the installed plugins -func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.PluginsListResponse, error) { - var plugins types.PluginsListResponse - query := url.Values{} - - if filter.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cli.version, filter) - if err != nil { - return plugins, err - } - query.Set("filters", filterJSON) - } - resp, err := cli.get(ctx, "/plugins", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return plugins, err - } - - err = json.NewDecoder(resp.Body).Decode(&plugins) - return plugins, err -} diff --git a/vendor/github.com/docker/docker/client/plugin_push.go b/vendor/github.com/docker/docker/client/plugin_push.go deleted file mode 100644 index 4574dcddbfd9..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_push.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "io" - "net/http" - - "github.com/docker/docker/api/types/registry" -) - -// PluginPush pushes a plugin to a registry -func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) { - name, err := trimID("plugin", name) - if err != nil { - return nil, err - } - resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{ - registry.AuthHeader: {registryAuth}, - }) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/plugin_remove.go b/vendor/github.com/docker/docker/client/plugin_remove.go deleted file mode 100644 index 2ba0a8ccc402..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_remove.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types" -) - -// PluginRemove removes a plugin -func (cli *Client) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error { - name, err := trimID("plugin", name) - if err != nil { - return err - } - - query := url.Values{} - if options.Force { - query.Set("force", "1") - } - - resp, err := cli.delete(ctx, "/plugins/"+name, query, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/plugin_set.go b/vendor/github.com/docker/docker/client/plugin_set.go deleted file mode 100644 index f0e4a0c30522..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_set.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -import ( - "context" -) - -// PluginSet modifies settings for an existing plugin -func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error { - name, err := trimID("plugin", name) - if err != nil { - return err - } - - resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/plugin_upgrade.go b/vendor/github.com/docker/docker/client/plugin_upgrade.go deleted file mode 100644 index cd0cf4d22225..000000000000 --- a/vendor/github.com/docker/docker/client/plugin_upgrade.go +++ /dev/null @@ -1,47 +0,0 @@ -package client - -import ( - "context" - "io" - "net/http" - "net/url" - - "github.com/distribution/reference" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// PluginUpgrade upgrades a plugin -func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - name, err := trimID("plugin", name) - if err != nil { - return nil, err - } - - if err := cli.NewVersionError(ctx, "1.26", "plugin upgrade"); err != nil { - return nil, err - } - query := url.Values{} - if _, err := reference.ParseNormalizedNamed(options.RemoteRef); err != nil { - return nil, errors.Wrap(err, "invalid remote reference") - } - query.Set("remote", options.RemoteRef) - - privileges, err := cli.checkPluginPermissions(ctx, query, options) - if err != nil { - return nil, err - } - - resp, err := cli.tryPluginUpgrade(ctx, query, privileges, name, options.RegistryAuth) - if err != nil { - return nil, err - } - return resp.Body, nil -} - -func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/plugins/"+name+"/upgrade", query, privileges, http.Header{ - registry.AuthHeader: {registryAuth}, - }) -} diff --git a/vendor/github.com/docker/docker/client/request.go b/vendor/github.com/docker/docker/client/request.go deleted file mode 100644 index 254138fc22f6..000000000000 --- a/vendor/github.com/docker/docker/client/request.go +++ /dev/null @@ -1,322 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "reflect" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/versions" - "github.com/pkg/errors" -) - -// head sends an http request to the docker API using the method HEAD. -func (cli *Client) head(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) { - return cli.sendRequest(ctx, http.MethodHead, path, query, nil, headers) -} - -// get sends an http request to the docker API using the method GET with a specific Go context. -func (cli *Client) get(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) { - return cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers) -} - -// post sends an http request to the docker API using the method POST with a specific Go context. -func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) { - body, headers, err := encodeBody(obj, headers) - if err != nil { - return nil, err - } - return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) -} - -func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) { - return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) -} - -func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) { - body, headers, err := encodeBody(obj, headers) - if err != nil { - return nil, err - } - return cli.putRaw(ctx, path, query, body, headers) -} - -// putRaw sends an http request to the docker API using the method PUT. -func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) { - // PUT requests are expected to always have a body (apparently) - // so explicitly pass an empty body to sendRequest to signal that - // it should set the Content-Type header if not already present. - if body == nil { - body = http.NoBody - } - return cli.sendRequest(ctx, http.MethodPut, path, query, body, headers) -} - -// delete sends an http request to the docker API using the method DELETE. -func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) { - return cli.sendRequest(ctx, http.MethodDelete, path, query, nil, headers) -} - -func encodeBody(obj interface{}, headers http.Header) (io.Reader, http.Header, error) { - if obj == nil { - return nil, headers, nil - } - // encoding/json encodes a nil pointer as the JSON document `null`, - // irrespective of whether the type implements json.Marshaler or encoding.TextMarshaler. - // That is almost certainly not what the caller intended as the request body. - if reflect.TypeOf(obj).Kind() == reflect.Ptr && reflect.ValueOf(obj).IsNil() { - return nil, headers, nil - } - - body, err := encodeData(obj) - if err != nil { - return nil, headers, err - } - if headers == nil { - headers = make(map[string][]string) - } - headers["Content-Type"] = []string{"application/json"} - return body, headers, nil -} - -func (cli *Client) buildRequest(ctx context.Context, method, path string, body io.Reader, headers http.Header) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, method, path, body) - if err != nil { - return nil, err - } - req = cli.addHeaders(req, headers) - req.URL.Scheme = cli.scheme - req.URL.Host = cli.addr - - if cli.proto == "unix" || cli.proto == "npipe" { - // Override host header for non-tcp connections. - req.Host = DummyHost - } - - if body != nil && req.Header.Get("Content-Type") == "" { - req.Header.Set("Content-Type", "text/plain") - } - return req, nil -} - -func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) { - req, err := cli.buildRequest(ctx, method, cli.getAPIPath(ctx, path, query), body, headers) - if err != nil { - return nil, err - } - - resp, err := cli.doRequest(req) - switch { - case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded): - return nil, err - case err == nil: - return resp, cli.checkResponseErr(resp) - default: - return resp, err - } -} - -func (cli *Client) doRequest(req *http.Request) (*http.Response, error) { - resp, err := cli.client.Do(req) - if err != nil { - if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { - return nil, errConnectionFailed{fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)} - } - - if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { - return nil, errConnectionFailed{errors.Wrap(err, "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings")} - } - - // Don't decorate context sentinel errors; users may be comparing to - // them directly. - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - - var uErr *url.Error - if errors.As(err, &uErr) { - var nErr *net.OpError - if errors.As(uErr.Err, &nErr) { - if os.IsPermission(nErr.Err) { - return nil, errConnectionFailed{errors.Wrapf(err, "permission denied while trying to connect to the Docker daemon socket at %v", cli.host)} - } - } - } - - var nErr net.Error - if errors.As(err, &nErr) { - // FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)? - if nErr.Timeout() { - return nil, connectionFailed(cli.host) - } - if strings.Contains(nErr.Error(), "connection refused") || strings.Contains(nErr.Error(), "dial unix") { - return nil, connectionFailed(cli.host) - } - } - - // Although there's not a strongly typed error for this in go-winio, - // lots of people are using the default configuration for the docker - // daemon on Windows where the daemon is listening on a named pipe - // `//./pipe/docker_engine, and the client must be running elevated. - // Give users a clue rather than the not-overly useful message - // such as `error during connect: Get http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.26/info: - // open //./pipe/docker_engine: The system cannot find the file specified.`. - // Note we can't string compare "The system cannot find the file specified" as - // this is localised - for example in French the error would be - // `open //./pipe/docker_engine: Le fichier spécifié est introuvable.` - if strings.Contains(err.Error(), `open //./pipe/docker_engine`) { - // Checks if client is running with elevated privileges - if f, elevatedErr := os.Open(`\\.\PHYSICALDRIVE0`); elevatedErr != nil { - err = errors.Wrap(err, "in the default daemon configuration on Windows, the docker client must be run with elevated privileges to connect") - } else { - _ = f.Close() - err = errors.Wrap(err, "this error may indicate that the docker daemon is not running") - } - } - - return nil, errConnectionFailed{errors.Wrap(err, "error during connect")} - } - - return resp, nil -} - -func (cli *Client) checkResponseErr(serverResp *http.Response) (retErr error) { - if serverResp == nil { - return nil - } - if serverResp.StatusCode >= http.StatusOK && serverResp.StatusCode < http.StatusBadRequest { - return nil - } - defer func() { - retErr = httpErrorFromStatusCode(retErr, serverResp.StatusCode) - }() - - var body []byte - var err error - var reqURL string - if serverResp.Request != nil { - reqURL = serverResp.Request.URL.String() - } - statusMsg := serverResp.Status - if statusMsg == "" { - statusMsg = http.StatusText(serverResp.StatusCode) - } - if serverResp.Body != nil { - bodyMax := 1 * 1024 * 1024 // 1 MiB - bodyR := &io.LimitedReader{ - R: serverResp.Body, - N: int64(bodyMax), - } - body, err = io.ReadAll(bodyR) - if err != nil { - return err - } - if bodyR.N == 0 { - if reqURL != "" { - return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", statusMsg, bodyMax, reqURL) - } - return fmt.Errorf("request returned %s with a message (> %d bytes); check if the server supports the requested API version", statusMsg, bodyMax) - } - } - if len(body) == 0 { - if reqURL != "" { - return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", statusMsg, reqURL) - } - return fmt.Errorf("request returned %s; check if the server supports the requested API version", statusMsg) - } - - var daemonErr error - if serverResp.Header.Get("Content-Type") == "application/json" { - var errorResponse types.ErrorResponse - if err := json.Unmarshal(body, &errorResponse); err != nil { - return errors.Wrap(err, "Error reading JSON") - } - if errorResponse.Message == "" { - // Error-message is empty, which means that we successfully parsed the - // JSON-response (no error produced), but it didn't contain an error - // message. This could either be because the response was empty, or - // the response was valid JSON, but not with the expected schema - // ([types.ErrorResponse]). - // - // We cannot use "strict" JSON handling (json.NewDecoder with DisallowUnknownFields) - // due to the API using an open schema (we must anticipate fields - // being added to [types.ErrorResponse] in the future, and not - // reject those responses. - // - // For these cases, we construct an error with the status-code - // returned, but we could consider returning (a truncated version - // of) the actual response as-is. - // - // TODO(thaJeztah): consider adding a log.Debug to allow clients to debug the actual response when enabling debug logging. - daemonErr = fmt.Errorf(`API returned a %d (%s) but provided no error-message`, - serverResp.StatusCode, - http.StatusText(serverResp.StatusCode), - ) - } else { - daemonErr = errors.New(strings.TrimSpace(errorResponse.Message)) - } - } else { - // Fall back to returning the response as-is for API versions < 1.24 - // that didn't support JSON error responses, and for situations - // where a plain text error is returned. This branch may also catch - // situations where a proxy is involved, returning a HTML response. - daemonErr = errors.New(strings.TrimSpace(string(body))) - } - return errors.Wrap(daemonErr, "Error response from daemon") -} - -func (cli *Client) addHeaders(req *http.Request, headers http.Header) *http.Request { - // Add CLI Config's HTTP Headers BEFORE we set the Docker headers - // then the user can't change OUR headers - for k, v := range cli.customHTTPHeaders { - if versions.LessThan(cli.version, "1.25") && http.CanonicalHeaderKey(k) == "User-Agent" { - continue - } - req.Header.Set(k, v) - } - - for k, v := range headers { - req.Header[http.CanonicalHeaderKey(k)] = v - } - - if cli.userAgent != nil { - if *cli.userAgent == "" { - req.Header.Del("User-Agent") - } else { - req.Header.Set("User-Agent", *cli.userAgent) - } - } - return req -} - -func encodeData(data interface{}) (*bytes.Buffer, error) { - params := bytes.NewBuffer(nil) - if data != nil { - if err := json.NewEncoder(params).Encode(data); err != nil { - return nil, err - } - } - return params, nil -} - -func ensureReaderClosed(response *http.Response) { - if response != nil && response.Body != nil { - // Drain up to 512 bytes and close the body to let the Transport reuse the connection - // see https://github.com/google/go-github/pull/317/files#r57536827 - // - // TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib, - // and check if context-cancellation should handle this as well. If still needed, consider - // wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related - // methods. - _, _ = io.CopyN(io.Discard, response.Body, 512) - _ = response.Body.Close() - } -} diff --git a/vendor/github.com/docker/docker/client/secret_create.go b/vendor/github.com/docker/docker/client/secret_create.go deleted file mode 100644 index be4a1da45640..000000000000 --- a/vendor/github.com/docker/docker/client/secret_create.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/swarm" -) - -// SecretCreate creates a new secret. -func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (swarm.SecretCreateResponse, error) { - if err := cli.NewVersionError(ctx, "1.25", "secret create"); err != nil { - return swarm.SecretCreateResponse{}, err - } - resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.SecretCreateResponse{}, err - } - - var response swarm.SecretCreateResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/secret_inspect.go b/vendor/github.com/docker/docker/client/secret_inspect.go deleted file mode 100644 index f44c00e75504..000000000000 --- a/vendor/github.com/docker/docker/client/secret_inspect.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types/swarm" -) - -// SecretInspectWithRaw returns the secret information with raw data -func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { - id, err := trimID("secret", id) - if err != nil { - return swarm.Secret{}, nil, err - } - if err := cli.NewVersionError(ctx, "1.25", "secret inspect"); err != nil { - return swarm.Secret{}, nil, err - } - resp, err := cli.get(ctx, "/secrets/"+id, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Secret{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return swarm.Secret{}, nil, err - } - - var secret swarm.Secret - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&secret) - - return secret, body, err -} diff --git a/vendor/github.com/docker/docker/client/secret_list.go b/vendor/github.com/docker/docker/client/secret_list.go deleted file mode 100644 index 2e37bda273c0..000000000000 --- a/vendor/github.com/docker/docker/client/secret_list.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" -) - -// SecretList returns the list of secrets. -func (cli *Client) SecretList(ctx context.Context, options swarm.SecretListOptions) ([]swarm.Secret, error) { - if err := cli.NewVersionError(ctx, "1.25", "secret list"); err != nil { - return nil, err - } - query := url.Values{} - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - resp, err := cli.get(ctx, "/secrets", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var secrets []swarm.Secret - err = json.NewDecoder(resp.Body).Decode(&secrets) - return secrets, err -} diff --git a/vendor/github.com/docker/docker/client/secret_remove.go b/vendor/github.com/docker/docker/client/secret_remove.go deleted file mode 100644 index d1044aaf857b..000000000000 --- a/vendor/github.com/docker/docker/client/secret_remove.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -import "context" - -// SecretRemove removes a secret. -func (cli *Client) SecretRemove(ctx context.Context, id string) error { - id, err := trimID("secret", id) - if err != nil { - return err - } - if err := cli.NewVersionError(ctx, "1.25", "secret remove"); err != nil { - return err - } - resp, err := cli.delete(ctx, "/secrets/"+id, nil, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/secret_update.go b/vendor/github.com/docker/docker/client/secret_update.go deleted file mode 100644 index a0aff7cb696d..000000000000 --- a/vendor/github.com/docker/docker/client/secret_update.go +++ /dev/null @@ -1,24 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/swarm" -) - -// SecretUpdate attempts to update a secret. -func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { - id, err := trimID("secret", id) - if err != nil { - return err - } - if err := cli.NewVersionError(ctx, "1.25", "secret update"); err != nil { - return err - } - query := url.Values{} - query.Set("version", version.String()) - resp, err := cli.post(ctx, "/secrets/"+id+"/update", query, secret, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/service_create.go b/vendor/github.com/docker/docker/client/service_create.go deleted file mode 100644 index db7566a85dbd..000000000000 --- a/vendor/github.com/docker/docker/client/service_create.go +++ /dev/null @@ -1,212 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" -) - -// ServiceCreate creates a new service. -func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec, options swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) { - var response swarm.ServiceCreateResponse - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return response, err - } - - // Make sure containerSpec is not nil when no runtime is set or the runtime is set to container - if service.TaskTemplate.ContainerSpec == nil && (service.TaskTemplate.Runtime == "" || service.TaskTemplate.Runtime == swarm.RuntimeContainer) { - service.TaskTemplate.ContainerSpec = &swarm.ContainerSpec{} - } - - if err := validateServiceSpec(service); err != nil { - return response, err - } - if versions.LessThan(cli.version, "1.30") { - if err := validateAPIVersion(service, cli.version); err != nil { - return response, err - } - } - - // ensure that the image is tagged - var resolveWarning string - switch { - case service.TaskTemplate.ContainerSpec != nil: - if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { - service.TaskTemplate.ContainerSpec.Image = taggedImg - } - if options.QueryRegistry { - resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) - } - case service.TaskTemplate.PluginSpec != nil: - if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { - service.TaskTemplate.PluginSpec.Remote = taggedImg - } - if options.QueryRegistry { - resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) - } - } - - headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } - if options.EncodedRegistryAuth != "" { - headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth} - } - resp, err := cli.post(ctx, "/services/create", nil, service, headers) - defer ensureReaderClosed(resp) - if err != nil { - return response, err - } - - err = json.NewDecoder(resp.Body).Decode(&response) - if resolveWarning != "" { - response.Warnings = append(response.Warnings, resolveWarning) - } - - return response, err -} - -func resolveContainerSpecImage(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.ContainerSpec.Image, encodedAuth); err != nil { - warning = digestWarning(taskSpec.ContainerSpec.Image) - } else { - taskSpec.ContainerSpec.Image = img - if len(imgPlatforms) > 0 { - if taskSpec.Placement == nil { - taskSpec.Placement = &swarm.Placement{} - } - taskSpec.Placement.Platforms = imgPlatforms - } - } - return warning -} - -func resolvePluginSpecRemote(ctx context.Context, cli DistributionAPIClient, taskSpec *swarm.TaskSpec, encodedAuth string) string { - var warning string - if img, imgPlatforms, err := imageDigestAndPlatforms(ctx, cli, taskSpec.PluginSpec.Remote, encodedAuth); err != nil { - warning = digestWarning(taskSpec.PluginSpec.Remote) - } else { - taskSpec.PluginSpec.Remote = img - if len(imgPlatforms) > 0 { - if taskSpec.Placement == nil { - taskSpec.Placement = &swarm.Placement{} - } - taskSpec.Placement.Platforms = imgPlatforms - } - } - return warning -} - -func imageDigestAndPlatforms(ctx context.Context, cli DistributionAPIClient, image, encodedAuth string) (string, []swarm.Platform, error) { - distributionInspect, err := cli.DistributionInspect(ctx, image, encodedAuth) - var platforms []swarm.Platform - if err != nil { - return "", nil, err - } - - imageWithDigest := imageWithDigestString(image, distributionInspect.Descriptor.Digest) - - if len(distributionInspect.Platforms) > 0 { - platforms = make([]swarm.Platform, 0, len(distributionInspect.Platforms)) - for _, p := range distributionInspect.Platforms { - // clear architecture field for arm. This is a temporary patch to address - // https://github.com/docker/swarmkit/issues/2294. The issue is that while - // image manifests report "arm" as the architecture, the node reports - // something like "armv7l" (includes the variant), which causes arm images - // to stop working with swarm mode. This patch removes the architecture - // constraint for arm images to ensure tasks get scheduled. - arch := p.Architecture - if strings.ToLower(arch) == "arm" { - arch = "" - } - platforms = append(platforms, swarm.Platform{ - Architecture: arch, - OS: p.OS, - }) - } - } - return imageWithDigest, platforms, err -} - -// imageWithDigestString takes an image string and a digest, and updates -// the image string if it didn't originally contain a digest. It returns -// image unmodified in other situations. -func imageWithDigestString(image string, dgst digest.Digest) string { - namedRef, err := reference.ParseNormalizedNamed(image) - if err == nil { - if _, isCanonical := namedRef.(reference.Canonical); !isCanonical { - // ensure that image gets a default tag if none is provided - img, err := reference.WithDigest(namedRef, dgst) - if err == nil { - return reference.FamiliarString(img) - } - } - } - return image -} - -// imageWithTagString takes an image string, and returns a tagged image -// string, adding a 'latest' tag if one was not provided. It returns an -// empty string if a canonical reference was provided -func imageWithTagString(image string) string { - namedRef, err := reference.ParseNormalizedNamed(image) - if err == nil { - return reference.FamiliarString(reference.TagNameOnly(namedRef)) - } - return "" -} - -// digestWarning constructs a formatted warning string using the -// image name that could not be pinned by digest. The formatting -// is hardcoded, but could me made smarter in the future -func digestWarning(image string) string { - return fmt.Sprintf("image %s could not be accessed on a registry to record\nits digest. Each node will access %s independently,\npossibly leading to different nodes running different\nversions of the image.\n", image, image) -} - -func validateServiceSpec(s swarm.ServiceSpec) error { - if s.TaskTemplate.ContainerSpec != nil && s.TaskTemplate.PluginSpec != nil { - return errors.New("must not specify both a container spec and a plugin spec in the task template") - } - if s.TaskTemplate.PluginSpec != nil && s.TaskTemplate.Runtime != swarm.RuntimePlugin { - return errors.New("mismatched runtime with plugin spec") - } - if s.TaskTemplate.ContainerSpec != nil && (s.TaskTemplate.Runtime != "" && s.TaskTemplate.Runtime != swarm.RuntimeContainer) { - return errors.New("mismatched runtime with container spec") - } - return nil -} - -func validateAPIVersion(c swarm.ServiceSpec, apiVersion string) error { - for _, m := range c.TaskTemplate.ContainerSpec.Mounts { - if m.BindOptions != nil { - if m.BindOptions.NonRecursive && versions.LessThan(apiVersion, "1.40") { - return errors.Errorf("bind-recursive=disabled requires API v1.40 or later") - } - // ReadOnlyNonRecursive can be safely ignored when API < 1.44 - if m.BindOptions.ReadOnlyForceRecursive && versions.LessThan(apiVersion, "1.44") { - return errors.Errorf("bind-recursive=readonly requires API v1.44 or later") - } - } - } - return nil -} diff --git a/vendor/github.com/docker/docker/client/service_inspect.go b/vendor/github.com/docker/docker/client/service_inspect.go deleted file mode 100644 index cb25ade174ad..000000000000 --- a/vendor/github.com/docker/docker/client/service_inspect.go +++ /dev/null @@ -1,38 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/url" - - "github.com/docker/docker/api/types/swarm" -) - -// ServiceInspectWithRaw returns the service information and the raw data. -func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts swarm.ServiceInspectOptions) (swarm.Service, []byte, error) { - serviceID, err := trimID("service", serviceID) - if err != nil { - return swarm.Service{}, nil, err - } - - query := url.Values{} - query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults)) - resp, err := cli.get(ctx, "/services/"+serviceID, query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Service{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return swarm.Service{}, nil, err - } - - var response swarm.Service - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&response) - return response, body, err -} diff --git a/vendor/github.com/docker/docker/client/service_list.go b/vendor/github.com/docker/docker/client/service_list.go deleted file mode 100644 index 26b25ff0be07..000000000000 --- a/vendor/github.com/docker/docker/client/service_list.go +++ /dev/null @@ -1,38 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" -) - -// ServiceList returns the list of services. -func (cli *Client) ServiceList(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { - query := url.Values{} - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - if options.Status { - query.Set("status", "true") - } - - resp, err := cli.get(ctx, "/services", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var services []swarm.Service - err = json.NewDecoder(resp.Body).Decode(&services) - return services, err -} diff --git a/vendor/github.com/docker/docker/client/service_logs.go b/vendor/github.com/docker/docker/client/service_logs.go deleted file mode 100644 index 8bf04082615d..000000000000 --- a/vendor/github.com/docker/docker/client/service_logs.go +++ /dev/null @@ -1,57 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" - "time" - - "github.com/docker/docker/api/types/container" - timetypes "github.com/docker/docker/api/types/time" - "github.com/pkg/errors" -) - -// ServiceLogs returns the logs generated by a service in an io.ReadCloser. -// It's up to the caller to close the stream. -func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options container.LogsOptions) (io.ReadCloser, error) { - serviceID, err := trimID("service", serviceID) - if err != nil { - return nil, err - } - - query := url.Values{} - if options.ShowStdout { - query.Set("stdout", "1") - } - - if options.ShowStderr { - query.Set("stderr", "1") - } - - if options.Since != "" { - ts, err := timetypes.GetTimestamp(options.Since, time.Now()) - if err != nil { - return nil, errors.Wrap(err, `invalid value for "since"`) - } - query.Set("since", ts) - } - - if options.Timestamps { - query.Set("timestamps", "1") - } - - if options.Details { - query.Set("details", "1") - } - - if options.Follow { - query.Set("follow", "1") - } - query.Set("tail", options.Tail) - - resp, err := cli.get(ctx, "/services/"+serviceID+"/logs", query, nil) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/service_remove.go b/vendor/github.com/docker/docker/client/service_remove.go deleted file mode 100644 index 0c7cc571e0c4..000000000000 --- a/vendor/github.com/docker/docker/client/service_remove.go +++ /dev/null @@ -1,15 +0,0 @@ -package client - -import "context" - -// ServiceRemove kills and removes a service. -func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error { - serviceID, err := trimID("service", serviceID) - if err != nil { - return err - } - - resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/service_update.go b/vendor/github.com/docker/docker/client/service_update.go deleted file mode 100644 index 278e305d0273..000000000000 --- a/vendor/github.com/docker/docker/client/service_update.go +++ /dev/null @@ -1,89 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/http" - "net/url" - - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/versions" -) - -// ServiceUpdate updates a Service. The version number is required to avoid conflicting writes. -// It should be the value as set *before* the update. You can find this value in the Meta field -// of swarm.Service, which can be found using ServiceInspectWithRaw. -func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { - serviceID, err := trimID("service", serviceID) - if err != nil { - return swarm.ServiceUpdateResponse{}, err - } - - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return swarm.ServiceUpdateResponse{}, err - } - - query := url.Values{} - if options.RegistryAuthFrom != "" { - query.Set("registryAuthFrom", options.RegistryAuthFrom) - } - - if options.Rollback != "" { - query.Set("rollback", options.Rollback) - } - - query.Set("version", version.String()) - - if err := validateServiceSpec(service); err != nil { - return swarm.ServiceUpdateResponse{}, err - } - - // ensure that the image is tagged - var resolveWarning string - switch { - case service.TaskTemplate.ContainerSpec != nil: - if taggedImg := imageWithTagString(service.TaskTemplate.ContainerSpec.Image); taggedImg != "" { - service.TaskTemplate.ContainerSpec.Image = taggedImg - } - if options.QueryRegistry { - resolveWarning = resolveContainerSpecImage(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) - } - case service.TaskTemplate.PluginSpec != nil: - if taggedImg := imageWithTagString(service.TaskTemplate.PluginSpec.Remote); taggedImg != "" { - service.TaskTemplate.PluginSpec.Remote = taggedImg - } - if options.QueryRegistry { - resolveWarning = resolvePluginSpecRemote(ctx, cli, &service.TaskTemplate, options.EncodedRegistryAuth) - } - } - - headers := http.Header{} - if versions.LessThan(cli.version, "1.30") { - // the custom "version" header was used by engine API before 20.10 - // (API 1.30) to switch between client- and server-side lookup of - // image digests. - headers["version"] = []string{cli.version} - } - if options.EncodedRegistryAuth != "" { - headers[registry.AuthHeader] = []string{options.EncodedRegistryAuth} - } - resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.ServiceUpdateResponse{}, err - } - - var response swarm.ServiceUpdateResponse - err = json.NewDecoder(resp.Body).Decode(&response) - if resolveWarning != "" { - response.Warnings = append(response.Warnings, resolveWarning) - } - - return response, err -} diff --git a/vendor/github.com/docker/docker/client/swarm_get_unlock_key.go b/vendor/github.com/docker/docker/client/swarm_get_unlock_key.go deleted file mode 100644 index 41151f6cd287..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_get_unlock_key.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmGetUnlockKey retrieves the swarm's unlock key. -func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (swarm.UnlockKeyResponse, error) { - resp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.UnlockKeyResponse{}, err - } - - var response swarm.UnlockKeyResponse - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/swarm_init.go b/vendor/github.com/docker/docker/client/swarm_init.go deleted file mode 100644 index 7f29165493c9..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_init.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmInit initializes the swarm. -func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { - resp, err := cli.post(ctx, "/swarm/init", nil, req, nil) - defer ensureReaderClosed(resp) - if err != nil { - return "", err - } - - var response string - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/swarm_inspect.go b/vendor/github.com/docker/docker/client/swarm_inspect.go deleted file mode 100644 index 597693bd33b6..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_inspect.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmInspect inspects the swarm. -func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { - resp, err := cli.get(ctx, "/swarm", nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Swarm{}, err - } - - var response swarm.Swarm - err = json.NewDecoder(resp.Body).Decode(&response) - return response, err -} diff --git a/vendor/github.com/docker/docker/client/swarm_join.go b/vendor/github.com/docker/docker/client/swarm_join.go deleted file mode 100644 index 446d4d0482d2..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_join.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmJoin joins the swarm. -func (cli *Client) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { - resp, err := cli.post(ctx, "/swarm/join", nil, req, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/swarm_leave.go b/vendor/github.com/docker/docker/client/swarm_leave.go deleted file mode 100644 index 709e5adb3536..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_leave.go +++ /dev/null @@ -1,17 +0,0 @@ -package client - -import ( - "context" - "net/url" -) - -// SwarmLeave leaves the swarm. -func (cli *Client) SwarmLeave(ctx context.Context, force bool) error { - query := url.Values{} - if force { - query.Set("force", "1") - } - resp, err := cli.post(ctx, "/swarm/leave", query, nil, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/swarm_unlock.go b/vendor/github.com/docker/docker/client/swarm_unlock.go deleted file mode 100644 index e3c756b661fa..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_unlock.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import ( - "context" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmUnlock unlocks locked swarm. -func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { - resp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/swarm_update.go b/vendor/github.com/docker/docker/client/swarm_update.go deleted file mode 100644 index 309ab194a46a..000000000000 --- a/vendor/github.com/docker/docker/client/swarm_update.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "net/url" - "strconv" - - "github.com/docker/docker/api/types/swarm" -) - -// SwarmUpdate updates the swarm. -func (cli *Client) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { - query := url.Values{} - query.Set("version", version.String()) - query.Set("rotateWorkerToken", strconv.FormatBool(flags.RotateWorkerToken)) - query.Set("rotateManagerToken", strconv.FormatBool(flags.RotateManagerToken)) - query.Set("rotateManagerUnlockKey", strconv.FormatBool(flags.RotateManagerUnlockKey)) - resp, err := cli.post(ctx, "/swarm/update", query, swarm, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/task_inspect.go b/vendor/github.com/docker/docker/client/task_inspect.go deleted file mode 100644 index ca3924fc4805..000000000000 --- a/vendor/github.com/docker/docker/client/task_inspect.go +++ /dev/null @@ -1,34 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types/swarm" -) - -// TaskInspectWithRaw returns the task information and its raw representation. -func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { - taskID, err := trimID("task", taskID) - if err != nil { - return swarm.Task{}, nil, err - } - - resp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return swarm.Task{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return swarm.Task{}, nil, err - } - - var response swarm.Task - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&response) - return response, body, err -} diff --git a/vendor/github.com/docker/docker/client/task_list.go b/vendor/github.com/docker/docker/client/task_list.go deleted file mode 100644 index de743e99c503..000000000000 --- a/vendor/github.com/docker/docker/client/task_list.go +++ /dev/null @@ -1,34 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" -) - -// TaskList returns the list of tasks. -func (cli *Client) TaskList(ctx context.Context, options swarm.TaskListOptions) ([]swarm.Task, error) { - query := url.Values{} - - if options.Filters.Len() > 0 { - filterJSON, err := filters.ToJSON(options.Filters) - if err != nil { - return nil, err - } - - query.Set("filters", filterJSON) - } - - resp, err := cli.get(ctx, "/tasks", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return nil, err - } - - var tasks []swarm.Task - err = json.NewDecoder(resp.Body).Decode(&tasks) - return tasks, err -} diff --git a/vendor/github.com/docker/docker/client/task_logs.go b/vendor/github.com/docker/docker/client/task_logs.go deleted file mode 100644 index baa55528a788..000000000000 --- a/vendor/github.com/docker/docker/client/task_logs.go +++ /dev/null @@ -1,51 +0,0 @@ -package client - -import ( - "context" - "io" - "net/url" - "time" - - "github.com/docker/docker/api/types/container" - timetypes "github.com/docker/docker/api/types/time" -) - -// TaskLogs returns the logs generated by a task in an io.ReadCloser. -// It's up to the caller to close the stream. -func (cli *Client) TaskLogs(ctx context.Context, taskID string, options container.LogsOptions) (io.ReadCloser, error) { - query := url.Values{} - if options.ShowStdout { - query.Set("stdout", "1") - } - - if options.ShowStderr { - query.Set("stderr", "1") - } - - if options.Since != "" { - ts, err := timetypes.GetTimestamp(options.Since, time.Now()) - if err != nil { - return nil, err - } - query.Set("since", ts) - } - - if options.Timestamps { - query.Set("timestamps", "1") - } - - if options.Details { - query.Set("details", "1") - } - - if options.Follow { - query.Set("follow", "1") - } - query.Set("tail", options.Tail) - - resp, err := cli.get(ctx, "/tasks/"+taskID+"/logs", query, nil) - if err != nil { - return nil, err - } - return resp.Body, nil -} diff --git a/vendor/github.com/docker/docker/client/utils.go b/vendor/github.com/docker/docker/client/utils.go deleted file mode 100644 index 67e1e6934b59..000000000000 --- a/vendor/github.com/docker/docker/client/utils.go +++ /dev/null @@ -1,96 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "net/url" - "strings" - - cerrdefs "github.com/containerd/errdefs" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/internal/lazyregexp" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" -) - -var headerRegexp = lazyregexp.New(`\ADocker/.+\s\((.+)\)\z`) - -type emptyIDError string - -func (e emptyIDError) InvalidParameter() {} - -func (e emptyIDError) Error() string { - return "invalid " + string(e) + " name or ID: value is empty" -} - -// trimID trims the given object-ID / name, returning an error if it's empty. -func trimID(objType, id string) (string, error) { - id = strings.TrimSpace(id) - if id == "" { - return "", emptyIDError(objType) - } - return id, nil -} - -// getDockerOS returns the operating system based on the server header from the daemon. -func getDockerOS(serverHeader string) string { - var osType string - matches := headerRegexp.FindStringSubmatch(serverHeader) - if len(matches) > 0 { - osType = matches[1] - } - return osType -} - -// getFiltersQuery returns a url query with "filters" query term, based on the -// filters provided. -func getFiltersQuery(f filters.Args) (url.Values, error) { - query := url.Values{} - if f.Len() > 0 { - filterJSON, err := filters.ToJSON(f) - if err != nil { - return query, err - } - query.Set("filters", filterJSON) - } - return query, nil -} - -// encodePlatforms marshals the given platform(s) to JSON format, to -// be used for query-parameters for filtering / selecting platforms. -func encodePlatforms(platform ...ocispec.Platform) ([]string, error) { - if len(platform) == 0 { - return []string{}, nil - } - if len(platform) == 1 { - p, err := encodePlatform(&platform[0]) - if err != nil { - return nil, err - } - return []string{p}, nil - } - - seen := make(map[string]struct{}, len(platform)) - out := make([]string, 0, len(platform)) - for i := range platform { - p, err := encodePlatform(&platform[i]) - if err != nil { - return nil, err - } - if _, ok := seen[p]; !ok { - out = append(out, p) - seen[p] = struct{}{} - } - } - return out, nil -} - -// encodePlatform marshals the given platform to JSON format, to -// be used for query-parameters for filtering / selecting platforms. It -// is used as a helper for encodePlatforms, -func encodePlatform(platform *ocispec.Platform) (string, error) { - p, err := json.Marshal(platform) - if err != nil { - return "", fmt.Errorf("%w: invalid platform: %v", cerrdefs.ErrInvalidArgument, err) - } - return string(p), nil -} diff --git a/vendor/github.com/docker/docker/client/version.go b/vendor/github.com/docker/docker/client/version.go deleted file mode 100644 index 046af16cc5ca..000000000000 --- a/vendor/github.com/docker/docker/client/version.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types" -) - -// ServerVersion returns information of the docker client and server host. -func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) { - resp, err := cli.get(ctx, "/version", nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return types.Version{}, err - } - - var server types.Version - err = json.NewDecoder(resp.Body).Decode(&server) - return server, err -} diff --git a/vendor/github.com/docker/docker/client/volume_create.go b/vendor/github.com/docker/docker/client/volume_create.go deleted file mode 100644 index 1aad3f479c4c..000000000000 --- a/vendor/github.com/docker/docker/client/volume_create.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - - "github.com/docker/docker/api/types/volume" -) - -// VolumeCreate creates a volume in the docker host. -func (cli *Client) VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) { - resp, err := cli.post(ctx, "/volumes/create", nil, options, nil) - defer ensureReaderClosed(resp) - if err != nil { - return volume.Volume{}, err - } - - var vol volume.Volume - err = json.NewDecoder(resp.Body).Decode(&vol) - return vol, err -} diff --git a/vendor/github.com/docker/docker/client/volume_inspect.go b/vendor/github.com/docker/docker/client/volume_inspect.go deleted file mode 100644 index 389a4a71aab6..000000000000 --- a/vendor/github.com/docker/docker/client/volume_inspect.go +++ /dev/null @@ -1,40 +0,0 @@ -package client - -import ( - "bytes" - "context" - "encoding/json" - "io" - - "github.com/docker/docker/api/types/volume" -) - -// VolumeInspect returns the information about a specific volume in the docker host. -func (cli *Client) VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error) { - vol, _, err := cli.VolumeInspectWithRaw(ctx, volumeID) - return vol, err -} - -// VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation -func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (volume.Volume, []byte, error) { - volumeID, err := trimID("volume", volumeID) - if err != nil { - return volume.Volume{}, nil, err - } - - resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return volume.Volume{}, nil, err - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return volume.Volume{}, nil, err - } - - var vol volume.Volume - rdr := bytes.NewReader(body) - err = json.NewDecoder(rdr).Decode(&vol) - return vol, body, err -} diff --git a/vendor/github.com/docker/docker/client/volume_list.go b/vendor/github.com/docker/docker/client/volume_list.go deleted file mode 100644 index 61ed518cd6cc..000000000000 --- a/vendor/github.com/docker/docker/client/volume_list.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "net/url" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" -) - -// VolumeList returns the volumes configured in the docker host. -func (cli *Client) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) { - query := url.Values{} - - if options.Filters.Len() > 0 { - //nolint:staticcheck // ignore SA1019 for old code - filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) - if err != nil { - return volume.ListResponse{}, err - } - query.Set("filters", filterJSON) - } - resp, err := cli.get(ctx, "/volumes", query, nil) - defer ensureReaderClosed(resp) - if err != nil { - return volume.ListResponse{}, err - } - - var volumes volume.ListResponse - err = json.NewDecoder(resp.Body).Decode(&volumes) - return volumes, err -} diff --git a/vendor/github.com/docker/docker/client/volume_prune.go b/vendor/github.com/docker/docker/client/volume_prune.go deleted file mode 100644 index e22f0072f9ec..000000000000 --- a/vendor/github.com/docker/docker/client/volume_prune.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "fmt" - - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/volume" -) - -// VolumesPrune requests the daemon to delete unused data -func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args) (volume.PruneReport, error) { - if err := cli.NewVersionError(ctx, "1.25", "volume prune"); err != nil { - return volume.PruneReport{}, err - } - - query, err := getFiltersQuery(pruneFilters) - if err != nil { - return volume.PruneReport{}, err - } - - resp, err := cli.post(ctx, "/volumes/prune", query, nil, nil) - defer ensureReaderClosed(resp) - if err != nil { - return volume.PruneReport{}, err - } - - var report volume.PruneReport - if err := json.NewDecoder(resp.Body).Decode(&report); err != nil { - return volume.PruneReport{}, fmt.Errorf("Error retrieving volume prune report: %v", err) - } - - return report, nil -} diff --git a/vendor/github.com/docker/docker/client/volume_remove.go b/vendor/github.com/docker/docker/client/volume_remove.go deleted file mode 100644 index e2a53fa9b89c..000000000000 --- a/vendor/github.com/docker/docker/client/volume_remove.go +++ /dev/null @@ -1,34 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/versions" -) - -// VolumeRemove removes a volume from the docker host. -func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error { - volumeID, err := trimID("volume", volumeID) - if err != nil { - return err - } - - query := url.Values{} - if force { - // Make sure we negotiated (if the client is configured to do so), - // as code below contains API-version specific handling of options. - // - // Normally, version-negotiation (if enabled) would not happen until - // the API request is made. - if err := cli.checkVersion(ctx); err != nil { - return err - } - if versions.GreaterThanOrEqualTo(cli.version, "1.25") { - query.Set("force", "1") - } - } - resp, err := cli.delete(ctx, "/volumes/"+volumeID, query, nil) - defer ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/client/volume_update.go b/vendor/github.com/docker/docker/client/volume_update.go deleted file mode 100644 index 879932f008e7..000000000000 --- a/vendor/github.com/docker/docker/client/volume_update.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - "net/url" - - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/api/types/volume" -) - -// VolumeUpdate updates a volume. This only works for Cluster Volumes, and -// only some fields can be updated. -func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error { - volumeID, err := trimID("volume", volumeID) - if err != nil { - return err - } - if err := cli.NewVersionError(ctx, "1.42", "volume update"); err != nil { - return err - } - - query := url.Values{} - query.Set("version", version.String()) - - resp, err := cli.put(ctx, "/volumes/"+volumeID, query, options, nil) - ensureReaderClosed(resp) - return err -} diff --git a/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go b/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go deleted file mode 100644 index 6334edb60dca..000000000000 --- a/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Code below was largely copied from golang.org/x/mod@v0.22; -// https://github.com/golang/mod/blob/v0.22.0/internal/lazyregexp/lazyre.go -// with some additional methods added. - -// Package lazyregexp is a thin wrapper over regexp, allowing the use of global -// regexp variables without forcing them to be compiled at init. -package lazyregexp - -import ( - "os" - "regexp" - "strings" - "sync" -) - -// Regexp is a wrapper around [regexp.Regexp], where the underlying regexp will be -// compiled the first time it is needed. -type Regexp struct { - str string - once sync.Once - rx *regexp.Regexp -} - -func (r *Regexp) re() *regexp.Regexp { - r.once.Do(r.build) - return r.rx -} - -func (r *Regexp) build() { - r.rx = regexp.MustCompile(r.str) - r.str = "" -} - -func (r *Regexp) FindSubmatch(s []byte) [][]byte { - return r.re().FindSubmatch(s) -} - -func (r *Regexp) FindAllStringSubmatch(s string, n int) [][]string { - return r.re().FindAllStringSubmatch(s, n) -} - -func (r *Regexp) FindStringSubmatch(s string) []string { - return r.re().FindStringSubmatch(s) -} - -func (r *Regexp) FindStringSubmatchIndex(s string) []int { - return r.re().FindStringSubmatchIndex(s) -} - -func (r *Regexp) ReplaceAllString(src, repl string) string { - return r.re().ReplaceAllString(src, repl) -} - -func (r *Regexp) FindString(s string) string { - return r.re().FindString(s) -} - -func (r *Regexp) FindAllString(s string, n int) []string { - return r.re().FindAllString(s, n) -} - -func (r *Regexp) MatchString(s string) bool { - return r.re().MatchString(s) -} - -func (r *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string { - return r.re().ReplaceAllStringFunc(src, repl) -} - -func (r *Regexp) SubexpNames() []string { - return r.re().SubexpNames() -} - -var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test") - -// New creates a new lazy regexp, delaying the compiling work until it is first -// needed. If the code is being run as part of tests, the regexp compiling will -// happen immediately. -func New(str string) *Regexp { - lr := &Regexp{str: str} - if inTest { - // In tests, always compile the regexps early. - lr.re() - } - return lr -} diff --git a/vendor/github.com/docker/docker/internal/multierror/multierror.go b/vendor/github.com/docker/docker/internal/multierror/multierror.go deleted file mode 100644 index e899f4de85c9..000000000000 --- a/vendor/github.com/docker/docker/internal/multierror/multierror.go +++ /dev/null @@ -1,46 +0,0 @@ -package multierror - -import ( - "strings" -) - -// Join is a drop-in replacement for errors.Join with better formatting. -func Join(errs ...error) error { - n := 0 - for _, err := range errs { - if err != nil { - n++ - } - } - if n == 0 { - return nil - } - e := &joinError{ - errs: make([]error, 0, n), - } - for _, err := range errs { - if err != nil { - e.errs = append(e.errs, err) - } - } - return e -} - -type joinError struct { - errs []error -} - -func (e *joinError) Error() string { - if len(e.errs) == 1 { - return strings.TrimSpace(e.errs[0].Error()) - } - stringErrs := make([]string, 0, len(e.errs)) - for _, subErr := range e.errs { - stringErrs = append(stringErrs, strings.ReplaceAll(subErr.Error(), "\n", "\n\t")) - } - return "* " + strings.Join(stringErrs, "\n* ") -} - -func (e *joinError) Unwrap() []error { - return e.errs -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir.go b/vendor/github.com/docker/docker/pkg/homedir/homedir.go deleted file mode 100644 index c0ab3f5bf359..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir.go +++ /dev/null @@ -1,28 +0,0 @@ -package homedir - -import ( - "os" - "os/user" - "runtime" -) - -// Get returns the home directory of the current user with the help of -// environment variables depending on the target operating system. -// Returned path should be used with "path/filepath" to form new paths. -// -// On non-Windows platforms, it falls back to nss lookups, if the home -// directory cannot be obtained from environment-variables. -// -// If linking statically with cgo enabled against glibc, ensure the -// osusergo build tag is used. -// -// If needing to do nss lookups, do not disable cgo or set osusergo. -func Get() string { - home, _ := os.UserHomeDir() - if home == "" && runtime.GOOS != "windows" { - if u, err := user.Current(); err == nil { - return u.HomeDir - } - } - return home -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go deleted file mode 100644 index 469395f16e2e..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go +++ /dev/null @@ -1,105 +0,0 @@ -package homedir - -import ( - "errors" - "os" - "path/filepath" - "strings" -) - -// GetRuntimeDir returns XDG_RUNTIME_DIR. -// XDG_RUNTIME_DIR is typically configured via pam_systemd. -// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetRuntimeDir() (string, error) { - if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" { - return xdgRuntimeDir, nil - } - return "", errors.New("could not get XDG_RUNTIME_DIR") -} - -// StickRuntimeDirContents sets the sticky bit on files that are under -// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system. -// -// StickyRuntimeDir returns slice of sticked files. -// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func StickRuntimeDirContents(files []string) ([]string, error) { - runtimeDir, err := GetRuntimeDir() - if err != nil { - // ignore error if runtimeDir is empty - return nil, nil - } - runtimeDir, err = filepath.Abs(runtimeDir) - if err != nil { - return nil, err - } - var sticked []string - for _, f := range files { - f, err = filepath.Abs(f) - if err != nil { - return sticked, err - } - if strings.HasPrefix(f, runtimeDir+"/") { - if err = stick(f); err != nil { - return sticked, err - } - sticked = append(sticked, f) - } - } - return sticked, nil -} - -func stick(f string) error { - st, err := os.Stat(f) - if err != nil { - return err - } - m := st.Mode() - m |= os.ModeSticky - return os.Chmod(f, m) -} - -// GetDataHome returns XDG_DATA_HOME. -// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set. -// If HOME and XDG_DATA_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetDataHome() (string, error) { - if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { - return xdgDataHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_DATA_HOME or HOME") - } - return filepath.Join(home, ".local", "share"), nil -} - -// GetConfigHome returns XDG_CONFIG_HOME. -// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set. -// If HOME and XDG_CONFIG_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetConfigHome() (string, error) { - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { - return xdgConfigHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_CONFIG_HOME or HOME") - } - return filepath.Join(home, ".config"), nil -} - -// GetLibHome returns $HOME/.local/lib -// If HOME is not set, getpwent(3) is consulted to determine the users home directory. -func GetLibHome() (string, error) { - home := Get() - if home == "" { - return "", errors.New("could not get HOME") - } - return filepath.Join(home, ".local/lib"), nil -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go deleted file mode 100644 index 1e41e6aab51f..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build !linux - -package homedir - -import ( - "errors" -) - -// GetRuntimeDir is unsupported on non-linux system. -func GetRuntimeDir() (string, error) { - return "", errors.New("homedir.GetRuntimeDir() is not supported on this system") -} - -// StickRuntimeDirContents is unsupported on non-linux system. -func StickRuntimeDirContents(files []string) ([]string, error) { - return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system") -} - -// GetDataHome is unsupported on non-linux system. -func GetDataHome() (string, error) { - return "", errors.New("homedir.GetDataHome() is not supported on this system") -} - -// GetConfigHome is unsupported on non-linux system. -func GetConfigHome() (string, error) { - return "", errors.New("homedir.GetConfigHome() is not supported on this system") -} - -// GetLibHome is unsupported on non-linux system. -func GetLibHome() (string, error) { - return "", errors.New("homedir.GetLibHome() is not supported on this system") -} diff --git a/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go b/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go deleted file mode 100644 index 3d072808f1de..000000000000 --- a/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go +++ /dev/null @@ -1,314 +0,0 @@ -package jsonmessage - -import ( - "encoding/json" - "fmt" - "io" - "strings" - "time" - - "github.com/docker/go-units" - "github.com/moby/term" - "github.com/morikuni/aec" -) - -// RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to -// ensure the formatted time isalways the same number of characters. -const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" - -// JSONError wraps a concrete Code and Message, Code is -// an integer error code, Message is the error message. -type JSONError struct { - Code int `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -func (e *JSONError) Error() string { - return e.Message -} - -// JSONProgress describes a progress message in a JSON stream. -type JSONProgress struct { - // Current is the current status and value of the progress made towards Total. - Current int64 `json:"current,omitempty"` - // Total is the end value describing when we made 100% progress for an operation. - Total int64 `json:"total,omitempty"` - // Start is the initial value for the operation. - Start int64 `json:"start,omitempty"` - // HideCounts. if true, hides the progress count indicator (xB/yB). - HideCounts bool `json:"hidecounts,omitempty"` - // Units is the unit to print for progress. It defaults to "bytes" if empty. - Units string `json:"units,omitempty"` - - // terminalFd is the fd of the current terminal, if any. It is used - // to get the terminal width. - terminalFd uintptr - - // nowFunc is used to override the current time in tests. - nowFunc func() time.Time - - // winSize is used to override the terminal width in tests. - winSize int -} - -func (p *JSONProgress) String() string { - var ( - width = p.width() - pbBox string - numbersBox string - ) - if p.Current <= 0 && p.Total <= 0 { - return "" - } - if p.Total <= 0 { - switch p.Units { - case "": - return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current))) - default: - return fmt.Sprintf("%d %s", p.Current, p.Units) - } - } - - percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 - if percentage > 50 { - percentage = 50 - } - if width > 110 { - // this number can't be negative gh#7136 - numSpaces := 0 - if 50-percentage > 0 { - numSpaces = 50 - percentage - } - pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) - } - - switch { - case p.HideCounts: - case p.Units == "": // no units, use bytes - current := units.HumanSize(float64(p.Current)) - total := units.HumanSize(float64(p.Total)) - - numbersBox = fmt.Sprintf("%8v/%v", current, total) - - if p.Current > p.Total { - // remove total display if the reported current is wonky. - numbersBox = fmt.Sprintf("%8v", current) - } - default: - numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) - - if p.Current > p.Total { - // remove total display if the reported current is wonky. - numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) - } - } - - // Show approximation of remaining time if there's enough width. - var timeLeftBox string - if width > 50 { - if p.Current > 0 && p.Start > 0 && percentage < 50 { - fromStart := p.now().Sub(time.Unix(p.Start, 0)) - perEntry := fromStart / time.Duration(p.Current) - left := time.Duration(p.Total-p.Current) * perEntry - timeLeftBox = " " + left.Round(time.Second).String() - } - } - return pbBox + numbersBox + timeLeftBox -} - -// now returns the current time in UTC, but can be overridden in tests -// by setting JSONProgress.nowFunc to a custom function. -func (p *JSONProgress) now() time.Time { - if p.nowFunc != nil { - return p.nowFunc() - } - return time.Now().UTC() -} - -// width returns the current terminal's width, but can be overridden -// in tests by setting JSONProgress.winSize to a non-zero value. -func (p *JSONProgress) width() int { - if p.winSize != 0 { - return p.winSize - } - ws, err := term.GetWinsize(p.terminalFd) - if err == nil { - return int(ws.Width) - } - return 200 -} - -// JSONMessage defines a message struct. It describes -// the created time, where it from, status, ID of the -// message. It's used for docker events. -type JSONMessage struct { - Stream string `json:"stream,omitempty"` - Status string `json:"status,omitempty"` - Progress *JSONProgress `json:"progressDetail,omitempty"` - - // ProgressMessage is a pre-formatted presentation of [Progress]. - // - // Deprecated: this field is deprecated since docker v0.7.1 / API v1.8. Use the information in [Progress] instead. This field will be omitted in a future release. - ProgressMessage string `json:"progress,omitempty"` - ID string `json:"id,omitempty"` - From string `json:"from,omitempty"` - Time int64 `json:"time,omitempty"` - TimeNano int64 `json:"timeNano,omitempty"` - Error *JSONError `json:"errorDetail,omitempty"` - - // ErrorMessage contains errors encountered during the operation. - // - // Deprecated: this field is deprecated since docker v0.6.0 / API v1.4. Use [Error.Message] instead. This field will be omitted in a future release. - ErrorMessage string `json:"error,omitempty"` // deprecated - // Aux contains out-of-band data, such as digests for push signing and image id after building. - Aux *json.RawMessage `json:"aux,omitempty"` -} - -func clearLine(out io.Writer) { - eraseMode := aec.EraseModes.All - cl := aec.EraseLine(eraseMode) - fmt.Fprint(out, cl) -} - -func cursorUp(out io.Writer, l uint) { - fmt.Fprint(out, aec.Up(l)) -} - -func cursorDown(out io.Writer, l uint) { - fmt.Fprint(out, aec.Down(l)) -} - -// Display prints the JSONMessage to out. If isTerminal is true, it erases -// the entire current line when displaying the progressbar. It returns an -// error if the [JSONMessage.Error] field is non-nil. -func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error { - if jm.Error != nil { - return jm.Error - } - var endl string - if isTerminal && jm.Stream == "" && jm.Progress != nil { - clearLine(out) - endl = "\r" - fmt.Fprint(out, endl) - } else if jm.Progress != nil && jm.Progress.String() != "" { // disable progressbar in non-terminal - return nil - } - if jm.TimeNano != 0 { - fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed)) - } else if jm.Time != 0 { - fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed)) - } - if jm.ID != "" { - fmt.Fprintf(out, "%s: ", jm.ID) - } - if jm.From != "" { - fmt.Fprintf(out, "(from %s) ", jm.From) - } - if jm.Progress != nil && isTerminal { - fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) - } else if jm.ProgressMessage != "" { // deprecated - fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) - } else if jm.Stream != "" { - fmt.Fprintf(out, "%s%s", jm.Stream, endl) - } else { - fmt.Fprintf(out, "%s%s\n", jm.Status, endl) - } - return nil -} - -// DisplayJSONMessagesStream reads a JSON message stream from in, and writes -// each [JSONMessage] to out. It returns an error if an invalid JSONMessage -// is received, or if a JSONMessage containers a non-zero [JSONMessage.Error]. -// -// Presentation of the JSONMessage depends on whether a terminal is attached, -// and on the terminal width. Progress bars ([JSONProgress]) are suppressed -// on narrower terminals (< 110 characters). -// -// - isTerminal describes if out is a terminal, in which case it prints -// a newline ("\n") at the end of each line and moves the cursor while -// displaying. -// - terminalFd is the fd of the current terminal (if any), and used -// to get the terminal width. -// - auxCallback allows handling the [JSONMessage.Aux] field. It is -// called if a JSONMessage contains an Aux field, in which case -// DisplayJSONMessagesStream does not present the JSONMessage. -func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(JSONMessage)) error { - var ( - dec = json.NewDecoder(in) - ids = make(map[string]uint) - ) - - for { - var diff uint - var jm JSONMessage - if err := dec.Decode(&jm); err != nil { - if err == io.EOF { - break - } - return err - } - - if jm.Aux != nil { - if auxCallback != nil { - auxCallback(jm) - } - continue - } - - if jm.Progress != nil { - jm.Progress.terminalFd = terminalFd - } - if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { - line, ok := ids[jm.ID] - if !ok { - // NOTE: This approach of using len(id) to - // figure out the number of lines of history - // only works as long as we clear the history - // when we output something that's not - // accounted for in the map, such as a line - // with no ID. - line = uint(len(ids)) - ids[jm.ID] = line - if isTerminal { - fmt.Fprintf(out, "\n") - } - } - diff = uint(len(ids)) - line - if isTerminal { - cursorUp(out, diff) - } - } else { - // When outputting something that isn't progress - // output, clear the history of previous lines. We - // don't want progress entries from some previous - // operation to be updated (for example, pull -a - // with multiple tags). - ids = make(map[string]uint) - } - err := jm.Display(out, isTerminal) - if jm.ID != "" && isTerminal { - cursorDown(out, diff) - } - if err != nil { - return err - } - } - return nil -} - -// Stream is an io.Writer for output with utilities to get the output's file -// descriptor and to detect whether it's a terminal. -// -// it is subset of the streams.Out type in -// https://pkg.go.dev/github.com/docker/cli@v20.10.17+incompatible/cli/streams#Out -type Stream interface { - io.Writer - FD() uintptr - IsTerminal() bool -} - -// DisplayJSONMessagesToStream prints json messages to the output Stream. It is -// used by the Docker CLI to print JSONMessage streams. -func DisplayJSONMessagesToStream(in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { - return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) -} diff --git a/vendor/github.com/docker/docker/pkg/process/doc.go b/vendor/github.com/docker/docker/pkg/process/doc.go deleted file mode 100644 index dae536d7dbb0..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package process provides a set of basic functions to manage individual -// processes. -package process diff --git a/vendor/github.com/docker/docker/pkg/process/process_unix.go b/vendor/github.com/docker/docker/pkg/process/process_unix.go deleted file mode 100644 index 13298bbdccb7..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/process_unix.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build !windows - -package process - -import ( - "bytes" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strconv" - - "golang.org/x/sys/unix" -) - -// Alive returns true if process with a given pid is running. It only considers -// positive PIDs; 0 (all processes in the current process group), -1 (all processes -// with a PID larger than 1), and negative (-n, all processes in process group -// "n") values for pid are never considered to be alive. -func Alive(pid int) bool { - if pid < 1 { - return false - } - switch runtime.GOOS { - case "darwin": - // OS X does not have a proc filesystem. Use kill -0 pid to judge if the - // process exists. From KILL(2): https://www.freebsd.org/cgi/man.cgi?query=kill&sektion=2&manpath=OpenDarwin+7.2.1 - // - // Sig may be one of the signals specified in sigaction(2) or it may - // be 0, in which case error checking is performed but no signal is - // actually sent. This can be used to check the validity of pid. - err := unix.Kill(pid, 0) - - // Either the PID was found (no error) or we get an EPERM, which means - // the PID exists, but we don't have permissions to signal it. - return err == nil || errors.Is(err, unix.EPERM) - default: - _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))) - return err == nil - } -} - -// Kill force-stops a process. It only considers positive PIDs; 0 (all processes -// in the current process group), -1 (all processes with a PID larger than 1), -// and negative (-n, all processes in process group "n") values for pid are -// ignored. Refer to [KILL(2)] for details. -// -// [KILL(2)]: https://man7.org/linux/man-pages/man2/kill.2.html -func Kill(pid int) error { - if pid < 1 { - return fmt.Errorf("invalid PID (%d): only positive PIDs are allowed", pid) - } - err := unix.Kill(pid, unix.SIGKILL) - if err != nil && !errors.Is(err, unix.ESRCH) { - return err - } - return nil -} - -// Zombie return true if process has a state with "Z". It only considers positive -// PIDs; 0 (all processes in the current process group), -1 (all processes with -// a PID larger than 1), and negative (-n, all processes in process group "n") -// values for pid are ignored. Refer to [PROC(5)] for details. -// -// [PROC(5)]: https://man7.org/linux/man-pages/man5/proc.5.html -func Zombie(pid int) (bool, error) { - if pid < 1 { - return false, nil - } - data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - if cols := bytes.SplitN(data, []byte(" "), 4); len(cols) >= 3 && string(cols[2]) == "Z" { - return true, nil - } - return false, nil -} diff --git a/vendor/github.com/docker/docker/pkg/process/process_windows.go b/vendor/github.com/docker/docker/pkg/process/process_windows.go deleted file mode 100644 index 2dd57e825452..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/process_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -package process - -import ( - "os" - - "golang.org/x/sys/windows" -) - -// Alive returns true if process with a given pid is running. -func Alive(pid int) bool { - h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) - if err != nil { - return false - } - var c uint32 - err = windows.GetExitCodeProcess(h, &c) - _ = windows.CloseHandle(h) - if err != nil { - // From the GetExitCodeProcess function (processthreadsapi.h) API docs: - // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess - // - // The GetExitCodeProcess function returns a valid error code defined by the - // application only after the thread terminates. Therefore, an application should - // not use STILL_ACTIVE (259) as an error code (STILL_ACTIVE is a macro for - // STATUS_PENDING (minwinbase.h)). If a thread returns STILL_ACTIVE (259) as - // an error code, then applications that test for that value could interpret it - // to mean that the thread is still running, and continue to test for the - // completion of the thread after the thread has terminated, which could put - // the application into an infinite loop. - return c == uint32(windows.STATUS_PENDING) - } - return true -} - -// Kill force-stops a process. -func Kill(pid int) error { - p, err := os.FindProcess(pid) - if err == nil { - err = p.Kill() - if err != nil && err != os.ErrProcessDone { - return err - } - } - return nil -} diff --git a/vendor/github.com/docker/docker/pkg/progress/progress.go b/vendor/github.com/docker/docker/pkg/progress/progress.go deleted file mode 100644 index 3f9887c5bbbd..000000000000 --- a/vendor/github.com/docker/docker/pkg/progress/progress.go +++ /dev/null @@ -1,93 +0,0 @@ -package progress - -import ( - "fmt" -) - -// Progress represents the progress of a transfer. -type Progress struct { - ID string - - // Progress contains a Message or... - Message string - - // ...progress of an action - Action string - Current int64 - Total int64 - - // If true, don't show xB/yB - HideCounts bool - // If not empty, use units instead of bytes for counts - Units string - - // Aux contains extra information not presented to the user, such as - // digests for push signing. - Aux interface{} - - LastUpdate bool -} - -// Output is an interface for writing progress information. It's -// like a writer for progress, but we don't call it Writer because -// that would be confusing next to ProgressReader (also, because it -// doesn't implement the io.Writer interface). -type Output interface { - WriteProgress(Progress) error -} - -type chanOutput chan<- Progress - -func (out chanOutput) WriteProgress(p Progress) error { - // FIXME: workaround for panic in #37735 - defer func() { - recover() - }() - out <- p - return nil -} - -// ChanOutput returns an Output that writes progress updates to the -// supplied channel. -func ChanOutput(progressChan chan<- Progress) Output { - return chanOutput(progressChan) -} - -type discardOutput struct{} - -func (discardOutput) WriteProgress(Progress) error { - return nil -} - -// DiscardOutput returns an Output that discards progress -func DiscardOutput() Output { - return discardOutput{} -} - -// Update is a convenience function to write a progress update to the channel. -func Update(out Output, id, action string) { - out.WriteProgress(Progress{ID: id, Action: action}) -} - -// Updatef is a convenience function to write a printf-formatted progress update -// to the channel. -func Updatef(out Output, id, format string, a ...interface{}) { - Update(out, id, fmt.Sprintf(format, a...)) -} - -// Message is a convenience function to write a progress message to the channel. -func Message(out Output, id, message string) { - out.WriteProgress(Progress{ID: id, Message: message}) -} - -// Messagef is a convenience function to write a printf-formatted progress -// message to the channel. -func Messagef(out Output, id, format string, a ...interface{}) { - Message(out, id, fmt.Sprintf(format, a...)) -} - -// Aux sends auxiliary information over a progress interface, which will not be -// formatted for the UI. This is used for things such as push signing. -func Aux(out Output, a interface{}) { - out.WriteProgress(Progress{Aux: a}) -} diff --git a/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go b/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go deleted file mode 100644 index 611432a6261f..000000000000 --- a/vendor/github.com/docker/docker/pkg/stdcopy/stdcopy.go +++ /dev/null @@ -1,190 +0,0 @@ -package stdcopy - -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "io" - "sync" -) - -// StdType is the type of standard stream -// a writer can multiplex to. -type StdType byte - -const ( - // Stdin represents standard input stream type. - Stdin StdType = iota - // Stdout represents standard output stream type. - Stdout - // Stderr represents standard error steam type. - Stderr - // Systemerr represents errors originating from the system that make it - // into the multiplexed stream. - Systemerr - - stdWriterPrefixLen = 8 - stdWriterFdIndex = 0 - stdWriterSizeIndex = 4 - - startingBufLen = 32*1024 + stdWriterPrefixLen + 1 -) - -var bufPool = &sync.Pool{New: func() interface{} { return bytes.NewBuffer(nil) }} - -// stdWriter is wrapper of io.Writer with extra customized info. -type stdWriter struct { - io.Writer - prefix byte -} - -// Write sends the buffer to the underneath writer. -// It inserts the prefix header before the buffer, -// so stdcopy.StdCopy knows where to multiplex the output. -// It makes stdWriter to implement io.Writer. -func (w *stdWriter) Write(p []byte) (int, error) { - if w == nil || w.Writer == nil { - return 0, errors.New("writer not instantiated") - } - if p == nil { - return 0, nil - } - - header := [stdWriterPrefixLen]byte{stdWriterFdIndex: w.prefix} - binary.BigEndian.PutUint32(header[stdWriterSizeIndex:], uint32(len(p))) - buf := bufPool.Get().(*bytes.Buffer) - buf.Write(header[:]) - buf.Write(p) - - n, err := w.Writer.Write(buf.Bytes()) - n -= stdWriterPrefixLen - if n < 0 { - n = 0 - } - - buf.Reset() - bufPool.Put(buf) - return n, err -} - -// NewStdWriter instantiates a new Writer. -// Everything written to it will be encapsulated using a custom format, -// and written to the underlying `w` stream. -// This allows multiple write streams (e.g. stdout and stderr) to be muxed into a single connection. -// `t` indicates the id of the stream to encapsulate. -// It can be stdcopy.Stdin, stdcopy.Stdout, stdcopy.Stderr. -func NewStdWriter(w io.Writer, t StdType) io.Writer { - return &stdWriter{ - Writer: w, - prefix: byte(t), - } -} - -// StdCopy is a modified version of io.Copy. -// -// StdCopy will demultiplex `src`, assuming that it contains two streams, -// previously multiplexed together using a StdWriter instance. -// As it reads from `src`, StdCopy will write to `dstout` and `dsterr`. -// -// StdCopy will read until it hits EOF on `src`. It will then return a nil error. -// In other words: if `err` is non nil, it indicates a real underlying error. -// -// `written` will hold the total number of bytes written to `dstout` and `dsterr`. -func StdCopy(dstout, dsterr io.Writer, src io.Reader) (written int64, _ error) { - var ( - buf = make([]byte, startingBufLen) - bufLen = len(buf) - nr, nw int - err error - out io.Writer - frameSize int - ) - - for { - // Make sure we have at least a full header - for nr < stdWriterPrefixLen { - var nr2 int - nr2, err = src.Read(buf[nr:]) - nr += nr2 - if errors.Is(err, io.EOF) { - if nr < stdWriterPrefixLen { - return written, nil - } - break - } - if err != nil { - return 0, err - } - } - - stream := StdType(buf[stdWriterFdIndex]) - // Check the first byte to know where to write - switch stream { - case Stdin: - fallthrough - case Stdout: - // Write on stdout - out = dstout - case Stderr: - // Write on stderr - out = dsterr - case Systemerr: - // If we're on Systemerr, we won't write anywhere. - // NB: if this code changes later, make sure you don't try to write - // to outstream if Systemerr is the stream - out = nil - default: - return 0, fmt.Errorf("Unrecognized input header: %d", buf[stdWriterFdIndex]) - } - - // Retrieve the size of the frame - frameSize = int(binary.BigEndian.Uint32(buf[stdWriterSizeIndex : stdWriterSizeIndex+4])) - - // Check if the buffer is big enough to read the frame. - // Extend it if necessary. - if frameSize+stdWriterPrefixLen > bufLen { - buf = append(buf, make([]byte, frameSize+stdWriterPrefixLen-bufLen+1)...) - bufLen = len(buf) - } - - // While the amount of bytes read is less than the size of the frame + header, we keep reading - for nr < frameSize+stdWriterPrefixLen { - var nr2 int - nr2, err = src.Read(buf[nr:]) - nr += nr2 - if errors.Is(err, io.EOF) { - if nr < frameSize+stdWriterPrefixLen { - return written, nil - } - break - } - if err != nil { - return 0, err - } - } - - // we might have an error from the source mixed up in our multiplexed - // stream. if we do, return it. - if stream == Systemerr { - return written, fmt.Errorf("error from daemon in stream: %s", string(buf[stdWriterPrefixLen:frameSize+stdWriterPrefixLen])) - } - - // Write the retrieved frame (without header) - nw, err = out.Write(buf[stdWriterPrefixLen : frameSize+stdWriterPrefixLen]) - if err != nil { - return 0, err - } - - // If the frame has not been fully written: error - if nw != frameSize { - return 0, io.ErrShortWrite - } - written += int64(nw) - - // Move the rest of the buffer to the beginning - copy(buf, buf[frameSize+stdWriterPrefixLen:]) - // Move the index - nr -= frameSize + stdWriterPrefixLen - } -} diff --git a/vendor/github.com/docker/docker/pkg/streamformatter/streamformatter.go b/vendor/github.com/docker/docker/pkg/streamformatter/streamformatter.go deleted file mode 100644 index 24e1b45f2704..000000000000 --- a/vendor/github.com/docker/docker/pkg/streamformatter/streamformatter.go +++ /dev/null @@ -1,164 +0,0 @@ -// Package streamformatter provides helper functions to format a stream. -package streamformatter - -import ( - "encoding/json" - "fmt" - "io" - "sync" - - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/pkg/progress" -) - -const streamNewline = "\r\n" - -type jsonProgressFormatter struct{} - -func appendNewline(source []byte) []byte { - return append(source, []byte(streamNewline)...) -} - -// FormatStatus formats the specified objects according to the specified format (and id). -func FormatStatus(id, format string, a ...interface{}) []byte { - str := fmt.Sprintf(format, a...) - b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str}) - if err != nil { - return FormatError(err) - } - return appendNewline(b) -} - -// FormatError formats the error as a JSON object -func FormatError(err error) []byte { - jsonError, ok := err.(*jsonmessage.JSONError) - if !ok { - jsonError = &jsonmessage.JSONError{Message: err.Error()} - } - if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil { - return appendNewline(b) - } - return []byte(`{"error":"format error"}` + streamNewline) -} - -func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte { - return FormatStatus(id, format, a...) -} - -// formatProgress formats the progress information for a specified action. -func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte { - if progress == nil { - progress = &jsonmessage.JSONProgress{} - } - var auxJSON *json.RawMessage - if aux != nil { - auxJSONBytes, err := json.Marshal(aux) - if err != nil { - return nil - } - auxJSON = new(json.RawMessage) - *auxJSON = auxJSONBytes - } - b, err := json.Marshal(&jsonmessage.JSONMessage{ - Status: action, - ProgressMessage: progress.String(), - Progress: progress, - ID: id, - Aux: auxJSON, - }) - if err != nil { - return nil - } - return appendNewline(b) -} - -type rawProgressFormatter struct{} - -func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte { - return []byte(fmt.Sprintf(format, a...) + streamNewline) -} - -func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte { - if progress == nil { - progress = &jsonmessage.JSONProgress{} - } - endl := "\r" - if progress.String() == "" { - endl += "\n" - } - return []byte(action + " " + progress.String() + endl) -} - -// NewProgressOutput returns a progress.Output object that can be passed to -// progress.NewProgressReader. -func NewProgressOutput(out io.Writer) progress.Output { - return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true} -} - -// NewJSONProgressOutput returns a progress.Output that formats output -// using JSON objects -func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output { - return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines} -} - -type formatProgress interface { - formatStatus(id, format string, a ...interface{}) []byte - formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte -} - -type progressOutput struct { - sf formatProgress - out io.Writer - newLines bool - mu sync.Mutex -} - -// WriteProgress formats progress information from a ProgressReader. -func (out *progressOutput) WriteProgress(prog progress.Progress) error { - var formatted []byte - if prog.Message != "" { - formatted = out.sf.formatStatus(prog.ID, prog.Message) - } else { - jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts, Units: prog.Units} - formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux) - } - - out.mu.Lock() - defer out.mu.Unlock() - _, err := out.out.Write(formatted) - if err != nil { - return err - } - - if out.newLines && prog.LastUpdate { - _, err = out.out.Write(out.sf.formatStatus("", "")) - return err - } - - return nil -} - -// AuxFormatter is a streamFormatter that writes aux progress messages -type AuxFormatter struct { - io.Writer -} - -// Emit emits the given interface as an aux progress message -func (sf *AuxFormatter) Emit(id string, aux interface{}) error { - auxJSONBytes, err := json.Marshal(aux) - if err != nil { - return err - } - auxJSON := new(json.RawMessage) - *auxJSON = auxJSONBytes - msgJSON, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Aux: auxJSON}) - if err != nil { - return err - } - msgJSON = appendNewline(msgJSON) - n, err := sf.Writer.Write(msgJSON) - if n != len(msgJSON) { - return io.ErrShortWrite - } - return err -} diff --git a/vendor/github.com/docker/docker/pkg/streamformatter/streamwriter.go b/vendor/github.com/docker/docker/pkg/streamformatter/streamwriter.go deleted file mode 100644 index 141d12e20eee..000000000000 --- a/vendor/github.com/docker/docker/pkg/streamformatter/streamwriter.go +++ /dev/null @@ -1,47 +0,0 @@ -package streamformatter - -import ( - "encoding/json" - "io" - - "github.com/docker/docker/pkg/jsonmessage" -) - -type streamWriter struct { - io.Writer - lineFormat func([]byte) string -} - -func (sw *streamWriter) Write(buf []byte) (int, error) { - formattedBuf := sw.format(buf) - n, err := sw.Writer.Write(formattedBuf) - if n != len(formattedBuf) { - return n, io.ErrShortWrite - } - return len(buf), err -} - -func (sw *streamWriter) format(buf []byte) []byte { - msg := &jsonmessage.JSONMessage{Stream: sw.lineFormat(buf)} - b, err := json.Marshal(msg) - if err != nil { - return FormatError(err) - } - return appendNewline(b) -} - -// NewStdoutWriter returns a writer which formats the output as json message -// representing stdout lines -func NewStdoutWriter(out io.Writer) io.Writer { - return &streamWriter{Writer: out, lineFormat: func(buf []byte) string { - return string(buf) - }} -} - -// NewStderrWriter returns a writer which formats the output as json message -// representing stderr lines -func NewStderrWriter(out io.Writer) io.Writer { - return &streamWriter{Writer: out, lineFormat: func(buf []byte) string { - return "\033[91m" + string(buf) + "\033[0m" - }} -} diff --git a/vendor/github.com/docker/docker/registry/auth.go b/vendor/github.com/docker/docker/registry/auth.go deleted file mode 100644 index 1b0eeeed0b1c..000000000000 --- a/vendor/github.com/docker/docker/registry/auth.go +++ /dev/null @@ -1,202 +0,0 @@ -package registry - -import ( - "context" - "net/http" - "net/url" - "strings" - "time" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/auth/challenge" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// AuthClientID is used the ClientID used for the token server -const AuthClientID = "docker" - -type loginCredentialStore struct { - authConfig *registry.AuthConfig -} - -func (lcs loginCredentialStore) Basic(*url.URL) (string, string) { - return lcs.authConfig.Username, lcs.authConfig.Password -} - -func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string { - return lcs.authConfig.IdentityToken -} - -func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) { - lcs.authConfig.IdentityToken = token -} - -type staticCredentialStore struct { - auth *registry.AuthConfig -} - -// NewStaticCredentialStore returns a credential store -// which always returns the same credential values. -func NewStaticCredentialStore(auth *registry.AuthConfig) auth.CredentialStore { - return staticCredentialStore{ - auth: auth, - } -} - -func (scs staticCredentialStore) Basic(*url.URL) (string, string) { - if scs.auth == nil { - return "", "" - } - return scs.auth.Username, scs.auth.Password -} - -func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { - if scs.auth == nil { - return "" - } - return scs.auth.IdentityToken -} - -func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - -// loginV2 tries to login to the v2 registry server. The given registry -// endpoint will be pinged to get authorization challenges. These challenges -// will be used to authenticate against the registry to validate credentials. -func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIEndpoint, userAgent string) (token string, _ error) { - endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/" - log.G(ctx).WithField("endpoint", endpointStr).Debug("attempting v2 login to registry endpoint") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpointStr, http.NoBody) - if err != nil { - return "", err - } - - var ( - modifiers = Headers(userAgent, nil) - authTrans = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...) - credentialAuthConfig = *authConfig - creds = loginCredentialStore{authConfig: &credentialAuthConfig} - ) - - loginClient, err := v2AuthHTTPClient(endpoint.URL, authTrans, modifiers, creds, nil) - if err != nil { - return "", err - } - - resp, err := loginClient.Do(req) - if err != nil { - err = translateV2AuthError(err) - return "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - // TODO(dmcgowan): Attempt to further interpret result, status code and error code string - return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) - } - - return credentialAuthConfig.IdentityToken, nil -} - -func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) { - challengeManager, err := PingV2Registry(endpoint, authTransport) - if err != nil { - return nil, err - } - - authHandlers := []auth.AuthenticationHandler{ - auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{ - Transport: authTransport, - Credentials: creds, - OfflineAccess: true, - ClientID: AuthClientID, - Scopes: scopes, - }), - auth.NewBasicHandler(creds), - } - - modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, authHandlers...)) - - return &http.Client{ - Transport: transport.NewTransport(authTransport, modifiers...), - Timeout: 15 * time.Second, - }, nil -} - -// ConvertToHostname normalizes a registry URL which has http|https prepended -// to just its hostname. It is used to match credentials, which may be either -// stored as hostname or as hostname including scheme (in legacy configuration -// files). -func ConvertToHostname(url string) string { - stripped := url - if strings.HasPrefix(stripped, "http://") { - stripped = strings.TrimPrefix(stripped, "http://") - } else if strings.HasPrefix(stripped, "https://") { - stripped = strings.TrimPrefix(stripped, "https://") - } - stripped, _, _ = strings.Cut(stripped, "/") - return stripped -} - -// ResolveAuthConfig matches an auth configuration to a server address or a URL -func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig { - configKey := GetAuthConfigKey(index) - // First try the happy case - if c, found := authConfigs[configKey]; found || index.Official { - return c - } - - // Maybe they have a legacy config file, we will iterate the keys converting - // them to the new format and testing - for registryURL, ac := range authConfigs { - if configKey == ConvertToHostname(registryURL) { - return ac - } - } - - // When all else fails, return an empty auth config - return registry.AuthConfig{} -} - -// PingResponseError is used when the response from a ping -// was received but invalid. -type PingResponseError struct { - Err error -} - -func (err PingResponseError) Error() string { - return err.Err.Error() -} - -// PingV2Registry attempts to ping a v2 registry and on success return a -// challenge manager for the supported authentication types. -// If a response is received but cannot be interpreted, a PingResponseError will be returned. -func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, error) { - pingClient := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, - } - endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" - req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody) - if err != nil { - return nil, err - } - resp, err := pingClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - challengeManager := challenge.NewSimpleManager() - if err := challengeManager.AddResponse(resp); err != nil { - return nil, PingResponseError{ - Err: err, - } - } - - return challengeManager, nil -} diff --git a/vendor/github.com/docker/docker/registry/config.go b/vendor/github.com/docker/docker/registry/config.go deleted file mode 100644 index 218a12683a63..000000000000 --- a/vendor/github.com/docker/docker/registry/config.go +++ /dev/null @@ -1,481 +0,0 @@ -package registry - -import ( - "context" - "net" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/internal/lazyregexp" - "github.com/docker/docker/pkg/homedir" -) - -// ServiceOptions holds command line options. -type ServiceOptions struct { - Mirrors []string `json:"registry-mirrors,omitempty"` - InsecureRegistries []string `json:"insecure-registries,omitempty"` -} - -// serviceConfig holds daemon configuration for the registry service. -type serviceConfig registry.ServiceConfig - -// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains -// are here for historic reasons and backward-compatibility. These domains -// are still supported by Docker Hub (and will continue to be supported), but -// there are new domains already in use, and plans to consolidate all legacy -// domains to new "canonical" domains. Once those domains are decided on, we -// should update these consts (but making sure to preserve compatibility with -// existing installs, clients, and user configuration). -const ( - // DefaultNamespace is the default namespace - DefaultNamespace = "docker.io" - // DefaultRegistryHost is the hostname for the default (Docker Hub) registry - // used for pushing and pulling images. This hostname is hard-coded to handle - // the conversion from image references without registry name (e.g. "ubuntu", - // or "ubuntu:latest"), as well as references using the "docker.io" domain - // name, which is used as canonical reference for images on Docker Hub, but - // does not match the domain-name of Docker Hub's registry. - DefaultRegistryHost = "registry-1.docker.io" - // IndexHostname is the index hostname, used for authentication and image search. - IndexHostname = "index.docker.io" - // IndexServer is used for user auth and image search - IndexServer = "https://" + IndexHostname + "/v1/" - // IndexName is the name of the index - IndexName = "docker.io" -) - -var ( - // DefaultV2Registry is the URI of the default (Docker Hub) registry. - DefaultV2Registry = &url.URL{ - Scheme: "https", - Host: DefaultRegistryHost, - } - - validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`) - - // certsDir is used to override defaultCertsDir when running with rootlessKit. - // - // TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir] - // TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed. - setCertsDirOnce sync.Once - certsDir string -) - -func setCertsDir(dir string) string { - setCertsDirOnce.Do(func() { - if dir != "" { - certsDir = dir - return - } - if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" { - // Configure registry.CertsDir() when running in rootless-mode - // This is the equivalent of [rootless.RunningWithRootlessKit], - // but inlining it to prevent adding that as a dependency - // for docker/cli. - // - // [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 - if configHome, _ := homedir.GetConfigHome(); configHome != "" { - certsDir = filepath.Join(configHome, "docker/certs.d") - return - } - } - certsDir = defaultCertsDir - }) - return certsDir -} - -// SetCertsDir allows the default certs directory to be changed. This function -// is used at daemon startup to set the correct location when running in -// rootless mode. -// -// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually. -func SetCertsDir(path string) { - setCertsDir(path) -} - -// CertsDir is the directory where certificates are stored. -func CertsDir() string { - // call setCertsDir with an empty path to synchronise with [SetCertsDir] - return setCertsDir("") -} - -// newServiceConfig returns a new instance of ServiceConfig -func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { - config := &serviceConfig{} - if err := config.loadMirrors(options.Mirrors); err != nil { - return nil, err - } - if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil { - return nil, err - } - - return config, nil -} - -// copy constructs a new ServiceConfig with a copy of the configuration in config. -func (config *serviceConfig) copy() *registry.ServiceConfig { - ic := make(map[string]*registry.IndexInfo) - for key, value := range config.IndexConfigs { - ic[key] = value - } - return ®istry.ServiceConfig{ - InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), - IndexConfigs: ic, - Mirrors: append([]string(nil), config.Mirrors...), - } -} - -// loadMirrors loads mirrors to config, after removing duplicates. -// Returns an error if mirrors contains an invalid mirror. -func (config *serviceConfig) loadMirrors(mirrors []string) error { - mMap := map[string]struct{}{} - unique := []string{} - - for _, mirror := range mirrors { - m, err := ValidateMirror(mirror) - if err != nil { - return err - } - if _, exist := mMap[m]; !exist { - mMap[m] = struct{}{} - unique = append(unique, m) - } - } - - config.Mirrors = unique - - // Configure public registry since mirrors may have changed. - config.IndexConfigs = map[string]*registry.IndexInfo{ - IndexName: { - Name: IndexName, - Mirrors: unique, - Secure: true, - Official: true, - }, - } - - return nil -} - -// loadInsecureRegistries loads insecure registries to config -func (config *serviceConfig) loadInsecureRegistries(registries []string) error { - // Localhost is by default considered as an insecure registry. This is a - // stop-gap for people who are running a private registry on localhost. - registries = append(registries, "::1/128", "127.0.0.0/8") - - var ( - insecureRegistryCIDRs = make([]*registry.NetIPNet, 0) - indexConfigs = make(map[string]*registry.IndexInfo) - ) - -skip: - for _, r := range registries { - // validate insecure registry - if _, err := ValidateIndexName(r); err != nil { - return err - } - if strings.HasPrefix(strings.ToLower(r), "http://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'http://' and 'http://' has been removed from the insecure registry config", r) - r = r[7:] - } else if strings.HasPrefix(strings.ToLower(r), "https://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r) - r = r[8:] - } else if hasScheme(r) { - return invalidParamf("insecure registry %s should not contain '://'", r) - } - // Check if CIDR was passed to --insecure-registry - _, ipnet, err := net.ParseCIDR(r) - if err == nil { - // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. - data := (*registry.NetIPNet)(ipnet) - for _, value := range insecureRegistryCIDRs { - if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() { - continue skip - } - } - // ipnet is not found, add it in config.InsecureRegistryCIDRs - insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) - } else { - if err := validateHostPort(r); err != nil { - return invalidParamWrapf(err, "insecure registry %s is not valid", r) - } - // Assume `host:port` if not CIDR. - indexConfigs[r] = ®istry.IndexInfo{ - Name: r, - Mirrors: []string{}, - Secure: false, - Official: false, - } - } - } - - // Configure public registry. - indexConfigs[IndexName] = ®istry.IndexInfo{ - Name: IndexName, - Mirrors: config.Mirrors, - Secure: true, - Official: true, - } - config.InsecureRegistryCIDRs = insecureRegistryCIDRs - config.IndexConfigs = indexConfigs - - return nil -} - -// isSecureIndex returns false if the provided indexName is part of the list of insecure registries -// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -// -// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered -// insecure. -// -// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name -// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element -// of insecureRegistries. -func (config *serviceConfig) isSecureIndex(indexName string) bool { - // Check for configured index, first. This is needed in case isSecureIndex - // is called from anything besides newIndexInfo, in order to honor per-index configurations. - if index, ok := config.IndexConfigs[indexName]; ok { - return index.Secure - } - - return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) -} - -// for mocking in unit tests. -var lookupIP = net.LookupIP - -// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`) -// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be -// resolved to IP addresses for matching. If resolution fails, false is returned. -func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool { - if len(cidrs) == 0 { - return false - } - - host, _, err := net.SplitHostPort(URLHost) - if err != nil { - // Assume URLHost is a host without port and go on. - host = URLHost - } - - var addresses []net.IP - if ip := net.ParseIP(host); ip != nil { - // Host is an IP-address. - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-address. - addresses, err = lookupIP(host) - if err != nil { - // We failed to resolve the host; assume there's no match. - return false - } - } - - for _, addr := range addresses { - for _, ipnet := range cidrs { - // check if the addr falls in the subnet - if (*net.IPNet)(ipnet).Contains(addr) { - return true - } - } - } - - return false -} - -// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It -// returns an error if the given mirrorURL is invalid, or the normalized -// format for the URL otherwise. -// -// It is used by the daemon to validate the daemon configuration. -func ValidateMirror(mirrorURL string) (string, error) { - // Fast path for missing scheme, as url.Parse splits by ":", which can - // cause the hostname to be considered the "scheme" when using "hostname:port". - if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" { - return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL) - } - uri, err := url.Parse(mirrorURL) - if err != nil { - return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL) - } - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri) - } - if uri.RawQuery != "" || uri.Fragment != "" { - return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri) - } - if uri.User != nil { - // strip password from output - uri.User = url.UserPassword(uri.User.Username(), "xxxxx") - return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri) - } - return strings.TrimSuffix(mirrorURL, "/") + "/", nil -} - -// ValidateIndexName validates an index name. It is used by the daemon to -// validate the daemon configuration. -func ValidateIndexName(val string) (string, error) { - val = normalizeIndexName(val) - if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { - return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val) - } - return val, nil -} - -func normalizeIndexName(val string) string { - // TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/". - // TODO: upstream this to check to reference package - if val == "index.docker.io" { - return "docker.io" - } - return val -} - -func hasScheme(reposName string) bool { - return strings.Contains(reposName, "://") -} - -func validateHostPort(s string) error { - // Split host and port, and in case s can not be split, assume host only - host, port, err := net.SplitHostPort(s) - if err != nil { - host = s - port = "" - } - // If match against the `host:port` pattern fails, - // it might be `IPv6:port`, which will be captured by net.ParseIP(host) - if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil { - return invalidParamf("invalid host %q", host) - } - if port != "" { - v, err := strconv.Atoi(port) - if err != nil { - return err - } - if v < 0 || v > 65535 { - return invalidParamf("invalid port %q", port) - } - } - return nil -} - -// newIndexInfo returns IndexInfo configuration from indexName -func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo { - indexName = normalizeIndexName(indexName) - - // Return any configured index info, first. - if index, ok := config.IndexConfigs[indexName]; ok { - return index - } - - // Construct a non-configured index info. - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: config.isSecureIndex(indexName), - } -} - -// GetAuthConfigKey special-cases using the full index address of the official -// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. -func GetAuthConfigKey(index *registry.IndexInfo) string { - if index.Official { - return IndexServer - } - return index.Name -} - -// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo -func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo { - index := newIndexInfo(config, reference.Domain(name)) - var officialRepo bool - if index.Official { - // RepositoryInfo.Official indicates whether the image repository - // is an official (docker library official images) repository. - // - // We only need to check this if the image-repository is on Docker Hub. - officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/') - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(name), - Index: index, - Official: officialRepo, - } -} - -// ParseRepositoryInfo performs the breakdown of a repository name into a -// [RepositoryInfo], but lacks registry configuration. -// -// It is used by the Docker cli to interact with registry-related endpoints. -func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { - indexName := normalizeIndexName(reference.Domain(reposName)) - if indexName == IndexName { - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, - Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'), - }, nil - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, - }, nil -} - -// isInsecure is used to detect whether a registry domain or IP-address is allowed -// to use an insecure (non-TLS, or self-signed cert) connection according to the -// defaults, which allows for insecure connections with registries running on a -// loopback address ("localhost", "::1/128", "127.0.0.0/8"). -// -// It is used in situations where we don't have access to the daemon's configuration, -// for example, when used from the client / CLI. -func isInsecure(hostNameOrIP string) bool { - // Attempt to strip port if present; this also strips brackets for - // IPv6 addresses with a port (e.g. "[::1]:5000"). - // - // This is best-effort; we'll continue using the address as-is if it fails. - if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { - hostNameOrIP = host - } - if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { - // Fast path; no need to resolve these, assuming nobody overrides - // "localhost" for anything else than a loopback address (sorry, not sorry). - return true - } - - var addresses []net.IP - if ip := net.ParseIP(hostNameOrIP); ip != nil { - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-addresses. - addrs, _ := lookupIP(hostNameOrIP) - addresses = append(addresses, addrs...) - } - - for _, addr := range addresses { - if addr.IsLoopback() { - return true - } - } - return false -} diff --git a/vendor/github.com/docker/docker/registry/config_unix.go b/vendor/github.com/docker/docker/registry/config_unix.go deleted file mode 100644 index 6aa6cdcca391..000000000000 --- a/vendor/github.com/docker/docker/registry/config_unix.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !windows - -package registry - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -const defaultCertsDir = "/etc/docker/certs.d" - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:/index.docker.io/v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return s -} diff --git a/vendor/github.com/docker/docker/registry/config_windows.go b/vendor/github.com/docker/docker/registry/config_windows.go deleted file mode 100644 index fd13bffde0f0..000000000000 --- a/vendor/github.com/docker/docker/registry/config_windows.go +++ /dev/null @@ -1,20 +0,0 @@ -package registry - -import ( - "os" - "path/filepath" - "strings" -) - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -var defaultCertsDir = os.Getenv("programdata") + `\docker\certs.d` - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:\index.docker.io\v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return filepath.FromSlash(strings.ReplaceAll(s, ":", "")) -} diff --git a/vendor/github.com/docker/docker/registry/errors.go b/vendor/github.com/docker/docker/registry/errors.go deleted file mode 100644 index cc3a37da6e92..000000000000 --- a/vendor/github.com/docker/docker/registry/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package registry - -import ( - "net/url" - - "github.com/docker/distribution/registry/api/errcode" - "github.com/pkg/errors" -) - -func translateV2AuthError(err error) error { - switch e := err.(type) { - case *url.Error: - switch e2 := e.Err.(type) { - case errcode.Error: - switch e2.Code { - case errcode.ErrorCodeUnauthorized: - return unauthorizedErr{err} - } - } - } - - return err -} - -func invalidParam(err error) error { - return invalidParameterErr{err} -} - -func invalidParamf(format string, args ...interface{}) error { - return invalidParameterErr{errors.Errorf(format, args...)} -} - -func invalidParamWrapf(err error, format string, args ...interface{}) error { - return invalidParameterErr{errors.Wrapf(err, format, args...)} -} - -type unauthorizedErr struct{ error } - -func (unauthorizedErr) Unauthorized() {} - -func (e unauthorizedErr) Cause() error { - return e.error -} - -func (e unauthorizedErr) Unwrap() error { - return e.error -} - -type invalidParameterErr struct{ error } - -func (invalidParameterErr) InvalidParameter() {} - -func (e invalidParameterErr) Unwrap() error { - return e.error -} - -type systemErr struct{ error } - -func (systemErr) System() {} - -func (e systemErr) Unwrap() error { - return e.error -} - -type errUnknown struct{ error } - -func (errUnknown) Unknown() {} - -func (e errUnknown) Unwrap() error { - return e.error -} diff --git a/vendor/github.com/docker/docker/registry/registry.go b/vendor/github.com/docker/docker/registry/registry.go deleted file mode 100644 index d3b3fbc9baef..000000000000 --- a/vendor/github.com/docker/docker/registry/registry.go +++ /dev/null @@ -1,154 +0,0 @@ -// Package registry contains client primitives to interact with a remote Docker registry. -package registry - -import ( - "context" - "crypto/tls" - "net" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/go-connections/tlsconfig" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" -) - -// HostCertsDir returns the config directory for a specific host. -// -// Deprecated: this function was only used internally, and will be removed in a future release. -func HostCertsDir(hostname string) string { - return hostCertsDir(hostname) -} - -// hostCertsDir returns the config directory for a specific host. -func hostCertsDir(hostname string) string { - return filepath.Join(CertsDir(), cleanPath(hostname)) -} - -// newTLSConfig constructs a client TLS configuration based on server defaults -func newTLSConfig(ctx context.Context, hostname string, isSecure bool) (*tls.Config, error) { - // PreferredServerCipherSuites should have no effect - tlsConfig := tlsconfig.ServerDefault() - tlsConfig.InsecureSkipVerify = !isSecure - - if isSecure { - hostDir := hostCertsDir(hostname) - log.G(ctx).Debugf("hostDir: %s", hostDir) - if err := loadTLSConfig(ctx, hostDir, tlsConfig); err != nil { - return nil, err - } - } - - return tlsConfig, nil -} - -func hasFile(files []os.DirEntry, name string) bool { - for _, f := range files { - if f.Name() == name { - return true - } - } - return false -} - -// ReadCertsDirectory reads the directory for TLS certificates -// including roots and certificate pairs and updates the -// provided TLS configuration. -func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error { - return loadTLSConfig(context.TODO(), directory, tlsConfig) -} - -// loadTLSConfig reads the directory for TLS certificates including roots and -// certificate pairs, and updates the provided TLS configuration. -func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) error { - fs, err := os.ReadDir(directory) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return invalidParam(err) - } - - for _, f := range fs { - if ctx.Err() != nil { - return ctx.Err() - } - switch filepath.Ext(f.Name()) { - case ".crt": - if tlsConfig.RootCAs == nil { - systemPool, err := tlsconfig.SystemCertPool() - if err != nil { - return invalidParamWrapf(err, "unable to get system cert pool") - } - tlsConfig.RootCAs = systemPool - } - fileName := filepath.Join(directory, f.Name()) - log.G(ctx).Debugf("crt: %s", fileName) - data, err := os.ReadFile(fileName) - if err != nil { - return err - } - tlsConfig.RootCAs.AppendCertsFromPEM(data) - case ".cert": - certName := f.Name() - keyName := certName[:len(certName)-5] + ".key" - log.G(ctx).Debugf("cert: %s", filepath.Join(directory, certName)) - if !hasFile(fs, keyName) { - return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName) - } - cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName)) - if err != nil { - return err - } - tlsConfig.Certificates = append(tlsConfig.Certificates, cert) - case ".key": - keyName := f.Name() - certName := keyName[:len(keyName)-4] + ".cert" - log.G(ctx).Debugf("key: %s", filepath.Join(directory, keyName)) - if !hasFile(fs, certName) { - return invalidParamf("missing client certificate %s for key %s", certName, keyName) - } - } - } - - return nil -} - -// Headers returns request modifiers with a User-Agent and metaHeaders -func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModifier { - modifiers := []transport.RequestModifier{} - if userAgent != "" { - modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{ - "User-Agent": []string{userAgent}, - })) - } - if metaHeaders != nil { - modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders)) - } - return modifiers -} - -// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the -// default TLS configuration. -func newTransport(tlsConfig *tls.Config) http.RoundTripper { - if tlsConfig == nil { - tlsConfig = tlsconfig.ServerDefault() - } - - return otelhttp.NewTransport( - &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: tlsConfig, - // TODO(dmcgowan): Call close idle connections when complete and use keep alive - DisableKeepAlives: true, - }, - ) -} diff --git a/vendor/github.com/docker/docker/registry/search.go b/vendor/github.com/docker/docker/registry/search.go deleted file mode 100644 index 26a14298ac6c..000000000000 --- a/vendor/github.com/docker/docker/registry/search.go +++ /dev/null @@ -1,170 +0,0 @@ -package registry - -import ( - "context" - "net/http" - "strconv" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -var acceptedSearchFilterTags = map[string]bool{ - "is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future. - "is-official": true, - "stars": true, -} - -// Search queries the public registry for repositories matching the specified -// search term and filters. -func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) { - if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil { - return nil, err - } - - isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false) - if err != nil { - return nil, err - } - - // "is-automated" is deprecated and filtering for `true` will yield no results. - if isAutomated { - return []registry.SearchResult{}, nil - } - - isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false) - if err != nil { - return nil, err - } - - hasStarFilter := 0 - if searchFilters.Contains("stars") { - hasStars := searchFilters.Get("stars") - for _, hasStar := range hasStars { - iHasStar, err := strconv.Atoi(hasStar) - if err != nil { - return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)} - } - if iHasStar > hasStarFilter { - hasStarFilter = iHasStar - } - } - } - - unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers) - if err != nil { - return nil, err - } - - filteredResults := []registry.SearchResult{} - for _, result := range unfilteredResult.Results { - if searchFilters.Contains("is-official") { - if isOfficial != result.IsOfficial { - continue - } - } - if searchFilters.Contains("stars") { - if result.StarCount < hasStarFilter { - continue - } - } - // "is-automated" is deprecated and the value in Docker Hub search - // results is untrustworthy. Force it to false so as to not mislead our - // clients. - result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated) - filteredResults = append(filteredResults, result) - } - - return filteredResults, nil -} - -func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) { - if hasScheme(term) { - return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term) - } - - indexName, remoteName := splitReposSearchTerm(term) - - // Search is a long-running operation, just lock s.config to avoid block others. - s.mu.RLock() - index := newIndexInfo(s.config, indexName) - s.mu.RUnlock() - if index.Official { - // If pull "library/foo", it's stored locally under "foo" - remoteName = strings.TrimPrefix(remoteName, "library/") - } - - endpoint, err := newV1Endpoint(ctx, index, headers) - if err != nil { - return nil, err - } - - var client *http.Client - if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { - creds := NewStaticCredentialStore(authConfig) - - // TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac) - modifiers := Headers(headers.Get("User-Agent"), nil) - v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{ - auth.RegistryScope{Name: "catalog", Actions: []string{"search"}}, - }) - if err != nil { - return nil, err - } - // Copy non transport http client features - v2Client.Timeout = endpoint.client.Timeout - v2Client.CheckRedirect = endpoint.client.CheckRedirect - v2Client.Jar = endpoint.client.Jar - - log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL) - client = v2Client - } else { - client = endpoint.client - if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil { - return nil, err - } - } - - return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit) -} - -// splitReposSearchTerm breaks a search term into an index name and remote name -func splitReposSearchTerm(reposName string) (string, string) { - nameParts := strings.SplitN(reposName, "/", 2) - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && - !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { - // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), - // use the default Docker Hub registry (docker.io) - return IndexName, reposName - } - return nameParts[0], nameParts[1] -} - -// ParseSearchIndexInfo will use repository name to get back an indexInfo. -// -// TODO(thaJeztah) this function is only used by the CLI, and used to get -// information of the registry (to provide credentials if needed). We should -// move this function (or equivalent) to the CLI, as it's doing too much just -// for that. -func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) { - indexName, _ := splitReposSearchTerm(reposName) - indexName = normalizeIndexName(indexName) - if indexName == IndexName { - return ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, nil - } - - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, nil -} diff --git a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go b/vendor/github.com/docker/docker/registry/search_endpoint_v1.go deleted file mode 100644 index 2ac3cee8296d..000000000000 --- a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go +++ /dev/null @@ -1,207 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "net/http" - "net/url" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types/registry" -) - -// v1PingResult contains the information returned when pinging a registry. It -// indicates whether the registry claims to be a standalone registry. -type v1PingResult struct { - // Standalone is set to true if the registry indicates it is a - // standalone registry in the X-Docker-Registry-Standalone - // header - Standalone bool `json:"standalone"` -} - -// v1Endpoint stores basic information about a V1 registry endpoint. -type v1Endpoint struct { - client *http.Client - URL *url.URL - IsSecure bool -} - -// newV1Endpoint parses the given address to return a registry endpoint. -// TODO: remove. This is only used by search. -func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) { - tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure) - if err != nil { - return nil, err - } - - endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers) - if err != nil { - return nil, err - } - - if endpoint.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fall back to http in case of error) - return endpoint, nil - } - - // Try HTTPS ping to registry - endpoint.URL.Scheme = "https" - if _, err := endpoint.ping(ctx); err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - if endpoint.IsSecure { - // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` - // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP. - return nil, invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) - } - - // registry is insecure and HTTPS failed, fallback to HTTP. - log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint) - endpoint.URL.Scheme = "http" - if _, err2 := endpoint.ping(ctx); err2 != nil { - return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) - } - } - - return endpoint, nil -} - -// trimV1Address trims the "v1" version suffix off the address and returns -// the trimmed address. It returns an error on "v2" endpoints. -func trimV1Address(address string) (string, error) { - trimmed := strings.TrimSuffix(address, "/") - if strings.HasSuffix(trimmed, "/v2") { - return "", invalidParamf("search is not supported on v2 endpoints: %s", address) - } - return strings.TrimSuffix(trimmed, "/v1"), nil -} - -func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) { - if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { - address = "https://" + address - } - - address, err := trimV1Address(address) - if err != nil { - return nil, err - } - - uri, err := url.Parse(address) - if err != nil { - return nil, invalidParam(err) - } - - // TODO(tiborvass): make sure a ConnectTimeout transport is used - tr := newTransport(tlsConfig) - - return &v1Endpoint{ - IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, - URL: uri, - client: httpClient(transport.NewTransport(tr, Headers("", headers)...)), - }, nil -} - -// Get the formatted URL for the root of this registry Endpoint -func (e *v1Endpoint) String() string { - return e.URL.String() + "/v1/" -} - -// ping returns a v1PingResult which indicates whether the registry is standalone or not. -func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) { - if e.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fallback to http in case of error) - return v1PingResult{}, nil - } - - pingURL := e.String() + "_ping" - log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody) - if err != nil { - return v1PingResult{}, invalidParam(err) - } - - resp, err := e.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return v1PingResult{}, err - } - return v1PingResult{}, invalidParam(err) - } - - defer resp.Body.Close() - - if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" { - info := v1PingResult{} - // Accepted values are "1", and "true" (case-insensitive). - if v == "1" || strings.EqualFold(v, "true") { - info.Standalone = true - } - log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone) - return info, nil - } - - // If the header is absent, we assume true for compatibility with earlier - // versions of the registry. default to true - info := v1PingResult{ - Standalone: true, - } - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - log.G(ctx).WithError(err).Debug("error unmarshaling _ping response") - // don't stop here. Just assume sane defaults - } - - log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone) - return info, nil -} - -// httpClient returns an HTTP client structure which uses the given transport -// and contains the necessary headers for redirected requests -func httpClient(transport http.RoundTripper) *http.Client { - return &http.Client{ - Transport: transport, - CheckRedirect: addRequiredHeadersToRedirectedRequests, - } -} - -func trustedLocation(req *http.Request) bool { - var ( - trusteds = []string{"docker.com", "docker.io"} - hostname = strings.SplitN(req.Host, ":", 2)[0] - ) - if req.URL.Scheme != "https" { - return false - } - - for _, trusted := range trusteds { - if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { - return true - } - } - return false -} - -// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers -// for redirected requests -func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { - if len(via) != 0 && via[0] != nil { - if trustedLocation(req) && trustedLocation(via[0]) { - req.Header = via[0].Header - return nil - } - for k, v := range via[0].Header { - if k != "Authorization" { - for _, vv := range v { - req.Header.Add(k, vv) - } - } - } - } - return nil -} diff --git a/vendor/github.com/docker/docker/registry/search_session.go b/vendor/github.com/docker/docker/registry/search_session.go deleted file mode 100644 index f2886b7d3859..000000000000 --- a/vendor/github.com/docker/docker/registry/search_session.go +++ /dev/null @@ -1,247 +0,0 @@ -package registry - -import ( - // this is required for some certificates - "context" - _ "crypto/sha512" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "net/url" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// A session is used to communicate with a V1 registry -type session struct { - indexEndpoint *v1Endpoint - client *http.Client -} - -type authTransport struct { - base http.RoundTripper - authConfig *registry.AuthConfig - - alwaysSetBasicAuth bool - token []string - - mu sync.Mutex // guards modReq - modReq map[*http.Request]*http.Request // original -> modified -} - -// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official) -// -// For private v1 registries, set alwaysSetBasicAuth to true. -// -// For the official v1 registry, if there isn't already an Authorization header in the request, -// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. -// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing -// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent -// requests. -// -// If the server sends a token without the client having requested it, it is ignored. -// -// This RoundTripper also has a CancelRequest method important for correct timeout handling. -func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport { - if base == nil { - base = http.DefaultTransport - } - return &authTransport{ - base: base, - authConfig: authConfig, - alwaysSetBasicAuth: alwaysSetBasicAuth, - modReq: make(map[*http.Request]*http.Request), - } -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header, len(r.Header)) - for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) - } - - return r2 -} - -// onEOFReader wraps an io.ReadCloser and a function -// the function will run at the end of file or close the file. -type onEOFReader struct { - Rc io.ReadCloser - Fn func() -} - -func (r *onEOFReader) Read(p []byte) (int, error) { - n, err := r.Rc.Read(p) - if err == io.EOF { - r.runFunc() - } - return n, err -} - -// Close closes the file and run the function. -func (r *onEOFReader) Close() error { - err := r.Rc.Close() - r.runFunc() - return err -} - -func (r *onEOFReader) runFunc() { - if fn := r.Fn; fn != nil { - fn() - r.Fn = nil - } -} - -// RoundTrip changes an HTTP request's headers to add the necessary -// authentication-related headers -func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { - // Authorization should not be set on 302 redirect for untrusted locations. - // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. - // As the authorization logic is currently implemented in RoundTrip, - // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. - // This is safe as Docker doesn't set Referrer in other scenarios. - if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { - return tr.base.RoundTrip(orig) - } - - req := cloneRequest(orig) - tr.mu.Lock() - tr.modReq[orig] = req - tr.mu.Unlock() - - if tr.alwaysSetBasicAuth { - if tr.authConfig == nil { - return nil, errors.New("unexpected error: empty auth config") - } - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - return tr.base.RoundTrip(req) - } - - // Don't override - if req.Header.Get("Authorization") == "" { - if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" { - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - } else if len(tr.token) > 0 { - req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) - } - } - resp, err := tr.base.RoundTrip(req) - if err != nil { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - return nil, err - } - if len(resp.Header["X-Docker-Token"]) > 0 { - tr.token = resp.Header["X-Docker-Token"] - } - resp.Body = &onEOFReader{ - Rc: resp.Body, - Fn: func() { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - }, - } - return resp, nil -} - -// CancelRequest cancels an in-flight request by closing its connection. -func (tr *authTransport) CancelRequest(req *http.Request) { - type canceler interface { - CancelRequest(*http.Request) - } - if cr, ok := tr.base.(canceler); ok { - tr.mu.Lock() - modReq := tr.modReq[req] - delete(tr.modReq, req) - tr.mu.Unlock() - cr.CancelRequest(modReq) - } -} - -func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error { - var alwaysSetBasicAuth bool - - // If we're working with a standalone private registry over HTTPS, send Basic Auth headers - // alongside all our requests. - if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { - info, err := endpoint.ping(ctx) - if err != nil { - return err - } - if info.Standalone && authConfig != nil { - log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth") - alwaysSetBasicAuth = true - } - } - - // Annotate the transport unconditionally so that v2 can - // properly fallback on v1 when an image is not found. - client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) - - jar, err := cookiejar.New(nil) - if err != nil { - return systemErr{errors.New("cookiejar.New is not supposed to return an error")} - } - client.Jar = jar - - return nil -} - -func newSession(client *http.Client, endpoint *v1Endpoint) *session { - return &session{ - client: client, - indexEndpoint: endpoint, - } -} - -// defaultSearchLimit is the default value for maximum number of returned search results. -const defaultSearchLimit = 25 - -// searchRepositories performs a search against the remote repository -func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) { - if limit == 0 { - limit = defaultSearchLimit - } - if limit < 1 || limit > 100 { - return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit) - } - u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) - log.G(ctx).WithField("url", u).Debug("searchRepositories") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) - if err != nil { - return nil, invalidParamWrapf(err, "error building request") - } - // Have the AuthTransport send authentication, when logged in. - req.Header.Set("X-Docker-Token", "true") - res, err := r.client.Do(req) - if err != nil { - return nil, systemErr{err} - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - // TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286). - // TODO(thaJeztah): handle other status-codes to return correct error-type - return nil, errUnknown{fmt.Errorf("Unexpected status code %d", res.StatusCode)} - } - result := ®istry.SearchResults{} - err = json.NewDecoder(res.Body).Decode(result) - if err != nil { - return nil, systemErr{errors.Wrap(err, "error decoding registry search results")} - } - return result, nil -} diff --git a/vendor/github.com/docker/docker/registry/service.go b/vendor/github.com/docker/docker/registry/service.go deleted file mode 100644 index 85299be32ec1..000000000000 --- a/vendor/github.com/docker/docker/registry/service.go +++ /dev/null @@ -1,174 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "errors" - "net/url" - "strings" - "sync" - - cerrdefs "github.com/containerd/errdefs" - "github.com/containerd/log" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" -) - -// Service is a registry service. It tracks configuration data such as a list -// of mirrors. -type Service struct { - config *serviceConfig - mu sync.RWMutex -} - -// NewService returns a new instance of [Service] ready to be installed into -// an engine. -func NewService(options ServiceOptions) (*Service, error) { - config, err := newServiceConfig(options) - if err != nil { - return nil, err - } - - return &Service{config: config}, err -} - -// ServiceConfig returns a copy of the public registry service's configuration. -func (s *Service) ServiceConfig() *registry.ServiceConfig { - s.mu.RLock() - defer s.mu.RUnlock() - return s.config.copy() -} - -// ReplaceConfig prepares a transaction which will atomically replace the -// registry service's configuration when the returned commit function is called. -func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) { - config, err := newServiceConfig(options) - if err != nil { - return nil, err - } - return func() { - s.mu.Lock() - defer s.mu.Unlock() - s.config = config - }, nil -} - -// Auth contacts the public registry with the provided credentials, -// and returns OK if authentication was successful. -// It can be used to verify the validity of a client's credentials. -func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) { - // TODO Use ctx when searching for repositories - registryHostName := IndexHostname - - if authConfig.ServerAddress != "" { - serverAddress := authConfig.ServerAddress - if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { - serverAddress = "https://" + serverAddress - } - u, err := url.Parse(serverAddress) - if err != nil { - return "", "", invalidParamWrapf(err, "unable to parse server address") - } - registryHostName = u.Host - } - - // Lookup endpoints for authentication but exclude mirrors to prevent - // sending credentials of the upstream registry to a mirror. - s.mu.RLock() - endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) - s.mu.RUnlock() - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return "", "", err - } - return "", "", invalidParam(err) - } - - var lastErr error - for _, endpoint := range endpoints { - authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) { - // Failed to authenticate; don't continue with (non-TLS) endpoints. - return "", "", err - } - // Try next endpoint - log.G(ctx).WithFields(log.Fields{ - "error": err, - "endpoint": endpoint, - }).Infof("Error logging in to endpoint, trying next endpoint") - lastErr = err - continue - } - - // TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here? - return "Login Succeeded", authToken, nil - } - - return "", "", lastErr -} - -// ResolveRepository splits a repository name into its components -// and configuration of the associated registry. -// -// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release. -func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { - s.mu.RLock() - defer s.mu.RUnlock() - // TODO(thaJeztah): remove error return as it's no longer used. - return newRepositoryInfo(s.config, name), nil -} - -// ResolveAuthConfig looks up authentication for the given reference from the -// given authConfigs. -// -// IMPORTANT: This function is for internal use and should not be used by external projects. -func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig { - s.mu.RLock() - defer s.mu.RUnlock() - // Simplified version of "newIndexInfo" without handling of insecure - // registries and mirrors, as we don't need that information to resolve - // the auth-config. - indexName := normalizeIndexName(reference.Domain(ref)) - registryInfo, ok := s.config.IndexConfigs[indexName] - if !ok { - registryInfo = ®istry.IndexInfo{Name: indexName} - } - return ResolveAuthConfig(authConfigs, registryInfo) -} - -// APIEndpoint represents a remote API endpoint -type APIEndpoint struct { - Mirror bool - URL *url.URL - AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. - Official bool // Deprecated: this field was only used internally, and will be removed in the next release. - TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release. - TLSConfig *tls.Config -} - -// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. -// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP. -func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.lookupV2Endpoints(context.TODO(), hostname, true) -} - -// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. -// It gives preference to HTTPS over plain HTTP. Mirrors are not included. -func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.lookupV2Endpoints(context.TODO(), hostname, false) -} - -// IsInsecureRegistry returns true if the registry at given host is configured as -// insecure registry. -func (s *Service) IsInsecureRegistry(host string) bool { - s.mu.RLock() - defer s.mu.RUnlock() - return !s.config.isSecureIndex(host) -} diff --git a/vendor/github.com/docker/docker/registry/service_v2.go b/vendor/github.com/docker/docker/registry/service_v2.go deleted file mode 100644 index 6b25a41dc327..000000000000 --- a/vendor/github.com/docker/docker/registry/service_v2.go +++ /dev/null @@ -1,74 +0,0 @@ -package registry - -import ( - "context" - "net/url" - "strings" - - "github.com/docker/go-connections/tlsconfig" -) - -func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) { - var endpoints []APIEndpoint - if hostname == DefaultNamespace || hostname == IndexHostname { - if includeMirrors { - for _, mirror := range s.config.Mirrors { - if ctx.Err() != nil { - return nil, ctx.Err() - } - if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { - mirror = "https://" + mirror - } - mirrorURL, err := url.Parse(mirror) - if err != nil { - return nil, invalidParam(err) - } - // TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time. - mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host)) - if err != nil { - return nil, err - } - endpoints = append(endpoints, APIEndpoint{ - URL: mirrorURL, - Mirror: true, - TLSConfig: mirrorTLSConfig, - }) - } - } - endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV2Registry, - Official: true, - TLSConfig: tlsconfig.ServerDefault(), - }) - - return endpoints, nil - } - - tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) - if err != nil { - return nil, err - } - - endpoints = []APIEndpoint{ - { - URL: &url.URL{ - Scheme: "https", - Host: hostname, - }, - TLSConfig: tlsConfig, - }, - } - - if tlsConfig.InsecureSkipVerify { - endpoints = append(endpoints, APIEndpoint{ - URL: &url.URL{ - Scheme: "http", - Host: hostname, - }, - // used to check if supposed to be secure via InsecureSkipVerify - TLSConfig: tlsConfig, - }) - } - - return endpoints, nil -} diff --git a/vendor/github.com/docker/docker/registry/types.go b/vendor/github.com/docker/docker/registry/types.go deleted file mode 100644 index bb081d5638a2..000000000000 --- a/vendor/github.com/docker/docker/registry/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package registry - -import ( - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" -) - -// RepositoryInfo describes a repository -type RepositoryInfo struct { - Name reference.Named - // Index points to registry information - Index *registry.IndexInfo - // Official indicates whether the repository is considered official. - // If the registry is official, and the normalized name does not - // contain a '/' (e.g. "foo"), then it is considered an official repo. - // - // Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead. - Official bool - // Class represents the class of the repository, such as "plugin" - // or "image". - // - // Deprecated: this field is no longer used, and will be removed in the next release. - Class string -} diff --git a/vendor/github.com/docker/go-connections/nat/nat.go b/vendor/github.com/docker/go-connections/nat/nat.go index 4049d780c54a..1ffe0355dc15 100644 --- a/vendor/github.com/docker/go-connections/nat/nat.go +++ b/vendor/github.com/docker/go-connections/nat/nat.go @@ -2,6 +2,7 @@ package nat import ( + "errors" "fmt" "net" "strconv" @@ -43,19 +44,19 @@ func NewPort(proto, port string) (Port, error) { // ParsePort parses the port number string and returns an int func ParsePort(rawPort string) (int, error) { - if len(rawPort) == 0 { + if rawPort == "" { return 0, nil } port, err := strconv.ParseUint(rawPort, 10, 16) if err != nil { - return 0, err + return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err)) } return int(port), nil } // ParsePortRangeToInt parses the port range string and returns start/end ints func ParsePortRangeToInt(rawPort string) (int, int, error) { - if len(rawPort) == 0 { + if rawPort == "" { return 0, 0, nil } start, end, err := ParsePortRange(rawPort) @@ -91,29 +92,31 @@ func (p Port) Range() (int, int, error) { return ParsePortRangeToInt(p.Port()) } -// SplitProtoPort splits a port in the format of proto/port -func SplitProtoPort(rawPort string) (string, string) { - parts := strings.Split(rawPort, "/") - l := len(parts) - if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 { +// SplitProtoPort splits a port(range) and protocol, formatted as "/[]" +// "/[]". It returns an empty string for both if +// no port(range) is provided. If a port(range) is provided, but no protocol, +// the default ("tcp") protocol is returned. +// +// SplitProtoPort does not validate or normalize the returned values. +func SplitProtoPort(rawPort string) (proto string, port string) { + port, proto, _ = strings.Cut(rawPort, "/") + if port == "" { return "", "" } - if l == 1 { - return "tcp", rawPort + if proto == "" { + proto = "tcp" } - if len(parts[1]) == 0 { - return "tcp", parts[0] - } - return parts[1], parts[0] + return proto, port } -func validateProto(proto string) bool { - for _, availableProto := range []string{"tcp", "udp", "sctp"} { - if availableProto == proto { - return true - } +func validateProto(proto string) error { + switch proto { + case "tcp", "udp", "sctp": + // All good + return nil + default: + return errors.New("invalid proto: " + proto) } - return false } // ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses @@ -123,22 +126,18 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, exposedPorts = make(map[Port]struct{}, len(ports)) bindings = make(map[Port][]PortBinding) ) - for _, rawPort := range ports { - portMappings, err := ParsePortSpec(rawPort) + for _, p := range ports { + portMappings, err := ParsePortSpec(p) if err != nil { return nil, nil, err } - for _, portMapping := range portMappings { - port := portMapping.Port - if _, exists := exposedPorts[port]; !exists { + for _, pm := range portMappings { + port := pm.Port + if _, ok := exposedPorts[port]; !ok { exposedPorts[port] = struct{}{} } - bslice, exists := bindings[port] - if !exists { - bslice = []PortBinding{} - } - bindings[port] = append(bslice, portMapping.Binding) + bindings[port] = append(bindings[port], pm.Binding) } } return exposedPorts, bindings, nil @@ -150,28 +149,34 @@ type PortMapping struct { Binding PortBinding } -func splitParts(rawport string) (string, string, string) { +func (p *PortMapping) String() string { + return net.JoinHostPort(p.Binding.HostIP, p.Binding.HostPort+":"+string(p.Port)) +} + +func splitParts(rawport string) (hostIP, hostPort, containerPort string) { parts := strings.Split(rawport, ":") - n := len(parts) - containerPort := parts[n-1] - switch n { + switch len(parts) { case 1: - return "", "", containerPort + return "", "", parts[0] case 2: - return "", parts[0], containerPort + return "", parts[0], parts[1] case 3: - return parts[0], parts[1], containerPort + return parts[0], parts[1], parts[2] default: - return strings.Join(parts[:n-2], ":"), parts[n-2], containerPort + n := len(parts) + return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1] } } // ParsePortSpec parses a port specification string into a slice of PortMappings func ParsePortSpec(rawPort string) ([]PortMapping, error) { - var proto string ip, hostPort, containerPort := splitParts(rawPort) - proto, containerPort = SplitProtoPort(containerPort) + proto, containerPort := SplitProtoPort(containerPort) + proto = strings.ToLower(proto) + if err := validateProto(proto); err != nil { + return nil, err + } if ip != "" && ip[0] == '[' { // Strip [] from IPV6 addresses @@ -182,7 +187,7 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { ip = rawIP } if ip != "" && net.ParseIP(ip) == nil { - return nil, fmt.Errorf("invalid IP address: %s", ip) + return nil, errors.New("invalid IP address: " + ip) } if containerPort == "" { return nil, fmt.Errorf("no port specified: %s", rawPort) @@ -190,51 +195,43 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { startPort, endPort, err := ParsePortRange(containerPort) if err != nil { - return nil, fmt.Errorf("invalid containerPort: %s", containerPort) + return nil, errors.New("invalid containerPort: " + containerPort) } - var startHostPort, endHostPort uint64 = 0, 0 - if len(hostPort) > 0 { + var startHostPort, endHostPort uint64 + if hostPort != "" { startHostPort, endHostPort, err = ParsePortRange(hostPort) if err != nil { - return nil, fmt.Errorf("invalid hostPort: %s", hostPort) + return nil, errors.New("invalid hostPort: " + hostPort) } - } - - if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { - // Allow host port range iff containerPort is not a range. - // In this case, use the host port range as the dynamic - // host port range to allocate into. - if endPort != startPort { - return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + if (endPort - startPort) != (endHostPort - startHostPort) { + // Allow host port range iff containerPort is not a range. + // In this case, use the host port range as the dynamic + // host port range to allocate into. + if endPort != startPort { + return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + } } } - if !validateProto(strings.ToLower(proto)) { - return nil, fmt.Errorf("invalid proto: %s", proto) - } - - ports := []PortMapping{} - for i := uint64(0); i <= (endPort - startPort); i++ { - containerPort = strconv.FormatUint(startPort+i, 10) - if len(hostPort) > 0 { - hostPort = strconv.FormatUint(startHostPort+i, 10) - } - // Set hostPort to a range only if there is a single container port - // and a dynamic host port. - if startPort == endPort && startHostPort != endHostPort { - hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10)) - } - port, err := NewPort(strings.ToLower(proto), containerPort) - if err != nil { - return nil, err - } + count := endPort - startPort + 1 + ports := make([]PortMapping, 0, count) - binding := PortBinding{ - HostIP: ip, - HostPort: hostPort, + for i := uint64(0); i < count; i++ { + cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto) + hPort := "" + if hostPort != "" { + hPort = strconv.FormatUint(startHostPort+i, 10) + // Set hostPort to a range only if there is a single container port + // and a dynamic host port. + if count == 1 && startHostPort != endHostPort { + hPort += "-" + strconv.FormatUint(endHostPort, 10) + } } - ports = append(ports, PortMapping{Port: port, Binding: binding}) + ports = append(ports, PortMapping{ + Port: cPort, + Binding: PortBinding{HostIP: ip, HostPort: hPort}, + }) } return ports, nil } diff --git a/vendor/github.com/docker/go-connections/nat/parse.go b/vendor/github.com/docker/go-connections/nat/parse.go index e4b53e8a3242..64affa2a904c 100644 --- a/vendor/github.com/docker/go-connections/nat/parse.go +++ b/vendor/github.com/docker/go-connections/nat/parse.go @@ -1,7 +1,7 @@ package nat import ( - "fmt" + "errors" "strconv" "strings" ) @@ -9,7 +9,7 @@ import ( // ParsePortRange parses and validates the specified string as a port-range (8000-9000) func ParsePortRange(ports string) (uint64, uint64, error) { if ports == "" { - return 0, 0, fmt.Errorf("empty string specified for ports") + return 0, 0, errors.New("empty string specified for ports") } if !strings.Contains(ports, "-") { start, err := strconv.ParseUint(ports, 10, 16) @@ -27,7 +27,7 @@ func ParsePortRange(ports string) (uint64, uint64, error) { return 0, 0, err } if end < start { - return 0, 0, fmt.Errorf("invalid range specified for port: %s", ports) + return 0, 0, errors.New("invalid range specified for port: " + ports) } return start, end, nil } diff --git a/vendor/github.com/docker/go-connections/sockets/README.md b/vendor/github.com/docker/go-connections/sockets/README.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/vendor/github.com/docker/go-connections/sockets/proxy.go b/vendor/github.com/docker/go-connections/sockets/proxy.go index c897cb02adea..f04980e40a5a 100644 --- a/vendor/github.com/docker/go-connections/sockets/proxy.go +++ b/vendor/github.com/docker/go-connections/sockets/proxy.go @@ -9,6 +9,8 @@ import ( // GetProxyEnv allows access to the uppercase and the lowercase forms of // proxy-related variables. See the Go specification for details on these // variables. https://golang.org/pkg/net/http/ +// +// Deprecated: this function was used as helper for [DialerFromEnvironment] and is no longer used. It will be removed in the next release. func GetProxyEnv(key string) string { proxyValue := os.Getenv(strings.ToUpper(key)) if proxyValue == "" { @@ -19,10 +21,11 @@ func GetProxyEnv(key string) string { // DialerFromEnvironment was previously used to configure a net.Dialer to route // connections through a SOCKS proxy. -// DEPRECATED: SOCKS proxies are now supported by configuring only +// +// Deprecated: SOCKS proxies are now supported by configuring only // http.Transport.Proxy, and no longer require changing http.Transport.Dial. -// Therefore, only sockets.ConfigureTransport() needs to be called, and any -// sockets.DialerFromEnvironment() calls can be dropped. +// Therefore, only [sockets.ConfigureTransport] needs to be called, and any +// [sockets.DialerFromEnvironment] calls can be dropped. func DialerFromEnvironment(direct *net.Dialer) (*net.Dialer, error) { return direct, nil } diff --git a/vendor/github.com/docker/go-connections/sockets/sockets.go b/vendor/github.com/docker/go-connections/sockets/sockets.go index b0eae239d2c5..6117297860db 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets.go @@ -2,13 +2,19 @@ package sockets import ( + "context" "errors" + "fmt" "net" "net/http" + "syscall" "time" ) -const defaultTimeout = 10 * time.Second +const ( + defaultTimeout = 10 * time.Second + maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) +) // ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system. var ErrProtocolNotAvailable = errors.New("protocol not available") @@ -35,3 +41,26 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error { } return nil } + +// DialPipe connects to a Windows named pipe. It is not supported on +// non-Windows platforms. +// +// Deprecated: use [github.com/Microsoft/go-winio.DialPipe] or [github.com/Microsoft/go-winio.DialPipeContext]. +func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { + return dialPipe(addr, timeout) +} + +func configureUnixTransport(tr *http.Transport, proto, addr string) error { + if len(addr) > maxUnixSocketPathSize { + return fmt.Errorf("unix socket path %q is too long", addr) + } + // No need for compression in local communications. + tr.DisableCompression = true + dialer := &net.Dialer{ + Timeout: defaultTimeout, + } + tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, proto, addr) + } + return nil +} diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go index 78a34a980d28..913d2f00dd2f 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go @@ -3,37 +3,16 @@ package sockets import ( - "context" - "fmt" "net" "net/http" "syscall" "time" ) -const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) - -func configureUnixTransport(tr *http.Transport, proto, addr string) error { - if len(addr) > maxUnixSocketPathSize { - return fmt.Errorf("unix socket path %q is too long", addr) - } - // No need for compression in local communications. - tr.DisableCompression = true - dialer := &net.Dialer{ - Timeout: defaultTimeout, - } - tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, proto, addr) - } - return nil -} - func configureNpipeTransport(tr *http.Transport, proto, addr string) error { return ErrProtocolNotAvailable } -// DialPipe connects to a Windows named pipe. -// This is not supported on other OSes. -func DialPipe(_ string, _ time.Duration) (net.Conn, error) { +func dialPipe(_ string, _ time.Duration) (net.Conn, error) { return nil, syscall.EAFNOSUPPORT } diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go index 7acafc5a2ad8..6d6beb3855c0 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go @@ -9,10 +9,6 @@ import ( "github.com/Microsoft/go-winio" ) -func configureUnixTransport(tr *http.Transport, proto, addr string) error { - return ErrProtocolNotAvailable -} - func configureNpipeTransport(tr *http.Transport, proto, addr string) error { // No need for compression in local communications. tr.DisableCompression = true @@ -22,7 +18,6 @@ func configureNpipeTransport(tr *http.Transport, proto, addr string) error { return nil } -// DialPipe connects to a Windows named pipe. -func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { +func dialPipe(addr string, timeout time.Duration) (net.Conn, error) { return winio.DialPipe(addr, &timeout) } diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket.go b/vendor/github.com/docker/go-connections/sockets/unix_socket.go index b9233521e49a..e736f71d38b1 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket.go @@ -1,5 +1,3 @@ -//go:build !windows - /* Package sockets is a simple unix domain socket wrapper. @@ -57,26 +55,6 @@ import ( // SockOption sets up socket file's creating option type SockOption func(string) error -// WithChown modifies the socket file's uid and gid -func WithChown(uid, gid int) SockOption { - return func(path string) error { - if err := os.Chown(path, uid, gid); err != nil { - return err - } - return nil - } -} - -// WithChmod modifies socket file's access mode. -func WithChmod(mask os.FileMode) SockOption { - return func(path string) error { - if err := os.Chmod(path, mask); err != nil { - return err - } - return nil - } -} - // NewUnixSocketWithOpts creates a unix socket with the specified options. // By default, socket permissions are 0000 (i.e.: no access for anyone); pass // WithChmod() and WithChown() to set the desired ownership and permissions. @@ -90,22 +68,7 @@ func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error return nil, err } - // net.Listen does not allow for permissions to be set. As a result, when - // specifying custom permissions ("WithChmod()"), there is a short time - // between creating the socket and applying the permissions, during which - // the socket permissions are Less restrictive than desired. - // - // To work around this limitation of net.Listen(), we temporarily set the - // umask to 0777, which forces the socket to be created with 000 permissions - // (i.e.: no access for anyone). After that, WithChmod() must be used to set - // the desired permissions. - // - // We don't use "defer" here, to reset the umask to its original value as soon - // as possible. Ideally we'd be able to detect if WithChmod() was passed as - // an option, and skip changing umask if default permissions are used. - origUmask := syscall.Umask(0o777) - l, err := net.Listen("unix", path) - syscall.Umask(origUmask) + l, err := listenUnix(path) if err != nil { return nil, err } @@ -119,8 +82,3 @@ func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error return l, nil } - -// NewUnixSocket creates a unix socket with the specified path and group. -func NewUnixSocket(path string, gid int) (net.Listener, error) { - return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) -} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go new file mode 100644 index 000000000000..a41a71654742 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go @@ -0,0 +1,54 @@ +//go:build !windows + +package sockets + +import ( + "net" + "os" + "syscall" +) + +// WithChown modifies the socket file's uid and gid +func WithChown(uid, gid int) SockOption { + return func(path string) error { + if err := os.Chown(path, uid, gid); err != nil { + return err + } + return nil + } +} + +// WithChmod modifies socket file's access mode. +func WithChmod(mask os.FileMode) SockOption { + return func(path string) error { + if err := os.Chmod(path, mask); err != nil { + return err + } + return nil + } +} + +// NewUnixSocket creates a unix socket with the specified path and group. +func NewUnixSocket(path string, gid int) (net.Listener, error) { + return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) +} + +func listenUnix(path string) (net.Listener, error) { + // net.Listen does not allow for permissions to be set. As a result, when + // specifying custom permissions ("WithChmod()"), there is a short time + // between creating the socket and applying the permissions, during which + // the socket permissions are Less restrictive than desired. + // + // To work around this limitation of net.Listen(), we temporarily set the + // umask to 0777, which forces the socket to be created with 000 permissions + // (i.e.: no access for anyone). After that, WithChmod() must be used to set + // the desired permissions. + // + // We don't use "defer" here, to reset the umask to its original value as soon + // as possible. Ideally we'd be able to detect if WithChmod() was passed as + // an option, and skip changing umask if default permissions are used. + origUmask := syscall.Umask(0o777) + l, err := net.Listen("unix", path) + syscall.Umask(origUmask) + return l, err +} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go new file mode 100644 index 000000000000..5ec29e059e78 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go @@ -0,0 +1,7 @@ +package sockets + +import "net" + +func listenUnix(path string) (net.Listener, error) { + return net.Listen("unix", path) +} diff --git a/vendor/github.com/docker/go-connections/tlsconfig/config.go b/vendor/github.com/docker/go-connections/tlsconfig/config.go index 606c98a38b51..8b0264f68b75 100644 --- a/vendor/github.com/docker/go-connections/tlsconfig/config.go +++ b/vendor/github.com/docker/go-connections/tlsconfig/config.go @@ -34,51 +34,37 @@ type Options struct { // the system pool will be used. ExclusiveRootPools bool MinVersion uint16 - // If Passphrase is set, it will be used to decrypt a TLS private key - // if the key is encrypted. - // - // Deprecated: Use of encrypted TLS private keys has been deprecated, and - // will be removed in a future release. Golang has deprecated support for - // legacy PEM encryption (as specified in RFC 1423), as it is insecure by - // design (see https://go-review.googlesource.com/c/go/+/264159). - Passphrase string -} - -// Extra (server-side) accepted CBC cipher suites - will phase out in the future -var acceptedCBCCiphers = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, } // DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls // options struct but wants to use a commonly accepted set of TLS cipher suites, with // known weak algorithms removed. -var DefaultServerAcceptedCiphers = append(clientCipherSuites, acceptedCBCCiphers...) +var DefaultServerAcceptedCiphers = defaultCipherSuites + +// defaultCipherSuites is shared by both client and server as the default set. +var defaultCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} // ServerDefault returns a secure-enough TLS configuration for the server TLS configuration. func ServerDefault(ops ...func(*tls.Config)) *tls.Config { - tlsConfig := &tls.Config{ - // Avoid fallback by default to SSL protocols < TLS1.2 - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - CipherSuites: DefaultServerAcceptedCiphers, - } - - for _, op := range ops { - op(tlsConfig) - } - - return tlsConfig + return defaultConfig(ops...) } // ClientDefault returns a secure-enough TLS configuration for the client TLS configuration. func ClientDefault(ops ...func(*tls.Config)) *tls.Config { + return defaultConfig(ops...) +} + +// defaultConfig is the default config used by both client and server TLS configuration. +func defaultConfig(ops ...func(*tls.Config)) *tls.Config { tlsConfig := &tls.Config{ - // Prefer TLS1.2 as the client minimum + // Avoid fallback by default to SSL protocols < TLS1.2 MinVersion: tls.VersionTLS12, - CipherSuites: clientCipherSuites, + CipherSuites: defaultCipherSuites, } for _, op := range ops { @@ -92,13 +78,13 @@ func ClientDefault(ops ...func(*tls.Config)) *tls.Config { func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { // If we should verify the server, we need to load a trusted ca var ( - certPool *x509.CertPool - err error + pool *x509.CertPool + err error ) if exclusivePool { - certPool = x509.NewCertPool() + pool = x509.NewCertPool() } else { - certPool, err = SystemCertPool() + pool, err = SystemCertPool() if err != nil { return nil, fmt.Errorf("failed to read system certificates: %v", err) } @@ -107,10 +93,10 @@ func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { if err != nil { return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err) } - if !certPool.AppendCertsFromPEM(pemData) { + if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) } - return certPool, nil + return pool, nil } // allTLSVersions lists all the TLS versions and is used by the code that validates @@ -144,34 +130,32 @@ func adjustMinVersion(options Options, config *tls.Config) error { return nil } -// IsErrEncryptedKey returns true if the 'err' is an error of incorrect -// password when trying to decrypt a TLS private key. +// errEncryptedKeyDeprecated is produced when we encounter an encrypted +// (password-protected) key. From https://go-review.googlesource.com/c/go/+/264159; // -// Deprecated: Use of encrypted TLS private keys has been deprecated, and -// will be removed in a future release. Golang has deprecated support for -// legacy PEM encryption (as specified in RFC 1423), as it is insecure by -// design (see https://go-review.googlesource.com/c/go/+/264159). -func IsErrEncryptedKey(err error) bool { - return errors.Is(err, x509.IncorrectPasswordError) -} +// > Legacy PEM encryption as specified in RFC 1423 is insecure by design. Since +// > it does not authenticate the ciphertext, it is vulnerable to padding oracle +// > attacks that can let an attacker recover the plaintext +// > +// > It's unfortunate that we don't implement PKCS#8 encryption so we can't +// > recommend an alternative but PEM encryption is so broken that it's worth +// > deprecating outright. +// +// Also see https://docs.docker.com/go/deprecated/ +var errEncryptedKeyDeprecated = errors.New("private key is encrypted; encrypted private keys are obsolete, and not supported") // getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format. -// If the private key is encrypted, 'passphrase' is used to decrypted the -// private key. -func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) { +// It returns an error if the file could not be decoded or was protected by +// a passphrase. +func getPrivateKey(keyBytes []byte) ([]byte, error) { // this section makes some small changes to code from notary/tuf/utils/x509.go pemBlock, _ := pem.Decode(keyBytes) if pemBlock == nil { return nil, fmt.Errorf("no valid private key found") } - var err error if x509.IsEncryptedPEMBlock(pemBlock) { //nolint:staticcheck // Ignore SA1019 (IsEncryptedPEMBlock is deprecated) - keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase)) //nolint:staticcheck // Ignore SA1019 (DecryptPEMBlock is deprecated) - if err != nil { - return nil, fmt.Errorf("private key is encrypted, but could not decrypt it: %w", err) - } - keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes}) + return nil, errEncryptedKeyDeprecated } return keyBytes, nil @@ -195,7 +179,7 @@ func getCert(options Options) ([]tls.Certificate, error) { return nil, err } - prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase) + prKeyBytes, err = getPrivateKey(prKeyBytes) if err != nil { return nil, err } @@ -210,7 +194,7 @@ func getCert(options Options) ([]tls.Certificate, error) { // Client returns a TLS configuration meant to be used by a client. func Client(options Options) (*tls.Config, error) { - tlsConfig := ClientDefault() + tlsConfig := defaultConfig() tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify if !options.InsecureSkipVerify && options.CAFile != "" { CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) @@ -235,7 +219,7 @@ func Client(options Options) (*tls.Config, error) { // Server returns a TLS configuration meant to be used by a server. func Server(options Options) (*tls.Config, error) { - tlsConfig := ServerDefault() + tlsConfig := defaultConfig() tlsConfig.ClientAuth = options.ClientAuth tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) if err != nil { diff --git a/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go b/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go deleted file mode 100644 index a82f9fa52e2e..000000000000 --- a/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. -package tlsconfig - -import ( - "crypto/tls" -) - -// Client TLS cipher suites (dropping CBC ciphers for client preferred suite set) -var clientCipherSuites = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, -} diff --git a/vendor/github.com/docker/go-events/README.md b/vendor/github.com/docker/go-events/README.md index 0acafc279a38..410f61cf746b 100644 --- a/vendor/github.com/docker/go-events/README.md +++ b/vendor/github.com/docker/go-events/README.md @@ -1,7 +1,8 @@ # Docker Events Package [![GoDoc](https://godoc.org/github.com/docker/go-events?status.svg)](https://godoc.org/github.com/docker/go-events) -[![Circle CI](https://circleci.com/gh/docker/go-events.svg?style=shield)](https://circleci.com/gh/docker/go-events) +[![ci](https://github.com/docker/go-events/actions/workflows/ci.yml/badge.svg)](https://github.com/docker/go-events/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/docker/go-events)](https://goreportcard.com/report/github.com/docker/go-events) The Docker `events` package implements a composable event distribution package for Go. diff --git a/vendor/github.com/docker/go-events/SECURITY.md b/vendor/github.com/docker/go-events/SECURITY.md new file mode 100644 index 000000000000..610eef2c9e62 --- /dev/null +++ b/vendor/github.com/docker/go-events/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policy + +The maintainers of the Docker Events package 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. diff --git a/vendor/github.com/docker/go-events/retry.go b/vendor/github.com/docker/go-events/retry.go index b7f0a5422528..db18f8b05123 100644 --- a/vendor/github.com/docker/go-events/retry.go +++ b/vendor/github.com/docker/go-events/retry.go @@ -151,7 +151,7 @@ func (b *Breaker) Proceed(event Event) time.Duration { return 0 } - return b.last.Add(b.backoff).Sub(time.Now()) + return time.Until(b.last.Add(b.backoff)) } // Success resets the breaker. diff --git a/vendor/github.com/docker/go-events/vendor.mod b/vendor/github.com/docker/go-events/vendor.mod new file mode 100644 index 000000000000..66a2cd9e5c6c --- /dev/null +++ b/vendor/github.com/docker/go-events/vendor.mod @@ -0,0 +1,5 @@ +module github.com/docker/go-events + +go 1.13 + +require github.com/sirupsen/logrus v1.9.3 diff --git a/vendor/github.com/docker/go-events/vendor.sum b/vendor/github.com/docker/go-events/vendor.sum new file mode 100644 index 000000000000..9243c28735dd --- /dev/null +++ b/vendor/github.com/docker/go-events/vendor.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/docker/go/LICENSE b/vendor/github.com/docker/go/LICENSE deleted file mode 100644 index 74487567632c..000000000000 --- a/vendor/github.com/docker/go/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/docker/go/canonical/json/decode.go b/vendor/github.com/docker/go/canonical/json/decode.go deleted file mode 100644 index 72b981c53595..000000000000 --- a/vendor/github.com/docker/go/canonical/json/decode.go +++ /dev/null @@ -1,1168 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Represents JSON data structure using native Go types: booleans, floats, -// strings, arrays, and maps. - -package json - -import ( - "bytes" - "encoding" - "encoding/base64" - "errors" - "fmt" - "reflect" - "runtime" - "strconv" - "unicode" - "unicode/utf16" - "unicode/utf8" -) - -// Unmarshal parses the JSON-encoded data and stores the result -// in the value pointed to by v. -// -// Unmarshal uses the inverse of the encodings that -// Marshal uses, allocating maps, slices, and pointers as necessary, -// with the following additional rules: -// -// To unmarshal JSON into a pointer, Unmarshal first handles the case of -// the JSON being the JSON literal null. In that case, Unmarshal sets -// the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into -// the value pointed at by the pointer. If the pointer is nil, Unmarshal -// allocates a new value for it to point to. -// -// To unmarshal JSON into a struct, Unmarshal matches incoming object -// keys to the keys used by Marshal (either the struct field name or its tag), -// preferring an exact match but also accepting a case-insensitive match. -// Unmarshal will only set exported fields of the struct. -// -// To unmarshal JSON into an interface value, -// Unmarshal stores one of these in the interface value: -// -// bool, for JSON booleans -// float64, for JSON numbers -// string, for JSON strings -// []interface{}, for JSON arrays -// map[string]interface{}, for JSON objects -// nil for JSON null -// -// To unmarshal a JSON array into a slice, Unmarshal resets the slice length -// to zero and then appends each element to the slice. -// As a special case, to unmarshal an empty JSON array into a slice, -// Unmarshal replaces the slice with a new empty slice. -// -// To unmarshal a JSON array into a Go array, Unmarshal decodes -// JSON array elements into corresponding Go array elements. -// If the Go array is smaller than the JSON array, -// the additional JSON array elements are discarded. -// If the JSON array is smaller than the Go array, -// the additional Go array elements are set to zero values. -// -// To unmarshal a JSON object into a string-keyed map, Unmarshal first -// establishes a map to use, If the map is nil, Unmarshal allocates a new map. -// Otherwise Unmarshal reuses the existing map, keeping existing entries. -// Unmarshal then stores key-value pairs from the JSON object into the map. -// -// If a JSON value is not appropriate for a given target type, -// or if a JSON number overflows the target type, Unmarshal -// skips that field and completes the unmarshaling as best it can. -// If no more serious errors are encountered, Unmarshal returns -// an UnmarshalTypeError describing the earliest such error. -// -// The JSON null value unmarshals into an interface, map, pointer, or slice -// by setting that Go value to nil. Because null is often used in JSON to mean -// ``not present,'' unmarshaling a JSON null into any other Go type has no effect -// on the value and produces no error. -// -// When unmarshaling quoted strings, invalid UTF-8 or -// invalid UTF-16 surrogate pairs are not treated as an error. -// Instead, they are replaced by the Unicode replacement -// character U+FFFD. -// -func Unmarshal(data []byte, v interface{}) error { - // Check for well-formedness. - // Avoids filling out half a data structure - // before discovering a JSON syntax error. - var d decodeState - err := checkValid(data, &d.scan) - if err != nil { - return err - } - - d.init(data) - return d.unmarshal(v) -} - -// Unmarshaler is the interface implemented by objects -// that can unmarshal a JSON description of themselves. -// The input can be assumed to be a valid encoding of -// a JSON value. UnmarshalJSON must copy the JSON data -// if it wishes to retain the data after returning. -type Unmarshaler interface { - UnmarshalJSON([]byte) error -} - -// An UnmarshalTypeError describes a JSON value that was -// not appropriate for a value of a specific Go type. -type UnmarshalTypeError struct { - Value string // description of JSON value - "bool", "array", "number -5" - Type reflect.Type // type of Go value it could not be assigned to - Offset int64 // error occurred after reading Offset bytes -} - -func (e *UnmarshalTypeError) Error() string { - return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() -} - -// An UnmarshalFieldError describes a JSON object key that -// led to an unexported (and therefore unwritable) struct field. -// (No longer used; kept for compatibility.) -type UnmarshalFieldError struct { - Key string - Type reflect.Type - Field reflect.StructField -} - -func (e *UnmarshalFieldError) Error() string { - return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() -} - -// An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. -// (The argument to Unmarshal must be a non-nil pointer.) -type InvalidUnmarshalError struct { - Type reflect.Type -} - -func (e *InvalidUnmarshalError) Error() string { - if e.Type == nil { - return "json: Unmarshal(nil)" - } - - if e.Type.Kind() != reflect.Ptr { - return "json: Unmarshal(non-pointer " + e.Type.String() + ")" - } - return "json: Unmarshal(nil " + e.Type.String() + ")" -} - -func (d *decodeState) unmarshal(v interface{}) (err error) { - defer func() { - if r := recover(); r != nil { - if _, ok := r.(runtime.Error); ok { - panic(r) - } - err = r.(error) - } - }() - - rv := reflect.ValueOf(v) - if rv.Kind() != reflect.Ptr || rv.IsNil() { - return &InvalidUnmarshalError{reflect.TypeOf(v)} - } - - d.scan.reset() - // We decode rv not rv.Elem because the Unmarshaler interface - // test must be applied at the top level of the value. - d.value(rv) - return d.savedError -} - -// A Number represents a JSON number literal. -type Number string - -// String returns the literal text of the number. -func (n Number) String() string { return string(n) } - -// Float64 returns the number as a float64. -func (n Number) Float64() (float64, error) { - return strconv.ParseFloat(string(n), 64) -} - -// Int64 returns the number as an int64. -func (n Number) Int64() (int64, error) { - return strconv.ParseInt(string(n), 10, 64) -} - -// isValidNumber reports whether s is a valid JSON number literal. -func isValidNumber(s string) bool { - // This function implements the JSON numbers grammar. - // See https://tools.ietf.org/html/rfc7159#section-6 - // and http://json.org/number.gif - - if s == "" { - return false - } - - // Optional - - if s[0] == '-' { - s = s[1:] - if s == "" { - return false - } - } - - // Digits - switch { - default: - return false - - case s[0] == '0': - s = s[1:] - - case '1' <= s[0] && s[0] <= '9': - s = s[1:] - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // . followed by 1 or more digits. - if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { - s = s[2:] - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // e or E followed by an optional - or + and - // 1 or more digits. - if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { - s = s[1:] - if s[0] == '+' || s[0] == '-' { - s = s[1:] - if s == "" { - return false - } - } - for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { - s = s[1:] - } - } - - // Make sure we are at the end. - return s == "" -} - -// decodeState represents the state while decoding a JSON value. -type decodeState struct { - data []byte - off int // read offset in data - scan scanner - nextscan scanner // for calls to nextValue - savedError error - useNumber bool - canonical bool -} - -// errPhase is used for errors that should not happen unless -// there is a bug in the JSON decoder or something is editing -// the data slice while the decoder executes. -var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") - -func (d *decodeState) init(data []byte) *decodeState { - d.data = data - d.off = 0 - d.savedError = nil - return d -} - -// error aborts the decoding by panicking with err. -func (d *decodeState) error(err error) { - panic(err) -} - -// saveError saves the first err it is called with, -// for reporting at the end of the unmarshal. -func (d *decodeState) saveError(err error) { - if d.savedError == nil { - d.savedError = err - } -} - -// next cuts off and returns the next full JSON value in d.data[d.off:]. -// The next value is known to be an object or array, not a literal. -func (d *decodeState) next() []byte { - c := d.data[d.off] - item, rest, err := nextValue(d.data[d.off:], &d.nextscan) - if err != nil { - d.error(err) - } - d.off = len(d.data) - len(rest) - - // Our scanner has seen the opening brace/bracket - // and thinks we're still in the middle of the object. - // invent a closing brace/bracket to get it out. - if c == '{' { - d.scan.step(&d.scan, '}') - } else { - d.scan.step(&d.scan, ']') - } - - return item -} - -// scanWhile processes bytes in d.data[d.off:] until it -// receives a scan code not equal to op. -// It updates d.off and returns the new scan code. -func (d *decodeState) scanWhile(op int) int { - var newOp int - for { - if d.off >= len(d.data) { - newOp = d.scan.eof() - d.off = len(d.data) + 1 // mark processed EOF with len+1 - } else { - c := d.data[d.off] - d.off++ - newOp = d.scan.step(&d.scan, c) - } - if newOp != op { - break - } - } - return newOp -} - -// value decodes a JSON value from d.data[d.off:] into the value. -// it updates d.off to point past the decoded value. -func (d *decodeState) value(v reflect.Value) { - if !v.IsValid() { - _, rest, err := nextValue(d.data[d.off:], &d.nextscan) - if err != nil { - d.error(err) - } - d.off = len(d.data) - len(rest) - - // d.scan thinks we're still at the beginning of the item. - // Feed in an empty string - the shortest, simplest value - - // so that it knows we got to the end of the value. - if d.scan.redo { - // rewind. - d.scan.redo = false - d.scan.step = stateBeginValue - } - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '"') - - n := len(d.scan.parseState) - if n > 0 && d.scan.parseState[n-1] == parseObjectKey { - // d.scan thinks we just read an object key; finish the object - d.scan.step(&d.scan, ':') - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '"') - d.scan.step(&d.scan, '}') - } - - return - } - - switch op := d.scanWhile(scanSkipSpace); op { - default: - d.error(errPhase) - - case scanBeginArray: - d.array(v) - - case scanBeginObject: - d.object(v) - - case scanBeginLiteral: - d.literal(v) - } -} - -type unquotedValue struct{} - -// valueQuoted is like value but decodes a -// quoted string literal or literal null into an interface value. -// If it finds anything other than a quoted string literal or null, -// valueQuoted returns unquotedValue{}. -func (d *decodeState) valueQuoted() interface{} { - switch op := d.scanWhile(scanSkipSpace); op { - default: - d.error(errPhase) - - case scanBeginArray: - d.array(reflect.Value{}) - - case scanBeginObject: - d.object(reflect.Value{}) - - case scanBeginLiteral: - switch v := d.literalInterface().(type) { - case nil, string: - return v - } - } - return unquotedValue{} -} - -// indirect walks down v allocating pointers as needed, -// until it gets to a non-pointer. -// if it encounters an Unmarshaler, indirect stops and returns that. -// if decodingNull is true, indirect stops at the last pointer so it can be set to nil. -func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { - // If v is a named type and is addressable, - // start with its address, so that if the type has pointer methods, - // we find them. - if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { - v = v.Addr() - } - for { - // Load value from interface, but only if the result will be - // usefully addressable. - if v.Kind() == reflect.Interface && !v.IsNil() { - e := v.Elem() - if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { - v = e - continue - } - } - - if v.Kind() != reflect.Ptr { - break - } - - if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { - break - } - if v.IsNil() { - v.Set(reflect.New(v.Type().Elem())) - } - if v.Type().NumMethod() > 0 { - if u, ok := v.Interface().(Unmarshaler); ok { - return u, nil, reflect.Value{} - } - if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { - return nil, u, reflect.Value{} - } - } - v = v.Elem() - } - return nil, nil, v -} - -// array consumes an array from d.data[d.off-1:], decoding into the value v. -// the first byte of the array ('[') has been read already. -func (d *decodeState) array(v reflect.Value) { - // Check for unmarshaler. - u, ut, pv := d.indirect(v, false) - if u != nil { - d.off-- - err := u.UnmarshalJSON(d.next()) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) - d.off-- - d.next() - return - } - - v = pv - - // Check type of target. - switch v.Kind() { - case reflect.Interface: - if v.NumMethod() == 0 { - // Decoding into nil interface? Switch to non-reflect code. - v.Set(reflect.ValueOf(d.arrayInterface())) - return - } - // Otherwise it's invalid. - fallthrough - default: - d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) - d.off-- - d.next() - return - case reflect.Array: - case reflect.Slice: - break - } - - i := 0 - for { - // Look ahead for ] - can only happen on first iteration. - op := d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - - // Back up so d.value can have the byte we just read. - d.off-- - d.scan.undo(op) - - // Get element of array, growing if necessary. - if v.Kind() == reflect.Slice { - // Grow slice if necessary - if i >= v.Cap() { - newcap := v.Cap() + v.Cap()/2 - if newcap < 4 { - newcap = 4 - } - newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) - reflect.Copy(newv, v) - v.Set(newv) - } - if i >= v.Len() { - v.SetLen(i + 1) - } - } - - if i < v.Len() { - // Decode into element. - d.value(v.Index(i)) - } else { - // Ran out of fixed array: skip. - d.value(reflect.Value{}) - } - i++ - - // Next token must be , or ]. - op = d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - if op != scanArrayValue { - d.error(errPhase) - } - } - - if i < v.Len() { - if v.Kind() == reflect.Array { - // Array. Zero the rest. - z := reflect.Zero(v.Type().Elem()) - for ; i < v.Len(); i++ { - v.Index(i).Set(z) - } - } else { - v.SetLen(i) - } - } - if i == 0 && v.Kind() == reflect.Slice { - v.Set(reflect.MakeSlice(v.Type(), 0, 0)) - } -} - -var nullLiteral = []byte("null") - -// object consumes an object from d.data[d.off-1:], decoding into the value v. -// the first byte ('{') of the object has been read already. -func (d *decodeState) object(v reflect.Value) { - // Check for unmarshaler. - u, ut, pv := d.indirect(v, false) - if u != nil { - d.off-- - err := u.UnmarshalJSON(d.next()) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - v = pv - - // Decoding into nil interface? Switch to non-reflect code. - if v.Kind() == reflect.Interface && v.NumMethod() == 0 { - v.Set(reflect.ValueOf(d.objectInterface())) - return - } - - // Check type of target: struct or map[string]T - switch v.Kind() { - case reflect.Map: - // map must have string kind - t := v.Type() - if t.Key().Kind() != reflect.String { - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - if v.IsNil() { - v.Set(reflect.MakeMap(t)) - } - case reflect.Struct: - - default: - d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) - d.off-- - d.next() // skip over { } in input - return - } - - var mapElem reflect.Value - - for { - // Read opening " of string key or closing }. - op := d.scanWhile(scanSkipSpace) - if op == scanEndObject { - // closing } - can only happen on first iteration. - break - } - if op != scanBeginLiteral { - d.error(errPhase) - } - - // Read key. - start := d.off - 1 - op = d.scanWhile(scanContinue) - item := d.data[start : d.off-1] - key, ok := unquoteBytes(item) - if !ok { - d.error(errPhase) - } - - // Figure out field corresponding to key. - var subv reflect.Value - destring := false // whether the value is wrapped in a string to be decoded first - - if v.Kind() == reflect.Map { - elemType := v.Type().Elem() - if !mapElem.IsValid() { - mapElem = reflect.New(elemType).Elem() - } else { - mapElem.Set(reflect.Zero(elemType)) - } - subv = mapElem - } else { - var f *field - fields := cachedTypeFields(v.Type(), false) - for i := range fields { - ff := &fields[i] - if bytes.Equal(ff.nameBytes, key) { - f = ff - break - } - if f == nil && ff.equalFold(ff.nameBytes, key) { - f = ff - } - } - if f != nil { - subv = v - destring = f.quoted - for _, i := range f.index { - if subv.Kind() == reflect.Ptr { - if subv.IsNil() { - subv.Set(reflect.New(subv.Type().Elem())) - } - subv = subv.Elem() - } - subv = subv.Field(i) - } - } - } - - // Read : before value. - if op == scanSkipSpace { - op = d.scanWhile(scanSkipSpace) - } - if op != scanObjectKey { - d.error(errPhase) - } - - // Read value. - if destring { - switch qv := d.valueQuoted().(type) { - case nil: - d.literalStore(nullLiteral, subv, false) - case string: - d.literalStore([]byte(qv), subv, true) - default: - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) - } - } else { - d.value(subv) - } - - // Write value back to map; - // if using struct, subv points into struct already. - if v.Kind() == reflect.Map { - kv := reflect.ValueOf(key).Convert(v.Type().Key()) - v.SetMapIndex(kv, subv) - } - - // Next token must be , or }. - op = d.scanWhile(scanSkipSpace) - if op == scanEndObject { - break - } - if op != scanObjectValue { - d.error(errPhase) - } - } -} - -// literal consumes a literal from d.data[d.off-1:], decoding into the value v. -// The first byte of the literal has been read already -// (that's how the caller knows it's a literal). -func (d *decodeState) literal(v reflect.Value) { - // All bytes inside literal return scanContinue op code. - start := d.off - 1 - op := d.scanWhile(scanContinue) - - // Scan read one byte too far; back up. - d.off-- - d.scan.undo(op) - - d.literalStore(d.data[start:d.off], v, false) -} - -// convertNumber converts the number literal s to a float64 or a Number -// depending on the setting of d.useNumber. -func (d *decodeState) convertNumber(s string) (interface{}, error) { - if d.useNumber { - return Number(s), nil - } - f, err := strconv.ParseFloat(s, 64) - if err != nil { - return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} - } - return f, nil -} - -var numberType = reflect.TypeOf(Number("")) - -// literalStore decodes a literal stored in item into v. -// -// fromQuoted indicates whether this literal came from unwrapping a -// string from the ",string" struct tag option. this is used only to -// produce more helpful error messages. -func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { - // Check for unmarshaler. - if len(item) == 0 { - //Empty string given - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - return - } - wantptr := item[0] == 'n' // null - u, ut, pv := d.indirect(v, wantptr) - if u != nil { - err := u.UnmarshalJSON(item) - if err != nil { - d.error(err) - } - return - } - if ut != nil { - if item[0] != '"' { - if fromQuoted { - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - } - return - } - s, ok := unquoteBytes(item) - if !ok { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - err := ut.UnmarshalText(s) - if err != nil { - d.error(err) - } - return - } - - v = pv - - switch c := item[0]; c { - case 'n': // null - switch v.Kind() { - case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: - v.Set(reflect.Zero(v.Type())) - // otherwise, ignore null for primitives/string - } - case 't', 'f': // true, false - value := c == 't' - switch v.Kind() { - default: - if fromQuoted { - d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) - } - case reflect.Bool: - v.SetBool(value) - case reflect.Interface: - if v.NumMethod() == 0 { - v.Set(reflect.ValueOf(value)) - } else { - d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) - } - } - - case '"': // string - s, ok := unquoteBytes(item) - if !ok { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - switch v.Kind() { - default: - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - case reflect.Slice: - if v.Type().Elem().Kind() != reflect.Uint8 { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - break - } - b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) - n, err := base64.StdEncoding.Decode(b, s) - if err != nil { - d.saveError(err) - break - } - v.SetBytes(b[:n]) - case reflect.String: - v.SetString(string(s)) - case reflect.Interface: - if v.NumMethod() == 0 { - v.Set(reflect.ValueOf(string(s))) - } else { - d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) - } - } - - default: // number - if c != '-' && (c < '0' || c > '9') { - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(errPhase) - } - } - s := string(item) - switch v.Kind() { - default: - if v.Kind() == reflect.String && v.Type() == numberType { - v.SetString(s) - if !isValidNumber(s) { - d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) - } - break - } - if fromQuoted { - d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) - } else { - d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) - } - case reflect.Interface: - n, err := d.convertNumber(s) - if err != nil { - d.saveError(err) - break - } - if v.NumMethod() != 0 { - d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) - break - } - v.Set(reflect.ValueOf(n)) - - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - n, err := strconv.ParseInt(s, 10, 64) - if err != nil || v.OverflowInt(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetInt(n) - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - n, err := strconv.ParseUint(s, 10, 64) - if err != nil || v.OverflowUint(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetUint(n) - - case reflect.Float32, reflect.Float64: - n, err := strconv.ParseFloat(s, v.Type().Bits()) - if err != nil || v.OverflowFloat(n) { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) - break - } - v.SetFloat(n) - } - } -} - -// The xxxInterface routines build up a value to be stored -// in an empty interface. They are not strictly necessary, -// but they avoid the weight of reflection in this common case. - -// valueInterface is like value but returns interface{} -func (d *decodeState) valueInterface() interface{} { - switch d.scanWhile(scanSkipSpace) { - default: - d.error(errPhase) - panic("unreachable") - case scanBeginArray: - return d.arrayInterface() - case scanBeginObject: - return d.objectInterface() - case scanBeginLiteral: - return d.literalInterface() - } -} - -// arrayInterface is like array but returns []interface{}. -func (d *decodeState) arrayInterface() []interface{} { - var v = make([]interface{}, 0) - for { - // Look ahead for ] - can only happen on first iteration. - op := d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - - // Back up so d.value can have the byte we just read. - d.off-- - d.scan.undo(op) - - v = append(v, d.valueInterface()) - - // Next token must be , or ]. - op = d.scanWhile(scanSkipSpace) - if op == scanEndArray { - break - } - if op != scanArrayValue { - d.error(errPhase) - } - } - return v -} - -// objectInterface is like object but returns map[string]interface{}. -func (d *decodeState) objectInterface() map[string]interface{} { - m := make(map[string]interface{}) - for { - // Read opening " of string key or closing }. - op := d.scanWhile(scanSkipSpace) - if op == scanEndObject { - // closing } - can only happen on first iteration. - break - } - if op != scanBeginLiteral { - d.error(errPhase) - } - - // Read string key. - start := d.off - 1 - op = d.scanWhile(scanContinue) - item := d.data[start : d.off-1] - key, ok := unquote(item) - if !ok { - d.error(errPhase) - } - - // Read : before value. - if op == scanSkipSpace { - op = d.scanWhile(scanSkipSpace) - } - if op != scanObjectKey { - d.error(errPhase) - } - - // Read value. - m[key] = d.valueInterface() - - // Next token must be , or }. - op = d.scanWhile(scanSkipSpace) - if op == scanEndObject { - break - } - if op != scanObjectValue { - d.error(errPhase) - } - } - return m -} - -// literalInterface is like literal but returns an interface value. -func (d *decodeState) literalInterface() interface{} { - // All bytes inside literal return scanContinue op code. - start := d.off - 1 - op := d.scanWhile(scanContinue) - - // Scan read one byte too far; back up. - d.off-- - d.scan.undo(op) - item := d.data[start:d.off] - - switch c := item[0]; c { - case 'n': // null - return nil - - case 't', 'f': // true, false - return c == 't' - - case '"': // string - s, ok := unquote(item) - if !ok { - d.error(errPhase) - } - return s - - default: // number - if c != '-' && (c < '0' || c > '9') { - d.error(errPhase) - } - n, err := d.convertNumber(string(item)) - if err != nil { - d.saveError(err) - } - return n - } -} - -// getu4 decodes \uXXXX from the beginning of s, returning the hex value, -// or it returns -1. -func getu4(s []byte) rune { - if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { - return -1 - } - r, err := strconv.ParseUint(string(s[2:6]), 16, 64) - if err != nil { - return -1 - } - return rune(r) -} - -// unquote converts a quoted JSON string literal s into an actual string t. -// The rules are different than for Go, so cannot use strconv.Unquote. -func unquote(s []byte) (t string, ok bool) { - s, ok = unquoteBytes(s) - t = string(s) - return -} - -func unquoteBytes(s []byte) (t []byte, ok bool) { - if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { - return - } - s = s[1 : len(s)-1] - - // Check for unusual characters. If there are none, - // then no unquoting is needed, so return a slice of the - // original bytes. - r := 0 - for r < len(s) { - c := s[r] - if c == '\\' || c == '"' || c < ' ' { - break - } - if c < utf8.RuneSelf { - r++ - continue - } - rr, size := utf8.DecodeRune(s[r:]) - if rr == utf8.RuneError && size == 1 { - break - } - r += size - } - if r == len(s) { - return s, true - } - - b := make([]byte, len(s)+2*utf8.UTFMax) - w := copy(b, s[0:r]) - for r < len(s) { - // Out of room? Can only happen if s is full of - // malformed UTF-8 and we're replacing each - // byte with RuneError. - if w >= len(b)-2*utf8.UTFMax { - nb := make([]byte, (len(b)+utf8.UTFMax)*2) - copy(nb, b[0:w]) - b = nb - } - switch c := s[r]; { - case c == '\\': - r++ - if r >= len(s) { - return - } - switch s[r] { - default: - return - case '"', '\\', '/', '\'': - b[w] = s[r] - r++ - w++ - case 'b': - b[w] = '\b' - r++ - w++ - case 'f': - b[w] = '\f' - r++ - w++ - case 'n': - b[w] = '\n' - r++ - w++ - case 'r': - b[w] = '\r' - r++ - w++ - case 't': - b[w] = '\t' - r++ - w++ - case 'u': - r-- - rr := getu4(s[r:]) - if rr < 0 { - return - } - r += 6 - if utf16.IsSurrogate(rr) { - rr1 := getu4(s[r:]) - if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { - // A valid pair; consume. - r += 6 - w += utf8.EncodeRune(b[w:], dec) - break - } - // Invalid surrogate; fall back to replacement rune. - rr = unicode.ReplacementChar - } - w += utf8.EncodeRune(b[w:], rr) - } - - // Quote, control characters are invalid. - case c == '"', c < ' ': - return - - // ASCII - case c < utf8.RuneSelf: - b[w] = c - r++ - w++ - - // Coerce to well-formed UTF-8. - default: - rr, size := utf8.DecodeRune(s[r:]) - r += size - w += utf8.EncodeRune(b[w:], rr) - } - } - return b[0:w], true -} diff --git a/vendor/github.com/docker/go/canonical/json/encode.go b/vendor/github.com/docker/go/canonical/json/encode.go deleted file mode 100644 index f3491b160363..000000000000 --- a/vendor/github.com/docker/go/canonical/json/encode.go +++ /dev/null @@ -1,1250 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package json implements encoding and decoding of JSON objects as defined in -// RFC 4627. The mapping between JSON objects and Go values is described -// in the documentation for the Marshal and Unmarshal functions. -// -// See "JSON and Go" for an introduction to this package: -// https://golang.org/doc/articles/json_and_go.html -package json - -import ( - "bytes" - "encoding" - "encoding/base64" - "fmt" - "math" - "reflect" - "runtime" - "sort" - "strconv" - "strings" - "sync" - "unicode" - "unicode/utf8" -) - -// Marshal returns the JSON encoding of v. -// -// Marshal traverses the value v recursively. -// If an encountered value implements the Marshaler interface -// and is not a nil pointer, Marshal calls its MarshalJSON method -// to produce JSON. If no MarshalJSON method is present but the -// value implements encoding.TextMarshaler instead, Marshal calls -// its MarshalText method. -// The nil pointer exception is not strictly necessary -// but mimics a similar, necessary exception in the behavior of -// UnmarshalJSON. -// -// Otherwise, Marshal uses the following type-dependent default encodings: -// -// Boolean values encode as JSON booleans. -// -// Floating point, integer, and Number values encode as JSON numbers. -// -// String values encode as JSON strings coerced to valid UTF-8, -// replacing invalid bytes with the Unicode replacement rune. -// The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" -// to keep some browsers from misinterpreting JSON output as HTML. -// Ampersand "&" is also escaped to "\u0026" for the same reason. -// -// Array and slice values encode as JSON arrays, except that -// []byte encodes as a base64-encoded string, and a nil slice -// encodes as the null JSON object. -// -// Struct values encode as JSON objects. Each exported struct field -// becomes a member of the object unless -// - the field's tag is "-", or -// - the field is empty and its tag specifies the "omitempty" option. -// The empty values are false, 0, any -// nil pointer or interface value, and any array, slice, map, or string of -// length zero. The object's default key string is the struct field name -// but can be specified in the struct field's tag value. The "json" key in -// the struct field's tag value is the key name, followed by an optional comma -// and options. Examples: -// -// // Field is ignored by this package. -// Field int `json:"-"` -// -// // Field appears in JSON as key "myName". -// Field int `json:"myName"` -// -// // Field appears in JSON as key "myName" and -// // the field is omitted from the object if its value is empty, -// // as defined above. -// Field int `json:"myName,omitempty"` -// -// // Field appears in JSON as key "Field" (the default), but -// // the field is skipped if empty. -// // Note the leading comma. -// Field int `json:",omitempty"` -// -// The "string" option signals that a field is stored as JSON inside a -// JSON-encoded string. It applies only to fields of string, floating point, -// integer, or boolean types. This extra level of encoding is sometimes used -// when communicating with JavaScript programs: -// -// Int64String int64 `json:",string"` -// -// The key name will be used if it's a non-empty string consisting of -// only Unicode letters, digits, dollar signs, percent signs, hyphens, -// underscores and slashes. -// -// Anonymous struct fields are usually marshaled as if their inner exported fields -// were fields in the outer struct, subject to the usual Go visibility rules amended -// as described in the next paragraph. -// An anonymous struct field with a name given in its JSON tag is treated as -// having that name, rather than being anonymous. -// An anonymous struct field of interface type is treated the same as having -// that type as its name, rather than being anonymous. -// -// The Go visibility rules for struct fields are amended for JSON when -// deciding which field to marshal or unmarshal. If there are -// multiple fields at the same level, and that level is the least -// nested (and would therefore be the nesting level selected by the -// usual Go rules), the following extra rules apply: -// -// 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, -// even if there are multiple untagged fields that would otherwise conflict. -// 2) If there is exactly one field (tagged or not according to the first rule), that is selected. -// 3) Otherwise there are multiple fields, and all are ignored; no error occurs. -// -// Handling of anonymous struct fields is new in Go 1.1. -// Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of -// an anonymous struct field in both current and earlier versions, give the field -// a JSON tag of "-". -// -// Map values encode as JSON objects. -// The map's key type must be string; the map keys are used as JSON object -// keys, subject to the UTF-8 coercion described for string values above. -// -// Pointer values encode as the value pointed to. -// A nil pointer encodes as the null JSON object. -// -// Interface values encode as the value contained in the interface. -// A nil interface value encodes as the null JSON object. -// -// Channel, complex, and function values cannot be encoded in JSON. -// Attempting to encode such a value causes Marshal to return -// an UnsupportedTypeError. -// -// JSON cannot represent cyclic data structures and Marshal does not -// handle them. Passing cyclic structures to Marshal will result in -// an infinite recursion. -// -func Marshal(v interface{}) ([]byte, error) { - return marshal(v, false) -} - -// MarshalIndent is like Marshal but applies Indent to format the output. -func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { - b, err := Marshal(v) - if err != nil { - return nil, err - } - var buf bytes.Buffer - err = Indent(&buf, b, prefix, indent) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -// MarshalCanonical is like Marshal but encodes into Canonical JSON. -// Read more at: http://wiki.laptop.org/go/Canonical_JSON -func MarshalCanonical(v interface{}) ([]byte, error) { - return marshal(v, true) -} - -func marshal(v interface{}, canonical bool) ([]byte, error) { - e := &encodeState{canonical: canonical} - err := e.marshal(v) - if err != nil { - return nil, err - } - return e.Bytes(), nil -} - -// HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 -// characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 -// so that the JSON will be safe to embed inside HTML