Compare commits
293 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecc694264d | |||
| c475c696cf | |||
| 7494d2cee4 | |||
| 5306df36fa | |||
| 5dbaa52325 | |||
| dd832b6d97 | |||
| a99e91cc89 | |||
| 579b72aa06 | |||
| 6fc5891770 | |||
| 9af6cbc489 | |||
| ba53322412 | |||
| 61d2e8aaad | |||
| 6abcd4a2a1 | |||
| 00c129b974 | |||
| fbf5c8a86d | |||
| 4c7d52534f | |||
| 70123c523c | |||
| 4942b2747f | |||
| d8bab71747 | |||
| 36d9523e31 | |||
| 7063a0ef07 | |||
| 22487ad6f6 | |||
| 4737ed4906 | |||
| 8cff0087da | |||
| e8b22ce03c | |||
| e180ab8ab8 | |||
| 0d799c556f | |||
| 887030fbe8 | |||
| 9c6a0e0ba9 | |||
| f784471104 | |||
| d7afcf9b98 | |||
| 9d9adf6346 | |||
| d4b7734f18 | |||
| a1061611fd | |||
| 5e42f826b4 | |||
| 645c23bd13 | |||
| e491078fc6 | |||
| a3ffb8a148 | |||
| a4ae5f2f7a | |||
| 394991e2ab | |||
| e5bce5cd2d | |||
| d5c181abf4 | |||
| 0351ece9e5 | |||
| ec00b85794 | |||
| a69c591c5a | |||
| f9d2820a20 | |||
| c28ec0e4ce | |||
| d1c8336395 | |||
| 61b9fd4068 | |||
| 2ef1b4eabe | |||
| bea31fccbb | |||
| e9c189e1c2 | |||
| 118548d02b | |||
| 026ef0df2d | |||
| 4665091715 | |||
| 261d8bcf8d | |||
| 3755161455 | |||
| db5a0ae673 | |||
| 843153da37 | |||
| a2d7989230 | |||
| 985cee2de0 | |||
| e1dd0e1501 | |||
| 82ff4b5634 | |||
| 4de56bc72f | |||
| 2ed0d99acc | |||
| cbeddb1390 | |||
| 1fb1577626 | |||
| 67885d0dcc | |||
| 2ed42a8ade | |||
| 4bac500fb2 | |||
| 9d9f632527 | |||
| a2e17eb9d5 | |||
| 5d201ca436 | |||
| d1122a2293 | |||
| d52de77ef4 | |||
| 01f8949484 | |||
| b9e3346b1e | |||
| 25421ace0b | |||
| ede7019b14 | |||
| d48256b26d | |||
| 09fcf8e3dd | |||
| 56f7bd0759 | |||
| 3bacc99580 | |||
| f77defa891 | |||
| 4a677b86a6 | |||
| 1cf78c49ab | |||
| 2fb1298416 | |||
| a0ebf3e35c | |||
| 428518a84a | |||
| c1914f73f5 | |||
| cab5014d57 | |||
| 7c3ec3ec02 | |||
| aa976192cb | |||
| 2de1ea9769 | |||
| 0ac676b171 | |||
| b627f18262 | |||
| d8eb465f86 | |||
| a83e3df40b | |||
| ff5ea757b4 | |||
| c1d14aef3c | |||
| 18adfd54a9 | |||
| 6e20b9d93c | |||
| 3cadbd82c5 | |||
| b3df92053d | |||
| b89b069082 | |||
| 9deb5e9cd3 | |||
| e8be20bc9c | |||
| f2cd1979c0 | |||
| 991d942cc3 | |||
| 8e49313c0c | |||
| 6cd42da462 | |||
| 2b9489d827 | |||
| fbeae8516b | |||
| d593e61275 | |||
| 139968dff2 | |||
| 925db59377 | |||
| aced30d906 | |||
| b9e15efde0 | |||
| 4108febfae | |||
| 8cfe1f712e | |||
| 7c34fd56d0 | |||
| 0a2eaa4d05 | |||
| d26f1fd6d3 | |||
| 4f6182a50c | |||
| 4caa99468d | |||
| c2117f956f | |||
| 19b86ef9e2 | |||
| 88d6197d4c | |||
| bda15c766a | |||
| e636ed2ae5 | |||
| 19b7dfa1cf | |||
| 91de2a6dae | |||
| 7c6f58affe | |||
| 8c6a7fcbc9 | |||
| 3e87f59859 | |||
| a621313658 | |||
| 69dd67fc06 | |||
| b90c5a8fcb | |||
| 99676e6c9c | |||
| 524fd156f5 | |||
| 07bcb9cca2 | |||
| 48ea3fa14f | |||
| a27a86289e | |||
| e112eeea4d | |||
| 3b68cde77a | |||
| c712d97172 | |||
| f385cb994f | |||
| 44c1d5775d | |||
| 1468bb1962 | |||
| 44fb305591 | |||
| b74574cd53 | |||
| e60b20f08e | |||
| 6f7865f89a | |||
| 23518a47dd | |||
| eb120d2c28 | |||
| 146f6492c7 | |||
| 8def226e53 | |||
| 82c5138c8e | |||
| bd3e420cc7 | |||
| bb2ed910bb | |||
| 1415080cfa | |||
| 0bbdda88e6 | |||
| 4133271530 | |||
| 0e17907266 | |||
| b0b7dcfe86 | |||
| 531ea22e7d | |||
| e8cf8e65f7 | |||
| 22c68c5366 | |||
| e424c50373 | |||
| 399861e7d2 | |||
| 694e92aa10 | |||
| 09a36f2ef1 | |||
| 73fbe6a020 | |||
| 4480024a80 | |||
| b4f2c0ca3d | |||
| 85a5e3da36 | |||
| 34a75fb6f4 | |||
| b52ab17a14 | |||
| ec89d0ba67 | |||
| aa44c436d7 | |||
| 39e1213615 | |||
| 5184f2ed65 | |||
| cb42a72704 | |||
| 5ef48d620d | |||
| 9e08776681 | |||
| 85512e35cd | |||
| ce242b0ee8 | |||
| ae3ee9b47b | |||
| 733762af0a | |||
| 46862878f7 | |||
| 4a2b024c57 | |||
| 82de0590b6 | |||
| 3e36fa624a | |||
| 01ea3a7da7 | |||
| 0f2721ee4f | |||
| 6fc62cf5b6 | |||
| c580c9741e | |||
| ba8b22e783 | |||
| def5bfc11c | |||
| 9d258edf27 | |||
| f68a9a06fe | |||
| 60caaa39e0 | |||
| c94245da66 | |||
| 79c03dcaaf | |||
| acb019a344 | |||
| 5a80c3e98d | |||
| e250694a35 | |||
| 942eade165 | |||
| a746a99d97 | |||
| 66c3dbdfed | |||
| cf44e3164b | |||
| ce3196c726 | |||
| 80884714de | |||
| 4e00c31c71 | |||
| 7126bf7d22 | |||
| 53ed958805 | |||
| be307b2925 | |||
| 8f8d39c35e | |||
| c61c0cdb3c | |||
| 830b5f1d2f | |||
| abfc1f4c76 | |||
| 31fa40cb25 | |||
| 4881921784 | |||
| 4313a2bbb7 | |||
| 20c34ef13b | |||
| 34a90bdd81 | |||
| 98b82d5156 | |||
| 0e474e38f1 | |||
| 6adde56036 | |||
| 7ccd6d29c6 | |||
| 14f297204f | |||
| d1617cb0c0 | |||
| 7091e8bea4 | |||
| ff42ff9f06 | |||
| bcc479b4c3 | |||
| 6a596c007c | |||
| 79e0b1d7d9 | |||
| 099820d8ca | |||
| ac88b74462 | |||
| 7f699066bd | |||
| fe1d790d8e | |||
| 5e29918a44 | |||
| 09efe3f408 | |||
| 79dd3a3c79 | |||
| a8fe4aaa7f | |||
| 034dc932d7 | |||
| 0718529a7e | |||
| c754ac3c5b | |||
| 5d327e8032 | |||
| bce66f6126 | |||
| 511821052a | |||
| 9fc26d43ed | |||
| 2b4e0a0f45 | |||
| 5d6e64c4a6 | |||
| ed52ada4a3 | |||
| 60d16e20ac | |||
| 713ed839fe | |||
| 6bfee62d6d | |||
| efdf008933 | |||
| 28ffe2416d | |||
| b0bb4acc11 | |||
| 2e5a36728b | |||
| a5a17eb2c7 | |||
| e1f79927f3 | |||
| cab6012db9 | |||
| 75a4cbbf8e | |||
| 9071d3868c | |||
| e7f4f04156 | |||
| 7e01a3a8a9 | |||
| 72f85ccd16 | |||
| ecd54bc6dd | |||
| 8b9baffdf7 | |||
| 93a51c39f4 | |||
| 2dc9daae32 | |||
| a4f8f22a33 | |||
| 7d3bde083c | |||
| 32207833d0 | |||
| 7399781944 | |||
| efee5879a1 | |||
| b82e19efe0 | |||
| eceff3dbc5 | |||
| 3598fc3745 | |||
| 2df466710b | |||
| 29f2ce760a | |||
| 363f4c0031 | |||
| 6d4ffec3fb | |||
| 5c1cee4630 | |||
| 88274f4805 | |||
| 5566c3a9b8 | |||
| 5edc6748f4 | |||
| 6007e83a75 | |||
| ce2a0a4ecb | |||
| 3cd108fcd0 |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.4"
|
||||
go-version: "1.24.9"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
1
.github/workflows/e2e.yml
vendored
1
.github/workflows/e2e.yml
vendored
@ -37,7 +37,6 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 29-rc # latest rc
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- 25 # mirantis lts
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -67,11 +67,11 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.4"
|
||||
go-version: "1.24.9"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
go tool cover -func=/tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
shell: bash
|
||||
|
||||
11
.github/workflows/validate-pr.yml
vendored
11
.github/workflows/validate-pr.yml
vendored
@ -11,23 +11,18 @@ permissions:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check-labels:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
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
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.25.4"
|
||||
go: "1.24.9"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@ -154,7 +154,6 @@ 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
|
||||
|
||||
11
.mailmap
11
.mailmap
@ -64,14 +64,11 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
|
||||
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <macedonv@amazon.com>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Golub <ben.golub@dotcloud.com>
|
||||
@ -153,8 +150,6 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||
David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
|
||||
12
AUTHORS
12
AUTHORS
@ -63,7 +63,6 @@ Andreas Köhler <andi5.py@gmx.net>
|
||||
Andres G. Aragoneses <knocte@gmail.com>
|
||||
Andres Leon Rangel <aleon1220@gmail.com>
|
||||
Andrew France <andrew@avito.co.uk>
|
||||
Andrew He <he.andrew.mail@gmail.com>
|
||||
Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
@ -87,12 +86,11 @@ Archimedes Trajano <developer@trajano.net>
|
||||
Arko Dasgupta <arko@tetrate.io>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Rebillout <elboulangero@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Peka <arthur.peka@outlook.com>
|
||||
Ashly Mathew <ashly.mathew@sap.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Aslam Ahemad <aslamahemad@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
@ -137,12 +135,10 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
carsontham <carsontham@outlook.com>
|
||||
Carston Schilds <Carston.Schilds@visier.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cesar Talledo <cesar.talledo@docker.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
@ -224,7 +220,7 @@ David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Beitey <david@davidjb.com>
|
||||
David Calavera <david.calavera@gmail.com>
|
||||
David Cramer <davcrame@cisco.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
||||
@ -269,7 +265,6 @@ Eli Uriegas <eli.uriegas@docker.com>
|
||||
Eli Uriegas <seemethere101@gmail.com>
|
||||
Elias Faxö <elias.faxo@tre.se>
|
||||
Elliot Luo <956941328@qq.com>
|
||||
Eng Zer Jun <engzerjun@gmail.com>
|
||||
Eric Bode <eric.bode@foundries.io>
|
||||
Eric Curtin <ericcurtin17@gmail.com>
|
||||
Eric Engestrom <eric@engestrom.ch>
|
||||
@ -350,7 +345,6 @@ Henning Sprang <henning.sprang@gmail.com>
|
||||
Henry N <henrynmail-github@yahoo.de>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
Hugo Chastel <Hugo-C@users.noreply.github.com>
|
||||
@ -601,7 +595,6 @@ Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael Tews <michael@tews.dev>
|
||||
Michael West <mwest@mdsol.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
@ -903,7 +896,6 @@ Wenlong Zhang <zhangwenlong@loongson.cn>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Wang <willww64@gmail.com>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@ -5,18 +5,12 @@ 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.22
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.25.4
|
||||
|
||||
# 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
|
||||
ARG GO_VERSION=1.24.9
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
|
||||
# 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.
|
||||
@ -25,12 +19,12 @@ 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.29.1
|
||||
ARG BUILDX_VERSION=0.25.0
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v2.40.0
|
||||
ARG COMPOSE_VERSION=v2.38.2
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
@ -97,7 +91,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/|/cmd/docker-trust')
|
||||
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test /tmp/coverage.txt /coverage.txt
|
||||
@ -122,6 +116,10 @@ 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/
|
||||
|
||||
15
Makefile
15
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/|/cmd/docker-trust')} $(TESTFLAGS)
|
||||
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS)
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
mkdir -p $(CURDIR)/build/coverage
|
||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||
|
||||
.PHONY: lint
|
||||
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.24 . ; \
|
||||
gofumpt -w -d -lang=1.23 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
@ -69,15 +69,6 @@ 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
|
||||
|
||||
@ -8,7 +8,6 @@ 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"
|
||||
)
|
||||
|
||||
@ -26,7 +25,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(), client.PingOptions{})
|
||||
ping, err := apiClient.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
40
cli-plugins/manager/annotations.go
Normal file
40
cli-plugins/manager/annotations.go
Normal file
@ -0,0 +1,40 @@
|
||||
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.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPlugin]. This alias will be removed in the next release.
|
||||
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginVendor]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginVersion]. This alias will be removed in the next release.
|
||||
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.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginInvalid]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginCommandPath]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
|
||||
)
|
||||
@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
//go:build go1.23
|
||||
|
||||
package manager
|
||||
|
||||
|
||||
@ -20,6 +20,16 @@ import (
|
||||
"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.
|
||||
//
|
||||
// Deprecated: use [metadata.ReexecEnvvar]. This alias will be removed in the next release.
|
||||
ReexecEnvvar = metadata.ReexecEnvvar
|
||||
)
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
|
||||
@ -29,6 +39,13 @@ func (e errPluginNotFound) Error() string {
|
||||
return "Error: No such CLI plugin: " + string(e)
|
||||
}
|
||||
|
||||
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||
//
|
||||
// Deprecated: use [errdefs.IsNotFound].
|
||||
func IsNotFound(err error) bool {
|
||||
return errdefs.IsNotFound(err)
|
||||
}
|
||||
|
||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
||||
// in order of preference.
|
||||
//
|
||||
@ -179,7 +196,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
||||
// 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 !isValidPluginName(name) {
|
||||
if !pluginNameRe.MatchString(name) {
|
||||
// We treat this as "not found" so that callers will
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
|
||||
31
cli-plugins/manager/metadata.go
Normal file
31
cli-plugins/manager/metadata.go
Normal file
@ -0,0 +1,31 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
//
|
||||
// Deprecated: use [metadata.NamePrefix]. This alias will be removed in a future release.
|
||||
NamePrefix = metadata.NamePrefix
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
//
|
||||
// Deprecated: use [metadata.MetadataSubcommandName]. This alias will be removed in a future release.
|
||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
||||
|
||||
// HookSubcommandName is the name of the plugin subcommand
|
||||
// which must be implemented by plugins declaring support
|
||||
// for hooks in their metadata.
|
||||
//
|
||||
// Deprecated: use [metadata.HookSubcommandName]. This alias will be removed in a future release.
|
||||
HookSubcommandName = metadata.HookSubcommandName
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.Metadata]. This alias will be removed in a future release.
|
||||
type Metadata = metadata.Metadata
|
||||
@ -13,9 +13,12 @@ import (
|
||||
"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
|
||||
@ -83,8 +86,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
}
|
||||
|
||||
// Now apply the candidate tests, so these update p.Err.
|
||||
if !isValidPluginName(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
|
||||
if !pluginNameRe.MatchString(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@ -170,26 +173,3 @@ 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
|
||||
}
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
@ -139,7 +139,7 @@ func withPluginClientConn(name string) command.CLIOption {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
|
||||
apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -168,11 +168,6 @@ 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())
|
||||
|
||||
35
cli/cobra.go
35
cli/cobra.go
@ -12,6 +12,7 @@ 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"
|
||||
)
|
||||
@ -166,6 +167,31 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
|
||||
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
||||
}
|
||||
|
||||
// VisitAll will traverse all commands from the root.
|
||||
//
|
||||
// Deprecated: this utility was only used internally and will be removed in the next release.
|
||||
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
visitAll(root, fn)
|
||||
}
|
||||
|
||||
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.
|
||||
//
|
||||
// Deprecated: this utility was only used internally and will be removed in the next release.
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
var helpCommand = &cobra.Command{
|
||||
Use: "help [command]",
|
||||
Short: "Help about the command",
|
||||
@ -174,7 +200,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 fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
helpFunc := cmd.HelpFunc()
|
||||
helpFunc(cmd, args)
|
||||
@ -250,12 +276,11 @@ func commandAliases(cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
parentPath = cmd.Parent().CommandPath() + " "
|
||||
}
|
||||
var aliases strings.Builder
|
||||
aliases.WriteString(cmd.CommandPath())
|
||||
aliases := cmd.CommandPath()
|
||||
for _, alias := range cmd.Aliases {
|
||||
aliases.WriteString(", " + parentPath + alias)
|
||||
aliases += ", " + parentPath + alias
|
||||
}
|
||||
return aliases.String()
|
||||
return aliases
|
||||
}
|
||||
|
||||
func topCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
|
||||
@ -3,17 +3,18 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
||||
builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
if c.builderPruneFunc != nil {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
return client.BuildCachePruneResult{}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -6,17 +6,15 @@ 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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newBuilderCommand)
|
||||
commands.Register(func(c command.Cli) *cobra.Command {
|
||||
return newBakeStubCommand(c)
|
||||
})
|
||||
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newBuilderCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "builder",
|
||||
@ -24,8 +22,6 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newPruneCommand(dockerCLI),
|
||||
@ -36,10 +32,16 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
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).
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return newBakeStubCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bake [OPTIONS] [TARGET...]",
|
||||
@ -52,6 +54,5 @@ func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
"aliases": "docker buildx bake",
|
||||
"version": "1.31",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,26 +8,25 @@ import (
|
||||
|
||||
"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/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
|
||||
}
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
reservedSpace opts.MemBytes
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return newPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// newPruneCommand returns a new cobra prune command for images
|
||||
@ -49,16 +48,15 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
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.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
|
||||
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -69,7 +67,8 @@ const (
|
||||
)
|
||||
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
@ -85,15 +84,18 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
ReservedSpace: options.reservedSpace.Value(),
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{
|
||||
All: options.all,
|
||||
// TODO(austinvazquez): remove when updated to use github.com/moby/moby/client@v0.1.0
|
||||
// See https://github.com/moby/moby/pull/50772 for more details.
|
||||
KeepStorage: options.keepStorage.Value(),
|
||||
ReservedSpace: options.keepStorage.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")
|
||||
@ -111,22 +113,7 @@ type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// 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,
|
||||
})
|
||||
// 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})
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
@ -15,8 +15,8 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
builderPruneFunc: func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := newPruneCommand(cli)
|
||||
|
||||
@ -3,33 +3,34 @@ package checkpoint
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return client.CheckpointCreateResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return client.CheckpointListResult{}, nil
|
||||
return []checkpoint.Summary{}, nil
|
||||
}
|
||||
|
||||
@ -3,15 +3,16 @@ package checkpoint
|
||||
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(newCheckpointCommand)
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCheckpointCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
@ -23,7 +24,6 @@ func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"ostype": "linux",
|
||||
"version": "1.25",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCLI),
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,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{
|
||||
@ -27,10 +27,9 @@ 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: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -41,7 +40,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, client.CheckpointCreateOptions{
|
||||
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
Exit: !opts.leaveRunning,
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"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 client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
||||
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -29,8 +29,8 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo")
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
return 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 client.CheckpointCreateOptions
|
||||
var actualOptions checkpoint.CreateOptions
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
actualContainerName = container
|
||||
actualOptions = options
|
||||
return client.CheckpointCreateResult{}, nil
|
||||
return 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 := client.CheckpointCreateOptions{
|
||||
expected := checkpoint.CreateOptions{
|
||||
CheckpointID: checkpointName,
|
||||
CheckpointDir: checkpointDir,
|
||||
Exit: !tc,
|
||||
|
||||
@ -2,7 +2,7 @@ package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -10,6 +10,13 @@ const (
|
||||
checkpointNameHeader = "CHECKPOINT NAME"
|
||||
)
|
||||
|
||||
// NewFormat returns a format for use with a checkpoint Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewFormat(source string) formatter.Format {
|
||||
return newFormat(source)
|
||||
}
|
||||
|
||||
// newFormat returns a format for use with a checkpointContext.
|
||||
func newFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
@ -18,23 +25,24 @@ func newFormat(source string) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes formatted checkpoints using the Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func FormatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
return formatWrite(fmtCtx, checkpoints)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
render := 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 fmtCtx.Write(newCheckpointContext(), render)
|
||||
}
|
||||
|
||||
type checkpointContext struct {
|
||||
@ -42,6 +50,14 @@ 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)
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"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,10 +24,9 @@ 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),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -36,8 +35,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, client.CheckpointListOptions{
|
||||
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
|
||||
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
if err != nil {
|
||||
@ -45,8 +44,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, container string, opts
|
||||
}
|
||||
|
||||
cpCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Output: dockerCli.Out(),
|
||||
Format: newFormat(formatter.TableFormatKey),
|
||||
}
|
||||
return formatWrite(cpCtx, checkpoints.Items)
|
||||
return formatWrite(cpCtx, checkpoints)
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -16,7 +15,7 @@ import (
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -29,8 +28,8 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo")
|
||||
},
|
||||
expectedError: "error getting checkpoints for container foo",
|
||||
},
|
||||
@ -51,19 +50,17 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointListResult{
|
||||
Items: []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
},
|
||||
return []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -11,7 +13,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{
|
||||
@ -20,14 +22,8 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "Remove a checkpoint",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
containerID, checkpointID := args[0], args[1]
|
||||
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
|
||||
CheckpointID: checkpointID,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
return err
|
||||
return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -35,3 +31,10 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"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 client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
||||
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint")
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
return 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 client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
||||
|
||||
@ -1,11 +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.24
|
||||
//go:build go1.23
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -24,8 +23,11 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -43,6 +45,7 @@ type Cli interface {
|
||||
Client() client.APIClient
|
||||
Streams
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...CLIOption) error
|
||||
config.Provider
|
||||
ServerInfo() ServerInfo
|
||||
CurrentVersion() string
|
||||
@ -65,6 +68,7 @@ type DockerCli struct {
|
||||
err *streams.Out
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
contentTrust bool
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
init sync.Once
|
||||
@ -83,12 +87,19 @@ type DockerCli struct {
|
||||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns [api.DefaultVersion].
|
||||
//
|
||||
// Deprecated: this function is no longer used and will be removed in the next release.
|
||||
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 client.MaxAPIVersion
|
||||
return api.DefaultVersion
|
||||
}
|
||||
return cli.client.ClientVersion()
|
||||
}
|
||||
@ -147,13 +158,21 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.serverInfo
|
||||
}
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
//
|
||||
// Deprecated: check the value of the DOCKER_CONTENT_TRUST environment variable to detect whether content-trust is enabled.
|
||||
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, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
@ -295,7 +314,7 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
|
||||
}
|
||||
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
||||
}
|
||||
@ -316,7 +335,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
opts = append(opts, withCustomHeaders)
|
||||
}
|
||||
opts = append(opts, extraOpts...)
|
||||
return client.New(opts...)
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||
@ -377,21 +396,24 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
||||
defer cancel()
|
||||
|
||||
ping, err := cli.client.Ping(ctx, client.PingOptions{
|
||||
NegotiateAPIVersion: true,
|
||||
ForceNegotiate: true,
|
||||
})
|
||||
ping, err := cli.client.Ping(ctx)
|
||||
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
|
||||
@ -529,7 +551,7 @@ func (cli *DockerCli) initialize() error {
|
||||
cli.init.Do(func() {
|
||||
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
||||
if cli.initErr != nil {
|
||||
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
|
||||
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
@ -546,6 +568,18 @@ func (cli *DockerCli) initialize() error {
|
||||
return cli.initErr
|
||||
}
|
||||
|
||||
// Apply all the operation on the cli
|
||||
//
|
||||
// Deprecated: this method is no longer used and will be removed in the next release if there are no remaining users.
|
||||
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 {
|
||||
@ -560,7 +594,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 *client.SwarmStatus
|
||||
SwarmStatus *swarm.Status
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||
@ -568,6 +602,7 @@ type ServerInfo struct {
|
||||
// environment.
|
||||
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||
defaultOps := []CLIOption{
|
||||
withContentTrustFromEnv(),
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
@ -590,7 +625,7 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
@ -75,6 +75,37 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
//
|
||||
// Deprecated: this option is no longer used, and will be removed in the next release.
|
||||
func WithContentTrustFromEnv() CLIOption {
|
||||
return withContentTrustFromEnv()
|
||||
}
|
||||
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
//
|
||||
// Deprecated: this option is no longer used, and will be removed in the next release.
|
||||
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 {
|
||||
@ -166,7 +197,7 @@ func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
return nil, invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
@ -183,7 +214,7 @@ func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
return nil, 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,
|
||||
))
|
||||
@ -194,7 +225,7 @@ func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
// 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(
|
||||
return nil, invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
|
||||
28
cli/command/cli_options_test.go
Normal file
28
cli/command/cli_options_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func contentTrustEnabled(t *testing.T) bool {
|
||||
t.Helper()
|
||||
var cli DockerCli
|
||||
assert.NilError(t, withContentTrustFromEnv()(&cli))
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||
const envvar = "DOCKER_CONTENT_TRUST"
|
||||
t.Setenv(envvar, "true")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "false")
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "invalid")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
os.Unsetenv(envvar)
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
}
|
||||
@ -20,7 +20,9 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -33,7 +35,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(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
@ -46,7 +48,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(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
@ -70,7 +72,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(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
|
||||
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||
@ -79,7 +81,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
@ -105,7 +107,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(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
@ -114,7 +116,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
"Four": []string{"four-value-override"},
|
||||
"User-Agent": []string{UserAgent()},
|
||||
}
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
@ -134,55 +136,51 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
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) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
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() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
@ -214,7 +212,7 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
|
||||
receiveReqCh := make(chan bool)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Simulate a server that hangs on connections.
|
||||
@ -258,14 +256,8 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
@ -275,6 +267,19 @@ 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)
|
||||
@ -292,9 +297,9 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
apiClient, err := client.New()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||
return client.NewClientWithOpts()
|
||||
})))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
@ -388,7 +393,7 @@ func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
|
||||
_, err = cli.Client().Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
@ -1,30 +1,168 @@
|
||||
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/volume"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
|
||||
for _, c := range commands.Commands() {
|
||||
cmd.AddCommand(c(dockerCLI))
|
||||
}
|
||||
// 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
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewRunCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewExecCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewPsCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewBuildCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewPullCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewPushCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewImagesCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewLoginCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewLogoutCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewSearchCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewVersionCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewInfoCommand(dockerCli),
|
||||
|
||||
// management commands
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
builder.NewBakeStubCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewContainerCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
context.NewContextCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewImageCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewSystemCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// orchestration (swarm) commands
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
config.NewConfigCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
node.NewNodeCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
service.NewServiceCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
stack.NewStackCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewAttachCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCommitCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCopyCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCreateCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewDiffCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewExportCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewKillCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewLogsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewPauseCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewPortCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRenameCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRestartCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRmCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStartCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStatsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStopCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewTopCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewUnpauseCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewUpdateCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewWaitCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewHistoryCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewImportCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewLoadCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewRemoveCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(system.NewInspectCommand(dockerCli)),
|
||||
)
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||
// will not be shown in output console.
|
||||
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||
return cmd
|
||||
}
|
||||
cmdCopy := *cmd
|
||||
cmdCopy.Hidden = true
|
||||
cmdCopy.Aliases = []string{}
|
||||
return &cmdCopy
|
||||
}
|
||||
|
||||
@ -4,14 +4,16 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/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/spf13/cobra"
|
||||
)
|
||||
|
||||
// APIClientProvider provides a method to get a [client.APIClient], initializing
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// it if needed.
|
||||
//
|
||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||
@ -27,57 +29,24 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, img := range res.Items {
|
||||
for _, img := range list {
|
||||
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) {
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
})
|
||||
if err != nil {
|
||||
@ -87,7 +56,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 res.Items {
|
||||
for _, ctr := range list {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
@ -110,12 +79,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) {
|
||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, vol := range res.Items {
|
||||
for _, vol := range list.Volumes {
|
||||
names = append(names, vol.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -125,12 +94,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) {
|
||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, nw := range res.Items {
|
||||
for _, nw := range list {
|
||||
names = append(names, nw.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -150,16 +119,14 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
// export MY_VAR=hello
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
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
|
||||
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)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
@ -170,10 +137,15 @@ 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.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
// NoComplete is used for commands where there's no relevant completion.
|
||||
//
|
||||
// Deprecated: use [cobra.NoFileCompletions]. This function will be removed in the next release.
|
||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
@ -213,8 +185,6 @@ 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.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
@ -6,11 +6,13 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"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/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/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -28,38 +30,38 @@ func (c fakeCLI) Client() client.APIClient {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
||||
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)
|
||||
}
|
||||
|
||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
if c.containerListFunc != nil {
|
||||
return c.containerListFunc(ctx, options)
|
||||
return c.containerListFunc(options)
|
||||
}
|
||||
return client.ContainerListResult{}, nil
|
||||
return []container.Summary{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||
if c.imageListFunc != nil {
|
||||
return c.imageListFunc(ctx, options)
|
||||
return c.imageListFunc(options)
|
||||
}
|
||||
return client.ImageListResult{}, nil
|
||||
return []image.Summary{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
if c.networkListFunc != nil {
|
||||
return c.networkListFunc(ctx, options)
|
||||
}
|
||||
return client.NetworkListResult{}, nil
|
||||
return []network.Inspect{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||
if c.volumeListFunc != nil {
|
||||
return c.volumeListFunc(ctx, options)
|
||||
return c.volumeListFunc(options.Filters)
|
||||
}
|
||||
return client.VolumeListResult{}, nil
|
||||
return volume.ListResponse{}, nil
|
||||
}
|
||||
|
||||
func TestCompleteContainerNames(t *testing.T) {
|
||||
@ -69,7 +71,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
filters []func(container.Summary) bool
|
||||
containers []container.Summary
|
||||
expOut []string
|
||||
expOpts client.ContainerListOptions
|
||||
expOpts container.ListOptions
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
@ -85,7 +87,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: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -98,7 +100,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: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -122,7 +124,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-b"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -138,7 +140,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@ -153,12 +155,12 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||
}
|
||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
||||
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.ContainerListResult{Items: tc.containers}, nil
|
||||
return tc.containers, nil
|
||||
},
|
||||
}}, tc.showAll, tc.filters...)
|
||||
|
||||
@ -174,7 +176,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)
|
||||
@ -183,7 +185,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))
|
||||
}
|
||||
@ -226,11 +228,11 @@ func TestCompleteImageNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := ImageNames(fakeCLI{&fakeClient{
|
||||
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ImageListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.ImageListResult{Items: tc.images}, nil
|
||||
return tc.images, nil
|
||||
},
|
||||
}}, -1)
|
||||
|
||||
@ -255,24 +257,9 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
{
|
||||
doc: "with results",
|
||||
networks: []network.Summary{
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-c",
|
||||
Name: "network-c",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-b",
|
||||
Name: "network-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-a",
|
||||
Name: "network-a",
|
||||
},
|
||||
},
|
||||
{ID: "nw-c", Name: "network-c"},
|
||||
{ID: "nw-b", Name: "network-b"},
|
||||
{ID: "nw-a", Name: "network-a"},
|
||||
},
|
||||
expOut: []string{"network-c", "network-b", "network-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
@ -286,11 +273,11 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.NetworkListResult{Items: tc.networks}, nil
|
||||
return tc.networks, nil
|
||||
},
|
||||
}})
|
||||
|
||||
@ -302,7 +289,7 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@ -310,7 +297,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
|
||||
}{
|
||||
@ -320,7 +307,7 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
},
|
||||
{
|
||||
doc: "with results",
|
||||
volumes: []volume.Volume{
|
||||
volumes: []*volume.Volume{
|
||||
{Name: "volume-c"},
|
||||
{Name: "volume-b"},
|
||||
{Name: "volume-a"},
|
||||
@ -337,11 +324,11 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
||||
return volume.ListResponse{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
||||
return volume.ListResponse{Volumes: tc.volumes}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
|
||||
@ -3,41 +3,42 @@ package config
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(ctx, options)
|
||||
return c.configCreateFunc(ctx, spec)
|
||||
}
|
||||
return client.ConfigCreateResult{}, nil
|
||||
return swarm.ConfigCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(ctx, id, options)
|
||||
return c.configInspectFunc(ctx, id)
|
||||
}
|
||||
return client.ConfigInspectResult{}, nil
|
||||
return swarm.Config{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(ctx, options)
|
||||
}
|
||||
return client.ConfigListResult{}, nil
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(ctx, name, options)
|
||||
return c.configRemoveFunc(name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -4,16 +4,17 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newConfigCommand)
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newConfigCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newConfigCommand returns a cobra command for `config` subcommands
|
||||
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
@ -24,7 +25,6 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"version": "1.30",
|
||||
"swarm": "manager",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCLI),
|
||||
@ -38,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) {
|
||||
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, config := range res.Items {
|
||||
for _, config := range list {
|
||||
names = append(names, config.ID)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
|
||||
@ -2,19 +2,28 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CreateOptions specifies some options that are used when creating a config.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
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
|
||||
@ -37,24 +46,7 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
createOpts.file = args[1]
|
||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
||||
},
|
||||
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,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||
@ -64,13 +56,25 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
||||
return runCreate(ctx, dockerCLI, createOptions{
|
||||
name: options.Name,
|
||||
templateDriver: options.TemplateDriver,
|
||||
file: options.File,
|
||||
labels: options.Labels,
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading content from %q: %v", options.file, err)
|
||||
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
@ -85,9 +89,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions
|
||||
Name: options.templateDriver,
|
||||
}
|
||||
}
|
||||
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
|
||||
Spec: spec,
|
||||
})
|
||||
r, err := apiClient.ConfigCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -12,8 +12,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -24,7 +23,7 @@ const configDataFile = "config-create-with-name.golden"
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -37,8 +36,8 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
return client.ConfigCreateResult{}, errors.New("error creating config")
|
||||
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
return swarm.ConfigCreateResponse{}, errors.New("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
@ -62,15 +61,15 @@ func TestConfigCreateWithName(t *testing.T) {
|
||||
const name = "config-with-name"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
actual = options.Spec.Data
|
||||
actual = spec.Data
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -101,13 +100,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -127,17 +126,17 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
const name = "config-with-template-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
if options.Spec.Templating.Name != expectedDriver.Name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
|
||||
if spec.Templating.Name != expectedDriver.Name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,9 +7,8 @@ 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 (
|
||||
@ -30,6 +29,13 @@ Data:
|
||||
{{.Data}}`
|
||||
)
|
||||
|
||||
// NewFormat returns a Format for rendering using a config Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewFormat(source string, quiet bool) formatter.Format {
|
||||
return newFormat(source, quiet)
|
||||
}
|
||||
|
||||
// newFormat returns a Format for rendering using a configContext.
|
||||
func newFormat(source string, quiet bool) formatter.Format {
|
||||
switch source {
|
||||
@ -44,28 +50,38 @@ func newFormat(source string, quiet bool) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes the context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func FormatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
|
||||
return formatWrite(fmtCtx, 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 {
|
||||
func formatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return fmtCtx.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 {
|
||||
@ -112,12 +128,19 @@ func (c *configContext) Label(name string) string {
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
||||
|
||||
// InspectFormatWrite renders the context for a list of configs
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
return inspectFormatWrite(fmtCtx, refs, 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)
|
||||
}
|
||||
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, ref := range refs {
|
||||
configI, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
@ -132,7 +155,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return fmtCtx.Write(&configInspectContext{}, render)
|
||||
}
|
||||
|
||||
type configInspectContext struct {
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -49,25 +48,23 @@ 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"}},
|
||||
},
|
||||
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"}},
|
||||
},
|
||||
}
|
||||
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, res); err != nil {
|
||||
if err := formatWrite(tc.context, configs); err != nil {
|
||||
assert.ErrorContains(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
//go:build go1.23
|
||||
|
||||
package config
|
||||
|
||||
@ -12,10 +12,18 @@ 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.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type InspectOptions struct {
|
||||
Names []string
|
||||
Format string
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
// inspectOptions contains options for the docker config inspect command.
|
||||
type inspectOptions struct {
|
||||
names []string
|
||||
@ -33,8 +41,9 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
opts.names = args
|
||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
@ -42,6 +51,17 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error {
|
||||
return runInspect(ctx, dockerCLI, inspectOptions{
|
||||
names: opts.Names,
|
||||
format: opts.Format,
|
||||
pretty: opts.Pretty,
|
||||
})
|
||||
}
|
||||
|
||||
// runInspect inspects the given Swarm config.
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
@ -51,8 +71,7 @@ func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions)
|
||||
}
|
||||
|
||||
getRef := func(id string) (any, []byte, error) {
|
||||
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
|
||||
return res.Config, res.Raw, err
|
||||
return apiClient.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
|
||||
// check if the user is trying to apply a template to the pretty format, which
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"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, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@ -41,13 +41,11 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
if configID == "foo" {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(builders.ConfigName("foo")),
|
||||
}, nil
|
||||
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
||||
}
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@ -72,34 +70,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
),
|
||||
}, nil
|
||||
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-configs-with-labels",
|
||||
args: []string{"foo", "bar"},
|
||||
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
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -113,19 +102,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
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
|
||||
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
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
@ -155,23 +141,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
func TestConfigInspectPretty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
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
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -9,11 +9,20 @@ 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"
|
||||
)
|
||||
|
||||
// ListOptions contains options for the docker config ls command.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type ListOptions struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
// listOptions contains options for the docker config ls command.
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
@ -32,8 +41,7 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), dockerCLI, listOpts)
|
||||
},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -44,11 +52,22 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigList lists Swarm configs.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
|
||||
return runList(ctx, dockerCLI, listOptions{
|
||||
quiet: options.Quiet,
|
||||
format: options.Format,
|
||||
filter: options.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
// runList lists Swarm configs.
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()})
|
||||
configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -62,13 +81,13 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(res.Items, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name)
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(format, options.quiet),
|
||||
}
|
||||
return formatWrite(configCtx, res)
|
||||
return formatWrite(configCtx, configs)
|
||||
}
|
||||
|
||||
@ -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/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"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, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) {
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{}, errors.New("error listing configs")
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.New("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
@ -48,28 +48,26 @@ func TestConfigListErrors(t *testing.T) {
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)),
|
||||
),
|
||||
},
|
||||
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)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -80,14 +78,12 @@ func TestConfigList(t *testing.T) {
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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",
|
||||
})),
|
||||
},
|
||||
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",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -99,14 +95,12 @@ func TestConfigListWithQuietOption(t *testing.T) {
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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",
|
||||
})),
|
||||
},
|
||||
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",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -120,14 +114,12 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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",
|
||||
})),
|
||||
},
|
||||
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",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -139,24 +131,22 @@ func TestConfigListWithFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)),
|
||||
),
|
||||
},
|
||||
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)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,10 +7,16 @@ 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.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type RemoveOptions struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
@ -20,18 +26,26 @@ func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCLI, args)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
|
||||
return runRemove(ctx, dockerCLI, opts.Names)
|
||||
}
|
||||
|
||||
// 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 names {
|
||||
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
|
||||
if err := apiClient.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
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"
|
||||
)
|
||||
@ -16,7 +14,7 @@ import (
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
configRemoveFunc func(string) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -25,8 +23,8 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config")
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.New("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
@ -48,9 +46,9 @@ func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
@ -65,12 +63,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
|
||||
return errors.New("error removing config: " + name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -23,24 +23,30 @@ type AttachOptions struct {
|
||||
}
|
||||
|
||||
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
||||
c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{})
|
||||
c, err := apiClient.ContainerInspect(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.Container.State.Running {
|
||||
return nil, errors.New("cannot attach to a stopped container, start it first")
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.Container.State.Paused {
|
||||
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.Container.State.Restarting {
|
||||
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c.Container, nil
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newAttachCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts AttachOptions
|
||||
|
||||
@ -58,7 +64,6 @@ 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()
|
||||
@ -74,7 +79,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
|
||||
// request channel to wait for client
|
||||
waitCtx := context.WithoutCancel(ctx)
|
||||
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
|
||||
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
@ -90,7 +95,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := client.ContainerAttachOptions{
|
||||
options := container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
@ -114,11 +119,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
res, err := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if err != nil {
|
||||
return err
|
||||
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
defer res.HijackedResponse.Close()
|
||||
defer resp.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
|
||||
@ -142,7 +147,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
inputStream: in,
|
||||
outputStream: dockerCLI.Out(),
|
||||
errorStream: dockerCLI.Err(),
|
||||
resp: res.HijackedResponse,
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
@ -152,19 +157,19 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
return err
|
||||
}
|
||||
|
||||
return getExitStatus(waitRes)
|
||||
return getExitStatus(errC, resultC)
|
||||
}
|
||||
|
||||
func getExitStatus(waitRes client.ContainerWaitResult) error {
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
||||
select {
|
||||
case result := <-waitRes.Result:
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return errors.New(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-waitRes.Error:
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
|
||||
@ -177,7 +182,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.
|
||||
|
||||
@ -7,8 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -17,23 +16,23 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: false,
|
||||
},
|
||||
@ -44,10 +43,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
@ -59,10 +58,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
@ -125,10 +124,7 @@ func TestGetExitStatus(t *testing.T) {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
|
||||
err := getExitStatus(client.ContainerWaitResult{
|
||||
Result: resultC,
|
||||
Error: errC,
|
||||
})
|
||||
err := getExitStatus(errC, resultC)
|
||||
|
||||
if testcase.expectedError == nil {
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -3,232 +3,232 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"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"
|
||||
)
|
||||
|
||||
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) (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)
|
||||
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)
|
||||
containerRenameFunc func(ctx context.Context, oldName, newName 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)
|
||||
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
|
||||
containerPauseFunc func(ctx context.Context, container string) error
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
if f.containerListFunc != nil {
|
||||
return f.containerListFunc(options)
|
||||
}
|
||||
return client.ContainerListResult{}, nil
|
||||
return []container.Summary{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return client.ContainerInspectResult{}, nil
|
||||
return container.InspectResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(containerID, config)
|
||||
}
|
||||
return client.ExecCreateResult{}, nil
|
||||
return container.ExecCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) {
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return client.ExecInspectResult{}, nil
|
||||
return container.ExecInspect{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) {
|
||||
return client.ExecStartResult{}, nil
|
||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
func (f *fakeClient) ContainerCreate(
|
||||
_ context.Context,
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
return f.createContainerFunc(options)
|
||||
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
|
||||
}
|
||||
return client.ContainerCreateResult{}, nil
|
||||
return container.CreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
||||
if f.containerRemoveFunc != nil {
|
||||
return f.containerRemoveFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
return fakeStreamResult{}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) {
|
||||
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||
if f.infoFunc != nil {
|
||||
return f.infoFunc()
|
||||
}
|
||||
return client.SystemInfoResult{}, nil
|
||||
return system.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(containerID, options.Path)
|
||||
return f.containerStatPathFunc(containerID, path)
|
||||
}
|
||||
return client.ContainerStatPathResult{}, nil
|
||||
return container.PathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(containerID, options.SourcePath)
|
||||
return f.containerCopyFromFunc(containerID, srcPath)
|
||||
}
|
||||
return client.CopyFromContainerResult{}, nil
|
||||
return nil, container.PathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(containerID, options)
|
||||
}
|
||||
return http.NoBody, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult {
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(containerID)
|
||||
}
|
||||
return client.ContainerWaitResult{}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
|
||||
if f.containerStartFunc != nil {
|
||||
return f.containerStartFunc(containerID, options)
|
||||
}
|
||||
return client.ContainerStartResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) {
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
|
||||
if f.containerExportFunc != nil {
|
||||
return f.containerExportFunc(containerID)
|
||||
}
|
||||
return http.NoBody, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
|
||||
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
|
||||
if f.containerExecResizeFunc != nil {
|
||||
return f.containerExecResizeFunc(id, options)
|
||||
}
|
||||
return client.ExecResizeResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
|
||||
if f.containerKillFunc != nil {
|
||||
return f.containerKillFunc(ctx, containerID, options)
|
||||
return f.containerKillFunc(ctx, containerID, signal)
|
||||
}
|
||||
return client.ContainerKillResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
if f.containerPruneFunc != nil {
|
||||
return f.containerPruneFunc(ctx, options)
|
||||
return f.containerPruneFunc(ctx, pruneFilters)
|
||||
}
|
||||
return client.ContainerPruneResult{}, nil
|
||||
return container.PruneReport{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerRestartFunc != nil {
|
||||
return f.containerRestartFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerRestartResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerStopFunc != nil {
|
||||
return f.containerStopFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerStopResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
if f.containerAttachFunc != nil {
|
||||
return f.containerAttachFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerAttachResult{}, nil
|
||||
return types.HijackedResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
||||
if f.containerDiffFunc != nil {
|
||||
return f.containerDiffFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return client.ContainerDiffResult{}, nil
|
||||
return []container.FilesystemChange{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
||||
if f.containerRenameFunc != nil {
|
||||
return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName)
|
||||
return f.containerRenameFunc(ctx, oldName, newName)
|
||||
}
|
||||
|
||||
return client.ContainerRenameResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
||||
if f.containerCommitFunc != nil {
|
||||
return f.containerCommitFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerCommitResult{}, nil
|
||||
return container.CommitResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
||||
if f.containerPauseFunc != nil {
|
||||
return f.containerPauseFunc(ctx, containerID, options)
|
||||
return f.containerPauseFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return client.ContainerPauseResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -3,46 +3,22 @@ package container
|
||||
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(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
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewContainerCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newContainerCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// 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()),
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newAttachCommand(dockerCLI),
|
||||
|
||||
@ -2,14 +2,13 @@ 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/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -18,13 +17,18 @@ 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`
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCommitCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options commitOptions
|
||||
|
||||
@ -37,29 +41,18 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
options.reference = args[1]
|
||||
}
|
||||
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),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
// 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.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
||||
|
||||
@ -70,17 +63,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, client.ContainerCommitOptions{
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
|
||||
Reference: options.reference,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes.GetSlice(),
|
||||
NoPause: options.noPause,
|
||||
Pause: options.pause,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -7,21 +7,25 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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 client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
|
||||
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.NoPause, true))
|
||||
assert.Check(t, is.Equal(options.Pause, false))
|
||||
assert.Check(t, is.Equal(ctr, "container-id"))
|
||||
|
||||
return client.ContainerCommitResult{ID: "image-id"}, nil
|
||||
return container.CommitResponse{ID: "image-id"}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -32,7 +36,7 @@ func TestRunCommit(t *testing.T) {
|
||||
"--author", "Author Name <author@name.com>",
|
||||
"--change", "EXPOSE 80",
|
||||
"--message", "commit message",
|
||||
"--no-pause",
|
||||
"--pause=false",
|
||||
"container-id",
|
||||
},
|
||||
)
|
||||
@ -47,8 +51,12 @@ func TestRunCommitClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
return client.ContainerCommitResult{}, clientError
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
return container.CommitResponse{}, clientError
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
//go:build go1.23
|
||||
|
||||
package container
|
||||
|
||||
@ -8,8 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/capability"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
@ -123,15 +122,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)
|
||||
@ -187,11 +186,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) {
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := res.Info.Plugins.Log
|
||||
drivers := info.Plugins.Log
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@ -280,12 +279,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) {
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
// fallback: the built-in drivers
|
||||
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := res.Info.Plugins.Volume
|
||||
drivers := info.Plugins.Volume
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -27,7 +26,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
||||
|
||||
func TestCompletePid(t *testing.T) {
|
||||
tests := []struct {
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
toComplete string
|
||||
expectedCompletions []string
|
||||
expectedDirective cobra.ShellCompDirective
|
||||
@ -43,12 +42,10 @@ func TestCompletePid(t *testing.T) {
|
||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
},
|
||||
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
}, nil
|
||||
},
|
||||
toComplete: "container:",
|
||||
|
||||
@ -3,7 +3,6 @@ package container
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -16,10 +15,11 @@ 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,7 +121,13 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6
|
||||
return restore, done
|
||||
}
|
||||
|
||||
// newCopyCommand creates a new `docker cp` command
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCopyCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
@ -154,7 +160,6 @@ container source to stdout.`,
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container cp, docker cp",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -230,13 +235,11 @@ 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 {
|
||||
src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{
|
||||
Path: srcPath,
|
||||
})
|
||||
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && src.Stat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := src.Stat.LinkTarget
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
@ -251,14 +254,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{
|
||||
SourcePath: srcPath,
|
||||
})
|
||||
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := cpRes.Content
|
||||
defer func() { _ = content.Close() }()
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCLI.Out(), content)
|
||||
@ -268,7 +268,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: cpRes.Stat.Mode.IsDir(),
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
RebaseName: rebaseName,
|
||||
}
|
||||
|
||||
@ -304,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) error {
|
||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
p, err := resolveLocalPath(srcPath)
|
||||
srcPath, 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}
|
||||
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)
|
||||
}
|
||||
dstStat, err := apiClient.ContainerStatPath(ctx, 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()
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
// FIXME(thaJeztah): unhandled error (should this return?)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
var (
|
||||
@ -360,7 +360,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
@ -403,27 +403,23 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
}
|
||||
}
|
||||
|
||||
options := client.CopyToContainerOptions{
|
||||
DestinationPath: resolvedDstPath,
|
||||
Content: content,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
options := container.CopyToContainerOptions{
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
if copyConfig.quiet {
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
return err
|
||||
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
|
||||
// TODO(thaJeztah): error-handling looks odd here; should it be handled differently?
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, 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 err
|
||||
return res
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
|
||||
@ -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,11 +52,9 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
return client.CopyFromContainerResult{
|
||||
Content: io.NopCloser(strings.NewReader(tarContent)),
|
||||
}, nil
|
||||
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@ -75,12 +73,10 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
return readCloser, container.PathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@ -103,12 +99,10 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
|
||||
defer destDir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
return readCloser, container.PathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
|
||||
@ -4,9 +4,9 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
@ -17,14 +17,20 @@ 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/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/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/client"
|
||||
"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"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@ -39,12 +45,19 @@ 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`
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCreateCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options createOptions
|
||||
var copts *containerOptions
|
||||
@ -63,8 +76,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container create, docker create",
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -74,24 +86,28 @@ 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) // Mark flag as experimental for now.
|
||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks 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")
|
||||
|
||||
// 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())
|
||||
addPlatformFlag(flags, &options.platform)
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||
|
||||
// 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.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
|
||||
copts = addFlags(flags)
|
||||
|
||||
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, cobra.NoFileCompletions)
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -103,7 +119,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 := make([]string, 0, len(proxyConfig))
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
@ -134,27 +150,20 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
return err
|
||||
}
|
||||
|
||||
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{
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platforms: ociPlatforms,
|
||||
Platform: options.platform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Close()
|
||||
}()
|
||||
defer responseBody.Close()
|
||||
|
||||
out := dockerCli.Err()
|
||||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
}
|
||||
return jsonstream.Display(ctx, resp, out)
|
||||
return jsonstream.Display(ctx, responseBody, out)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
@ -167,13 +176,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 fmt.Errorf("failed to remove the CID file '%s': %w", cid.path, err)
|
||||
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -184,7 +193,7 @@ func (cid *cidFile) Write(id string) error {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return fmt.Errorf("failed to write the container ID to the file: %w", err)
|
||||
return errors.Wrap(err, "failed to write the container ID to the file")
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
@ -195,12 +204,12 @@ func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(cidPath); err == nil {
|
||||
return nil, errors.New("container ID file found, make sure the other container isn't running or delete " + cidPath)
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(cidPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the container ID file: %w", err)
|
||||
return nil, errors.Wrap(err, "failed to create the container ID file")
|
||||
}
|
||||
|
||||
return &cidFile{path: cidPath, file: f}, nil
|
||||
@ -212,23 +221,16 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
hostConfig := containerCfg.HostConfig
|
||||
networkingConfig := containerCfg.NetworkingConfig
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
)
|
||||
|
||||
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = containerIDFile.Close()
|
||||
}()
|
||||
defer containerIDFile.Close()
|
||||
|
||||
ref, err := reference.ParseAnyReference(config.Image)
|
||||
if err != nil {
|
||||
@ -236,6 +238,15 @@ 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"
|
||||
@ -306,10 +317,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
var platform *ocispec.Platform
|
||||
if options.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") {
|
||||
p, err := platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err))
|
||||
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
@ -318,6 +333,9 @@ 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
|
||||
}
|
||||
|
||||
@ -329,14 +347,7 @@ 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, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
})
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
if err != nil {
|
||||
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
||||
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||
@ -350,14 +361,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
var retryErr error
|
||||
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,
|
||||
})
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
if retryErr != nil {
|
||||
return "", retryErr
|
||||
}
|
||||
@ -366,6 +370,10 @@ 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)
|
||||
@ -386,6 +394,19 @@ 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, "":
|
||||
@ -407,7 +428,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, apiClient client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI 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)
|
||||
@ -416,7 +437,7 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient 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,
|
||||
@ -430,11 +451,8 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
|
||||
return fmt.Errorf("closing tar for config copy failed: %w", err)
|
||||
}
|
||||
|
||||
_, err := apiClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{
|
||||
DestinationPath: "/",
|
||||
Content: &tarBuf,
|
||||
})
|
||||
if err != nil {
|
||||
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
|
||||
&tarBuf, container.CopyToContainerOptions{}); err != nil {
|
||||
return fmt.Errorf("copying config.json into container failed: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@ -13,10 +13,13 @@ 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"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -114,30 +117,35 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
pullCounter := 0
|
||||
|
||||
apiClient := &fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
createContainerFunc: func(
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
defer func() { tc.ResponseCounter++ }()
|
||||
switch tc.ResponseCounter {
|
||||
case 0:
|
||||
return client.ContainerCreateResult{}, fakeNotFound{}
|
||||
return container.CreateResponse{}, fakeNotFound{}
|
||||
default:
|
||||
return client.ContainerCreateResult{ID: containerID}, nil
|
||||
return container.CreateResponse{ID: containerID}, nil
|
||||
}
|
||||
},
|
||||
imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
defer func() { pullCounter++ }()
|
||||
return fakeStreamResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
infoFunc: func() (client.SystemInfoResult, error) {
|
||||
return client.SystemInfoResult{
|
||||
Info: system.Info{IndexServerAddress: "https://indexserver.example.com"},
|
||||
}, nil
|
||||
infoFunc: func() (system.Info, error) {
|
||||
return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
|
||||
},
|
||||
}
|
||||
fakeCLI := test.NewFakeCli(apiClient)
|
||||
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
pull: tc.PullPolicy,
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
untrusted: true,
|
||||
pull: tc.PullPolicy,
|
||||
})
|
||||
|
||||
if tc.ExpectedErrMsg != "" {
|
||||
@ -213,6 +221,56 @@ 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) {
|
||||
t.Setenv("DOCKER_CONTENT_TRUST", "true")
|
||||
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")
|
||||
},
|
||||
})
|
||||
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
|
||||
@ -234,12 +292,27 @@ 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(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{Warnings: tc.warnings}, nil
|
||||
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
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
@ -272,10 +345,15 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||
sort.Strings(expected)
|
||||
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
sort.Strings(options.Config.Env)
|
||||
assert.DeepEqual(t, options.Config.Env, expected)
|
||||
return client.ContainerCreateResult{}, nil
|
||||
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
|
||||
},
|
||||
})
|
||||
fakeCLI.SetConfigFile(&configfile.ConfigFile{
|
||||
|
||||
@ -7,11 +7,16 @@ 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`
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newDiffCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
@ -23,13 +28,12 @@ func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container diff, docker diff",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
|
||||
res, err := dockerCLI.Client().ContainerDiff(ctx, containerID, client.ContainerDiffOptions{})
|
||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -37,5 +41,5 @@ func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) err
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newDiffFormat("{{.Type}} {{.Path}}"),
|
||||
}
|
||||
return diffFormatWrite(diffCtx, res)
|
||||
return diffFormatWrite(diffCtx, changes)
|
||||
}
|
||||
|
||||
@ -8,29 +8,29 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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) (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",
|
||||
},
|
||||
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",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
@ -60,8 +60,11 @@ func TestRunDiffClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
|
||||
return client.ContainerDiffResult{}, clientError
|
||||
containerDiffFunc: func(
|
||||
ctx context.Context,
|
||||
containerID string,
|
||||
) ([]container.FilesystemChange, error) {
|
||||
return nil, clientError
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@ -11,8 +10,9 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -39,7 +39,13 @@ func NewExecOptions() ExecOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// newExecCommand creates a new cobra.Command for "docker exec".
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newExecCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := NewExecOptions()
|
||||
|
||||
@ -59,7 +65,6 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"category-top": "2",
|
||||
"aliases": "docker container exec, docker exec",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -72,14 +77,14 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
|
||||
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
|
||||
}
|
||||
@ -97,18 +102,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, client.ContainerInspectOptions{}); err != nil {
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); 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.ExecCreate(ctx, containerIDorName, *execOptions)
|
||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -119,24 +124,23 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
}
|
||||
|
||||
if options.Detach {
|
||||
_, err := apiClient.ExecStart(ctx, execID, client.ExecStartOptions{
|
||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
Detach: options.Detach,
|
||||
TTY: execOptions.TTY,
|
||||
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
return err
|
||||
}
|
||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
||||
}
|
||||
|
||||
func fillConsoleSize(execOptions *client.ExecCreateOptions, dockerCli command.Cli) {
|
||||
if execOptions.TTY {
|
||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||
if execOptions.Tty {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
execOptions.ConsoleSize = client.ConsoleSize{Height: height, Width: width}
|
||||
execOptions.ConsoleSize = &[2]uint{height, width}
|
||||
}
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *client.ExecCreateOptions, execID string) error {
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
@ -150,7 +154,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execOptions.AttachStderr {
|
||||
if execOptions.TTY {
|
||||
if execOptions.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
@ -159,9 +163,9 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
fillConsoleSize(execOptions, dockerCli)
|
||||
|
||||
apiClient := dockerCli.Client()
|
||||
resp, err := apiClient.ExecAttach(ctx, execID, client.ExecAttachOptions{
|
||||
TTY: execOptions.TTY,
|
||||
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
|
||||
resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -178,8 +182,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp.HijackedResponse,
|
||||
tty: execOptions.TTY,
|
||||
resp: resp,
|
||||
tty: execOptions.Tty,
|
||||
detachKeys: execOptions.DetachKeys,
|
||||
}
|
||||
|
||||
@ -187,7 +191,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
}()
|
||||
}()
|
||||
|
||||
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)
|
||||
}
|
||||
@ -201,8 +205,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
return getExecExitStatus(ctx, apiClient, execID)
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, execID string) error {
|
||||
resp, err := apiClient.ExecInspect(ctx, execID, client.ExecInspectOptions{})
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
|
||||
resp, err := apiClient.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
@ -219,11 +223,11 @@ func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, exec
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client.ExecCreateOptions, error) {
|
||||
execOptions := &client.ExecCreateOptions{
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) {
|
||||
execOptions := &container.ExecOptions{
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
TTY: execOpts.TTY,
|
||||
Tty: execOpts.TTY,
|
||||
Cmd: execOpts.Command,
|
||||
WorkingDir: execOpts.Workdir,
|
||||
}
|
||||
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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 client.ExecCreateOptions
|
||||
expected container.ExecOptions
|
||||
}{
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -49,7 +49,7 @@ TWO=2
|
||||
options: withDefaultOpts(ExecOptions{}),
|
||||
},
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@ -64,18 +64,18 @@ TWO=2
|
||||
TTY: true,
|
||||
User: "uid",
|
||||
}),
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
TTY: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
@ -85,15 +85,15 @@ TWO=2
|
||||
Interactive: true,
|
||||
Detach: true,
|
||||
}),
|
||||
expected: client.ExecCreateOptions{
|
||||
TTY: true,
|
||||
expected: container.ExecOptions{
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
},
|
||||
@ -104,13 +104,13 @@ TWO=2
|
||||
DetachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
},
|
||||
},
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
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: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
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{})
|
||||
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
|
||||
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) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("failed inspect")
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, 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, "the-container", testcase.options)
|
||||
err := RunExec(context.TODO(), fakeCLI, "thecontainer", 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, _ client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
return client.ExecCreateResult{ID: "exec-id"}, nil
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
@ -235,9 +235,9 @@ func TestGetExecExitStatus(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
apiClient := &fakeClient{
|
||||
execInspectFunc: func(id string) (client.ExecInspectResult, error) {
|
||||
execInspectFunc: func(id string) (container.ExecInspect, error) {
|
||||
assert.Check(t, is.Equal(execID, id))
|
||||
return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), apiClient, execID)
|
||||
@ -250,14 +250,14 @@ func TestNewExecCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,15 +2,13 @@ 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"
|
||||
)
|
||||
|
||||
@ -19,7 +17,13 @@ type exportOptions struct {
|
||||
output string
|
||||
}
|
||||
|
||||
// newExportCommand creates a new "docker container export" command.
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newExportCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
@ -34,8 +38,7 @@ func newExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container export, docker export",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -55,13 +58,13 @@ func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) e
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to export container: %w", err)
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
}
|
||||
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container, client.ContainerExportOptions{})
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -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,9 +15,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
defer dir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("bar"), nil
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("bar")), nil
|
||||
},
|
||||
})
|
||||
cmd := newExportCommand(cli)
|
||||
@ -34,9 +33,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
|
||||
func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("foo"), nil
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("foo")), nil
|
||||
},
|
||||
})
|
||||
cmd := newExportCommand(cli)
|
||||
|
||||
@ -2,8 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -13,6 +12,13 @@ const (
|
||||
pathHeader = "PATH"
|
||||
)
|
||||
|
||||
// NewDiffFormat returns a format for use with a diff Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewDiffFormat(source string) formatter.Format {
|
||||
return newDiffFormat(source)
|
||||
}
|
||||
|
||||
// newDiffFormat returns a format for use with a diff [formatter.Context].
|
||||
func newDiffFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
@ -21,10 +27,17 @@ func newDiffFormat(source string) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// DiffFormatWrite writes formatted diff using the Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
||||
return diffFormatWrite(fmtCtx, changes)
|
||||
}
|
||||
|
||||
// diffFormatWrite writes formatted diff using the [formatter.Context].
|
||||
func diffFormatWrite(fmtCtx formatter.Context, changes client.ContainerDiffResult) error {
|
||||
func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
||||
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes.Changes {
|
||||
for _, change := range changes {
|
||||
if err := format(&diffContext{c: change}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -41,12 +40,10 @@ D: /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"},
|
||||
},
|
||||
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"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@ -8,8 +8,8 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -38,7 +38,7 @@ type hijackedIOStreamer struct {
|
||||
outputStream io.Writer
|
||||
errorStream io.Writer
|
||||
|
||||
resp client.HijackedResponse
|
||||
resp types.HijackedResponse
|
||||
|
||||
tty bool
|
||||
detachKeys string
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
//go:build go1.23
|
||||
|
||||
package container
|
||||
|
||||
@ -11,7 +11,6 @@ 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"
|
||||
)
|
||||
|
||||
@ -22,7 +21,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{
|
||||
@ -31,10 +30,9 @@ 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),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -47,10 +45,6 @@ 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) {
|
||||
res, err := apiClient.ContainerInspect(ctx, ref, client.ContainerInspectOptions{Size: opts.size})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &res.Container, res.Raw, nil
|
||||
return apiClient.ContainerInspectWithRaw(ctx, ref, opts.size)
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ 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"
|
||||
)
|
||||
|
||||
@ -18,7 +17,13 @@ type killOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// newKillCommand creates a new cobra.Command for "docker container kill"
|
||||
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewKillCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newKillCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newKillCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts killOptions
|
||||
|
||||
@ -33,8 +38,7 @@ func newKillCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container kill, docker kill",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -48,10 +52,7 @@ 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 {
|
||||
_, err := apiClient.ContainerKill(ctx, container, client.ContainerKillOptions{
|
||||
Signal: opts.signal,
|
||||
})
|
||||
return err
|
||||
return apiClient.ContainerKill(ctx, container, opts.signal)
|
||||
})
|
||||
|
||||
var errs []error
|
||||
|
||||
@ -8,16 +8,19 @@ 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, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
assert.Assert(t, is.Equal(options.Signal, "STOP"))
|
||||
return client.ContainerKillResult{}, nil
|
||||
containerKillFunc: func(
|
||||
ctx context.Context,
|
||||
container string,
|
||||
signal string,
|
||||
) error {
|
||||
assert.Assert(t, is.Equal(signal, "STOP"))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -44,8 +47,12 @@ func TestRunKill(t *testing.T) {
|
||||
|
||||
func TestRunKillClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
return client.ContainerKillResult{}, fmt.Errorf("client error for container %s", container)
|
||||
containerKillFunc: func(
|
||||
ctx context.Context,
|
||||
container string,
|
||||
signal string,
|
||||
) error {
|
||||
return fmt.Errorf("client error for container %s", container)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
@ -11,7 +10,8 @@ import (
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -27,7 +27,13 @@ type psOptions struct {
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
// newPsCommand creates a new cobra.Command for "docker container ps"
|
||||
// NewPsCommand creates a new cobra.Command for `docker ps`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newPsCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := psOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
@ -43,8 +49,7 @@ func newPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"category-top": "3",
|
||||
"aliases": "docker container ls, docker container list, docker container ps, docker ps",
|
||||
},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -68,8 +73,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func buildContainerListOptions(options *psOptions) (client.ContainerListOptions, error) {
|
||||
listOptions := client.ContainerListOptions{
|
||||
func buildContainerListOptions(options *psOptions) (*container.ListOptions, error) {
|
||||
listOptions := &container.ListOptions{
|
||||
All: options.all,
|
||||
Limit: options.last,
|
||||
Size: options.size,
|
||||
@ -84,7 +89,7 @@ func buildContainerListOptions(options *psOptions) (client.ContainerListOptions,
|
||||
if len(options.format) > 0 {
|
||||
tmpl, err := templates.Parse(options.format)
|
||||
if err != nil {
|
||||
return client.ContainerListOptions{}, fmt.Errorf("failed to parse template: %w", err)
|
||||
return nil, errors.Wrap(err, "failed to parse template")
|
||||
}
|
||||
|
||||
optionsProcessor := formatter.NewContainerContext()
|
||||
@ -92,7 +97,7 @@ func buildContainerListOptions(options *psOptions) (client.ContainerListOptions,
|
||||
// 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 client.ContainerListOptions{}, fmt.Errorf("failed to execute template: %w", err)
|
||||
return nil, errors.Wrap(err, "failed to execute template")
|
||||
}
|
||||
|
||||
// if `size` was not explicitly set to false (with `--size=false`)
|
||||
@ -127,7 +132,7 @@ func runPs(ctx context.Context, dockerCLI command.Cli, options *psOptions) error
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := dockerCLI.Client().ContainerList(ctx, listOptions)
|
||||
containers, err := dockerCLI.Client().ContainerList(ctx, *listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -137,5 +142,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, res.Items)
|
||||
return formatter.ContainerWrite(containerCtx, containers)
|
||||
}
|
||||
|
||||
@ -9,8 +9,7 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -26,7 +25,7 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
expectedAll bool
|
||||
expectedSize bool
|
||||
expectedLimit int
|
||||
expectedFilters client.Filters
|
||||
expectedFilters map[string]string
|
||||
}{
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -35,10 +34,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
last: 5,
|
||||
filter: filters,
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -47,9 +49,10 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
last: -1,
|
||||
nLatest: true,
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 1,
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 1,
|
||||
expectedFilters: make(map[string]string),
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -60,10 +63,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// With .Size, size should be true
|
||||
format: "{{.Size}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -74,10 +80,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// With .Size, size should be true
|
||||
format: "{{.Size}} {{.CreatedAt}} {{upper .Networks}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
expectedAll: true,
|
||||
expectedSize: true,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
psOpts: &psOptions{
|
||||
@ -88,10 +97,13 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||
// Without .Size, size should be false
|
||||
format: "{{.CreatedAt}} {{.Networks}}",
|
||||
},
|
||||
expectedAll: true,
|
||||
expectedSize: false,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: make(client.Filters).Add("foo", "bar").Add("baz", "foo"),
|
||||
expectedAll: true,
|
||||
expectedSize: false,
|
||||
expectedLimit: 5,
|
||||
expectedFilters: map[string]string{
|
||||
"foo": "bar",
|
||||
"baz": "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -102,14 +114,21 @@ 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.DeepEqual(c.expectedFilters, options.Filters))
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
flags map[string]string
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -125,8 +144,8 @@ func TestContainerListErrors(t *testing.T) {
|
||||
expectedError: `wrong number of args for join`,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{}, errors.New("error listing containers")
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return nil, errors.New("error listing containers")
|
||||
},
|
||||
expectedError: "error listing containers",
|
||||
},
|
||||
@ -149,15 +168,13 @@ func TestContainerListErrors(t *testing.T) {
|
||||
|
||||
func TestContainerListWithoutFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)),
|
||||
},
|
||||
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)),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -171,12 +188,10 @@ func TestContainerListWithoutFormat(t *testing.T) {
|
||||
|
||||
func TestContainerListNoTrunc(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
},
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -192,12 +207,10 @@ func TestContainerListNoTrunc(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#21772
|
||||
func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(_ client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
},
|
||||
containerListFunc: func(_ container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2", builders.WithName("foo/bar")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -213,12 +226,10 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||
// Test for GitHub issue docker/docker#30291
|
||||
func TestContainerListFormatTemplateWithArg(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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")),
|
||||
},
|
||||
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")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -268,9 +279,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 client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
|
||||
assert.Check(t, is.Equal(options.Size, tc.sizeExpected))
|
||||
return client.ContainerListResult{}, nil
|
||||
return []container.Summary{}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
@ -288,12 +299,10 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||
|
||||
func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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)),
|
||||
},
|
||||
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)),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@ -310,12 +319,10 @@ func TestContainerListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestContainerListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
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")),
|
||||
},
|
||||
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")),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -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/moby/moby/api/pkg/stdcopy"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -23,7 +23,13 @@ type logsOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// newLogsCommand creates a new cobra.Command for "docker container logs"
|
||||
// NewLogsCommand creates a new cobra.Command for `docker logs`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewLogsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newLogsCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts logsOptions
|
||||
|
||||
@ -38,8 +44,7 @@ func newLogsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container logs, docker logs",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -54,12 +59,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, client.ContainerInspectOptions{})
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := dockerCli.Client().ContainerLogs(ctx, c.Container.ID, client.ContainerLogsOptions{
|
||||
responseBody, err := dockerCli.Client().ContainerLogs(ctx, c.ID, container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
@ -72,12 +77,12 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = resp.Close() }()
|
||||
defer responseBody.Close()
|
||||
|
||||
if c.Container.Config.Tty {
|
||||
_, err = io.Copy(dockerCli.Out(), resp)
|
||||
if c.Config.Tty {
|
||||
_, err = io.Copy(dockerCli.Out(), responseBody)
|
||||
} else {
|
||||
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), resp)
|
||||
_, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@ -2,22 +2,27 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
Config: &container.Config{Tty: true},
|
||||
State: &container.State{Running: false},
|
||||
},
|
||||
inspectFn := func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
Config: &container.Config{Tty: true},
|
||||
ContainerJSONBase: &container.ContainerJSONBase{State: &container.State{Running: false}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -33,13 +38,7 @@ func TestRunLogs(t *testing.T) {
|
||||
doc: "successful logs",
|
||||
expectedOut: "foo",
|
||||
options: &logsOptions{},
|
||||
client: &fakeClient{
|
||||
logFunc: func(container string, opts client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerLogsResult("foo"), nil
|
||||
},
|
||||
inspectFunc: inspectFn,
|
||||
},
|
||||
client: &fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,30 +1,26 @@
|
||||
// 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"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||
)
|
||||
@ -56,7 +52,7 @@ type containerOptions struct {
|
||||
deviceWriteBps opts.ThrottledeviceOpt
|
||||
links opts.ListOpts
|
||||
aliases opts.ListOpts
|
||||
linkLocalIPs opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
|
||||
linkLocalIPs opts.ListOpts
|
||||
deviceReadIOps opts.ThrottledeviceOpt
|
||||
deviceWriteIOps opts.ThrottledeviceOpt
|
||||
env opts.ListOpts
|
||||
@ -68,7 +64,7 @@ type containerOptions struct {
|
||||
sysctls *opts.MapOpts
|
||||
publish opts.ListOpts
|
||||
expose opts.ListOpts
|
||||
dns opts.ListOpts // TODO(thaJeztah): we need a flag-type to handle []netip.Addr directly
|
||||
dns opts.ListOpts
|
||||
dnsSearch opts.ListOpts
|
||||
dnsOptions opts.ListOpts
|
||||
extraHosts opts.ListOpts
|
||||
@ -98,6 +94,7 @@ type containerOptions struct {
|
||||
memory opts.MemBytes
|
||||
memoryReservation opts.MemBytes
|
||||
memorySwap opts.MemSwapBytes
|
||||
kernelMemory opts.MemBytes
|
||||
user string
|
||||
workingDir string
|
||||
cpuCount int64
|
||||
@ -116,8 +113,8 @@ type containerOptions struct {
|
||||
swappiness int64
|
||||
netMode opts.NetworkOpt
|
||||
macAddress 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
|
||||
ipv4Address string
|
||||
ipv6Address string
|
||||
ipcMode string
|
||||
pidsLimit int64
|
||||
restartPolicy string
|
||||
@ -145,6 +142,16 @@ type containerOptions struct {
|
||||
Args []string
|
||||
}
|
||||
|
||||
// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and
|
||||
// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default.
|
||||
//
|
||||
// It should not be used for new uses, which may have a different API version
|
||||
// requirement.
|
||||
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"})
|
||||
}
|
||||
|
||||
// addFlags adds all command line flags that will be used by parse to the FlagSet
|
||||
func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
copts := &containerOptions{
|
||||
@ -233,8 +240,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.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.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.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)")
|
||||
@ -297,6 +304,7 @@ 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")
|
||||
@ -320,18 +328,13 @@ 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 *network.NetworkingConfig
|
||||
NetworkingConfig *networktypes.NetworkingConfig
|
||||
}
|
||||
|
||||
// parse parses the args for the specified command and generates a Config,
|
||||
@ -349,7 +352,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
// Validate the input mac address
|
||||
if 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)
|
||||
return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress)
|
||||
}
|
||||
}
|
||||
if copts.stdin {
|
||||
@ -365,7 +368,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
|
||||
swappiness := copts.swappiness
|
||||
if swappiness != -1 && (swappiness < 0 || swappiness > 100) {
|
||||
return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
|
||||
return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness)
|
||||
}
|
||||
|
||||
var binds []string
|
||||
@ -380,7 +383,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
if parsed.Source != "" {
|
||||
toBind := bind
|
||||
|
||||
if parsed.Type == string(mount.TypeBind) {
|
||||
if parsed.Type == string(mounttypes.TypeBind) {
|
||||
if hostPart, targetPath, ok := strings.Cut(bind, ":"); ok {
|
||||
if !filepath.IsAbs(hostPart) && strings.HasPrefix(hostPart, ".") {
|
||||
if absHostPart, err := filepath.Abs(hostPart); err == nil {
|
||||
@ -420,60 +423,45 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
entrypoint = []string{""}
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): remove uses of go-connections/nat here.
|
||||
convertedOpts, err := convertToStandardNotation(copts.publish.GetSlice())
|
||||
publishOpts := copts.publish.GetSlice()
|
||||
var (
|
||||
ports map[nat.Port]struct{}
|
||||
portBindings map[nat.Port][]nat.PortBinding
|
||||
convertedOpts []string
|
||||
)
|
||||
|
||||
convertedOpts, err = convertToStandardNotation(publishOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts)
|
||||
ports, portBindings, 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 <portnum>/[<proto>]
|
||||
// or <startport-endport>/[<proto>]
|
||||
pr, err := network.ParsePortRange(e)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid range format for --expose: %w", err)
|
||||
}
|
||||
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
|
||||
for p := range pr.All() {
|
||||
exposedPorts[p] = struct{}{}
|
||||
start, end, err := nat.ParsePortRange(port)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, 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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,19 +469,23 @@ 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.
|
||||
devices := copts.devices.GetSlice()
|
||||
deviceMappings := make([]container.DeviceMapping, 0, len(devices))
|
||||
cdiDeviceNames := make([]string, 0, len(devices))
|
||||
for _, device := range devices {
|
||||
deviceMappings := []container.DeviceMapping{}
|
||||
var cdiDeviceNames []string
|
||||
for _, device := range copts.devices.GetSlice() {
|
||||
var (
|
||||
validated string
|
||||
deviceMapping container.DeviceMapping
|
||||
err error
|
||||
)
|
||||
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
|
||||
}
|
||||
@ -514,22 +506,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
|
||||
pidMode := container.PidMode(copts.pidMode)
|
||||
if !pidMode.Valid() {
|
||||
return nil, errors.New("--pid: invalid PID mode")
|
||||
return nil, errors.Errorf("--pid: invalid PID mode")
|
||||
}
|
||||
|
||||
utsMode := container.UTSMode(copts.utsMode)
|
||||
if !utsMode.Valid() {
|
||||
return nil, errors.New("--uts: invalid UTS mode")
|
||||
return nil, errors.Errorf("--uts: invalid UTS mode")
|
||||
}
|
||||
|
||||
usernsMode := container.UsernsMode(copts.usernsMode)
|
||||
if !usernsMode.Valid() {
|
||||
return nil, errors.New("--userns: invalid USER mode")
|
||||
return nil, errors.Errorf("--userns: invalid USER mode")
|
||||
}
|
||||
|
||||
cgroupnsMode := container.CgroupnsMode(copts.cgroupnsMode)
|
||||
if !cgroupnsMode.Valid() {
|
||||
return nil, errors.New("--cgroupns: invalid CGROUP mode")
|
||||
return nil, errors.Errorf("--cgroupns: invalid CGROUP mode")
|
||||
}
|
||||
|
||||
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
|
||||
@ -564,7 +556,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
copts.healthStartInterval != 0
|
||||
if copts.noHealthcheck {
|
||||
if haveHealthSettings {
|
||||
return nil, errors.New("--no-healthcheck conflicts with --health-* options")
|
||||
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
|
||||
}
|
||||
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
|
||||
} else if haveHealthSettings {
|
||||
@ -573,13 +565,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
probe = []string{"CMD-SHELL", copts.healthCmd}
|
||||
}
|
||||
if copts.healthInterval < 0 {
|
||||
return nil, errors.New("--health-interval cannot be negative")
|
||||
return nil, errors.Errorf("--health-interval cannot be negative")
|
||||
}
|
||||
if copts.healthTimeout < 0 {
|
||||
return nil, errors.New("--health-timeout cannot be negative")
|
||||
return nil, errors.Errorf("--health-timeout cannot be negative")
|
||||
}
|
||||
if copts.healthRetries < 0 {
|
||||
return nil, errors.New("--health-retries cannot be negative")
|
||||
return nil, errors.Errorf("--health-retries cannot be negative")
|
||||
}
|
||||
if copts.healthStartPeriod < 0 {
|
||||
return nil, errors.New("--health-start-period cannot be negative")
|
||||
@ -613,6 +605,7 @@ 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,
|
||||
@ -642,7 +635,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
config := &container.Config{
|
||||
Hostname: copts.hostname,
|
||||
Domainname: copts.domainname,
|
||||
ExposedPorts: exposedPorts,
|
||||
ExposedPorts: ports,
|
||||
User: copts.user,
|
||||
Tty: copts.tty,
|
||||
OpenStdin: copts.stdin,
|
||||
@ -653,6 +646,7 @@ 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),
|
||||
@ -677,7 +671,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: toNetipAddrSlice(copts.dns.GetAllOrEmpty()),
|
||||
DNS: copts.dns.GetAllOrEmpty(),
|
||||
DNSSearch: copts.dnsSearch.GetAllOrEmpty(),
|
||||
DNSOptions: copts.dnsOptions.GetAllOrEmpty(),
|
||||
ExtraHosts: copts.extraHosts.GetSlice(),
|
||||
@ -710,7 +704,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
}
|
||||
|
||||
if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() {
|
||||
return nil, errors.New("conflicting options: cannot specify both --restart and --rm")
|
||||
return nil, errors.Errorf("conflicting options: cannot specify both --restart and --rm")
|
||||
}
|
||||
|
||||
// only set this value if the user provided the flag, else it should default to nil
|
||||
@ -723,17 +717,25 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
config.StdinOnce = true
|
||||
}
|
||||
|
||||
epCfg, err := parseNetworkOpts(copts)
|
||||
networkingConfig := &networktypes.NetworkingConfig{
|
||||
EndpointsConfig: make(map[string]*networktypes.EndpointSettings),
|
||||
}
|
||||
|
||||
networkingConfig.EndpointsConfig, 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: &network.NetworkingConfig{
|
||||
EndpointsConfig: epCfg,
|
||||
},
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -744,9 +746,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]*network.EndpointSettings, error) {
|
||||
func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.EndpointSettings, error) {
|
||||
var (
|
||||
endpoints = make(map[string]*network.EndpointSettings, len(copts.netMode.Value()))
|
||||
endpoints = make(map[string]*networktypes.EndpointSettings, len(copts.netMode.Value()))
|
||||
hasUserDefined, hasNonUserDefined bool
|
||||
)
|
||||
|
||||
@ -785,14 +787,14 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSett
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := endpoints[n.Target]; ok {
|
||||
return nil, invalidParameter(fmt.Errorf("network %q is specified multiple times", n.Target))
|
||||
return nil, invalidParameter(errors.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, network.EndpointSettings{}) {
|
||||
if ep == nil || reflect.DeepEqual(*ep, networktypes.EndpointSettings{}) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -812,10 +814,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.IsValid() && copts.ipv4Address != nil {
|
||||
if n.IPv4Address != "" && copts.ipv4Address != "" {
|
||||
return invalidParameter(errors.New("conflicting options: cannot specify both --ip and per-network IPv4 address"))
|
||||
}
|
||||
if n.IPv6Address.IsValid() && copts.ipv6Address != nil {
|
||||
if n.IPv6Address != "" && copts.ipv6Address != "" {
|
||||
return invalidParameter(errors.New("conflicting options: cannot specify both --ip6 and per-network IPv6 address"))
|
||||
}
|
||||
if n.MacAddress != "" && copts.macAddress != "" {
|
||||
@ -834,26 +836,23 @@ func applyContainerOptions(n *opts.NetworkAttachmentOpts, copts *containerOption
|
||||
n.Links = make([]string, copts.links.Len())
|
||||
copy(n.Links, copts.links.GetSlice())
|
||||
}
|
||||
if copts.ipv4Address != nil {
|
||||
if ipv4, ok := netip.AddrFromSlice(copts.ipv4Address.To4()); ok {
|
||||
n.IPv4Address = ipv4
|
||||
}
|
||||
if copts.ipv4Address != "" {
|
||||
n.IPv4Address = copts.ipv4Address
|
||||
}
|
||||
if copts.ipv6Address != nil {
|
||||
if ipv6, ok := netip.AddrFromSlice(copts.ipv6Address.To16()); ok {
|
||||
n.IPv6Address = ipv6
|
||||
}
|
||||
if copts.ipv6Address != "" {
|
||||
n.IPv6Address = copts.ipv6Address
|
||||
}
|
||||
if copts.macAddress != "" {
|
||||
n.MacAddress = copts.macAddress
|
||||
}
|
||||
if copts.linkLocalIPs.Len() > 0 {
|
||||
n.LinkLocalIPs = toNetipAddrSlice(copts.linkLocalIPs.GetSlice())
|
||||
n.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len())
|
||||
copy(n.LinkLocalIPs, copts.linkLocalIPs.GetSlice())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.EndpointSettings, error) {
|
||||
func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.EndpointSettings, error) {
|
||||
if strings.TrimSpace(ep.Target) == "" {
|
||||
return nil, errors.New("no name set for network")
|
||||
}
|
||||
@ -866,7 +865,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.Endpoint
|
||||
}
|
||||
}
|
||||
|
||||
epConfig := &network.EndpointSettings{
|
||||
epConfig := &networktypes.EndpointSettings{
|
||||
GwPriority: ep.GwPriority,
|
||||
}
|
||||
epConfig.Aliases = append(epConfig.Aliases, ep.Aliases...)
|
||||
@ -877,19 +876,18 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*network.Endpoint
|
||||
if len(ep.Links) > 0 {
|
||||
epConfig.Links = ep.Links
|
||||
}
|
||||
if ep.IPv4Address.IsValid() || ep.IPv6Address.IsValid() || len(ep.LinkLocalIPs) > 0 {
|
||||
epConfig.IPAMConfig = &network.EndpointIPAMConfig{
|
||||
if ep.IPv4Address != "" || ep.IPv6Address != "" || len(ep.LinkLocalIPs) > 0 {
|
||||
epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: ep.IPv4Address,
|
||||
IPv6Address: ep.IPv6Address,
|
||||
LinkLocalIPs: ep.LinkLocalIPs,
|
||||
}
|
||||
}
|
||||
if 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)
|
||||
if _, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress)); err != nil {
|
||||
return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress)
|
||||
}
|
||||
epConfig.MacAddress = network.HardwareAddr(ma)
|
||||
epConfig.MacAddress = ep.MacAddress
|
||||
}
|
||||
return epConfig, nil
|
||||
}
|
||||
@ -902,7 +900,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
|
||||
for _, param := range strings.Split(publish, ",") {
|
||||
k, v, ok := strings.Cut(param, "=")
|
||||
if !ok || k == "" {
|
||||
return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
|
||||
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
|
||||
}
|
||||
params[k] = v
|
||||
}
|
||||
@ -917,7 +915,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{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
}
|
||||
return loggingOptsMap, nil
|
||||
}
|
||||
@ -931,7 +929,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, fmt.Errorf("invalid --security-opt: %q", opt)
|
||||
return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt)
|
||||
}
|
||||
if k == "seccomp" {
|
||||
switch v {
|
||||
@ -942,11 +940,11 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
|
||||
// content if it's valid JSON.
|
||||
f, err := os.ReadFile(v)
|
||||
if err != nil {
|
||||
return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err)
|
||||
return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", v, err)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if err := json.Compact(b, f); err != nil {
|
||||
return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err)
|
||||
return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", v, err)
|
||||
}
|
||||
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
|
||||
}
|
||||
@ -981,7 +979,7 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) {
|
||||
for _, option := range storageOpts {
|
||||
k, v, ok := strings.Cut(option, "=")
|
||||
if !ok {
|
||||
return nil, errors.New("invalid storage option")
|
||||
return nil, errors.Errorf("invalid storage option")
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
@ -996,7 +994,7 @@ func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
|
||||
case "windows":
|
||||
return parseWindowsDevice(device)
|
||||
}
|
||||
return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
return container.DeviceMapping{}, errors.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
|
||||
// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
|
||||
@ -1020,7 +1018,7 @@ func parseLinuxDevice(device string) (container.DeviceMapping, error) {
|
||||
case 1:
|
||||
src = arr[0]
|
||||
default:
|
||||
return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device)
|
||||
return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device)
|
||||
}
|
||||
|
||||
if dst == "" {
|
||||
@ -1050,7 +1048,7 @@ func validateDeviceCgroupRule(val string) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return val, fmt.Errorf("invalid device cgroup format '%s'", val)
|
||||
return val, errors.Errorf("invalid device cgroup format '%s'", val)
|
||||
}
|
||||
|
||||
// validDeviceMode checks if the mode for device is valid or not.
|
||||
@ -1082,7 +1080,7 @@ func validateDevice(val string, serverOS string) (string, error) {
|
||||
// Windows does validation entirely server-side
|
||||
return val, nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
return "", errors.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
|
||||
// validateLinuxPath is the implementation of validateDevice knowing that the
|
||||
@ -1097,12 +1095,12 @@ func validateLinuxPath(val string, validator func(string) bool) (string, error)
|
||||
var mode string
|
||||
|
||||
if strings.Count(val, ":") > 2 {
|
||||
return val, fmt.Errorf("bad format for path: %s", val)
|
||||
return val, errors.Errorf("bad format for path: %s", val)
|
||||
}
|
||||
|
||||
split := strings.SplitN(val, ":", 3)
|
||||
if split[0] == "" {
|
||||
return val, fmt.Errorf("bad format for path: %s", val)
|
||||
return val, errors.Errorf("bad format for path: %s", val)
|
||||
}
|
||||
switch len(split) {
|
||||
case 1:
|
||||
@ -1121,13 +1119,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, fmt.Errorf("bad mode specified: %s", mode)
|
||||
return val, errors.Errorf("bad mode specified: %s", mode)
|
||||
}
|
||||
val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode)
|
||||
}
|
||||
|
||||
if !path.IsAbs(containerPath) {
|
||||
return val, fmt.Errorf("%s is not an absolute path", containerPath)
|
||||
return val, errors.Errorf("%s is not an absolute path", containerPath)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
@ -1135,23 +1133,10 @@ 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)
|
||||
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
|
||||
for _, str := range []string{"stdin", "stdout", "stderr"} {
|
||||
if s == str {
|
||||
return s, nil
|
||||
}
|
||||
netIPs = append(netIPs, addr)
|
||||
}
|
||||
return netIPs
|
||||
return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR")
|
||||
}
|
||||
|
||||
@ -4,31 +4,21 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
networktypes "github.com/moby/moby/api/types/network"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"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",
|
||||
@ -58,7 +48,7 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
// TODO(dnephin): fix tests to accept ContainerConfig; see https://github.com/moby/moby/pull/31621
|
||||
// TODO: fix tests to accept ContainerConfig
|
||||
containerCfg, err := parse(flags, copts, runtime.GOOS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@ -360,9 +350,13 @@ 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)
|
||||
}
|
||||
_, hostConfig, nwConfig := mustParse(t, validMacAddress)
|
||||
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.
|
||||
}
|
||||
defaultNw := hostConfig.NetworkMode.NetworkName()
|
||||
if nwConfig.EndpointsConfig[defaultNw].MacAddress.String() != "92:d0:c6:0a:29:33" {
|
||||
if nwConfig.EndpointsConfig[defaultNw].MacAddress != "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)
|
||||
}
|
||||
}
|
||||
@ -435,55 +429,56 @@ func TestParseHostnameDomainname(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseWithExpose(t *testing.T) {
|
||||
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`,
|
||||
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)
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
for expose, exposedPorts := range valids {
|
||||
config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
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")},
|
||||
if len(config.ExposedPorts) != len(exposedPorts) {
|
||||
t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(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])
|
||||
}
|
||||
})
|
||||
for _, port := range exposedPorts {
|
||||
if _, ok := config.ExposedPorts[port]; !ok {
|
||||
t.Fatalf("Expected %v, got %v", exposedPorts, 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])
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDevice(t *testing.T) {
|
||||
@ -581,6 +576,7 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
name string
|
||||
flags []string
|
||||
expected map[string]*networktypes.EndpointSettings
|
||||
expectedCfg container.Config
|
||||
expectedHostCfg container.HostConfig
|
||||
expectedErr string
|
||||
}{
|
||||
@ -612,9 +608,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
expected: map[string]*networktypes.EndpointSettings{
|
||||
"net1": {
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
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")},
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
||||
},
|
||||
Links: []string{"foo:bar", "bar:baz"},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
@ -642,9 +638,9 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"net1": {
|
||||
DriverOpts: map[string]string{"field1": "value1"},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
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")},
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
LinkLocalIPs: []string{"169.254.2.2", "fe80::169:254:2:2"},
|
||||
},
|
||||
Links: []string{"foo:bar", "bar:baz"},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
@ -653,15 +649,15 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"net3": {
|
||||
DriverOpts: map[string]string{"field3": "value3"},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
},
|
||||
Aliases: []string{"web3"},
|
||||
},
|
||||
"net4": {
|
||||
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
||||
MacAddress: "02:32:1c:23:00:04",
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
LinkLocalIPs: []netip.Addr{netip.MustParseAddr("169.254.169.254")},
|
||||
LinkLocalIPs: []string{"169.254.169.254"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -677,13 +673,14 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
"field2": "value2",
|
||||
},
|
||||
IPAMConfig: &networktypes.EndpointIPAMConfig{
|
||||
IPv4Address: netip.MustParseAddr("172.20.88.22"),
|
||||
IPv6Address: netip.MustParseAddr("2001:db8::8822"),
|
||||
IPv4Address: "172.20.88.22",
|
||||
IPv6Address: "2001:db8::8822",
|
||||
},
|
||||
Aliases: []string{"web1", "web2"},
|
||||
MacAddress: mustParseMAC("02:32:1c:23:00:04"),
|
||||
MacAddress: "02:32:1c:23:00:04",
|
||||
},
|
||||
},
|
||||
expectedCfg: container.Config{MacAddress: "02:32:1c:23:00:04"},
|
||||
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
||||
},
|
||||
{
|
||||
@ -698,9 +695,10 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
expected: map[string]*networktypes.EndpointSettings{
|
||||
"net1": {
|
||||
Aliases: []string{"foobar"},
|
||||
MacAddress: mustParseMAC("52:0f:f3:dc:50:10"),
|
||||
MacAddress: "52:0f:f3:dc:50:10",
|
||||
},
|
||||
},
|
||||
expectedCfg: container.Config{MacAddress: "52:0f:f3:dc:50:10"},
|
||||
expectedHostCfg: container.HostConfig{NetworkMode: "net1"},
|
||||
},
|
||||
{
|
||||
@ -747,7 +745,7 @@ func TestParseNetworkConfig(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, hConfig, nwConfig, err := parseRun(tc.flags)
|
||||
config, hConfig, nwConfig, err := parseRun(tc.flags)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, err, tc.expectedErr)
|
||||
@ -755,8 +753,9 @@ 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, cmpopts.EquateComparable(netip.Addr{}))
|
||||
assert.DeepEqual(t, nwConfig.EndpointsConfig, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1018,7 +1017,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(config.Entrypoint, []string{"anything"}))
|
||||
assert.Check(t, is.DeepEqual([]string(config.Entrypoint), []string{"anything"}))
|
||||
}
|
||||
|
||||
func TestValidateDevice(t *testing.T) {
|
||||
|
||||
@ -8,8 +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/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -17,7 +16,13 @@ type pauseOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// newPauseCommand creates a new cobra.Command for "docker container pause"
|
||||
// NewPauseCommand creates a new cobra.Command for `docker pause`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPauseCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newPauseCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newPauseCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts pauseOptions
|
||||
|
||||
@ -35,16 +40,12 @@ func newPauseCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
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, func(ctx context.Context, container string) error {
|
||||
_, err := apiClient.ContainerPause(ctx, container, client.ContainerPauseOptions{})
|
||||
return err
|
||||
})
|
||||
errChan := parallelOperation(ctx, opts.containers, apiClient.ContainerPause)
|
||||
|
||||
var errs []error
|
||||
for _, ctr := range opts.containers {
|
||||
|
||||
@ -8,13 +8,18 @@ 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{})
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cmd := newPauseCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
@ -36,8 +41,8 @@ func TestRunPause(t *testing.T) {
|
||||
func TestRunPauseClientError(t *testing.T) {
|
||||
cli := test.NewFakeCli(
|
||||
&fakeClient{
|
||||
containerPauseFunc: func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
||||
return client.ContainerPauseResult{}, fmt.Errorf("client error for container %s", container)
|
||||
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||
return fmt.Errorf("client error for container %s", container)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@ -5,14 +5,15 @@ 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/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -22,8 +23,14 @@ type portOptions struct {
|
||||
port string
|
||||
}
|
||||
|
||||
// newPortCommand creates a new cobra.Command for "docker container port".
|
||||
func newPortCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewPortCommand creates a new cobra.Command for `docker port`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return newPortCommand(dockerCli)
|
||||
}
|
||||
|
||||
func newPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts portOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -35,13 +42,12 @@ 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),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
@ -53,28 +59,31 @@ 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, client.ContainerInspectOptions{})
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out []string
|
||||
if opts.port != "" {
|
||||
port, err := network.ParsePort(opts.port)
|
||||
if err != nil {
|
||||
return err
|
||||
port, proto, _ := strings.Cut(opts.port, "/")
|
||||
if proto == "" {
|
||||
proto = "tcp"
|
||||
}
|
||||
frontends, exists := c.Container.NetworkSettings.Ports[port]
|
||||
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)]
|
||||
if !exists || len(frontends) == 0 {
|
||||
return fmt.Errorf("no public port '%s' published for %s", opts.port, opts.container)
|
||||
return errors.Errorf("Error: No public port '%s' published for %s", opts.port, opts.container)
|
||||
}
|
||||
for _, frontend := range frontends {
|
||||
out = append(out, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort))
|
||||
out = append(out, net.JoinHostPort(frontend.HostIP, frontend.HostPort))
|
||||
}
|
||||
} else {
|
||||
for from, frontends := range c.Container.NetworkSettings.Ports {
|
||||
for from, frontends := range c.NetworkSettings.Ports {
|
||||
for _, frontend := range frontends {
|
||||
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP.String(), frontend.HostPort)))
|
||||
out = append(out, fmt.Sprintf("%s -> %s", from, net.JoinHostPort(frontend.HostIP, frontend.HostPort)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,11 @@ package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
@ -16,56 +14,56 @@ import (
|
||||
func TestNewPortCommandOutput(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
ips []netip.Addr
|
||||
ips []string
|
||||
port string
|
||||
}{
|
||||
{
|
||||
name: "container-port-ipv4",
|
||||
ips: []netip.Addr{netip.MustParseAddr("0.0.0.0")},
|
||||
ips: []string{"0.0.0.0"},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6",
|
||||
ips: []netip.Addr{netip.MustParseAddr("::")},
|
||||
ips: []string{"::"},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6-and-ipv4",
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
port: "80",
|
||||
},
|
||||
{
|
||||
name: "container-port-ipv6-and-ipv4-443-udp",
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
port: "443/udp",
|
||||
},
|
||||
{
|
||||
name: "container-port-all-ports",
|
||||
ips: []netip.Addr{netip.MustParseAddr("::"), netip.MustParseAddr("0.0.0.0")},
|
||||
ips: []string{"::", "0.0.0.0"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
inspectFunc: func(string) (client.ContainerInspectResult, error) {
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
ci := container.InspectResponse{NetworkSettings: &container.NetworkSettings{}}
|
||||
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)),
|
||||
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)),
|
||||
}
|
||||
for i, ip := range tc.ips {
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("80/tcp")][i] = network.PortBinding{
|
||||
ci.NetworkSettings.Ports["80/tcp"][i] = nat.PortBinding{
|
||||
HostIP: ip, HostPort: "3456",
|
||||
}
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("443/tcp")][i] = network.PortBinding{
|
||||
ci.NetworkSettings.Ports["443/tcp"][i] = nat.PortBinding{
|
||||
HostIP: ip, HostPort: "4567",
|
||||
}
|
||||
ci.NetworkSettings.Ports[network.MustParsePort("443/udp")][i] = network.PortBinding{
|
||||
ci.NetworkSettings.Ports["443/udp"][i] = nat.PortBinding{
|
||||
HostIP: ip, HostPort: "5678",
|
||||
}
|
||||
}
|
||||
return client.ContainerInspectResult{Container: ci}, nil
|
||||
return ci, nil
|
||||
},
|
||||
})
|
||||
cmd := newPortCommand(cli)
|
||||
|
||||
@ -2,33 +2,29 @@ package container
|
||||
|
||||
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/docker/go-units"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
"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.
|
||||
// NewPruneCommand returns a new cobra prune command for containers
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newPruneCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := pruneOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
@ -47,9 +43,8 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -62,7 +57,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, _ error) {
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
@ -75,39 +70,28 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := dockerCli.Client().ContainerPrune(ctx, client.ContainerPruneOptions{
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
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")
|
||||
if len(report.ContainersDeleted) > 0 {
|
||||
output = "Deleted Containers:\n"
|
||||
for _, id := range report.ContainersDeleted {
|
||||
output += id + "\n"
|
||||
}
|
||||
spaceReclaimed = res.Report.SpaceReclaimed
|
||||
spaceReclaimed = report.SpaceReclaimed
|
||||
}
|
||||
|
||||
return spaceReclaimed, out.String(), nil
|
||||
return spaceReclaimed, output, nil
|
||||
}
|
||||
|
||||
type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// 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,
|
||||
})
|
||||
// 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})
|
||||
}
|
||||
|
||||
@ -7,7 +7,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
@ -15,8 +16,8 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerPruneFunc: func(ctx context.Context, opts client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
||||
return client.ContainerPruneResult{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := newPruneCommand(cli)
|
||||
|
||||
@ -1,36 +1,60 @@
|
||||
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/moby/moby/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newRenameCommand creates a new cobra.Command for "docker container rename".
|
||||
type renameOptions struct {
|
||||
oldName string
|
||||
newName string
|
||||
}
|
||||
|
||||
// NewRenameCommand creates a new cobra.Command for `docker rename`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewRenameCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newRenameCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newRenameCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts renameOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rename CONTAINER NEW_NAME",
|
||||
Short: "Rename a container",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
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
|
||||
opts.oldName = args[0]
|
||||
opts.newName = args[1]
|
||||
return runRename(cmd.Context(), dockerCLI, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container rename, docker rename",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, 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
|
||||
}
|
||||
|
||||
77
cli/command/container/rename_test.go
Normal file
77
cli/command/container/rename_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
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"))
|
||||
}
|
||||
@ -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/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -20,7 +20,13 @@ type restartOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// newRestartCommand creates a new cobra.Command for "docker container restart".
|
||||
// NewRestartCommand creates a new cobra.Command for `docker restart`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewRestartCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newRestartCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newRestartCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts restartOptions
|
||||
|
||||
@ -39,8 +45,7 @@ func newRestartCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container restart, docker restart",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -66,7 +71,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, client.ContainerRestartOptions{
|
||||
err := apiClient.ContainerRestart(ctx, name, container.StopOptions{
|
||||
Signal: opts.signal,
|
||||
Timeout: timeout,
|
||||
})
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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 client.ContainerRestartOptions
|
||||
expectedOpts container.StopOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
@ -36,19 +36,19 @@ func TestRestart(t *testing.T) {
|
||||
{
|
||||
name: "with -t",
|
||||
args: []string{"-t", "2", "container-1"},
|
||||
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --timeout",
|
||||
args: []string{"--timeout", "2", "container-1"},
|
||||
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
name: "with --time",
|
||||
args: []string{"--time", "2", "container-1"},
|
||||
expectedOpts: client.ContainerRestartOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
expectedOpts: container.StopOptions{Timeout: func(to int) *int { return &to }(2)},
|
||||
restarted: []string{"container-1"},
|
||||
},
|
||||
{
|
||||
@ -62,17 +62,17 @@ func TestRestart(t *testing.T) {
|
||||
mutex := new(sync.Mutex)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerRestartFunc: func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
|
||||
containerRestartFunc: func(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
assert.Check(t, is.DeepEqual(options, tc.expectedOpts))
|
||||
if containerID == "nosuchcontainer" {
|
||||
return client.ContainerRestartResult{}, notFound(errors.New("Error: no such container: " + containerID))
|
||||
return 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 client.ContainerRestartResult{}, nil
|
||||
return nil
|
||||
},
|
||||
Version: "1.36",
|
||||
})
|
||||
|
||||
@ -10,8 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -23,7 +22,13 @@ type rmOptions struct {
|
||||
containers []string
|
||||
}
|
||||
|
||||
// newRmCommand creates a new cobra.Command for "docker container rm".
|
||||
// NewRmCommand creates a new cobra.Command for `docker rm`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewRmCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newRmCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newRmCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts rmOptions
|
||||
|
||||
@ -41,7 +46,6 @@ func newRmCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
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()
|
||||
@ -67,12 +71,11 @@ func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error {
|
||||
if ctrID == "" {
|
||||
return errors.New("container name cannot be empty")
|
||||
}
|
||||
_, err := apiClient.ContainerRemove(ctx, ctrID, client.ContainerRemoveOptions{
|
||||
return apiClient.ContainerRemove(ctx, ctrID, container.RemoveOptions{
|
||||
RemoveVolumes: opts.rmVolumes,
|
||||
RemoveLinks: opts.rmLink,
|
||||
Force: opts.force,
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
var errs []error
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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 client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
|
||||
containerRemoveFunc: func(ctx context.Context, container string, options container.RemoveOptions) error {
|
||||
// containerRemoveFunc is called in parallel for each container
|
||||
// by the remove command so append must be synchronized.
|
||||
mutex.Lock()
|
||||
@ -35,9 +35,9 @@ func TestRemoveForce(t *testing.T) {
|
||||
mutex.Unlock()
|
||||
|
||||
if container == "nosuchcontainer" {
|
||||
return client.ContainerRemoveResult{}, notFound(errors.New("Error: no such container: " + container))
|
||||
return notFound(errors.New("Error: no such container: " + container))
|
||||
}
|
||||
return client.ContainerRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
Version: "1.36",
|
||||
})
|
||||
|
||||
@ -2,21 +2,20 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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"
|
||||
@ -29,7 +28,13 @@ type runOptions struct {
|
||||
detachKeys string
|
||||
}
|
||||
|
||||
// newRunCommand create a new "docker run" command.
|
||||
// NewRunCommand create a new `docker run` command
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewRunCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newRunCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newRunCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options runOptions
|
||||
var copts *containerOptions
|
||||
@ -50,7 +55,6 @@ func newRunCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"category-top": "1",
|
||||
"aliases": "docker container run, docker run",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -70,17 +74,20 @@ func newRunCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
// 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")
|
||||
addPlatformFlag(flags, &options.platform)
|
||||
flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification")
|
||||
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, cobra.NoFileCompletions)
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -175,7 +182,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, client.ContainerAttachOptions{
|
||||
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: config.AttachStdin,
|
||||
Stdout: config.AttachStdout,
|
||||
@ -195,7 +202,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, client.ContainerStartOptions{}); err != nil {
|
||||
if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {
|
||||
// If we have hijackedIOStreamer, we should notify
|
||||
// hijackedIOStreamer we are going to exit and wait
|
||||
// to avoid the terminal are not restored.
|
||||
@ -258,7 +265,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 client.ContainerAttachOptions) (func(), error) {
|
||||
func attachContainer(ctx context.Context, dockerCli command.Cli, containerID string, errCh *chan error, config *container.Config, options container.AttachOptions) (func(), error) {
|
||||
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options)
|
||||
if errAttach != nil {
|
||||
return nil, errAttach
|
||||
@ -292,7 +299,7 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: cerr,
|
||||
resp: resp.HijackedResponse,
|
||||
resp: resp,
|
||||
tty: config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
@ -303,7 +310,7 @@ func attachContainer(ctx context.Context, dockerCli command.Cli, containerID str
|
||||
return errAttach
|
||||
}()
|
||||
}()
|
||||
return resp.HijackedResponse.Close, nil
|
||||
return resp.Close, nil
|
||||
}
|
||||
|
||||
// withHelp decorates the error with a suggestion to use "--help".
|
||||
|
||||
@ -13,11 +13,14 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"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/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/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -53,10 +56,12 @@ func TestRunValidateFlags(t *testing.T) {
|
||||
|
||||
func TestRunLabel(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{ID: "id"}, nil
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
Version: client.MaxAPIVersion,
|
||||
Version: "1.36",
|
||||
})
|
||||
cmd := newRunCommand(fakeCLI)
|
||||
cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"})
|
||||
@ -74,35 +79,32 @@ func TestRunAttach(t *testing.T) {
|
||||
var conn net.Conn
|
||||
attachCh := make(chan struct{})
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{ID: "id"}, nil
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
||||
server, clientConn := net.Pipe()
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
server, client := net.Pipe()
|
||||
conn = server
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
attachCh <- struct{}{}
|
||||
return client.ContainerAttachResult{
|
||||
HijackedResponse: client.NewHijackedResponse(clientConn, types.MediaTypeRawStream),
|
||||
}, nil
|
||||
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
|
||||
},
|
||||
waitFunc: func(_ string) client.ContainerWaitResult {
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 33,
|
||||
}
|
||||
return client.ContainerWaitResult{
|
||||
Result: responseChan,
|
||||
Error: errChan,
|
||||
}
|
||||
return responseChan, errChan
|
||||
},
|
||||
// use new (non-legacy) wait API
|
||||
// see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: client.MaxAPIVersion,
|
||||
// see: 38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: "1.30",
|
||||
}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
@ -148,41 +150,38 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
killCh := make(chan struct{})
|
||||
attachCh := make(chan struct{})
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{ID: "id"}, nil
|
||||
createContainerFunc: func(_ *container.Config, _ *container.HostConfig, _ *network.NetworkingConfig, _ *ocispec.Platform, _ string) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
if options.Signal == "TERM" {
|
||||
containerKillFunc: func(ctx context.Context, containerID, sig string) error {
|
||||
if sig == "TERM" {
|
||||
close(killCh)
|
||||
}
|
||||
return client.ContainerKillResult{}, nil
|
||||
return nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
||||
server, clientConn := net.Pipe()
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
server, client := net.Pipe()
|
||||
conn = server
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
attachCh <- struct{}{}
|
||||
return client.ContainerAttachResult{
|
||||
HijackedResponse: client.NewHijackedResponse(clientConn, types.MediaTypeRawStream),
|
||||
}, nil
|
||||
return types.NewHijackedResponse(client, types.MediaTypeRawStream), nil
|
||||
},
|
||||
waitFunc: func(_ string) client.ContainerWaitResult {
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
<-killCh
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 130,
|
||||
}
|
||||
return client.ContainerWaitResult{
|
||||
Result: responseChan,
|
||||
Error: errChan,
|
||||
}
|
||||
return responseChan, errChan
|
||||
},
|
||||
// use new (non-legacy) wait API
|
||||
// see: https://github.com/docker/cli/commit/38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: client.MaxAPIVersion,
|
||||
// see: 38591f20d07795aaef45d400df89ca12f29c603b
|
||||
Version: "1.30",
|
||||
}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
@ -228,14 +227,16 @@ func TestRunPullTermination(t *testing.T) {
|
||||
|
||||
attachCh := make(chan struct{})
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{}, errors.New("shouldn't try to create a container")
|
||||
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")
|
||||
},
|
||||
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")
|
||||
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")
|
||||
},
|
||||
imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
server, respReader := net.Pipe()
|
||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
server, client := net.Pipe()
|
||||
t.Cleanup(func() {
|
||||
_ = server.Close()
|
||||
})
|
||||
@ -259,9 +260,9 @@ func TestRunPullTermination(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
attachCh <- struct{}{}
|
||||
return fakeStreamResult{ReadCloser: respReader}, nil
|
||||
return client, nil
|
||||
},
|
||||
Version: client.MaxAPIVersion,
|
||||
Version: "1.30",
|
||||
})
|
||||
|
||||
cmd := newRunCommand(fakeCLI)
|
||||
@ -294,6 +295,59 @@ 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) {
|
||||
t.Setenv("DOCKER_CONTENT_TRUST", "true")
|
||||
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")
|
||||
},
|
||||
})
|
||||
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
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -48,10 +48,7 @@ func ForwardAllSignals(ctx context.Context, apiClient client.ContainerAPIClient,
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := apiClient.ContainerKill(ctx, cid, client.ContainerKillOptions{
|
||||
Signal: sig,
|
||||
})
|
||||
if err != nil {
|
||||
if err := apiClient.ContainerKill(ctx, cid, sig); err != nil {
|
||||
logrus.Debugf("Error sending signal: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/signal"
|
||||
)
|
||||
|
||||
@ -15,9 +14,9 @@ func TestForwardSignals(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
called := make(chan struct{})
|
||||
apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
|
||||
close(called)
|
||||
return client.ContainerKillResult{}, nil
|
||||
return nil
|
||||
}}
|
||||
|
||||
sigc := make(chan os.Signal)
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"golang.org/x/sys/unix"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
@ -23,9 +22,9 @@ func TestIgnoredSignals(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
var called bool
|
||||
apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
|
||||
called = true
|
||||
return client.ContainerKillResult{}, nil
|
||||
return nil
|
||||
}}
|
||||
|
||||
sigc := make(chan os.Signal)
|
||||
|
||||
@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
@ -10,10 +9,10 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -28,8 +27,14 @@ type StartOptions struct {
|
||||
Containers []string
|
||||
}
|
||||
|
||||
// newStartCommand creates a new cobra.Command for "docker container start".
|
||||
func newStartCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return newStartCommand(dockerCli)
|
||||
}
|
||||
|
||||
func newStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts StartOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -38,15 +43,14 @@ 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()
|
||||
@ -80,16 +84,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, client.ContainerInspectOptions{})
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, ctr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We always use c.ID instead of container to maintain consistency during `docker start`
|
||||
if !c.Container.Config.Tty {
|
||||
if !c.Config.Tty {
|
||||
sigc := notifyAllSignals()
|
||||
bgCtx := context.WithoutCancel(ctx)
|
||||
go ForwardAllSignals(bgCtx, dockerCli.Client(), c.Container.ID, sigc)
|
||||
go ForwardAllSignals(bgCtx, dockerCli.Client(), c.ID, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
@ -98,9 +102,9 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := client.ContainerAttachOptions{
|
||||
options := container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: opts.OpenStdin && c.Container.Config.OpenStdin,
|
||||
Stdin: opts.OpenStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
DetachKeys: detachKeys,
|
||||
@ -112,11 +116,11 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
in = dockerCli.In()
|
||||
}
|
||||
|
||||
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.Container.ID, options)
|
||||
resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
defer resp.HijackedResponse.Close()
|
||||
defer resp.Close()
|
||||
|
||||
cErr := make(chan error, 1)
|
||||
|
||||
@ -127,8 +131,8 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
inputStream: in,
|
||||
outputStream: dockerCli.Out(),
|
||||
errorStream: dockerCli.Err(),
|
||||
resp: resp.HijackedResponse,
|
||||
tty: c.Container.Config.Tty,
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
@ -142,17 +146,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.Container.ID, c.Container.HostConfig.AutoRemove)
|
||||
statusChan := waitExitOrRemoved(ctx, dockerCli.Client(), c.ID, c.HostConfig.AutoRemove)
|
||||
|
||||
// 4. Start the container.
|
||||
_, err = dockerCli.Client().ContainerStart(ctx, c.Container.ID, client.ContainerStartOptions{
|
||||
err = dockerCli.Client().ContainerStart(ctx, c.ID, container.StartOptions{
|
||||
CheckpointID: opts.Checkpoint,
|
||||
CheckpointDir: opts.CheckpointDir,
|
||||
})
|
||||
if err != nil {
|
||||
cancelFun()
|
||||
<-cErr
|
||||
if c.Container.HostConfig.AutoRemove {
|
||||
if c.HostConfig.AutoRemove {
|
||||
// wait container to be removed
|
||||
<-statusChan
|
||||
}
|
||||
@ -160,14 +164,13 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
}
|
||||
|
||||
// 5. Wait for attachment to break.
|
||||
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 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 attachErr := <-cErr; attachErr != nil {
|
||||
var escapeError term.EscapeError
|
||||
if errors.As(attachErr, &escapeError) {
|
||||
if _, ok := attachErr.(term.EscapeError); ok {
|
||||
// The user entered the detach escape sequence.
|
||||
return nil
|
||||
}
|
||||
@ -183,11 +186,10 @@ 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]
|
||||
_, err := dockerCli.Client().ContainerStart(ctx, ctr, client.ContainerStartOptions{
|
||||
return dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{
|
||||
CheckpointID: opts.Checkpoint,
|
||||
CheckpointDir: opts.CheckpointDir,
|
||||
})
|
||||
return err
|
||||
default:
|
||||
// We're not going to attach to anything.
|
||||
// Start as many containers as we want.
|
||||
@ -198,16 +200,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, client.ContainerStartOptions{}); err != nil {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), err)
|
||||
if err := dockerCli.Client().ContainerStart(ctx, ctr, container.StartOptions{}); 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 fmt.Errorf("failed to start containers: %s", strings.Join(failedContainers, ", "))
|
||||
return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -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/moby/moby/api/types/events"
|
||||
"github.com/moby/moby/client"
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -60,10 +60,16 @@ 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 client.Filters
|
||||
Filters *filters.Args
|
||||
}
|
||||
|
||||
// NewStatsCommand creates a new [cobra.Command] for "docker stats".
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewStatsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newStatsCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newStatsCommand creates a new [cobra.Command] for "docker container stats".
|
||||
func newStatsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := StatsOptions{}
|
||||
|
||||
@ -78,8 +84,7 @@ func newStatsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container stats, docker stats",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -108,17 +113,10 @@ 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 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)
|
||||
// closeChan is a non-buffered channel used to collect errors from goroutines.
|
||||
closeChan := make(chan error)
|
||||
cStats := stats{}
|
||||
|
||||
showAll := len(options.Containers) == 0
|
||||
@ -130,47 +128,35 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
started := make(chan struct{})
|
||||
|
||||
if options.Filters == nil {
|
||||
options.Filters = make(client.Filters)
|
||||
f := filters.NewArgs()
|
||||
options.Filters = &f
|
||||
}
|
||||
|
||||
// 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 + "'")
|
||||
}
|
||||
if err := options.Filters.Validate(acceptedStatsFilters); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eh := newEventHandler()
|
||||
if options.All {
|
||||
eh.setHandler(events.ActionCreate, func(e events.Message) {
|
||||
if s := NewStats(e.Actor.ID); cStats.add(s) {
|
||||
s := NewStats(e.Actor.ID)
|
||||
if 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) {
|
||||
if s := NewStats(e.Actor.ID); cStats.add(s) {
|
||||
s := NewStats(e.Actor.ID)
|
||||
if 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) {
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"event": e.Action,
|
||||
"container": e.Actor.ID,
|
||||
}).Debug("stop collecting stats for container")
|
||||
cStats.remove(e.Actor.ID)
|
||||
})
|
||||
}
|
||||
@ -182,8 +168,9 @@ 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().Add("type", string(events.ContainerEventType))
|
||||
res := apiClient.Events(ctx, client.EventsListOptions{
|
||||
f := options.Filters.Clone()
|
||||
f.Add("type", string(events.ContainerEventType))
|
||||
eventChan, errChan := apiClient.Events(ctx, events.ListOptions{
|
||||
Filters: f,
|
||||
})
|
||||
|
||||
@ -196,17 +183,10 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
select {
|
||||
case <-stopped:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case event := <-res.Messages:
|
||||
case event := <-eventChan:
|
||||
c <- event
|
||||
case err := <-res.Err:
|
||||
// Prevent blocking if closeChan is full or unread
|
||||
select {
|
||||
case closeChan <- err:
|
||||
default:
|
||||
// drop if not read; avoids deadlock
|
||||
}
|
||||
case err := <-errChan:
|
||||
closeChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -222,19 +202,17 @@ 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, client.ContainerListOptions{
|
||||
cs, err := apiClient.ContainerList(ctx, container.ListOptions{
|
||||
All: options.All,
|
||||
Filters: options.Filters,
|
||||
Filters: *options.Filters,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ctr := range cs.Items {
|
||||
if s := NewStats(ctr.ID); cStats.add(s) {
|
||||
for _, ctr := range cs {
|
||||
s := NewStats(ctr.ID)
|
||||
if 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)
|
||||
}
|
||||
}
|
||||
@ -246,25 +224,22 @@ 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 len(options.Filters) > 0 {
|
||||
if options.Filters != nil && options.Filters.Len() > 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 {
|
||||
if s := NewStats(ctr); cStats.add(s) {
|
||||
s := NewStats(ctr)
|
||||
if 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 and disabled.
|
||||
// We don't expect any asynchronous errors: closeChan can be closed.
|
||||
close(closeChan)
|
||||
closeChan = nil
|
||||
|
||||
// make sure each container get at least one valid stat data
|
||||
waitFirst.Wait()
|
||||
@ -283,13 +258,19 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
|
||||
format := options.Format
|
||||
if format == "" {
|
||||
if len(format) == 0 {
|
||||
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.
|
||||
@ -300,68 +281,63 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
Format: NewStatsFormat(format, daemonOSType),
|
||||
}
|
||||
|
||||
if options.NoStream {
|
||||
var err error
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
var ccStats []StatsEntry
|
||||
cStats.mu.RLock()
|
||||
ccStats := make([]StatsEntry, 0, len(cStats.cs))
|
||||
for _, c := range cStats.cs {
|
||||
ccStats = append(ccStats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.RUnlock()
|
||||
|
||||
if len(ccStats) == 0 {
|
||||
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()
|
||||
|
||||
if !options.NoStream {
|
||||
// Start by moving the cursor to the top-left
|
||||
_, _ = fmt.Fprint(&statsTextBuffer, "\033[H")
|
||||
}
|
||||
|
||||
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
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(ccStats) == 0 && !showAll {
|
||||
return nil
|
||||
}
|
||||
if len(cStats.cs) == 0 && !showAll {
|
||||
break
|
||||
}
|
||||
if options.NoStream {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case err, ok := <-closeChan:
|
||||
if !ok || err == nil || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
// Suppress "unexpected EOF" errors in the CLI so that
|
||||
// it shuts down cleanly when the daemon restarts.
|
||||
return nil
|
||||
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
|
||||
}
|
||||
}
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
// just skip
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newEventHandler initializes and returns an eventHandler
|
||||
@ -388,11 +364,11 @@ func (eh *eventHandler) watch(c <-chan events.Message) {
|
||||
continue
|
||||
}
|
||||
if e.Actor.ID == "" {
|
||||
log.G(context.TODO()).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action)
|
||||
logrus.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)
|
||||
logrus.WithField("event", e).Debugf("event handler: received %s event for: %s", e.Action, e.Actor.ID)
|
||||
go h(e)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user