mirror of https://github.com/docker/cli.git
Compare commits
169 Commits
2b6cc64716
...
ef1405f869
Author | SHA1 | Date |
---|---|---|
Sebastiaan van Stijn | ef1405f869 | |
Sebastiaan van Stijn | 917d2dc837 | |
Sebastiaan van Stijn | f9497b8a46 | |
Paweł Gronowski | 382d4c34a9 | |
aevesdocker | 1440f9f8cf | |
Sebastiaan van Stijn | 9c01d924fb | |
Sebastiaan van Stijn | 8c22315e31 | |
Laura Brehm | 13754f6776 | |
Sebastiaan van Stijn | 9eb7b52189 | |
David Karlsson | 172f340112 | |
Sebastiaan van Stijn | 750b8ebcdc | |
Sebastiaan van Stijn | 4a7b04d412 | |
Sebastiaan van Stijn | d77760fe53 | |
Sebastiaan van Stijn | 32b40deb46 | |
Sebastiaan van Stijn | 40833fd296 | |
Sebastiaan van Stijn | 78a7e15032 | |
Sebastiaan van Stijn | 4a71ce02e6 | |
Sebastiaan van Stijn | 7d9ea25564 | |
Sebastiaan van Stijn | 046ac9714c | |
Sebastiaan van Stijn | 762b5a8df3 | |
Sebastiaan van Stijn | 417974cdc3 | |
Sebastiaan van Stijn | bf37e26b33 | |
Sebastiaan van Stijn | 6489a777e5 | |
Sebastiaan van Stijn | 20de861134 | |
Sebastiaan van Stijn | 1448cecba1 | |
Sebastiaan van Stijn | 67458f710d | |
Sebastiaan van Stijn | b45477bffa | |
Noah Silas | 0c999fe95b | |
Sebastiaan van Stijn | 5f1311ae8d | |
Sebastiaan van Stijn | 10c5a57927 | |
Sebastiaan van Stijn | 5e40d288c7 | |
Sebastiaan van Stijn | 9ba73a1a05 | |
Sebastiaan van Stijn | f3cf1b4213 | |
Sebastiaan van Stijn | cae19e3928 | |
Sebastiaan van Stijn | 074d1028b5 | |
Sebastiaan van Stijn | 1dbcce2057 | |
Sebastiaan van Stijn | 1bba009944 | |
Sebastiaan van Stijn | e3942d46a0 | |
Sebastiaan van Stijn | 97ff1b7c0a | |
Sebastiaan van Stijn | 4c85feb4dd | |
Sebastiaan van Stijn | 3b48a57b04 | |
Sebastiaan van Stijn | 36e6c42977 | |
Sebastiaan van Stijn | 84bfa52a6c | |
Sebastiaan van Stijn | 7a94f592ed | |
Sebastiaan van Stijn | ef197f7314 | |
Sebastiaan van Stijn | 02b92c699d | |
Laura Brehm | 2995631498 | |
Paweł Gronowski | fb103cb982 | |
Sebastiaan van Stijn | 42cda38840 | |
Sebastiaan van Stijn | 5e51513a8b | |
Giedrius Jonikas | 0b16070ae6 | |
Sebastiaan van Stijn | 9af049c618 | |
Sebastiaan van Stijn | 745629bd55 | |
Sebastiaan van Stijn | 7451339ab0 | |
Sebastiaan van Stijn | 020f3a7ad9 | |
Sebastiaan van Stijn | aa331e94cc | |
Sebastiaan van Stijn | 1875d9fdcb | |
Harald Albers | e1c5180dba | |
Harald Albers | d4f4cf1418 | |
Sebastiaan van Stijn | 59b90305f7 | |
Sebastiaan van Stijn | 32ff200fe6 | |
Sebastiaan van Stijn | e9ae9f788b | |
Sebastiaan van Stijn | 61baf2a3d9 | |
Sebastiaan van Stijn | c34b80bc65 | |
Sebastiaan van Stijn | ea511f0de2 | |
Sebastiaan van Stijn | 3c78069240 | |
Sebastiaan van Stijn | 0dd6f7f1b3 | |
Sebastiaan van Stijn | 5f4b14950e | |
Sebastiaan van Stijn | 1aab64dd90 | |
Sebastiaan van Stijn | 0ab0eca8bd | |
Sebastiaan van Stijn | d96f8b7f91 | |
Sebastiaan van Stijn | abb8e9b78a | |
Laura Brehm | 7029147458 | |
Paweł Gronowski | d2b87a0a3b | |
Sebastiaan van Stijn | 24ee5f228a | |
Sebastiaan van Stijn | 8b6133a2b7 | |
Sebastiaan van Stijn | d3f6867e4d | |
Sebastiaan van Stijn | 6b9083776f | |
Sebastiaan van Stijn | fb61156b05 | |
Sebastiaan van Stijn | 062eecf14a | |
Harald Albers | 3f7b156c85 | |
Sebastiaan van Stijn | 4b7a1e4613 | |
Sebastiaan van Stijn | 378a3d7d36 | |
Sebastiaan van Stijn | 54e3685bcd | |
Sebastiaan van Stijn | 3d8b49523d | |
Sebastiaan van Stijn | a21a5f4243 | |
Sebastiaan van Stijn | eda78e9cdc | |
Sebastiaan van Stijn | 581cf36bd4 | |
Sebastiaan van Stijn | a55cfe5f82 | |
Sebastiaan van Stijn | 3a8485085d | |
Sebastiaan van Stijn | d3bafa5f3e | |
Sebastiaan van Stijn | 71ebbb81ae | |
Sebastiaan van Stijn | 87acf77aef | |
Sebastiaan van Stijn | 9b525bc9d1 | |
Sebastiaan van Stijn | 8a7c5ae68f | |
Sebastiaan van Stijn | da9e984231 | |
Sebastiaan van Stijn | 670f81803f | |
Paweł Gronowski | 38653277af | |
Sebastiaan van Stijn | 12dcc6e25c | |
Sebastiaan van Stijn | cbbb917323 | |
Paweł Gronowski | 3590f946a3 | |
David Karlsson | 2c6b80491b | |
Paweł Gronowski | 09e16fc9c6 | |
Laura Brehm | dba4b15d6b | |
David Karlsson | 50ef0c58c2 | |
Sebastiaan van Stijn | 35d7b1a7a6 | |
Sebastiaan van Stijn | 3b38dc67be | |
Sebastiaan van Stijn | 31eeed7ca4 | |
Sebastiaan van Stijn | 089448ba6d | |
Sebastiaan van Stijn | 6ed137f7dd | |
Sebastiaan van Stijn | e1c472a436 | |
Sebastiaan van Stijn | 302d73f990 | |
Sebastiaan van Stijn | ab418a38d8 | |
Sebastiaan van Stijn | f3b4094eb0 | |
Sebastiaan van Stijn | be197da6b8 | |
Sebastiaan van Stijn | 51713196c9 | |
Sebastiaan van Stijn | a5ca5b33f1 | |
Sebastiaan van Stijn | 8f2e5662e7 | |
Sebastiaan van Stijn | b8cddc63ad | |
Sebastiaan van Stijn | a58faf7971 | |
Sebastiaan van Stijn | b6d27ff60e | |
Sebastiaan van Stijn | 200225f530 | |
Sebastiaan van Stijn | 9599251d07 | |
Sebastiaan van Stijn | ea8aa2a419 | |
Sebastiaan van Stijn | 61867feecf | |
Sebastiaan van Stijn | 843ae6d7e2 | |
Sebastiaan van Stijn | bea4ee6588 | |
Sebastiaan van Stijn | a88ee33f71 | |
Sebastiaan van Stijn | 21eea1e003 | |
Harald Albers | 147630a309 | |
Sebastiaan van Stijn | 88f1e99e8e | |
Sebastiaan van Stijn | f483aacd6b | |
Paweł Gronowski | d085e2445c | |
Paweł Gronowski | b0bb4ba7f2 | |
Paweł Gronowski | a20eb45b26 | |
Sebastiaan van Stijn | 6a78e9231a | |
Stavros Panakakis | 0319795d4f | |
Sebastiaan van Stijn | 305985c1b1 | |
Sebastiaan van Stijn | 2d47c9b222 | |
Sebastiaan van Stijn | 839dbbcf27 | |
Sebastiaan van Stijn | b64f265d0f | |
Sebastiaan van Stijn | 19eeb10155 | |
Sebastiaan van Stijn | 185622986e | |
Sebastiaan van Stijn | eb53046ad2 | |
Sebastiaan van Stijn | d42cf96e15 | |
Sebastiaan van Stijn | 8c7f713db6 | |
Sebastiaan van Stijn | ce1aebcc30 | |
Sebastiaan van Stijn | 55ae7d7113 | |
Sebastiaan van Stijn | fc7e779c57 | |
David Karlsson | 2f2b16a966 | |
Paul Rogalski | c70b2165a9 | |
Stavros Panakakis | 442c38636f | |
Austin Vazquez | a6ab65948e | |
Sebastiaan van Stijn | 7908982543 | |
Sebastiaan van Stijn | ff3f94a542 | |
Sebastiaan van Stijn | 9025c932b9 | |
Nicolas De Loof | 9ecfe4f5a7 | |
Sebastiaan van Stijn | 9dee12ff69 | |
Sebastiaan van Stijn | 76196dbb01 | |
Sebastiaan van Stijn | dac7319f10 | |
Sebastiaan van Stijn | 95e221ef4d | |
Sebastiaan van Stijn | 7c85db6e1a | |
Sebastiaan van Stijn | b129660dd3 | |
Laura Brehm | 30e9abbd3f | |
Sebastiaan van Stijn | d49e72c0ac | |
Stavros Panakakis | 46b360b059 | |
David Karlsson | 8a3d838a19 | |
Dilep Dev | 45d55961d0 | |
Sebastiaan van Stijn | e2986f4467 |
|
@ -62,7 +62,7 @@ jobs:
|
||||||
name: Update Go
|
name: Update Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22.7
|
go-version: 1.23.3
|
||||||
-
|
-
|
||||||
name: Initialize CodeQL
|
name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v3
|
||||||
|
|
|
@ -68,7 +68,7 @@ jobs:
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.22.7
|
go-version: 1.23.3
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
- copyloopvar # Detects places where loop variables are copied.
|
||||||
- depguard
|
- depguard
|
||||||
- dogsled
|
- dogsled
|
||||||
- dupword # Detects duplicate words.
|
- dupword # Detects duplicate words.
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- errchkjson
|
- errchkjson
|
||||||
- exportloopref # Detects pointers to enclosing loop variables.
|
|
||||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- gofumpt # Detects whether code was gofumpt-ed.
|
- gofumpt # Detects whether code was gofumpt-ed.
|
||||||
|
@ -41,6 +41,9 @@ linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
|
|
||||||
run:
|
run:
|
||||||
|
# prevent golangci-lint from deducting the go version to lint for through go.mod,
|
||||||
|
# which causes it to fallback to go1.17 semantics.
|
||||||
|
go: "1.23.3"
|
||||||
timeout: 5m
|
timeout: 5m
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
@ -52,6 +55,13 @@ linters-settings:
|
||||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 16
|
min-complexity: 16
|
||||||
|
gosec:
|
||||||
|
excludes:
|
||||||
|
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
||||||
|
- G113 # G113: Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772); (only affects go < 1.16.14. and go < 1.17.7)
|
||||||
|
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
||||||
|
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
|
||||||
|
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
|
||||||
govet:
|
govet:
|
||||||
enable:
|
enable:
|
||||||
- shadow
|
- shadow
|
||||||
|
@ -87,6 +97,10 @@ issues:
|
||||||
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
# The default exclusion rules are a bit too permissive, so copying the relevant ones below
|
||||||
exclude-use-default: false
|
exclude-use-default: false
|
||||||
|
|
||||||
|
# This option has been defined when Go modules was not existed and when the
|
||||||
|
# golangci-lint core was different, this is not something we still recommend.
|
||||||
|
exclude-dirs-use-default: false
|
||||||
|
|
||||||
exclude:
|
exclude:
|
||||||
- parameter .* always receives
|
- parameter .* always receives
|
||||||
|
|
||||||
|
@ -104,6 +118,9 @@ issues:
|
||||||
#
|
#
|
||||||
# These exclusion patterns are copied from the default excluses at:
|
# These exclusion patterns are copied from the default excluses at:
|
||||||
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
|
# https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
|
||||||
|
#
|
||||||
|
# The default list of exclusions can be found at:
|
||||||
|
# https://golangci-lint.run/usage/false-positives/#default-exclusions
|
||||||
|
|
||||||
# EXC0001
|
# EXC0001
|
||||||
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
- text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
|
||||||
|
@ -121,11 +138,6 @@ issues:
|
||||||
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
- text: "Subprocess launch(ed with variable|ing should be audited)"
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
# EXC0008
|
|
||||||
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
|
|
||||||
- text: "G307"
|
|
||||||
linters:
|
|
||||||
- gosec
|
|
||||||
# EXC0009
|
# EXC0009
|
||||||
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
|
||||||
linters:
|
linters:
|
||||||
|
@ -135,26 +147,6 @@ issues:
|
||||||
linters:
|
linters:
|
||||||
- gosec
|
- gosec
|
||||||
|
|
||||||
# G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
|
|
||||||
# only affects gp < 1.16.14. and go < 1.17.7
|
|
||||||
- text: "G113"
|
|
||||||
linters:
|
|
||||||
- gosec
|
|
||||||
# TODO: G104: Errors unhandled. (gosec)
|
|
||||||
- text: "G104"
|
|
||||||
linters:
|
|
||||||
- gosec
|
|
||||||
# Looks like the match in "EXC0007" above doesn't catch this one
|
|
||||||
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
|
||||||
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
|
|
||||||
linters:
|
|
||||||
- gosec
|
|
||||||
# Looks like the match in "EXC0009" above doesn't catch this one
|
|
||||||
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
|
|
||||||
- text: "G306: Expect WriteFile permissions to be 0600 or less"
|
|
||||||
linters:
|
|
||||||
- gosec
|
|
||||||
|
|
||||||
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
|
||||||
- text: "package-comments: should have a package comment"
|
- text: "package-comments: should have a package comment"
|
||||||
linters:
|
linters:
|
||||||
|
|
|
@ -66,7 +66,7 @@ anybody starts working on it.
|
||||||
We are always thrilled to receive pull requests. We do our best to process them
|
We are always thrilled to receive pull requests. We do our best to process them
|
||||||
quickly. If your pull request is not accepted on the first try,
|
quickly. If your pull request is not accepted on the first try,
|
||||||
don't get discouraged! Our contributor's guide explains [the review process we
|
don't get discouraged! Our contributor's guide explains [the review process we
|
||||||
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
use for simple changes](https://github.com/docker/docker/blob/master/project/REVIEWING.md).
|
||||||
|
|
||||||
### Talking to other Docker users and contributors
|
### Talking to other Docker users and contributors
|
||||||
|
|
||||||
|
@ -124,8 +124,8 @@ submitting a pull request.
|
||||||
Update the documentation when creating or modifying features. Test your
|
Update the documentation when creating or modifying features. Test your
|
||||||
documentation changes for clarity, concision, and correctness, as well as a
|
documentation changes for clarity, concision, and correctness, as well as a
|
||||||
clean documentation build. See our contributors guide for [our style
|
clean documentation build. See our contributors guide for [our style
|
||||||
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
guide](https://docs.docker.com/contribute/style/grammar/) and instructions on [building
|
||||||
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
|
the documentation](https://docs.docker.com/contribute/).
|
||||||
|
|
||||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||||
|
|
|
@ -4,7 +4,7 @@ ARG BASE_VARIANT=alpine
|
||||||
ARG ALPINE_VERSION=3.20
|
ARG ALPINE_VERSION=3.20
|
||||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||||
|
|
||||||
ARG GO_VERSION=1.22.7
|
ARG GO_VERSION=1.23.3
|
||||||
ARG XX_VERSION=1.5.0
|
ARG XX_VERSION=1.5.0
|
||||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||||
ARG GOTESTSUM_VERSION=v1.10.0
|
ARG GOTESTSUM_VERSION=v1.10.0
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# Docker CLI
|
# Docker CLI
|
||||||
|
|
||||||
[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/cli)
|
[![PkgGoDev](https://pkg.go.dev/badge/github.com/docker/cli)](https://pkg.go.dev/github.com/docker/cli)
|
||||||
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
|
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
|
||||||
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
|
||||||
|
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
||||||
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
|
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
|
@ -17,5 +17,5 @@ func (c *candidate) Path() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *candidate) Metadata() ([]byte, error) {
|
func (c *candidate) Metadata() ([]byte, error) {
|
||||||
return exec.Command(c.path, MetadataSubcommandName).Output()
|
return exec.Command(c.path, MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,6 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, p := range plugins {
|
for _, p := range plugins {
|
||||||
p := p
|
|
||||||
vendor := p.Vendor
|
vendor := p.Vendor
|
||||||
if vendor == "" {
|
if vendor == "" {
|
||||||
vendor = "unknown"
|
vendor = "unknown"
|
||||||
|
|
|
@ -240,7 +240,8 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||||
// TODO: why are we not returning plugin.Err?
|
// TODO: why are we not returning plugin.Err?
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
}
|
}
|
||||||
cmd := exec.Command(plugin.Path, args...)
|
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
|
|
||||||
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
|
||||||
// See: - https://github.com/golang/go/issues/10338
|
// See: - https://github.com/golang/go/issues/10338
|
||||||
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
|
||||||
|
|
|
@ -112,7 +112,7 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
|
||||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||||
}
|
}
|
||||||
|
|
||||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes))
|
pCmd := exec.CommandContext(ctx, p.Path, p.Name, HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||||
pCmd.Env = os.Environ()
|
pCmd.Env = os.Environ()
|
||||||
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||||
hookCmdOutput, err := pCmd.Output()
|
hookCmdOutput, err := pCmd.Output()
|
||||||
|
|
|
@ -187,19 +187,18 @@ func TestInitializeFromClient(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range testcases {
|
for _, tc := range testcases {
|
||||||
testcase := testcase
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
t.Run(testcase.doc, func(t *testing.T) {
|
|
||||||
apiclient := &fakeClient{
|
apiclient := &fakeClient{
|
||||||
pingFunc: testcase.pingFunc,
|
pingFunc: tc.pingFunc,
|
||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &DockerCli{client: apiclient}
|
cli := &DockerCli{client: apiclient}
|
||||||
err := cli.Initialize(flags.NewClientOptions())
|
err := cli.Initialize(flags.NewClientOptions())
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
|
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer)
|
||||||
assert.Equal(t, apiclient.negotiated, testcase.negotiated)
|
assert.Equal(t, apiclient.negotiated, tc.negotiated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,10 +276,9 @@ func TestExperimentalCLI(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range testcases {
|
for _, tc := range testcases {
|
||||||
testcase := testcase
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
t.Run(testcase.doc, func(t *testing.T) {
|
dir := fs.NewDir(t, tc.doc, fs.WithFile("config.json", tc.configfile))
|
||||||
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
|
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
apiclient := &fakeClient{
|
apiclient := &fakeClient{
|
||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
|
|
|
@ -59,7 +59,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||||
for _, ctr := range list {
|
for _, ctr := range list {
|
||||||
skip := false
|
skip := false
|
||||||
for _, fn := range filters {
|
for _, fn := range filters {
|
||||||
if !fn(ctr) {
|
if fn != nil && !fn(ctr) {
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -145,3 +145,47 @@ func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCom
|
||||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var commonPlatforms = []string{
|
||||||
|
"linux",
|
||||||
|
"linux/386",
|
||||||
|
"linux/amd64",
|
||||||
|
"linux/arm",
|
||||||
|
"linux/arm/v5",
|
||||||
|
"linux/arm/v6",
|
||||||
|
"linux/arm/v7",
|
||||||
|
"linux/arm64",
|
||||||
|
"linux/arm64/v8",
|
||||||
|
|
||||||
|
// IBM power and z platforms
|
||||||
|
"linux/ppc64le",
|
||||||
|
"linux/s390x",
|
||||||
|
|
||||||
|
// Not yet supported
|
||||||
|
"linux/riscv64",
|
||||||
|
|
||||||
|
"windows",
|
||||||
|
"windows/amd64",
|
||||||
|
|
||||||
|
"wasip1",
|
||||||
|
"wasip1/wasm",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platforms offers completion for platform-strings. It provides a non-exhaustive
|
||||||
|
// list of platforms to be used for completion. Platform-strings are based on
|
||||||
|
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
|
||||||
|
// list of recognised os/arch combinations from the Go runtime can be obtained
|
||||||
|
// through "go tool dist list".
|
||||||
|
//
|
||||||
|
// Some noteworthy exclusions from this list:
|
||||||
|
//
|
||||||
|
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
|
||||||
|
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
|
||||||
|
// - we don't (yet) include platforms for which we don't build binaries, such as
|
||||||
|
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
|
||||||
|
// - we currently exclude architectures that may have unofficial builds,
|
||||||
|
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
||||||
|
// ppc64 (non-le) to prevent confusion.
|
||||||
|
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
||||||
|
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,346 @@
|
||||||
|
package completion
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
"gotest.tools/v3/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeCLI struct {
|
||||||
|
*fakeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client implements [APIClientProvider].
|
||||||
|
func (c fakeCLI) Client() client.APIClient {
|
||||||
|
return c.fakeClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeClient struct {
|
||||||
|
client.Client
|
||||||
|
containerListFunc func(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(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||||
|
if c.containerListFunc != nil {
|
||||||
|
return c.containerListFunc(options)
|
||||||
|
}
|
||||||
|
return []container.Summary{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||||
|
if c.imageListFunc != nil {
|
||||||
|
return c.imageListFunc(options)
|
||||||
|
}
|
||||||
|
return []image.Summary{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||||
|
if c.networkListFunc != nil {
|
||||||
|
return c.networkListFunc(ctx, options)
|
||||||
|
}
|
||||||
|
return []network.Inspect{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||||
|
if c.volumeListFunc != nil {
|
||||||
|
return c.volumeListFunc(options.Filters)
|
||||||
|
}
|
||||||
|
return volume.ListResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteContainerNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
doc string
|
||||||
|
showAll, showIDs bool
|
||||||
|
filters []func(container.Summary) bool
|
||||||
|
containers []container.Summary
|
||||||
|
expOut []string
|
||||||
|
expOpts container.ListOptions
|
||||||
|
expDirective cobra.ShellCompDirective
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "no results",
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "all containers",
|
||||||
|
showAll: true,
|
||||||
|
containers: []container.Summary{
|
||||||
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||||
|
expOpts: container.ListOptions{All: true},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "all containers with ids",
|
||||||
|
showAll: true,
|
||||||
|
showIDs: true,
|
||||||
|
containers: []container.Summary{
|
||||||
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||||
|
expOpts: container.ListOptions{All: true},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "only running containers",
|
||||||
|
showAll: false,
|
||||||
|
containers: []container.Summary{
|
||||||
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"container-c", "container-c/link-b"},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with filter",
|
||||||
|
showAll: true,
|
||||||
|
filters: []func(container.Summary) bool{
|
||||||
|
func(container container.Summary) bool { return container.State == "created" },
|
||||||
|
},
|
||||||
|
containers: []container.Summary{
|
||||||
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
|
{ID: "id-a", State: "exited", Names: []string{"/container-a"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"container-b"},
|
||||||
|
expOpts: container.ListOptions{All: true},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "multiple filters",
|
||||||
|
showAll: true,
|
||||||
|
filters: []func(container.Summary) bool{
|
||||||
|
func(container container.Summary) bool { return container.ID == "id-a" },
|
||||||
|
func(container container.Summary) bool { return container.State == "created" },
|
||||||
|
},
|
||||||
|
containers: []container.Summary{
|
||||||
|
{ID: "id-c", State: "running", Names: []string{"/container-c", "/container-c/link-b"}},
|
||||||
|
{ID: "id-b", State: "created", Names: []string{"/container-b"}},
|
||||||
|
{ID: "id-a", State: "created", Names: []string{"/container-a"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"container-a"},
|
||||||
|
expOpts: container.ListOptions{All: true},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with error",
|
||||||
|
expDirective: cobra.ShellCompDirectiveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
|
if tc.showIDs {
|
||||||
|
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||||
|
}
|
||||||
|
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||||
|
containerListFunc: func(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 nil, errors.New("some error occurred")
|
||||||
|
}
|
||||||
|
return tc.containers, nil
|
||||||
|
},
|
||||||
|
}}, tc.showAll, tc.filters...)
|
||||||
|
|
||||||
|
containers, directives := comp(&cobra.Command{}, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||||
|
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteEnvVarNames(t *testing.T) {
|
||||||
|
env.PatchAll(t, map[string]string{
|
||||||
|
"ENV_A": "hello-a",
|
||||||
|
"ENV_B": "hello-b",
|
||||||
|
})
|
||||||
|
values, directives := EnvVarNames(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
|
||||||
|
sort.Strings(values)
|
||||||
|
expected := []string{"ENV_A", "ENV_B"}
|
||||||
|
assert.Check(t, is.DeepEqual(values, expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteFileNames(t *testing.T) {
|
||||||
|
values, directives := FileNames(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
||||||
|
assert.Check(t, is.Len(values, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteFromList(t *testing.T) {
|
||||||
|
expected := []string{"one", "two", "three"}
|
||||||
|
|
||||||
|
values, directives := FromList(expected...)(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
assert.Check(t, is.DeepEqual(values, expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteImageNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
doc string
|
||||||
|
images []image.Summary
|
||||||
|
expOut []string
|
||||||
|
expDirective cobra.ShellCompDirective
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "no results",
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with results",
|
||||||
|
images: []image.Summary{
|
||||||
|
{RepoTags: []string{"image-c:latest", "image-c:other"}},
|
||||||
|
{RepoTags: []string{"image-b:latest", "image-b:other"}},
|
||||||
|
{RepoTags: []string{"image-a:latest", "image-a:other"}},
|
||||||
|
},
|
||||||
|
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with error",
|
||||||
|
expDirective: cobra.ShellCompDirectiveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
|
comp := ImageNames(fakeCLI{&fakeClient{
|
||||||
|
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||||
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
|
return nil, errors.New("some error occurred")
|
||||||
|
}
|
||||||
|
return tc.images, nil
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
|
||||||
|
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||||
|
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteNetworkNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
doc string
|
||||||
|
networks []network.Summary
|
||||||
|
expOut []string
|
||||||
|
expDirective cobra.ShellCompDirective
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "no results",
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with results",
|
||||||
|
networks: []network.Summary{
|
||||||
|
{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,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with error",
|
||||||
|
expDirective: cobra.ShellCompDirectiveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
|
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||||
|
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||||
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
|
return nil, errors.New("some error occurred")
|
||||||
|
}
|
||||||
|
return tc.networks, nil
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
|
||||||
|
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||||
|
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteNoComplete(t *testing.T) {
|
||||||
|
values, directives := NoComplete(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
|
||||||
|
assert.Check(t, is.Len(values, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompletePlatforms(t *testing.T) {
|
||||||
|
values, directives := Platforms(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteVolumeNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
doc string
|
||||||
|
volumes []*volume.Volume
|
||||||
|
expOut []string
|
||||||
|
expDirective cobra.ShellCompDirective
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
doc: "no results",
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with results",
|
||||||
|
volumes: []*volume.Volume{
|
||||||
|
{Name: "volume-c"},
|
||||||
|
{Name: "volume-b"},
|
||||||
|
{Name: "volume-a"},
|
||||||
|
},
|
||||||
|
expOut: []string{"volume-c", "volume-b", "volume-a"},
|
||||||
|
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc: "with error",
|
||||||
|
expDirective: cobra.ShellCompDirectiveError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
|
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||||
|
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
||||||
|
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||||
|
return volume.ListResponse{}, errors.New("some error occurred")
|
||||||
|
}
|
||||||
|
return volume.ListResponse{Volumes: tc.volumes}, nil
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
|
||||||
|
volumes, directives := comp(&cobra.Command{}, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||||
|
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cmd := newConfigCreateCommand(
|
cmd := newConfigCreateCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
|
|
|
@ -61,7 +61,6 @@ id_rsa
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -73,7 +73,6 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
|
|
@ -40,6 +40,10 @@ type fakeClient struct {
|
||||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, 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)
|
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 container.CommitOptions) (types.IDResponse, error)
|
||||||
|
containerPauseFunc func(ctx context.Context, container string) error
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,3 +201,34 @@ func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, op
|
||||||
}
|
}
|
||||||
return types.HijackedResponse{}, nil
|
return types.HijackedResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
||||||
|
if f.containerDiffFunc != nil {
|
||||||
|
return f.containerDiffFunc(ctx, containerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []container.FilesystemChange{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
||||||
|
if f.containerRenameFunc != nil {
|
||||||
|
return f.containerRenameFunc(ctx, oldName, newName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (types.IDResponse, error) {
|
||||||
|
if f.containerCommitFunc != nil {
|
||||||
|
return f.containerCommitFunc(ctx, containerID, options)
|
||||||
|
}
|
||||||
|
return types.IDResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
||||||
|
if f.containerPauseFunc != nil {
|
||||||
|
return f.containerPauseFunc(ctx, containerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunCommit(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerCommitFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
container string,
|
||||||
|
options container.CommitOptions,
|
||||||
|
) (types.IDResponse, 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.Pause, false))
|
||||||
|
assert.Check(t, is.Equal(container, "container-id"))
|
||||||
|
|
||||||
|
return types.IDResponse{ID: "image-id"}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewCommitCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetArgs(
|
||||||
|
[]string{
|
||||||
|
"--author", "Author Name <author@name.com>",
|
||||||
|
"--change", "EXPOSE 80",
|
||||||
|
"--message", "commit message",
|
||||||
|
"--pause=false",
|
||||||
|
"container-id",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Assert(t, is.Equal(cli.OutBuffer().String(), "image-id\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunCommitClientError(t *testing.T) {
|
||||||
|
clientError := errors.New("client error")
|
||||||
|
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerCommitFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
container string,
|
||||||
|
options container.CommitOptions,
|
||||||
|
) (types.IDResponse, error) {
|
||||||
|
return types.IDResponse{}, clientError
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewCommitCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
|
cmd.SetArgs([]string{"container-id"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.ErrorIs(t, err, clientError)
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/moby/sys/signal"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
@ -19,3 +20,17 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
||||||
assert.Check(t, is.Equal(name, strings.ToUpper(name)), "Should be formatted uppercase")
|
assert.Check(t, is.Equal(name, strings.ToUpper(name)), "Should be formatted uppercase")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompleteRestartPolicies(t *testing.T) {
|
||||||
|
values, directives := completeRestartPolicies(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
expected := restartPolicies
|
||||||
|
assert.Check(t, is.DeepEqual(values, expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompleteSignals(t *testing.T) {
|
||||||
|
values, directives := completeSignals(nil, nil, "")
|
||||||
|
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||||
|
assert.Check(t, len(values) > 1)
|
||||||
|
assert.Check(t, is.Len(values, len(signal.SignalMap)))
|
||||||
|
}
|
||||||
|
|
|
@ -178,7 +178,6 @@ func TestSplitCpArg(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
if tc.os == "windows" && runtime.GOOS != "windows" {
|
if tc.os == "windows" && runtime.GOOS != "windows" {
|
||||||
t.Skip("skipping windows test on non-windows platform")
|
t.Skip("skipping windows test on non-windows platform")
|
||||||
|
|
|
@ -83,6 +83,7 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||||
|
|
|
@ -113,7 +113,6 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||||
pullCounter := 0
|
pullCounter := 0
|
||||||
|
|
||||||
|
@ -176,7 +175,6 @@ func TestCreateContainerImagePullPolicyInvalid(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||||
dockerCli := test.NewFakeCli(&fakeClient{})
|
dockerCli := test.NewFakeCli(&fakeClient{})
|
||||||
err := runCreate(
|
err := runCreate(
|
||||||
|
@ -207,7 +205,6 @@ func TestCreateContainerValidateFlags(t *testing.T) {
|
||||||
expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
expectedErr: `invalid argument "STDINFO" for "-a, --attach" flag: valid streams are STDIN, STDOUT and STDERR`,
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
|
cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
@ -251,7 +248,6 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
hostConfig *container.HostConfig,
|
hostConfig *container.HostConfig,
|
||||||
|
@ -312,7 +308,6 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunDiff(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerDiffFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
containerID string,
|
||||||
|
) ([]container.FilesystemChange, error) {
|
||||||
|
return []container.FilesystemChange{
|
||||||
|
{
|
||||||
|
Kind: container.ChangeModify,
|
||||||
|
Path: "/path/to/file0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: container.ChangeAdd,
|
||||||
|
Path: "/path/to/file1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Kind: container.ChangeDelete,
|
||||||
|
Path: "/path/to/file2",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewDiffCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
|
||||||
|
cmd.SetArgs([]string{"container-id"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
diff := strings.SplitN(cli.OutBuffer().String(), "\n", 3)
|
||||||
|
assert.Assert(t, is.Len(diff, 3))
|
||||||
|
|
||||||
|
file0 := strings.TrimSpace(diff[0])
|
||||||
|
file1 := strings.TrimSpace(diff[1])
|
||||||
|
file2 := strings.TrimSpace(diff[2])
|
||||||
|
|
||||||
|
assert.Check(t, is.Equal(file0, "C /path/to/file0"))
|
||||||
|
assert.Check(t, is.Equal(file1, "A /path/to/file1"))
|
||||||
|
assert.Check(t, is.Equal(file2, "D /path/to/file2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunDiffClientError(t *testing.T) {
|
||||||
|
clientError := errors.New("client error")
|
||||||
|
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerDiffFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
containerID string,
|
||||||
|
) ([]container.FilesystemChange, error) {
|
||||||
|
return nil, clientError
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewDiffCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
|
|
||||||
|
cmd.SetArgs([]string{"container-id"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.ErrorIs(t, err, clientError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunDiffEmptyContainerError(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{})
|
||||||
|
|
||||||
|
cmd := NewDiffCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
|
|
||||||
|
containerID := ""
|
||||||
|
cmd.SetArgs([]string{containerID})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.Error(t, err, "Container name cannot be empty")
|
||||||
|
}
|
|
@ -47,7 +47,6 @@ D: /usr/app/old_app.js
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
out := bytes.NewBufferString("")
|
out := bytes.NewBufferString("")
|
||||||
tc.context.Output = out
|
tc.context.Output = out
|
||||||
|
|
|
@ -178,7 +178,6 @@ container2 -- --
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
@ -223,7 +222,6 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
err := statsFormatWrite(tc.context, []StatsEntry{}, "linux", false)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
@ -265,7 +263,6 @@ func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
err := statsFormatWrite(tc.context, []StatsEntry{}, "windows", false)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunKill(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerKillFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
container string,
|
||||||
|
signal string,
|
||||||
|
) error {
|
||||||
|
assert.Assert(t, is.Equal(signal, "STOP"))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewKillCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
|
||||||
|
cmd.SetArgs([]string{
|
||||||
|
"--signal", "STOP",
|
||||||
|
"container-id-1",
|
||||||
|
"container-id-2",
|
||||||
|
})
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
containerIDs := strings.SplitN(cli.OutBuffer().String(), "\n", 2)
|
||||||
|
assert.Assert(t, is.Len(containerIDs, 2))
|
||||||
|
|
||||||
|
containerID1 := strings.TrimSpace(containerIDs[0])
|
||||||
|
containerID2 := strings.TrimSpace(containerIDs[1])
|
||||||
|
|
||||||
|
assert.Check(t, is.Equal(containerID1, "container-id-1"))
|
||||||
|
assert.Check(t, is.Equal(containerID2, "container-id-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunKillClientError(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
|
containerKillFunc: func(
|
||||||
|
ctx context.Context,
|
||||||
|
container string,
|
||||||
|
signal string,
|
||||||
|
) error {
|
||||||
|
return fmt.Errorf("client error for container %s", container)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := NewKillCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
|
|
||||||
|
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
||||||
|
err := cmd.Execute()
|
||||||
|
|
||||||
|
errs := strings.SplitN(err.Error(), "\n", 2)
|
||||||
|
assert.Assert(t, is.Len(errs, 2))
|
||||||
|
|
||||||
|
errContainerID1 := errs[0]
|
||||||
|
errContainerID2 := errs[1]
|
||||||
|
|
||||||
|
assert.Assert(t, is.Equal(errContainerID1, "client error for container container-id-1"))
|
||||||
|
assert.Assert(t, is.Equal(errContainerID2, "client error for container container-id-2"))
|
||||||
|
}
|
|
@ -127,7 +127,6 @@ func TestContainerListBuildContainerListOptions(t *testing.T) {
|
||||||
|
|
||||||
func TestContainerListErrors(t *testing.T) {
|
func TestContainerListErrors(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
|
||||||
flags map[string]string
|
flags map[string]string
|
||||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||||
expectedError string
|
expectedError string
|
||||||
|
@ -157,10 +156,10 @@ func TestContainerListErrors(t *testing.T) {
|
||||||
containerListFunc: tc.containerListFunc,
|
containerListFunc: tc.containerListFunc,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
cmd.SetArgs(tc.args)
|
|
||||||
for key, value := range tc.flags {
|
for key, value := range tc.flags {
|
||||||
assert.Check(t, cmd.Flags().Set(key, value))
|
assert.Check(t, cmd.Flags().Set(key, value))
|
||||||
}
|
}
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||||
|
@ -180,6 +179,9 @@ func TestContainerListWithoutFormat(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format.golden")
|
||||||
}
|
}
|
||||||
|
@ -194,6 +196,9 @@ func TestContainerListNoTrunc(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("no-trunc", "true"))
|
assert.Check(t, cmd.Flags().Set("no-trunc", "true"))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-without-format-no-trunc.golden")
|
||||||
|
@ -210,6 +215,9 @@ func TestContainerListNamesMultipleTime(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}"))
|
assert.Check(t, cmd.Flags().Set("format", "{{.Names}} {{.Names}}"))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-name-name.golden")
|
||||||
|
@ -226,6 +234,9 @@ func TestContainerListFormatTemplateWithArg(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`))
|
assert.Check(t, cmd.Flags().Set("format", `{{.Names}} {{.Label "some.label"}}`))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-format-with-arg.golden")
|
||||||
|
@ -266,7 +277,6 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
|
containerListFunc: func(options container.ListOptions) ([]container.Summary, error) {
|
||||||
|
@ -275,6 +285,9 @@ func TestContainerListFormatSizeSetsOption(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("format", tc.format))
|
assert.Check(t, cmd.Flags().Set("format", tc.format))
|
||||||
if tc.sizeFlag != "" {
|
if tc.sizeFlag != "" {
|
||||||
assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag))
|
assert.Check(t, cmd.Flags().Set("size", tc.sizeFlag))
|
||||||
|
@ -297,6 +310,9 @@ func TestContainerListWithConfigFormat(t *testing.T) {
|
||||||
PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}",
|
PsFormat: "{{ .Names }} {{ .Image }} {{ .Labels }} {{ .Size}}",
|
||||||
})
|
})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-config-format.golden")
|
||||||
}
|
}
|
||||||
|
@ -314,6 +330,9 @@ func TestContainerListWithFormat(t *testing.T) {
|
||||||
t.Run("with format", func(t *testing.T) {
|
t.Run("with format", func(t *testing.T) {
|
||||||
cli.OutBuffer().Reset()
|
cli.OutBuffer().Reset()
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden")
|
golden.Assert(t, cli.OutBuffer().String(), "container-list-with-format.golden")
|
||||||
|
@ -322,6 +341,9 @@ func TestContainerListWithFormat(t *testing.T) {
|
||||||
t.Run("with format and quiet", func(t *testing.T) {
|
t.Run("with format and quiet", func(t *testing.T) {
|
||||||
cli.OutBuffer().Reset()
|
cli.OutBuffer().Reset()
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
assert.Check(t, cmd.Flags().Set("format", "{{ .Names }} {{ .Image }} {{ .Labels }}"))
|
||||||
assert.Check(t, cmd.Flags().Set("quiet", "true"))
|
assert.Check(t, cmd.Flags().Set("quiet", "true"))
|
||||||
assert.NilError(t, cmd.Execute())
|
assert.NilError(t, cmd.Execute())
|
||||||
|
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"github.com/docker/docker/errdefs"
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
cdi "tags.cncf.io/container-device-interface/pkg/parser"
|
||||||
)
|
)
|
||||||
|
@ -364,10 +363,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||||
return nil, errors.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
mounts := copts.mounts.Value()
|
|
||||||
if len(mounts) > 0 && copts.volumeDriver != "" {
|
|
||||||
logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.")
|
|
||||||
}
|
|
||||||
var binds []string
|
var binds []string
|
||||||
volumes := copts.volumes.GetMap()
|
volumes := copts.volumes.GetMap()
|
||||||
// add any bind targets to the list of container volumes
|
// add any bind targets to the list of container volumes
|
||||||
|
@ -697,7 +692,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||||
Tmpfs: tmpfs,
|
Tmpfs: tmpfs,
|
||||||
Sysctls: copts.sysctls.GetAll(),
|
Sysctls: copts.sysctls.GetAll(),
|
||||||
Runtime: copts.runtime,
|
Runtime: copts.runtime,
|
||||||
Mounts: mounts,
|
Mounts: copts.mounts.Value(),
|
||||||
MaskedPaths: maskedPaths,
|
MaskedPaths: maskedPaths,
|
||||||
ReadonlyPaths: readonlyPaths,
|
ReadonlyPaths: readonlyPaths,
|
||||||
Annotations: copts.annotations.GetAll(),
|
Annotations: copts.annotations.GetAll(),
|
||||||
|
@ -767,7 +762,6 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*networktypes.Endpoin
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, n := range copts.netMode.Value() {
|
for i, n := range copts.netMode.Value() {
|
||||||
n := n
|
|
||||||
if container.NetworkMode(n.Target).IsUserDefined() {
|
if container.NetworkMode(n.Target).IsUserDefined() {
|
||||||
hasUserDefined = true
|
hasUserDefined = true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -126,7 +126,6 @@ func TestParseRunAttach(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
config, _, _ := mustParse(t, tc.input)
|
config, _, _ := mustParse(t, tc.input)
|
||||||
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
assert.Equal(t, config.AttachStdin, tc.expected.AttachStdin)
|
||||||
|
@ -802,7 +801,6 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
t.Run(tc.input, func(t *testing.T) {
|
||||||
_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
|
_, hostConfig, _, err := parseRun([]string{"--restart=" + tc.input, "img", "cmd"})
|
||||||
if tc.expectedErr != "" {
|
if tc.expectedErr != "" {
|
||||||
|
@ -919,7 +917,7 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UTF16 with BOM
|
// UTF16 with BOM
|
||||||
e := "contains invalid utf8 bytes at line"
|
e := "invalid utf8 bytes at line"
|
||||||
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) {
|
||||||
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunPause(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(
|
||||||
|
&fakeClient{
|
||||||
|
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := NewPauseCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
containerIDs := strings.SplitN(cli.OutBuffer().String(), "\n", 2)
|
||||||
|
assert.Assert(t, is.Len(containerIDs, 2))
|
||||||
|
|
||||||
|
containerID1 := strings.TrimSpace(containerIDs[0])
|
||||||
|
containerID2 := strings.TrimSpace(containerIDs[1])
|
||||||
|
|
||||||
|
assert.Check(t, is.Equal(containerID1, "container-id-1"))
|
||||||
|
assert.Check(t, is.Equal(containerID2, "container-id-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunPauseClientError(t *testing.T) {
|
||||||
|
cli := test.NewFakeCli(
|
||||||
|
&fakeClient{
|
||||||
|
containerPauseFunc: func(ctx context.Context, container string) error {
|
||||||
|
return fmt.Errorf("client error for container %s", container)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd := NewPauseCommand(cli)
|
||||||
|
cmd.SetOut(io.Discard)
|
||||||
|
cmd.SetErr(io.Discard)
|
||||||
|
cmd.SetArgs([]string{"container-id-1", "container-id-2"})
|
||||||
|
|
||||||
|
err := cmd.Execute()
|
||||||
|
|
||||||
|
errs := strings.SplitN(err.Error(), "\n", 2)
|
||||||
|
assert.Assert(t, is.Len(errs, 2))
|
||||||
|
|
||||||
|
errContainerID1 := errs[0]
|
||||||
|
errContainerID2 := errs[1]
|
||||||
|
|
||||||
|
assert.Assert(t, is.Equal(errContainerID1, "client error for container container-id-1"))
|
||||||
|
assert.Assert(t, is.Equal(errContainerID2, "client error for container container-id-2"))
|
||||||
|
}
|
|
@ -43,7 +43,6 @@ func TestNewPortCommandOutput(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
cmd := NewPruneCommand(cli)
|
cmd := NewPruneCommand(cli)
|
||||||
|
cmd.SetArgs([]string{})
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/internal/test"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"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"))
|
||||||
|
}
|
|
@ -58,7 +58,6 @@ func TestRestart(t *testing.T) {
|
||||||
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var restarted []string
|
var restarted []string
|
||||||
mutex := new(sync.Mutex)
|
mutex := new(sync.Mutex)
|
||||||
|
|
|
@ -38,7 +38,9 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
"aliases": "docker container rm, docker container remove, docker rm",
|
"aliases": "docker container rm, docker container remove, docker rm",
|
||||||
},
|
},
|
||||||
ValidArgsFunction: completion.ContainerNames(dockerCli, true),
|
ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool {
|
||||||
|
return opts.force || ctr.State == "exited" || ctr.State == "created"
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
|
|
|
@ -23,7 +23,6 @@ func TestRemoveForce(t *testing.T) {
|
||||||
{name: "without force", args: []string{"nosuchcontainer", "mycontainer"}, expectedErr: "no such container"},
|
{name: "without force", args: []string{"nosuchcontainer", "mycontainer"}, expectedErr: "no such container"},
|
||||||
{name: "with force", args: []string{"--force", "nosuchcontainer", "mycontainer"}, expectedErr: ""},
|
{name: "with force", args: []string{"--force", "nosuchcontainer", "mycontainer"}, expectedErr: ""},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var removed []string
|
var removed []string
|
||||||
mutex := new(sync.Mutex)
|
mutex := new(sync.Mutex)
|
||||||
|
|
|
@ -74,6 +74,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||||
|
|
|
@ -35,7 +35,6 @@ func TestRunValidateFlags(t *testing.T) {
|
||||||
expectedErr: "conflicting options: cannot specify both --attach and --detach",
|
expectedErr: "conflicting options: cannot specify both --attach and --detach",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewRunCommand(test.NewFakeCli(&fakeClient{}))
|
cmd := NewRunCommand(test.NewFakeCli(&fakeClient{}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
@ -245,7 +244,6 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||||
createContainerFunc: func(config *container.Config,
|
createContainerFunc: func(config *container.Config,
|
||||||
|
@ -286,7 +284,6 @@ func TestRunContainerImagePullPolicyInvalid(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.PullPolicy, func(t *testing.T) {
|
t.Run(tc.PullPolicy, func(t *testing.T) {
|
||||||
dockerCli := test.NewFakeCli(&fakeClient{})
|
dockerCli := test.NewFakeCli(&fakeClient{})
|
||||||
err := runRun(
|
err := runRun(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -264,31 +265,40 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||||
// so we unlikely hit this code in practice.
|
// so we unlikely hit this code in practice.
|
||||||
daemonOSType = dockerCLI.ServerInfo().OSType
|
daemonOSType = dockerCLI.ServerInfo().OSType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buffer to store formatted stats text.
|
||||||
|
// Once formatted, it will be printed in one write to avoid screen flickering.
|
||||||
|
var statsTextBuffer bytes.Buffer
|
||||||
|
|
||||||
statsCtx := formatter.Context{
|
statsCtx := formatter.Context{
|
||||||
Output: dockerCLI.Out(),
|
Output: &statsTextBuffer,
|
||||||
Format: NewStatsFormat(format, daemonOSType),
|
Format: NewStatsFormat(format, daemonOSType),
|
||||||
}
|
}
|
||||||
cleanScreen := func() {
|
|
||||||
if !options.NoStream {
|
|
||||||
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[2J")
|
|
||||||
_, _ = fmt.Fprint(dockerCLI.Out(), "\033[H")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
ticker := time.NewTicker(500 * time.Millisecond)
|
ticker := time.NewTicker(500 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
cleanScreen()
|
|
||||||
var ccStats []StatsEntry
|
var ccStats []StatsEntry
|
||||||
cStats.mu.RLock()
|
cStats.mu.RLock()
|
||||||
for _, c := range cStats.cs {
|
for _, c := range cStats.cs {
|
||||||
ccStats = append(ccStats, c.GetStatistics())
|
ccStats = append(ccStats, c.GetStatistics())
|
||||||
}
|
}
|
||||||
cStats.mu.RUnlock()
|
cStats.mu.RUnlock()
|
||||||
|
|
||||||
|
if !options.NoStream {
|
||||||
|
// Start by clearing the screen and moving the cursor to the top-left
|
||||||
|
_, _ = fmt.Fprint(&statsTextBuffer, "\033[2J\033[H")
|
||||||
|
}
|
||||||
|
|
||||||
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
if err = statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
|
||||||
|
|
||||||
|
statsTextBuffer.Reset()
|
||||||
|
|
||||||
if len(cStats.cs) == 0 && !showAll {
|
if len(cStats.cs) == 0 && !showAll {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,6 @@ func TestStop(t *testing.T) {
|
||||||
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
expectedErr: "conflicting options: cannot specify both --timeout and --time",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var stopped []string
|
var stopped []string
|
||||||
mutex := new(sync.Mutex)
|
mutex := new(sync.Mutex)
|
||||||
|
|
|
@ -38,7 +38,7 @@ func waitFn(cid string) (<-chan container.WaitResponse, <-chan error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWaitExitOrRemoved(t *testing.T) {
|
func TestWaitExitOrRemoved(t *testing.T) {
|
||||||
testcases := []struct {
|
tests := []struct {
|
||||||
cid string
|
cid string
|
||||||
exitCode int
|
exitCode int
|
||||||
}{
|
}{
|
||||||
|
@ -61,9 +61,11 @@ func TestWaitExitOrRemoved(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion}
|
client := &fakeClient{waitFunc: waitFn, Version: api.DefaultVersion}
|
||||||
for _, testcase := range testcases {
|
for _, tc := range tests {
|
||||||
statusC := waitExitOrRemoved(context.Background(), client, testcase.cid, true)
|
t.Run(tc.cid, func(t *testing.T) {
|
||||||
|
statusC := waitExitOrRemoved(context.Background(), client, tc.cid, true)
|
||||||
exitCode := <-statusC
|
exitCode := <-statusC
|
||||||
assert.Check(t, is.Equal(testcase.exitCode, exitCode))
|
assert.Check(t, is.Equal(tc.exitCode, exitCode))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,6 @@ func TestCreate(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.options.Name, func(t *testing.T) {
|
t.Run(tc.options.Name, func(t *testing.T) {
|
||||||
err := RunCreate(cli, &tc.options)
|
err := RunCreate(cli, &tc.options)
|
||||||
if tc.expecterErr == "" {
|
if tc.expecterErr == "" {
|
||||||
|
@ -164,25 +163,24 @@ func TestCreateFromContext(t *testing.T) {
|
||||||
|
|
||||||
cli.SetCurrentContext("dummy")
|
cli.SetCurrentContext("dummy")
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, tc := range cases {
|
||||||
c := c
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
cli.ResetOutputBuffers()
|
cli.ResetOutputBuffers()
|
||||||
err := RunCreate(cli, &CreateOptions{
|
err := RunCreate(cli, &CreateOptions{
|
||||||
From: "original",
|
From: "original",
|
||||||
Name: c.name,
|
Name: tc.name,
|
||||||
Description: c.description,
|
Description: tc.description,
|
||||||
Docker: c.docker,
|
Docker: tc.docker,
|
||||||
})
|
})
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assertContextCreateLogging(t, cli, c.name)
|
assertContextCreateLogging(t, cli, tc.name)
|
||||||
newContext, err := cli.ContextStore().GetMetadata(c.name)
|
newContext, err := cli.ContextStore().GetMetadata(tc.name)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
newContextTyped, err := command.GetDockerContext(newContext)
|
newContextTyped, err := command.GetDockerContext(newContext)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
dockerEndpoint, err := docker.EndpointFromContext(newContext)
|
dockerEndpoint, err := docker.EndpointFromContext(newContext)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, newContextTyped.Description, c.expectedDescription)
|
assert.Equal(t, newContextTyped.Description, tc.expectedDescription)
|
||||||
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
|
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -219,23 +217,22 @@ func TestCreateFromCurrent(t *testing.T) {
|
||||||
|
|
||||||
cli.SetCurrentContext("original")
|
cli.SetCurrentContext("original")
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, tc := range cases {
|
||||||
c := c
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
|
||||||
cli.ResetOutputBuffers()
|
cli.ResetOutputBuffers()
|
||||||
err := RunCreate(cli, &CreateOptions{
|
err := RunCreate(cli, &CreateOptions{
|
||||||
Name: c.name,
|
Name: tc.name,
|
||||||
Description: c.description,
|
Description: tc.description,
|
||||||
})
|
})
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assertContextCreateLogging(t, cli, c.name)
|
assertContextCreateLogging(t, cli, tc.name)
|
||||||
newContext, err := cli.ContextStore().GetMetadata(c.name)
|
newContext, err := cli.ContextStore().GetMetadata(tc.name)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
newContextTyped, err := command.GetDockerContext(newContext)
|
newContextTyped, err := command.GetDockerContext(newContext)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
dockerEndpoint, err := docker.EndpointFromContext(newContext)
|
dockerEndpoint, err := docker.EndpointFromContext(newContext)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
assert.Equal(t, newContextTyped.Description, c.expectedDescription)
|
assert.Equal(t, newContextTyped.Description, tc.expectedDescription)
|
||||||
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
|
assert.Equal(t, dockerEndpoint.Host, "tcp://42.42.42.42:2375")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -346,7 +346,6 @@ size: 0B
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
@ -411,7 +410,6 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
err := ContainerWrite(tc.context, containers)
|
err := ContainerWrite(tc.context, containers)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -106,7 +106,6 @@ Build Cache 0 0 0B 0B
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -304,7 +304,6 @@ image_id: imageID3
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
@ -365,7 +364,6 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
err := ImageWrite(tc.context, images)
|
err := ImageWrite(tc.context, images)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -131,7 +131,6 @@ foobar_bar
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"github.com/docker/cli-docs-tool/annotation"
|
"github.com/docker/cli-docs-tool/annotation"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/command/image/build"
|
"github.com/docker/cli/cli/command/image/build"
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api"
|
"github.com/docker/docker/api"
|
||||||
|
@ -159,6 +160,8 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.SetAnnotation("squash", "experimental", nil)
|
flags.SetAnnotation("squash", "experimental", nil)
|
||||||
flags.SetAnnotation("squash", "version", []string{"1.25"})
|
flags.SetAnnotation("squash", "version", []string{"1.25"})
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -255,7 +255,6 @@ imageID6 17 years ago /bin/bash echo 183MB
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
err := HistoryWrite(tc.context, true, histories)
|
err := HistoryWrite(tc.context, true, histories)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -3,17 +3,20 @@ package image
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
flagsHelper "github.com/docker/cli/cli/flags"
|
flagsHelper "github.com/docker/cli/cli/flags"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
type historyOptions struct {
|
type historyOptions struct {
|
||||||
image string
|
image string
|
||||||
|
platform string
|
||||||
|
|
||||||
human bool
|
human bool
|
||||||
quiet bool
|
quiet bool
|
||||||
|
@ -45,12 +48,24 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show image IDs")
|
||||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||||
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp)
|
flags.StringVar(&opts.format, "format", "", flagsHelper.FormatHelp)
|
||||||
|
flags.StringVar(&opts.platform, "platform", "", `Show history for the given platform. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
|
||||||
|
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
|
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
|
||||||
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, image.HistoryOptions{})
|
var options image.HistoryOptions
|
||||||
|
if opts.platform != "" {
|
||||||
|
p, err := platforms.Parse(opts.platform)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid platform")
|
||||||
|
}
|
||||||
|
options.Platform = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,9 +35,13 @@ func TestNewHistoryCommandErrors(t *testing.T) {
|
||||||
return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
|
return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid platform",
|
||||||
|
args: []string{"--platform", "<invalid>", "arg1"},
|
||||||
|
expectedError: `invalid platform`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
|
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
@ -89,9 +95,19 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
|
||||||
}}, nil
|
}}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "platform",
|
||||||
|
args: []string{"--platform", "linux/amd64", "image:tag"},
|
||||||
|
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) {
|
||||||
|
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
|
||||||
|
return []image.HistoryResponseItem{{
|
||||||
|
ID: "1234567890123456789",
|
||||||
|
Created: time.Now().Unix(),
|
||||||
|
}}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
// Set to UTC timezone as timestamps in output are
|
// Set to UTC timezone as timestamps in output are
|
||||||
// printed in the current timezone
|
// printed in the current timezone
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/command/completion"
|
||||||
dockeropts "github.com/docker/cli/opts"
|
dockeropts "github.com/docker/cli/opts"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/pkg/jsonmessage"
|
"github.com/docker/docker/pkg/jsonmessage"
|
||||||
|
@ -47,6 +48,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||||
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
|
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
|
||||||
command.AddPlatformFlag(flags, &options.platform)
|
command.AddPlatformFlag(flags, &options.platform)
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,6 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
|
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
|
|
@ -25,7 +25,6 @@ func TestNewInspectCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
|
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
@ -79,7 +78,6 @@ func TestNewInspectCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
imageInspectInvocationCount = 0
|
imageInspectInvocationCount = 0
|
||||||
cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc})
|
cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc})
|
||||||
|
|
|
@ -35,7 +35,6 @@ func TestNewImagesCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
|
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
|
@ -83,7 +82,6 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
|
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
|
||||||
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
|
@ -17,6 +18,7 @@ import (
|
||||||
type loadOptions struct {
|
type loadOptions struct {
|
||||||
input string
|
input string
|
||||||
quiet bool
|
quiet bool
|
||||||
|
platform string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLoadCommand creates a new `docker load` command
|
// NewLoadCommand creates a new `docker load` command
|
||||||
|
@ -40,7 +42,10 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
|
||||||
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
|
flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN")
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output")
|
||||||
|
flags.StringVar(&opts.platform, "platform", "", `Load only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
|
||||||
|
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,12 +68,20 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
|
||||||
return errors.Errorf("requested load from stdin, but stdin is empty")
|
return errors.Errorf("requested load from stdin, but stdin is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
var loadOpts image.LoadOptions
|
var options image.LoadOptions
|
||||||
if opts.quiet || !dockerCli.Out().IsTerminal() {
|
if opts.quiet || !dockerCli.Out().IsTerminal() {
|
||||||
loadOpts.Quiet = true
|
options.Quiet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := dockerCli.Client().ImageLoad(ctx, input, loadOpts)
|
if opts.platform != "" {
|
||||||
|
p, err := platforms.Parse(opts.platform)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid platform")
|
||||||
|
}
|
||||||
|
options.Platform = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := dockerCli.Client().ImageLoad(ctx, input, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,19 +30,28 @@ func TestNewLoadCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "input-to-terminal",
|
name: "input-to-terminal",
|
||||||
|
args: []string{},
|
||||||
isTerminalIn: true,
|
isTerminalIn: true,
|
||||||
expectedError: "requested load from stdin, but stdin is empty",
|
expectedError: "requested load from stdin, but stdin is empty",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pull-error",
|
name: "pull-error",
|
||||||
|
args: []string{},
|
||||||
expectedError: "something went wrong",
|
expectedError: "something went wrong",
|
||||||
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
||||||
return image.LoadResponse{}, errors.Errorf("something went wrong")
|
return image.LoadResponse{}, errors.Errorf("something went wrong")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid platform",
|
||||||
|
args: []string{"--platform", "<invalid>"},
|
||||||
|
expectedError: `invalid platform`,
|
||||||
|
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
||||||
|
return image.LoadResponse{}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||||
cli.In().SetIsTerminal(tc.isTerminalIn)
|
cli.In().SetIsTerminal(tc.isTerminalIn)
|
||||||
|
@ -71,12 +82,14 @@ func TestNewLoadCommandSuccess(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple",
|
name: "simple",
|
||||||
|
args: []string{},
|
||||||
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
||||||
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
|
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "json",
|
name: "json",
|
||||||
|
args: []string{},
|
||||||
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
||||||
json := "{\"ID\": \"1\"}"
|
json := "{\"ID\": \"1\"}"
|
||||||
return image.LoadResponse{
|
return image.LoadResponse{
|
||||||
|
@ -92,9 +105,16 @@ func TestNewLoadCommandSuccess(t *testing.T) {
|
||||||
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
|
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with platform",
|
||||||
|
args: []string{"--platform", "linux/amd64"},
|
||||||
|
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) {
|
||||||
|
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
|
||||||
|
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||||
cmd := NewLoadCommand(cli)
|
cmd := NewLoadCommand(cli)
|
||||||
|
|
|
@ -39,7 +39,6 @@ func TestNewPruneCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
||||||
imagesPruneFunc: tc.imagesPruneFunc,
|
imagesPruneFunc: tc.imagesPruneFunc,
|
||||||
|
@ -98,7 +97,6 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||||
// when prompted, answer "Y" to confirm the prune.
|
// when prompted, answer "Y" to confirm the prune.
|
||||||
|
|
|
@ -50,6 +50,8 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
command.AddPlatformFlag(flags, &opts.platform)
|
command.AddPlatformFlag(flags, &opts.platform)
|
||||||
command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{})
|
cli := test.NewFakeCli(&fakeClient{})
|
||||||
cmd := NewPullCommand(cli)
|
cmd := NewPullCommand(cli)
|
||||||
|
@ -73,7 +72,6 @@ func TestNewPullCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||||
|
@ -119,7 +117,6 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||||
|
|
|
@ -68,6 +68,8 @@ Image index won't be pushed, meaning that other manifests, including attestation
|
||||||
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
||||||
flags.SetAnnotation("platform", "version", []string{"1.46"})
|
flags.SetAnnotation("platform", "version", []string{"1.46"})
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
|
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
|
||||||
cmd := NewPushCommand(cli)
|
cmd := NewPushCommand(cli)
|
||||||
|
@ -68,7 +67,6 @@ func TestNewPushCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
|
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
|
||||||
|
|
|
@ -62,7 +62,6 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||||
imageRemoveFunc: tc.imageRemoveFunc,
|
imageRemoveFunc: tc.imageRemoveFunc,
|
||||||
|
@ -121,7 +120,6 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
||||||
cmd := NewRemoveCommand(cli)
|
cmd := NewRemoveCommand(cli)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/containerd/platforms"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/completion"
|
"github.com/docker/cli/cli/command/completion"
|
||||||
|
@ -15,6 +16,7 @@ import (
|
||||||
type saveOptions struct {
|
type saveOptions struct {
|
||||||
images []string
|
images []string
|
||||||
output string
|
output string
|
||||||
|
platform string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSaveCommand creates a new `docker save` command
|
// NewSaveCommand creates a new `docker save` command
|
||||||
|
@ -38,7 +40,10 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
|
|
||||||
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT")
|
||||||
|
flags.StringVar(&opts.platform, "platform", "", `Save only the given platform variant. Formatted as "os[/arch[/variant]]" (e.g., "linux/amd64")`)
|
||||||
|
_ = flags.SetAnnotation("platform", "version", []string{"1.48"})
|
||||||
|
|
||||||
|
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +57,16 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
|
||||||
return errors.Wrap(err, "failed to save image")
|
return errors.Wrap(err, "failed to save image")
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, image.SaveOptions{})
|
var options image.SaveOptions
|
||||||
|
if opts.platform != "" {
|
||||||
|
p, err := platforms.Parse(opts.platform)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "invalid platform")
|
||||||
|
}
|
||||||
|
options.Platform = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
|
@ -51,9 +52,13 @@ func TestNewSaveCommandErrors(t *testing.T) {
|
||||||
args: []string{"-o", "/dev/null", "arg1"},
|
args: []string{"-o", "/dev/null", "arg1"},
|
||||||
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
|
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid platform",
|
||||||
|
args: []string{"--platform", "<invalid>", "arg1"},
|
||||||
|
expectedError: `invalid platform`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
|
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
|
||||||
cli.Out().SetIsTerminal(tc.isTerminal)
|
cli.Out().SetIsTerminal(tc.isTerminal)
|
||||||
|
@ -70,13 +75,13 @@ func TestNewSaveCommandSuccess(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
args []string
|
args []string
|
||||||
isTerminal bool
|
isTerminal bool
|
||||||
imageSaveFunc func(images []string) (io.ReadCloser, error)
|
imageSaveFunc func(images []string, options image.SaveOptions) (io.ReadCloser, error)
|
||||||
deferredFunc func()
|
deferredFunc func()
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{"-o", "save_tmp_file", "arg1"},
|
args: []string{"-o", "save_tmp_file", "arg1"},
|
||||||
isTerminal: true,
|
isTerminal: true,
|
||||||
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
|
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) {
|
||||||
assert.Assert(t, is.Len(images, 1))
|
assert.Assert(t, is.Len(images, 1))
|
||||||
assert.Check(t, is.Equal("arg1", images[0]))
|
assert.Check(t, is.Equal("arg1", images[0]))
|
||||||
return io.NopCloser(strings.NewReader("")), nil
|
return io.NopCloser(strings.NewReader("")), nil
|
||||||
|
@ -88,21 +93,28 @@ func TestNewSaveCommandSuccess(t *testing.T) {
|
||||||
{
|
{
|
||||||
args: []string{"arg1", "arg2"},
|
args: []string{"arg1", "arg2"},
|
||||||
isTerminal: false,
|
isTerminal: false,
|
||||||
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
|
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) {
|
||||||
assert.Assert(t, is.Len(images, 2))
|
assert.Assert(t, is.Len(images, 2))
|
||||||
assert.Check(t, is.Equal("arg1", images[0]))
|
assert.Check(t, is.Equal("arg1", images[0]))
|
||||||
assert.Check(t, is.Equal("arg2", images[1]))
|
assert.Check(t, is.Equal("arg2", images[1]))
|
||||||
return io.NopCloser(strings.NewReader("")), nil
|
return io.NopCloser(strings.NewReader("")), nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
for _, tc := range testCases {
|
args: []string{"--platform", "linux/amd64", "arg1"},
|
||||||
tc := tc
|
isTerminal: false,
|
||||||
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
|
||||||
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
|
|
||||||
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
|
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) {
|
||||||
|
assert.Assert(t, is.Len(images, 1))
|
||||||
|
assert.Check(t, is.Equal("arg1", images[0]))
|
||||||
|
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform))
|
||||||
return io.NopCloser(strings.NewReader("")), nil
|
return io.NopCloser(strings.NewReader("")), nil
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
||||||
|
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
|
||||||
|
imageSaveFunc: tc.imageSaveFunc,
|
||||||
}))
|
}))
|
||||||
cmd.SetOut(io.Discard)
|
cmd.SetOut(io.Discard)
|
||||||
cmd.SetErr(io.Discard)
|
cmd.SetErr(io.Discard)
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
IMAGE CREATED CREATED BY SIZE COMMENT
|
||||||
|
123456789012 Less than a second ago 0B
|
|
@ -0,0 +1 @@
|
||||||
|
Success
|
|
@ -56,7 +56,6 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
im := im
|
|
||||||
sub := subImage{
|
sub := subImage{
|
||||||
Platform: platforms.Format(im.ImageData.Platform),
|
Platform: platforms.Format(im.ImageData.Platform),
|
||||||
Available: im.Available,
|
Available: im.Available,
|
||||||
|
|
|
@ -31,7 +31,6 @@ func TestManifestCreateErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(nil)
|
cli := test.NewFakeCli(nil)
|
||||||
cmd := newCreateListCommand(cli)
|
cmd := newCreateListCommand(cli)
|
||||||
|
|
|
@ -218,7 +218,6 @@ func TestNetworkCreateIPv6(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) {
|
networkCreateFunc: func(ctx context.Context, name string, createBody network.CreateOptions) (network.CreateResponse, error) {
|
||||||
|
|
|
@ -161,7 +161,6 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -83,7 +83,6 @@ func TestNetworkList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
|
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
|
|
@ -63,7 +63,6 @@ func TestNetworkRemoveForce(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
fakeCli := test.NewFakeCli(&fakeClient{
|
fakeCli := test.NewFakeCli(&fakeClient{
|
||||||
networkRemoveFunc: func(ctx context.Context, networkID string) error {
|
networkRemoveFunc: func(ctx context.Context, networkID string) error {
|
||||||
|
|
|
@ -202,7 +202,6 @@ foobar_boo Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -106,7 +106,6 @@ func TestNodeInspectPretty(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
nodeInspectFunc: tc.nodeInspectFunc,
|
nodeInspectFunc: tc.nodeInspectFunc,
|
||||||
|
|
|
@ -134,7 +134,6 @@ func TestNodePs(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
infoFunc: tc.infoFunc,
|
infoFunc: tc.infoFunc,
|
||||||
|
|
|
@ -131,7 +131,6 @@ foobar_bar
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -66,7 +66,6 @@ func TestInspectErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
||||||
cmd := newInspectCommand(cli)
|
cmd := newInspectCommand(cli)
|
||||||
|
@ -138,7 +137,6 @@ func TestInspect(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
||||||
cmd := newInspectCommand(cli)
|
cmd := newInspectCommand(cli)
|
||||||
|
|
|
@ -54,7 +54,6 @@ func TestInstallErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||||
cmd := newInstallCommand(cli)
|
cmd := newInstallCommand(cli)
|
||||||
|
@ -94,7 +93,6 @@ func TestInstallContentTrustErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
||||||
|
@ -138,7 +136,6 @@ func TestInstall(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||||
cmd := newInstallCommand(cli)
|
cmd := newInstallCommand(cli)
|
||||||
|
|
|
@ -46,7 +46,6 @@ func TestListErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
@ -166,7 +165,6 @@ func TestList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.description, func(t *testing.T) {
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||||
cmd := newListCommand(cli)
|
cmd := newListCommand(cli)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
|
@ -36,17 +37,13 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
|
func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
|
||||||
var errs cli.Errors
|
var errs error
|
||||||
for _, name := range opts.plugins {
|
for _, name := range opts.plugins {
|
||||||
if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil {
|
if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil {
|
||||||
errs = append(errs, err)
|
errs = errors.Join(errs, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintln(dockerCli.Out(), name)
|
_, _ = fmt.Fprintln(dockerCli.Out(), name)
|
||||||
}
|
}
|
||||||
// Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value.
|
|
||||||
if errs != nil {
|
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,20 +18,24 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const patSuggest = "You can log in with your password or a Personal Access " +
|
const (
|
||||||
|
registerSuggest = "Log in with your Docker ID or email address to push and pull images from Docker Hub. " +
|
||||||
|
"If you don't have a Docker ID, head over to https://hub.docker.com/ to create one."
|
||||||
|
patSuggest = "You can log in with your password or a Personal Access " +
|
||||||
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
|
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
|
||||||
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
|
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
|
||||||
|
)
|
||||||
|
|
||||||
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
||||||
// for the given command.
|
// for the given command.
|
||||||
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
|
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
|
||||||
return func(ctx context.Context) (string, error) {
|
return func(ctx context.Context) (string, error) {
|
||||||
fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
|
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
|
||||||
indexServer := registry.GetAuthConfigKey(index)
|
indexServer := registry.GetAuthConfigKey(index)
|
||||||
isDefaultRegistry := indexServer == registry.IndexServer
|
isDefaultRegistry := indexServer == registry.IndexServer
|
||||||
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
|
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
|
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
@ -86,7 +90,8 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureAuth handles prompting of user's username and password if needed.
|
// ConfigureAuth handles prompting of user's username and password if needed.
|
||||||
// Deprecated: use PromptUserForCredentials instead.
|
//
|
||||||
|
// Deprecated: use [PromptUserForCredentials] instead.
|
||||||
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
|
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
|
||||||
defaultUsername := authConfig.Username
|
defaultUsername := authConfig.Username
|
||||||
serverAddress := authConfig.ServerAddress
|
serverAddress := authConfig.ServerAddress
|
||||||
|
@ -110,7 +115,7 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
|
||||||
// If defaultUsername is not empty, the username prompt includes that username
|
// If defaultUsername is not empty, the username prompt includes that username
|
||||||
// and the user can hit enter without inputting a username to use that default
|
// and the user can hit enter without inputting a username to use that default
|
||||||
// username.
|
// username.
|
||||||
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
|
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
|
||||||
// On Windows, force the use of the regular OS stdin stream.
|
// On Windows, force the use of the regular OS stdin stream.
|
||||||
//
|
//
|
||||||
// See:
|
// See:
|
||||||
|
@ -123,57 +128,71 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||||
cli.SetIn(streams.NewIn(os.Stdin))
|
cli.SetIn(streams.NewIn(os.Stdin))
|
||||||
}
|
}
|
||||||
|
|
||||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
argUser = strings.TrimSpace(argUser)
|
||||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
if argUser == "" {
|
||||||
|
if serverAddress == registry.IndexServer {
|
||||||
if argUser = strings.TrimSpace(argUser); argUser == "" {
|
// When signing in to the default (Docker Hub) registry, we display
|
||||||
if isDefaultRegistry {
|
// hints for creating an account, and (if hints are enabled), using
|
||||||
// if this is a default registry (docker hub), then display the following message.
|
// a token instead of a password.
|
||||||
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
|
_, _ = fmt.Fprintln(cli.Out(), registerSuggest)
|
||||||
if hints.Enabled() {
|
if hints.Enabled() {
|
||||||
fmt.Fprintln(cli.Out(), patSuggest)
|
_, _ = fmt.Fprintln(cli.Out(), patSuggest)
|
||||||
fmt.Fprintln(cli.Out())
|
_, _ = fmt.Fprintln(cli.Out())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var prompt string
|
var prompt string
|
||||||
|
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||||
if defaultUsername == "" {
|
if defaultUsername == "" {
|
||||||
prompt = "Username: "
|
prompt = "Username: "
|
||||||
} else {
|
} else {
|
||||||
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
|
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return authConfig, err
|
return registrytypes.AuthConfig{}, err
|
||||||
}
|
}
|
||||||
if argUser == "" {
|
if argUser == "" {
|
||||||
argUser = defaultUsername
|
argUser = defaultUsername
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if argUser == "" {
|
if argUser == "" {
|
||||||
return authConfig, errors.Errorf("Error: Non-null Username Required")
|
return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
argPassword = strings.TrimSpace(argPassword)
|
||||||
if argPassword == "" {
|
if argPassword == "" {
|
||||||
restoreInput, err := DisableInputEcho(cli.In())
|
restoreInput, err := DisableInputEcho(cli.In())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return authConfig, err
|
return registrytypes.AuthConfig{}, err
|
||||||
}
|
}
|
||||||
defer restoreInput()
|
defer func() {
|
||||||
|
if err := restoreInput(); err != nil {
|
||||||
|
// TODO(thaJeztah): we should consider printing instructions how
|
||||||
|
// to restore this manually (other than restarting the shell).
|
||||||
|
// e.g., 'run stty echo' when in a Linux or macOS shell, but
|
||||||
|
// PowerShell and CMD.exe may need different instructions.
|
||||||
|
_, _ = fmt.Fprintln(cli.Err(), "Error: failed to restore terminal state to echo input:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return authConfig, err
|
return registrytypes.AuthConfig{}, err
|
||||||
}
|
}
|
||||||
fmt.Fprint(cli.Out(), "\n")
|
_, _ = fmt.Fprintln(cli.Out())
|
||||||
if argPassword == "" {
|
if argPassword == "" {
|
||||||
return authConfig, errors.Errorf("Error: Password Required")
|
return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authConfig.Username = argUser
|
return registrytypes.AuthConfig{
|
||||||
authConfig.Password = argPassword
|
Username: argUser,
|
||||||
authConfig.ServerAddress = serverAddress
|
Password: argPassword,
|
||||||
return authConfig, nil
|
ServerAddress: serverAddress,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
||||||
|
|
|
@ -203,7 +203,6 @@ result2 5
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results)
|
err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results)
|
||||||
|
|
|
@ -58,9 +58,9 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
|
func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
|
||||||
if opts.password != "" {
|
if opts.password != "" {
|
||||||
fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
|
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
|
||||||
if opts.passwordStdin {
|
if opts.passwordStdin {
|
||||||
return errors.New("--password and --password-stdin are mutually exclusive")
|
return errors.New("--password and --password-stdin are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
|
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
|
||||||
if err := verifyloginOptions(dockerCli, &opts); err != nil {
|
if err := verifyLoginOptions(dockerCli, &opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
|
@ -174,7 +174,7 @@ func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, de
|
||||||
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
|
if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
|
||||||
return response, err
|
return response, err
|
||||||
}
|
}
|
||||||
fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
|
_, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
|
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)
|
||||||
|
|
|
@ -61,7 +61,6 @@ id_rsa
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -93,7 +93,6 @@ func TestSecretInspectWithoutFormat(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
secretInspectFunc: tc.secretInspectFunc,
|
secretInspectFunc: tc.secretInspectFunc,
|
||||||
|
@ -132,7 +131,6 @@ func TestSecretInspectWithFormat(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
secretInspectFunc: tc.secretInspectFunc,
|
secretInspectFunc: tc.secretInspectFunc,
|
||||||
|
|
|
@ -223,7 +223,6 @@ zarp2
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -168,7 +168,6 @@ func TestServiceListServiceStatus(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range matrix {
|
for _, tc := range matrix {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
if tc.cluster == nil {
|
if tc.cluster == nil {
|
||||||
tc.cluster = generateCluster(t, tc.opts)
|
tc.cluster = generateCluster(t, tc.opts)
|
||||||
|
|
|
@ -50,7 +50,6 @@ func TestCredentialSpecOpt(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var cs credentialSpecOpt
|
var cs credentialSpecOpt
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,6 @@ func TestRollbackWithErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newRollbackCommand(
|
cmd := newRollbackCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
|
|
|
@ -1058,7 +1058,6 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error {
|
||||||
|
|
||||||
// Build the current list of portConfig
|
// Build the current list of portConfig
|
||||||
for _, entry := range *portConfig {
|
for _, entry := range *portConfig {
|
||||||
entry := entry
|
|
||||||
if _, ok := portSet[portConfigToString(&entry)]; !ok {
|
if _, ok := portSet[portConfigToString(&entry)]; !ok {
|
||||||
portSet[portConfigToString(&entry)] = entry
|
portSet[portConfigToString(&entry)] = entry
|
||||||
}
|
}
|
||||||
|
@ -1086,7 +1085,6 @@ portLoop:
|
||||||
ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
|
ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value()
|
||||||
|
|
||||||
for _, port := range ports {
|
for _, port := range ports {
|
||||||
port := port
|
|
||||||
if _, ok := portSet[portConfigToString(&port)]; ok {
|
if _, ok := portSet[portConfigToString(&port)]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -1690,7 +1690,6 @@ func TestUpdateUlimits(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
svc := swarm.ServiceSpec{
|
svc := swarm.ServiceSpec{
|
||||||
TaskTemplate: swarm.TaskSpec{
|
TaskTemplate: swarm.TaskSpec{
|
||||||
|
|
|
@ -51,7 +51,6 @@ bar
|
||||||
{Name: "bar", Services: 1},
|
{Name: "bar", Services: 1},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
tc := tc
|
|
||||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
tc.context.Output = &out
|
tc.context.Output = &out
|
||||||
|
|
|
@ -48,7 +48,6 @@ func TestListErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||||
serviceListFunc: tc.serviceListFunc,
|
serviceListFunc: tc.serviceListFunc,
|
||||||
|
@ -104,7 +103,6 @@ func TestStackList(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
var services []swarm.Service
|
var services []swarm.Service
|
||||||
for _, name := range tc.serviceNames {
|
for _, name := range tc.serviceNames {
|
||||||
|
|
|
@ -40,7 +40,6 @@ func TestStackPsErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
||||||
taskListFunc: tc.taskListFunc,
|
taskListFunc: tc.taskListFunc,
|
||||||
|
@ -160,7 +159,6 @@ func TestStackPs(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.doc, func(t *testing.T) {
|
t.Run(tc.doc, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
taskListFunc: tc.taskListFunc,
|
taskListFunc: tc.taskListFunc,
|
||||||
|
|
|
@ -67,7 +67,6 @@ func TestStackServicesErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.expectedError, func(t *testing.T) {
|
t.Run(tc.expectedError, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
serviceListFunc: tc.serviceListFunc,
|
serviceListFunc: tc.serviceListFunc,
|
||||||
|
|
|
@ -88,7 +88,6 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for _, tc := range testcases {
|
for _, tc := range testcases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.image, func(t *testing.T) {
|
t.Run(tc.image, func(t *testing.T) {
|
||||||
spec := map[string]swarm.ServiceSpec{
|
spec := map[string]swarm.ServiceSpec{
|
||||||
"myservice": {
|
"myservice": {
|
||||||
|
|
|
@ -63,7 +63,6 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newInitCommand(
|
cmd := newInitCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
|
|
|
@ -48,7 +48,6 @@ func TestSwarmJoinErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newJoinCommand(
|
cmd := newJoinCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
|
@ -93,7 +92,6 @@ func TestSwarmJoin(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
infoFunc: tc.infoFunc,
|
infoFunc: tc.infoFunc,
|
||||||
|
|
|
@ -87,7 +87,6 @@ func TestSwarmJoinTokenErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
swarmInspectFunc: tc.swarmInspectFunc,
|
swarmInspectFunc: tc.swarmInspectFunc,
|
||||||
|
@ -198,7 +197,6 @@ func TestSwarmJoinToken(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cli := test.NewFakeCli(&fakeClient{
|
cli := test.NewFakeCli(&fakeClient{
|
||||||
swarmInspectFunc: tc.swarmInspectFunc,
|
swarmInspectFunc: tc.swarmInspectFunc,
|
||||||
|
|
|
@ -32,7 +32,6 @@ func TestSwarmLeaveErrors(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
cmd := newLeaveCommand(
|
cmd := newLeaveCommand(
|
||||||
test.NewFakeCli(&fakeClient{
|
test.NewFakeCli(&fakeClient{
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue