Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63125853e3 | |||
| c599566439 | |||
| fb19def3ce | |||
| bccd4786f7 | |||
| 8992378c87 | |||
| f90273c340 | |||
| ca9636a1c3 | |||
| ad47d2a2c1 | |||
| a2a0fb73ea | |||
| 16d6c90a94 | |||
| f7be714467 | |||
| 33bf62c4cb | |||
| 7e972d2f5c | |||
| a0791d0e8a | |||
| 259d5e0f57 | |||
| c365c3cc8a | |||
| 54c8eb7941 | |||
| 5ab44a4690 | |||
| 33e5c87957 | |||
| 674f8d2979 | |||
| 2794ebd599 | |||
| 76863b46b7 | |||
| e7bcae9053 | |||
| 3577a17ce1 | |||
| a8e2130643 | |||
| 4f1a67e06b | |||
| dfe3c0c074 | |||
| 66ec7142ae | |||
| ab8a2c0716 | |||
| d11e73d6e6 | |||
| 36802176a7 | |||
| 3926ed6b24 | |||
| 787caf2fe4 | |||
| 4473f48847 | |||
| 7a4062a4a9 | |||
| d914a3f97e | |||
| 68fe829c96 | |||
| d60252954b | |||
| 6cd1f6f26d | |||
| 338e7a604c | |||
| 5dea9d881c | |||
| 4d67ef09c8 | |||
| 69a2c9fb6d | |||
| 333103d93f | |||
| d36bdb0d84 | |||
| 5777558c77 | |||
| 52848fb798 | |||
| bd43ca786b | |||
| 330a8e4e23 | |||
| e0b44d6d3f | |||
| d41cb083c3 | |||
| 0a8bb6e5b4 | |||
| c777dd16df | |||
| eea26c50dd | |||
| 1cf2c4efb3 | |||
| c2b9c1474a | |||
| 598442d37d | |||
| c964b80e53 | |||
| 0ca7be015e | |||
| 59fb099da2 | |||
| 27bf78d335 | |||
| 71b756be32 | |||
| 63a27bc9b6 | |||
| 5ed8f858cf | |||
| 6b99e0370c | |||
| 7d4bcd863a | |||
| 3134d55821 | |||
| 912c1ddf8a | |||
| c97e8091a6 | |||
| 82bd8158f7 | |||
| 8945848025 | |||
| b54897bcb8 | |||
| cd560916f3 | |||
| 9a101a955b | |||
| 50fae20748 | |||
| 37533c2f55 | |||
| 217971d481 | |||
| fce24d5f8d | |||
| 0e4f16f3bf | |||
| 6e35a78fd9 | |||
| bf1a701820 |
5
.github/workflows/e2e.yml
vendored
5
.github/workflows/e2e.yml
vendored
@ -28,9 +28,8 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 27-rc # testing
|
||||
- 26.1 # latest
|
||||
- 25.0 # latest - 1
|
||||
- 27.0 # latest
|
||||
- 26.1 # latest - 1
|
||||
- 23.0 # mirantis lts
|
||||
# TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
|
||||
# we could have a separate job that tests it against ubuntu 20.04
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@ -46,7 +46,8 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-12
|
||||
- macos-13 # macOS 13 on Intel
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
-
|
||||
@ -64,7 +65,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.11
|
||||
go-version: 1.21.12
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
18
.github/workflows/validate-pr.yml
vendored
18
.github/workflows/validate-pr.yml
vendored
@ -53,10 +53,16 @@ jobs:
|
||||
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
|
||||
# [X.Y backport] Some change that needs backporting to X.Y
|
||||
# [X.Y] Change directly targeting the X.Y branch
|
||||
- name: Get branch from PR title
|
||||
id: title_branch
|
||||
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check release branch
|
||||
if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '')
|
||||
run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1
|
||||
id: title_branch
|
||||
run: |
|
||||
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
|
||||
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
|
||||
|
||||
# get major version prefix from the release branch ("27.x -> "27.")
|
||||
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF"
|
||||
|
||||
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
|
||||
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -4,12 +4,12 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.11
|
||||
ARG GO_VERSION=1.21.12
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.15.1
|
||||
ARG COMPOSE_VERSION=v2.28.0
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
ARG COMPOSE_VERSION=v2.29.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
10
Makefile
10
Makefile
@ -86,6 +86,16 @@ mod-outdated: ## check outdated dependencies
|
||||
authors: ## generate AUTHORS file from git history
|
||||
scripts/docs/generate-authors.sh
|
||||
|
||||
.PHONY: completion
|
||||
completion: binary
|
||||
completion: /etc/bash_completion.d/docker
|
||||
completion: ## generate and install the completion scripts
|
||||
|
||||
.PHONY: /etc/bash_completion.d/docker
|
||||
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
|
||||
mkdir -p /etc/bash_completion.d
|
||||
docker completion bash > /etc/bash_completion.d/docker
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
@ -3,6 +3,7 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -19,5 +20,7 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -324,7 +324,7 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
if len(configFile.HTTPHeaders) > 0 {
|
||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||
}
|
||||
opts = append(opts, client.WithUserAgent(UserAgent()))
|
||||
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
|
||||
@ -2,13 +2,18 @@ package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
@ -108,3 +113,107 @@ func WithAPIClient(c client.APIClient) CLIOption {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders is the name of the environment-variable that can be
|
||||
// used to set custom HTTP headers to be sent by the client. This environment
|
||||
// variable is the equivalent to the HttpHeaders field in the configuration
|
||||
// file.
|
||||
//
|
||||
// WARNING: If both config and environment-variable are set, the environment
|
||||
// variable currently overrides all headers set in the configuration file.
|
||||
// This behavior may change in a future update, as we are considering the
|
||||
// environment variable to be appending to existing headers (and to only
|
||||
// override headers with the same name).
|
||||
//
|
||||
// While this env-var allows for custom headers to be set, it does not allow
|
||||
// for built-in headers (such as "User-Agent", if set) to be overridden.
|
||||
// Also see [client.WithHTTPHeaders] and [client.WithUserAgent].
|
||||
//
|
||||
// This environment variable can be used in situations where headers must be
|
||||
// set for a specific invocation of the CLI, but should not be set by default,
|
||||
// and therefore cannot be set in the config-file.
|
||||
//
|
||||
// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs,
|
||||
// where key must be a non-empty, valid MIME header format. Whitespaces surrounding
|
||||
// the key are trimmed, and the key is normalised. Whitespaces in values are
|
||||
// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored.
|
||||
// Tuples without a "=" produce an error.
|
||||
//
|
||||
// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted
|
||||
// if they must contain commas, which allows for multiple values for a single
|
||||
// header to be set. If a key is repeated in the list, later values override
|
||||
// prior values.
|
||||
//
|
||||
// For example, the following value:
|
||||
//
|
||||
// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two
|
||||
//
|
||||
// Produces four headers (four is omitted as it has an empty value set):
|
||||
//
|
||||
// - one (value is "one-value")
|
||||
// - two (value is "two,value")
|
||||
// - three (value is " a value with whitespace ")
|
||||
// - five (value is "five-two", the later value has overridden the prior value)
|
||||
const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
||||
|
||||
// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the
|
||||
// client through the [envOverrideHTTPHeaders] environment-variable. This
|
||||
// environment variable is the equivalent to the HttpHeaders field in the
|
||||
// configuration file.
|
||||
//
|
||||
// WARNING: If both config and environment-variable are set, the environment-
|
||||
// variable currently overrides all headers set in the configuration file.
|
||||
// This behavior may change in a future update, as we are considering the
|
||||
// environment-variable to be appending to existing headers (and to only
|
||||
// override headers with the same name).
|
||||
//
|
||||
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
|
||||
func withCustomHeadersFromEnv() client.Opt {
|
||||
return func(apiClient *client.Client) error {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return errdefs.InvalidParameter(errors.Errorf("failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, envOverrideHTTPHeaders, kv))
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return errdefs.InvalidParameter(errors.Errorf(`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, envOverrideHTTPHeaders, kv))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env)(apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +87,41 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
var received http.Header
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.Header.Clone()
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value from config-file",
|
||||
},
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
|
||||
// so "My-Header" should not be present.
|
||||
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
"Two": []string{"two,value"},
|
||||
"Three": []string{""},
|
||||
"Four": []string{"four-value-override"},
|
||||
"User-Agent": []string{UserAgent()},
|
||||
}
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
customVersion := "v3.3.3"
|
||||
t.Setenv("DOCKER_API_VERSION", customVersion)
|
||||
|
||||
@ -2,6 +2,7 @@ package completion
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -106,6 +107,41 @@ func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarNames offers completion for environment-variable names. This
|
||||
// completion can be used for "--env" and "--build-arg" flags, which
|
||||
// allow obtaining the value of the given environment-variable if present
|
||||
// in the local environment, so we only should complete the names of the
|
||||
// environment variables, and not their value. This also prevents the
|
||||
// completion script from printing values of environment variables
|
||||
// containing sensitive values.
|
||||
//
|
||||
// For example;
|
||||
//
|
||||
// export MY_VAR=hello
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
func FromList(options ...string) ValidArgsFn {
|
||||
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||
}
|
||||
|
||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||
// which indicates to let the shell perform its default behavior after
|
||||
// completions have been provided.
|
||||
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
// NoComplete is used for commands where there's no relevant completion
|
||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
|
||||
@ -43,14 +43,18 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -61,6 +61,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ func TestConfigListErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -74,6 +75,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Check(t, is.DeepEqual(names, removedConfigs))
|
||||
}
|
||||
|
||||
@ -69,10 +69,14 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
94
cli/command/container/completion.go
Normal file
94
cli/command/container/completion.go
Normal file
@ -0,0 +1,94 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// allLinuxCapabilities is a list of all known Linux capabilities.
|
||||
//
|
||||
// This list was based on the containerd pkg/cap package;
|
||||
// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181
|
||||
//
|
||||
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
|
||||
var allLinuxCapabilities = []string{
|
||||
"ALL", // magic value for "all capabilities"
|
||||
|
||||
// caps35 is the caps of kernel 3.5 (37 entries)
|
||||
"CAP_CHOWN", // 2.2
|
||||
"CAP_DAC_OVERRIDE", // 2.2
|
||||
"CAP_DAC_READ_SEARCH", // 2.2
|
||||
"CAP_FOWNER", // 2.2
|
||||
"CAP_FSETID", // 2.2
|
||||
"CAP_KILL", // 2.2
|
||||
"CAP_SETGID", // 2.2
|
||||
"CAP_SETUID", // 2.2
|
||||
"CAP_SETPCAP", // 2.2
|
||||
"CAP_LINUX_IMMUTABLE", // 2.2
|
||||
"CAP_NET_BIND_SERVICE", // 2.2
|
||||
"CAP_NET_BROADCAST", // 2.2
|
||||
"CAP_NET_ADMIN", // 2.2
|
||||
"CAP_NET_RAW", // 2.2
|
||||
"CAP_IPC_LOCK", // 2.2
|
||||
"CAP_IPC_OWNER", // 2.2
|
||||
"CAP_SYS_MODULE", // 2.2
|
||||
"CAP_SYS_RAWIO", // 2.2
|
||||
"CAP_SYS_CHROOT", // 2.2
|
||||
"CAP_SYS_PTRACE", // 2.2
|
||||
"CAP_SYS_PACCT", // 2.2
|
||||
"CAP_SYS_ADMIN", // 2.2
|
||||
"CAP_SYS_BOOT", // 2.2
|
||||
"CAP_SYS_NICE", // 2.2
|
||||
"CAP_SYS_RESOURCE", // 2.2
|
||||
"CAP_SYS_TIME", // 2.2
|
||||
"CAP_SYS_TTY_CONFIG", // 2.2
|
||||
"CAP_MKNOD", // 2.4
|
||||
"CAP_LEASE", // 2.4
|
||||
"CAP_AUDIT_WRITE", // 2.6.11
|
||||
"CAP_AUDIT_CONTROL", // 2.6.11
|
||||
"CAP_SETFCAP", // 2.6.24
|
||||
"CAP_MAC_OVERRIDE", // 2.6.25
|
||||
"CAP_MAC_ADMIN", // 2.6.25
|
||||
"CAP_SYSLOG", // 2.6.37
|
||||
"CAP_WAKE_ALARM", // 3.0
|
||||
"CAP_BLOCK_SUSPEND", // 3.5
|
||||
|
||||
// caps316 is the caps of kernel 3.16 (38 entries)
|
||||
"CAP_AUDIT_READ",
|
||||
|
||||
// caps58 is the caps of kernel 5.8 (40 entries)
|
||||
"CAP_PERFMON",
|
||||
"CAP_BPF",
|
||||
|
||||
// caps59 is the caps of kernel 5.9 (41 entries)
|
||||
"CAP_CHECKPOINT_RESTORE",
|
||||
}
|
||||
|
||||
// restartPolicies is a list of all valid restart-policies..
|
||||
//
|
||||
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
|
||||
var restartPolicies = []string{
|
||||
string(container.RestartPolicyDisabled),
|
||||
string(container.RestartPolicyAlways),
|
||||
string(container.RestartPolicyOnFailure),
|
||||
string(container.RestartPolicyUnlessStopped),
|
||||
}
|
||||
|
||||
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||
return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete)
|
||||
}
|
||||
|
||||
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||
return completion.FromList(restartPolicies...)(cmd, args, toComplete)
|
||||
}
|
||||
|
||||
func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
|
||||
// TODO(thaJeztah): do we want to provide the full list here, or a subset?
|
||||
signalNames := make([]string, 0, len(signal.SignalMap))
|
||||
for k := range signal.SignalMap {
|
||||
signalNames = append(signalNames, k)
|
||||
}
|
||||
return completion.FromList(signalNames...)(cmd, args, toComplete)
|
||||
}
|
||||
@ -178,13 +178,14 @@ func TestSplitCpArg(t *testing.T) {
|
||||
expectedContainer: "container",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
skip.If(t, testcase.os != "" && testcase.os != runtime.GOOS)
|
||||
for _, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
skip.If(t, tc.os == "windows" && runtime.GOOS != "windows" || tc.os == "linux" && runtime.GOOS == "windows")
|
||||
|
||||
ctr, path := splitCpArg(testcase.path)
|
||||
assert.Check(t, is.Equal(testcase.expectedContainer, ctr))
|
||||
assert.Check(t, is.Equal(testcase.expectedPath, path))
|
||||
ctr, path := splitCpArg(tc.path)
|
||||
assert.Check(t, is.Equal(tc.expectedContainer, ctr))
|
||||
assert.Check(t, is.Equal(tc.expectedPath, path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,6 +77,16 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
copts = addFlags(flags)
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -236,6 +236,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -79,12 +78,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault // _filedir
|
||||
})
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -39,6 +39,7 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
})
|
||||
cmd := NewExportCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -38,6 +38,9 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -50,7 +53,7 @@ func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) erro
|
||||
if err := <-errChan; err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
fmt.Fprintln(dockerCli.Out(), name)
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), name)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
|
||||
@ -163,6 +163,7 @@ func TestContainerListErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -20,5 +21,7 @@ func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -43,6 +43,9 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -45,6 +45,7 @@ func TestRemoveForce(t *testing.T) {
|
||||
})
|
||||
cmd := NewRmCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@ -70,22 +69,15 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
copts = addFlags(flags)
|
||||
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"env",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"env-file",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
},
|
||||
)
|
||||
cmd.RegisterFlagCompletionFunc(
|
||||
"network",
|
||||
completion.NetworkNames(dockerCli),
|
||||
)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
|
||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
|
||||
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -119,6 +111,8 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
|
||||
|
||||
//nolint:gocyclo
|
||||
func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOptions, copts *containerOptions, containerCfg *containerConfig) error {
|
||||
ctx = context.WithoutCancel(ctx)
|
||||
|
||||
config := containerCfg.Config
|
||||
stdout, stderr := dockerCli.Out(), dockerCli.Err()
|
||||
apiClient := dockerCli.Client()
|
||||
@ -178,6 +172,9 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
detachKeys = runOpts.detachKeys
|
||||
}
|
||||
|
||||
// ctx should not be cancellable here, as this would kill the stream to the container
|
||||
// and we want to keep the stream open until the process in the container exits or until
|
||||
// the user forcefully terminates the CLI.
|
||||
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: config.AttachStdin,
|
||||
|
||||
@ -127,23 +127,27 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err != nil)
|
||||
assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError))
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewRunCommand(fakeCLI)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err != nil)
|
||||
assert.Assert(t, is.Contains(fakeCLI.ErrBuffer().String(), tc.expectedError))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -43,6 +43,9 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
|
||||
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +83,8 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.Var(&options.cpus, "cpus", "Number of CPUs")
|
||||
flags.SetAnnotation("cpus", "version", []string{"1.29"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli-docs-tool/annotation"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
@ -104,7 +105,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"category-top": "4",
|
||||
"aliases": "docker image build, docker build, docker buildx build, docker builder build",
|
||||
"aliases": "docker image build, docker build, docker builder build",
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveFilterDirs
|
||||
@ -114,9 +115,12 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.VarP(&options.tags, "tag", "t", `Name and optionally a tag in the "name:tag" format`)
|
||||
flags.SetAnnotation("tag", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#tag"})
|
||||
flags.Var(&options.buildArgs, "build-arg", "Set build-time variables")
|
||||
flags.SetAnnotation("build-arg", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#build-arg"})
|
||||
flags.Var(options.ulimits, "ulimit", "Ulimit options")
|
||||
flags.StringVarP(&options.dockerfileName, "file", "f", "", `Name of the Dockerfile (Default is "PATH/Dockerfile")`)
|
||||
flags.SetAnnotation("file", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#file"})
|
||||
flags.VarP(&options.memory, "memory", "m", "Memory limit")
|
||||
flags.Var(&options.memorySwap, "memory-swap", `Swap limit equal to memory plus swap: -1 to enable unlimited swap`)
|
||||
flags.Var(&options.shmSize, "shm-size", `Size of "/dev/shm"`)
|
||||
@ -126,6 +130,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
|
||||
flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
|
||||
flags.StringVar(&options.cgroupParent, "cgroup-parent", "", `Set the parent cgroup for the "RUN" instructions during build`)
|
||||
flags.SetAnnotation("cgroup-parent", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#cgroup-parent"})
|
||||
flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology")
|
||||
flags.Var(&options.labels, "label", "Set metadata for an image")
|
||||
flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image")
|
||||
@ -138,8 +143,11 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
|
||||
flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build")
|
||||
flags.SetAnnotation("network", "version", []string{"1.25"})
|
||||
flags.SetAnnotation("network", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#network"})
|
||||
flags.Var(&options.extraHosts, "add-host", `Add a custom host-to-IP mapping ("host:ip")`)
|
||||
flags.SetAnnotation("add-host", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#add-host"})
|
||||
flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")
|
||||
flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"})
|
||||
flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
|
||||
|
||||
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
|
||||
|
||||
@ -127,6 +127,7 @@ func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
|
||||
// Clone a small repo that exists so git doesn't prompt for credentials
|
||||
cmd.SetArgs([]string{"github.com/docker/for-win"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "unable to prepare context")
|
||||
assert.ErrorContains(t, err, "docker-build-git")
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/spf13/cobra"
|
||||
@ -31,6 +32,7 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts.image = args[0]
|
||||
return runHistory(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker image history, docker history",
|
||||
},
|
||||
|
||||
@ -35,10 +35,14 @@ func TestNewHistoryCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
@ -44,6 +45,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
func TestNewImportCommandInvalidFile(t *testing.T) {
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"})
|
||||
assert.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file")
|
||||
}
|
||||
@ -96,9 +98,13 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/spf13/cobra"
|
||||
@ -30,6 +31,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts.refs = args
|
||||
return runInspect(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -25,10 +25,14 @@ func TestNewInspectCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -35,10 +35,14 @@ func TestNewImagesCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,14 +83,18 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
|
||||
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
||||
cmd := NewImagesCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name))
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
|
||||
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
||||
cmd := NewImagesCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,12 +40,16 @@ func TestNewLoadCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||
cli.In().SetIsTerminal(tc.isTerminalIn)
|
||||
cmd := NewLoadCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||
cli.In().SetIsTerminal(tc.isTerminalIn)
|
||||
cmd := NewLoadCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +57,7 @@ func TestNewLoadCommandInvalidInput(t *testing.T) {
|
||||
expectedError := "open *"
|
||||
cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"--input", "*"})
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, expectedError)
|
||||
@ -89,12 +94,15 @@ func TestNewLoadCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||
cmd := NewLoadCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name))
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||
cmd := NewLoadCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,12 +39,16 @@ func TestNewPruneCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
||||
imagesPruneFunc: tc.imagesPruneFunc,
|
||||
}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
||||
imagesPruneFunc: tc.imagesPruneFunc,
|
||||
}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +98,7 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
|
||||
// when prompted, answer "Y" to confirm the prune.
|
||||
@ -119,5 +124,8 @@ func TestPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -38,11 +38,15 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,18 +73,22 @@ func TestNewPullCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name)
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal(tc.expectedTag, ref), tc.name)
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
})
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name))
|
||||
})
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,16 +119,20 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
cli.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
cli.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewPullCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
@ -58,8 +58,13 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
|
||||
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
||||
flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"),
|
||||
|
||||
// Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to
|
||||
// pushing the image as-is. This also avoids forcing the platform selection
|
||||
// on older APIs which don't support it.
|
||||
flags.StringVar(&opts.platform, "platform", "",
|
||||
`Push a platform-specific manifest as a single-platform image to the registry.
|
||||
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
|
||||
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
||||
flags.SetAnnotation("platform", "version", []string{"1.46"})
|
||||
|
||||
@ -79,9 +84,9 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||
}
|
||||
platform = &p
|
||||
|
||||
printNote(dockerCli, `Selecting a single platform will only push one matching image manifest from a multi-platform image index.
|
||||
This means that any other components attached to the multi-platform image index (like Buildkit attestations) won't be pushed.
|
||||
If you want to only push a single platform image while preserving the attestations, please use 'docker convert\n'
|
||||
printNote(dockerCli, `Using --platform pushes only the specified platform manifest of a multi-platform image index.
|
||||
Other components, like attestations, will not be included.
|
||||
To push the complete multi-platform image, remove the --platform flag.
|
||||
`)
|
||||
}
|
||||
|
||||
@ -179,9 +184,22 @@ func handleAux(dockerCli command.Cli) func(jm jsonmessage.JSONMessage) {
|
||||
|
||||
func printNote(dockerCli command.Cli, format string, args ...any) {
|
||||
if dockerCli.Err().IsTerminal() {
|
||||
_, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ")
|
||||
} else {
|
||||
_, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ")
|
||||
format = strings.ReplaceAll(format, "--platform", aec.Bold.Apply("--platform"))
|
||||
}
|
||||
|
||||
header := " Info -> "
|
||||
padding := len(header)
|
||||
if dockerCli.Err().IsTerminal() {
|
||||
padding = len("i Info > ")
|
||||
header = aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → "))
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprint(dockerCli.Err(), header)
|
||||
s := fmt.Sprintf(format, args...)
|
||||
for idx, line := range strings.Split(s, "\n") {
|
||||
if idx > 0 {
|
||||
_, _ = fmt.Fprint(dockerCli.Err(), strings.Repeat(" ", padding))
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), aec.Italic.Apply(line))
|
||||
}
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), aec.Bold.Apply(format)+"\n", args...)
|
||||
}
|
||||
|
||||
@ -38,11 +38,15 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
|
||||
cmd := NewPushCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
|
||||
cmd := NewPushCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +77,7 @@ func TestNewPushCommandSuccess(t *testing.T) {
|
||||
})
|
||||
cmd := NewPushCommand(cli)
|
||||
cmd.SetOut(cli.OutBuffer())
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
if tc.output != "" {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
@ -29,6 +30,7 @@ func NewRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCli, opts, args)
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCli),
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker image rm, docker image remove, docker rmi",
|
||||
},
|
||||
|
||||
@ -62,11 +62,13 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
@ -119,10 +121,12 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
||||
cmd := NewRemoveCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(tc.expectedStderr, cli.ErrBuffer().String()))
|
||||
|
||||
@ -52,12 +52,16 @@ func TestNewSaveCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
|
||||
cli.Out().SetIsTerminal(tc.isTerminal)
|
||||
cmd := NewSaveCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
|
||||
cli.Out().SetIsTerminal(tc.isTerminal)
|
||||
cmd := NewSaveCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +81,7 @@ func TestNewSaveCommandSuccess(t *testing.T) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
deferredFunc: func() {
|
||||
os.Remove("save_tmp_file")
|
||||
_ = os.Remove("save_tmp_file")
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -92,16 +96,20 @@ func TestNewSaveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
if tc.deferredFunc != nil {
|
||||
tc.deferredFunc()
|
||||
}
|
||||
tc := tc
|
||||
t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
|
||||
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
if tc.deferredFunc != nil {
|
||||
tc.deferredFunc()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ func TestCliNewTagCommandErrors(t *testing.T) {
|
||||
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs(args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,6 +35,7 @@ func TestManifestAnnotateError(t *testing.T) {
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -52,6 +53,7 @@ func TestManifestAnnotate(t *testing.T) {
|
||||
cmd := newAnnotateCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/fake:0.0"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
expectedError := "manifest for image example.com/fake:0.0 does not exist"
|
||||
assert.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
|
||||
@ -71,6 +73,7 @@ func TestManifestAnnotate(t *testing.T) {
|
||||
err = cmd.Flags().Set("verbose", "true")
|
||||
assert.NilError(t, err)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer()
|
||||
expected := golden.Get(t, "inspect-annotate.golden")
|
||||
|
||||
@ -31,11 +31,15 @@ func TestManifestCreateErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,6 +91,7 @@ func TestManifestCreateRefuseAmend(t *testing.T) {
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err = cmd.Execute()
|
||||
assert.Error(t, err, "refusing to amend an existing manifest list with no --amend flag")
|
||||
}
|
||||
@ -109,6 +114,7 @@ func TestManifestCreateNoManifest(t *testing.T) {
|
||||
cmd := newCreateListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "No such image: example.com/alpine:3.0")
|
||||
}
|
||||
|
||||
@ -70,6 +70,7 @@ func TestInspectCommandLocalManifestNotFound(t *testing.T) {
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
@ -91,6 +92,7 @@ func TestInspectCommandNotFound(t *testing.T) {
|
||||
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"example.com/alpine:3.0"})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "No such manifest: example.com/alpine:3.0")
|
||||
|
||||
@ -44,6 +44,7 @@ func TestManifestPushErrors(t *testing.T) {
|
||||
cmd := newPushListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,6 +56,7 @@ func TestRmManifestNotCreated(t *testing.T) {
|
||||
cmd := newRmManifestListCommand(cli)
|
||||
cmd.SetArgs([]string{"example.com/first:1", "example.com/second:2"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
err = cmd.Execute()
|
||||
assert.Error(t, err, "No such manifest: example.com/first:1")
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ func TestNetworkConnectErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,6 +137,7 @@ func TestNetworkCreateErrors(t *testing.T) {
|
||||
assert.NilError(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ func TestNetworkDisconnectErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ func TestNetworkListErrors(t *testing.T) {
|
||||
}),
|
||||
)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -82,6 +83,7 @@ func TestNetworkList(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
|
||||
cmd := newListCommand(cli)
|
||||
|
||||
@ -2,6 +2,7 @@ package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -20,5 +21,8 @@ func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -114,5 +114,7 @@ func TestNetworkRemovePromptTermination(t *testing.T) {
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"existing-network"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ func TestNodeDemoteErrors(t *testing.T) {
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,6 +74,7 @@ func TestNodeInspectErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -105,13 +106,16 @@ func TestNodeInspectPretty(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
})
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"nodeID"})
|
||||
assert.Check(t, cmd.Flags().Set("pretty", "true"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
|
||||
})
|
||||
cmd := newInspectCommand(cli)
|
||||
cmd.SetArgs([]string{"nodeID"})
|
||||
assert.Check(t, cmd.Flags().Set("pretty", "true"))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ func TestNodePromoteErrors(t *testing.T) {
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ func TestNodePsErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -133,19 +134,22 @@ func TestNodePs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
taskInspectFunc: tc.taskInspectFunc,
|
||||
taskListFunc: tc.taskListFunc,
|
||||
serviceInspectFunc: tc.serviceInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
taskInspectFunc: tc.taskInspectFunc,
|
||||
taskListFunc: tc.taskListFunc,
|
||||
serviceInspectFunc: tc.serviceInspectFunc,
|
||||
})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name))
|
||||
})
|
||||
cmd := newPsCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ func TestNodeRemoveErrors(t *testing.T) {
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,6 +64,7 @@ func TestNodeUpdateErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ func TestCreateErrors(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -53,6 +54,7 @@ func TestCreateErrorOnFileAsContextDir(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs([]string{"plugin-foo", tmpFile.Path()})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), "context must be a directory")
|
||||
}
|
||||
|
||||
@ -64,6 +66,7 @@ func TestCreateErrorOnContextDirWithoutConfig(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
expectedErr := "config.json: no such file or directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
@ -82,6 +85,7 @@ func TestCreateErrorOnInvalidConfig(t *testing.T) {
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), "invalid")
|
||||
}
|
||||
|
||||
@ -98,6 +102,7 @@ func TestCreateErrorFromDaemon(t *testing.T) {
|
||||
}))
|
||||
cmd.SetArgs([]string{"plugin-foo", tmpDir.Path()})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), "error creating plugin")
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +41,7 @@ func TestPluginDisableErrors(t *testing.T) {
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ func TestPluginEnableErrors(t *testing.T) {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,6 +66,7 @@ func TestInspectErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
||||
cmd := newInspectCommand(cli)
|
||||
@ -74,6 +75,7 @@ func TestInspectErrors(t *testing.T) {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
@ -136,6 +138,7 @@ func TestInspect(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInspectFunc: tc.inspectFunc})
|
||||
cmd := newInspectCommand(cli)
|
||||
|
||||
@ -54,11 +54,15 @@ func TestInstallErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,16 +94,20 @@ func TestInstallContentTrustErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
||||
return nil, errors.New("should not try to install plugin")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
cli.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
||||
return nil, errors.New("should not try to install plugin")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
cli.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,10 +138,13 @@ func TestInstall(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput))
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
|
||||
cmd := newInstallCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,14 +46,18 @@ func TestListErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,13 +166,16 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
|
||||
tc := tc
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ func TestRemoveErrors(t *testing.T) {
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,8 @@ func TestUpgradePromptTermination(t *testing.T) {
|
||||
// need to set a remote address that does not match the plugin
|
||||
// reference sent by the `pluginInspectFunc`
|
||||
cmd.SetArgs([]string{"foo/bar", "localhost:5000/foo/bar:v1.0.0"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
@ -18,7 +16,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -44,7 +41,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||
default:
|
||||
}
|
||||
|
||||
err = ConfigureAuth(cli, "", "", &authConfig, isDefaultRegistry)
|
||||
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -90,7 +87,7 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
|
||||
}
|
||||
|
||||
// ConfigureAuth handles prompting of user's username and password if needed
|
||||
func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
|
||||
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
|
||||
// On Windows, force the use of the regular OS stdin stream.
|
||||
//
|
||||
// See:
|
||||
@ -125,9 +122,15 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
|
||||
fmt.Fprintln(cli.Out())
|
||||
}
|
||||
}
|
||||
promptWithDefault(cli.Out(), "Username", authconfig.Username)
|
||||
|
||||
var prompt string
|
||||
if authconfig.Username == "" {
|
||||
prompt = "Username: "
|
||||
} else {
|
||||
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
|
||||
}
|
||||
var err error
|
||||
flUser, err = readInput(cli.In())
|
||||
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -139,16 +142,13 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
|
||||
return errors.Errorf("Error: Non-null Username Required")
|
||||
}
|
||||
if flPassword == "" {
|
||||
oldState, err := term.SaveState(cli.In().FD())
|
||||
restoreInput, err := DisableInputEcho(cli.In())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cli.Out(), "Password: ")
|
||||
_ = term.DisableEcho(cli.In().FD(), oldState)
|
||||
defer func() {
|
||||
_ = term.RestoreTerminal(cli.In().FD(), oldState)
|
||||
}()
|
||||
flPassword, err = readInput(cli.In())
|
||||
defer restoreInput()
|
||||
|
||||
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -164,25 +164,6 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
|
||||
return nil
|
||||
}
|
||||
|
||||
// readInput reads, and returns user input from in. It tries to return a
|
||||
// single line, not including the end-of-line bytes, and trims leading
|
||||
// and trailing whitespace.
|
||||
func readInput(in io.Reader) (string, error) {
|
||||
line, _, err := bufio.NewReader(in).ReadLine()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error while reading input")
|
||||
}
|
||||
return strings.TrimSpace(string(line)), nil
|
||||
}
|
||||
|
||||
func promptWithDefault(out io.Writer, prompt string, configDefault string) {
|
||||
if configDefault == "" {
|
||||
fmt.Fprintf(out, "%s: ", prompt)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s (%s): ", prompt, configDefault)
|
||||
}
|
||||
}
|
||||
|
||||
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
|
||||
// image. The auth configuration is serialized as a base64url encoded RFC4648,
|
||||
// section 5) JSON string for sending through the X-Registry-Auth header.
|
||||
|
||||
@ -121,7 +121,7 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
|
||||
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
|
||||
}
|
||||
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
||||
err = command.ConfigureAuth(dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
|
||||
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -6,7 +6,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/cli/cli/command"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
@ -185,3 +188,41 @@ func TestRunLogin(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginTermination(t *testing.T) {
|
||||
p, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = tty.Close()
|
||||
_ = p.Close()
|
||||
})
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
})
|
||||
tmpFile := fs.NewFile(t, "test-login-termination")
|
||||
defer tmpFile.Remove()
|
||||
|
||||
configFile := cli.ConfigFile()
|
||||
configFile.Filename = tmpFile.Path()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
runErr := make(chan error)
|
||||
go func() {
|
||||
runErr <- runLogin(ctx, cli, loginOptions{})
|
||||
}()
|
||||
|
||||
// Let the prompt get canceled by the context
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timed out after 1 second. `runLogin` did not return")
|
||||
case err := <-runErr:
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ func TestSecretCreateErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,7 @@ func TestSecretInspectErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -92,13 +93,16 @@ func TestSecretInspectWithoutFormat(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretInspectFunc: tc.secretInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretInspectFunc: tc.secretInspectFunc,
|
||||
})
|
||||
cmd := newSecretInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
|
||||
})
|
||||
cmd := newSecretInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,14 +132,17 @@ func TestSecretInspectWithFormat(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretInspectFunc: tc.secretInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
secretInspectFunc: tc.secretInspectFunc,
|
||||
})
|
||||
cmd := newSecretInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.Check(t, cmd.Flags().Set("format", tc.format))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
|
||||
})
|
||||
cmd := newSecretInspectCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.Check(t, cmd.Flags().Set("format", tc.format))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ func TestSecretListErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +38,7 @@ func TestSecretRemoveErrors(t *testing.T) {
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -74,6 +75,7 @@ func TestSecretRemoveContinueAfterError(t *testing.T) {
|
||||
|
||||
cmd := newSecretRemoveCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(names)
|
||||
assert.Error(t, cmd.Execute(), "error removing secret: foo")
|
||||
assert.Check(t, is.DeepEqual(names, removedSecrets))
|
||||
|
||||
@ -68,6 +68,8 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.SetAnnotation(flagSysCtl, "version", []string{"1.40"})
|
||||
flags.Var(&opts.ulimits, flagUlimit, "Ulimit options")
|
||||
flags.SetAnnotation(flagUlimit, "version", []string{"1.41"})
|
||||
flags.Int64Var(&opts.oomScoreAdj, flagOomScoreAdj, 0, "Tune host's OOM preferences (-1000 to 1000) ")
|
||||
flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"})
|
||||
|
||||
flags.Var(cliopts.NewListOptsRef(&opts.resources.resGenericResources, ValidateSingleGenericResource), "generic-resource", "User defined resources")
|
||||
flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"})
|
||||
|
||||
@ -529,6 +529,7 @@ type serviceOptions struct {
|
||||
capAdd opts.ListOpts
|
||||
capDrop opts.ListOpts
|
||||
ulimits opts.UlimitOpt
|
||||
oomScoreAdj int64
|
||||
|
||||
resources resourceOptions
|
||||
stopGrace opts.DurationOpt
|
||||
@ -747,6 +748,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
|
||||
CapabilityAdd: capAdd,
|
||||
CapabilityDrop: capDrop,
|
||||
Ulimits: options.ulimits.GetList(),
|
||||
OomScoreAdj: options.oomScoreAdj,
|
||||
},
|
||||
Networks: networks,
|
||||
Resources: resources,
|
||||
@ -1043,6 +1045,7 @@ const (
|
||||
flagUlimit = "ulimit"
|
||||
flagUlimitAdd = "ulimit-add"
|
||||
flagUlimitRemove = "ulimit-rm"
|
||||
flagOomScoreAdj = "oom-score-adj"
|
||||
)
|
||||
|
||||
func validateAPIVersion(c swarm.ServiceSpec, serverAPIVersion string) error {
|
||||
|
||||
@ -91,14 +91,18 @@ func TestRollbackWithErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newRollbackCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc,
|
||||
serviceUpdateFunc: tc.serviceUpdateFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newRollbackCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
serviceInspectWithRawFunc: tc.serviceInspectWithRawFunc,
|
||||
serviceUpdateFunc: tc.serviceUpdateFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,6 +108,8 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.SetAnnotation(flagUlimitAdd, "version", []string{"1.41"})
|
||||
flags.Var(newListOptsVar(), flagUlimitRemove, "Remove a ulimit option")
|
||||
flags.SetAnnotation(flagUlimitRemove, "version", []string{"1.41"})
|
||||
flags.Int64Var(&options.oomScoreAdj, flagOomScoreAdj, 0, "Tune host's OOM preferences (-1000 to 1000) ")
|
||||
flags.SetAnnotation(flagOomScoreAdj, "version", []string{"1.46"})
|
||||
|
||||
// Add needs parsing, Remove only needs the key
|
||||
flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource")
|
||||
@ -367,6 +369,10 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
|
||||
updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes)
|
||||
}
|
||||
|
||||
if anyChanged(flags, flagOomScoreAdj) {
|
||||
updateInt64(flagOomScoreAdj, &task.ContainerSpec.OomScoreAdj)
|
||||
}
|
||||
|
||||
if err := addGenericResources(flags, task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
func TestConfigWithEmptyComposeFile(t *testing.T) {
|
||||
cmd := newConfigCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
assert.ErrorContains(t, cmd.Execute(), `Specify a Compose file`)
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ func TestDeployWithEmptyName(t *testing.T) {
|
||||
cmd := newDeployCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs([]string{"' '"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
|
||||
}
|
||||
|
||||
@ -25,18 +25,21 @@ func TestListErrors(t *testing.T) {
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
args: []string{},
|
||||
flags: map[string]string{
|
||||
"format": "{{invalid format}}",
|
||||
},
|
||||
expectedError: "template parsing error",
|
||||
},
|
||||
{
|
||||
args: []string{},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{}, errors.Errorf("error getting services")
|
||||
},
|
||||
expectedError: "error getting services",
|
||||
},
|
||||
{
|
||||
args: []string{},
|
||||
serviceListFunc: func(options types.ServiceListOptions) ([]swarm.Service, error) {
|
||||
return []swarm.Service{*builders.Service()}, nil
|
||||
},
|
||||
@ -45,15 +48,19 @@ func TestListErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: tc.serviceListFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{
|
||||
serviceListFunc: tc.serviceListFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,6 +104,7 @@ func TestStackList(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
var services []swarm.Service
|
||||
for _, name := range tc.serviceNames {
|
||||
|
||||
@ -40,12 +40,16 @@ func TestStackPsErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: tc.taskListFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
cmd := newPsCommand(test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: tc.taskListFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,6 +160,7 @@ func TestStackPs(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
taskListFunc: tc.taskListFunc,
|
||||
@ -169,6 +174,7 @@ func TestStackPs(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, cmd.Execute(), tc.expectedErr)
|
||||
|
||||
@ -45,6 +45,7 @@ func TestRemoveWithEmptyName(t *testing.T) {
|
||||
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs([]string{"good", "' '", "alsogood"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
|
||||
}
|
||||
@ -157,6 +158,7 @@ func TestRemoveContinueAfterError(t *testing.T) {
|
||||
}
|
||||
cmd := newRemoveCommand(test.NewFakeCli(cli))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"foo", "bar"})
|
||||
|
||||
assert.Error(t, cmd.Execute(), "Failed to remove some resources from stack: foo")
|
||||
|
||||
@ -80,6 +80,7 @@ func TestStackServicesErrors(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
@ -89,6 +90,7 @@ func TestRunServicesWithEmptyName(t *testing.T) {
|
||||
cmd := newServicesCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetArgs([]string{"' '"})
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
|
||||
}
|
||||
|
||||
@ -145,6 +145,7 @@ func TestDisplayTrustRootInvalidFlags(t *testing.T) {
|
||||
}))
|
||||
assert.Check(t, cmd.Flags().Parse(testCase.args))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), testCase.errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,18 +63,22 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newInitCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmInitFunc: tc.swarmInitFunc,
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
}))
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newInitCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmInitFunc: tc.swarmInitFunc,
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
}))
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.Error(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,14 +48,18 @@ func TestSwarmJoinErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newJoinCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmJoinFunc: tc.swarmJoinFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newJoinCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmJoinFunc: tc.swarmJoinFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +93,15 @@ func TestSwarmJoin(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
})
|
||||
cmd := newJoinCommand(cli)
|
||||
cmd.SetArgs([]string{"remote"})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), tc.expected))
|
||||
})
|
||||
cmd := newJoinCommand(cli)
|
||||
cmd.SetArgs([]string{"remote"})
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), tc.expected))
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,19 +87,23 @@ func TestSwarmJoinTokenErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
})
|
||||
cmd := newJoinTokenCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
cmd := newJoinTokenCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,17 +198,20 @@ func TestSwarmJoinToken(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
})
|
||||
cmd := newJoinTokenCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("jointoken-%s.golden", tc.name))
|
||||
})
|
||||
cmd := newJoinTokenCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("jointoken-%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,13 +32,17 @@ func TestSwarmLeaveErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newLeaveCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmLeaveFunc: tc.swarmLeaveFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newLeaveCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmLeaveFunc: tc.swarmLeaveFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -80,18 +80,22 @@ func TestSwarmUnlockKeyErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newUnlockKeyCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newUnlockKeyCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,17 +158,20 @@ func TestSwarmUnlockKey(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
swarmInspectFunc: tc.swarmInspectFunc,
|
||||
swarmUpdateFunc: tc.swarmUpdateFunc,
|
||||
swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc,
|
||||
})
|
||||
cmd := newUnlockKeyCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("unlockkeys-%s.golden", tc.name))
|
||||
})
|
||||
cmd := newUnlockKeyCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
assert.Check(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
assert.NilError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("unlockkeys-%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,14 +64,18 @@ func TestSwarmUnlockErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := newUnlockCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
swarmUnlockFunc: tc.swarmUnlockFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newUnlockCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
swarmUnlockFunc: tc.swarmUnlockFunc,
|
||||
}))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user