Compare commits
168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d260a54c81 | |||
| 3369ffe3e5 | |||
| 3cf84fbc5b | |||
| b1b03b3caa | |||
| 57d2fbb70e | |||
| c33cc928bb | |||
| 156e20ca86 | |||
| 7522a62d26 | |||
| 073e4e850e | |||
| 6d46ff7438 | |||
| 1d214b009d | |||
| 3092f67d5d | |||
| f01c09045d | |||
| 2ae903e86c | |||
| 5931a2f592 | |||
| ed9dd75575 | |||
| 69575f6175 | |||
| fa84cfd802 | |||
| b70a26deaf | |||
| 23563728bc | |||
| ea3201c575 | |||
| c050bf0909 | |||
| 4eef4afbf4 | |||
| 396a0823f8 | |||
| f96d8e78c4 | |||
| b120b96ac7 | |||
| 24186d8008 | |||
| 48b5efee03 | |||
| 38c3ff67aa | |||
| f8fc5b6bc3 | |||
| a4a79d75c0 | |||
| 115c8d56e5 | |||
| 38fcd1ca63 | |||
| 9392831817 | |||
| 4e9abfecf5 | |||
| dc4163fb1a | |||
| 8adf1ddb86 | |||
| c8e470057a | |||
| 3da26a5e79 | |||
| 4bb2abaa54 | |||
| 90c33dbfd9 | |||
| d17b3b2d80 | |||
| 9349f58b8a | |||
| adb018084c | |||
| a2f3f40233 | |||
| 46afd26c45 | |||
| d06f137170 | |||
| b55cf2f71f | |||
| 412d6fca9c | |||
| 5c54f75f2a | |||
| 617377c045 | |||
| 860b4f3a7d | |||
| 645b973521 | |||
| 3cf2fe0fff | |||
| 952c807716 | |||
| a8379092af | |||
| a1361a1372 | |||
| ab9d560570 | |||
| ba6460829b | |||
| 3b77477943 | |||
| 181575bf55 | |||
| 10bf91a02d | |||
| 310daf2db9 | |||
| 238d659ff9 | |||
| 4a43b8eaed | |||
| b086d72769 | |||
| 35e6a41ff2 | |||
| 5e80232398 | |||
| 19d02cd101 | |||
| ec35bab4fa | |||
| d51ba41791 | |||
| a5b82e9f87 | |||
| abe78b79de | |||
| 9015b71163 | |||
| 5786f20687 | |||
| 85dcacd78f | |||
| eb306df13d | |||
| efb9206433 | |||
| acc675014f | |||
| 79541b7e21 | |||
| 096ced0894 | |||
| f3c77df31e | |||
| 1b42d04d63 | |||
| f5a29ff8eb | |||
| dc6bfac81a | |||
| 9767d02493 | |||
| 2663c10574 | |||
| 48a670f26b | |||
| 1443014cbd | |||
| f2e98f9a93 | |||
| f6b7a11b93 | |||
| caf72655fb | |||
| e244044944 | |||
| a10debe6c4 | |||
| 20b9d489e0 | |||
| 67105e0995 | |||
| c514003e69 | |||
| 49a44fb73f | |||
| ff5b0d18fe | |||
| 21d96ffb8f | |||
| 155b5b4eaa | |||
| 3b5e814242 | |||
| 69ed6588a8 | |||
| 6b67b95493 | |||
| edc09e6038 | |||
| a253318869 | |||
| 9831fea4db | |||
| d1b88930c3 | |||
| d7f195833d | |||
| b807ea8084 | |||
| 1cda2c45f8 | |||
| 8bae662713 | |||
| df6220d434 | |||
| 30dd7c1319 | |||
| 2c214241fa | |||
| 34797d1678 | |||
| c986d09bca | |||
| 79fa65e7b5 | |||
| 324309b086 | |||
| 93ad9fbdf6 | |||
| 57d72237d7 | |||
| 69e0f53a03 | |||
| ce3b07c0db | |||
| 809eb8cdee | |||
| b158181a1d | |||
| 1328bb3381 | |||
| 843951e84a | |||
| b123ce6526 | |||
| 297704984b | |||
| d001ac4892 | |||
| 1587b70ee7 | |||
| 3865da2bc8 | |||
| cfa9fef77d | |||
| 6dcf285ff1 | |||
| dfdff11a22 | |||
| abf8cff233 | |||
| 0ef5e95481 | |||
| 690f63e6d3 | |||
| 53e2e54c29 | |||
| 1c4d6d85dd | |||
| a1bd689a4d | |||
| 4fa2fe9b9e | |||
| 68dac842a1 | |||
| bb1cb4992c | |||
| 9e2615bc46 | |||
| 4b1ed1f442 | |||
| d9294f06b5 | |||
| c6ae74956c | |||
| 69a4fcc3bb | |||
| ea84a3fc31 | |||
| 1e8555a38e | |||
| ec0a62436e | |||
| 469bfc05ed | |||
| c89975d531 | |||
| 091af560ca | |||
| 2402dac819 | |||
| b43377a38b | |||
| a71d39bcad | |||
| f18a476b6d | |||
| 8cd3b00420 | |||
| e2519aefcb | |||
| 1c73abb634 | |||
| 337dd82d8b | |||
| d633890f91 | |||
| ca95badb36 | |||
| aff4649cb7 | |||
| 852d198bb5 | |||
| 1f9573bb05 |
@ -1,19 +0,0 @@
|
||||
# This is a dummy CircleCI config file to avoid GitHub status failures reported
|
||||
# on branches that don't use CircleCI. This file should be deleted when all
|
||||
# branches are no longer dependent on CircleCI.
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
dummy:
|
||||
docker:
|
||||
- image: busybox
|
||||
steps:
|
||||
- run:
|
||||
name: "dummy"
|
||||
command: echo "dummy job"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- dummy
|
||||
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@ -19,7 +19,7 @@ on:
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- prepare
|
||||
strategy:
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||
steps:
|
||||
-
|
||||
@ -134,7 +134,7 @@ jobs:
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
|
||||
prepare-plugins:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
matrix: ${{ steps.platforms.outputs.matrix }}
|
||||
steps:
|
||||
@ -152,7 +152,7 @@ jobs:
|
||||
echo ${{ steps.platforms.outputs.matrix }}
|
||||
|
||||
plugins:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- prepare-plugins
|
||||
strategy:
|
||||
|
||||
12
.github/workflows/codeql.yml
vendored
12
.github/workflows/codeql.yml
vendored
@ -26,6 +26,8 @@ jobs:
|
||||
codeql:
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 360
|
||||
env:
|
||||
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
@ -52,6 +54,16 @@ jobs:
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
# and is trying to be smart by searching for modules in every directory,
|
||||
# including vendor directories. If no module is found, it's creating one
|
||||
# which is ... not what we want, so let's give it a "go.mod".
|
||||
# see: https://github.com/docker/cli/pull/4944#issuecomment-2002034698
|
||||
-
|
||||
name: Create go.mod
|
||||
run: |
|
||||
ln -s vendor.mod go.mod
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
17
.github/workflows/e2e.yml
vendored
17
.github/workflows/e2e.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -28,11 +28,11 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
# - 20.10-dind # FIXME: Fails on 20.10
|
||||
- stable-dind # TODO: Use 20.10-dind, stable-dind is deprecated
|
||||
include:
|
||||
- target: non-experimental
|
||||
engine-version: 19.03-dind
|
||||
- 25.0 # latest
|
||||
- 24.0 # 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
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -55,10 +55,11 @@ jobs:
|
||||
make -f docker.Makefile test-e2e-${{ matrix.target }}
|
||||
env:
|
||||
BASE_VARIANT: ${{ matrix.base }}
|
||||
E2E_ENGINE_VERSION: ${{ matrix.engine-version }}
|
||||
ENGINE_VERSION: ${{ matrix.engine-version }}
|
||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
ctn:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -31,9 +31,10 @@ jobs:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
host:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@ -63,7 +64,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.6
|
||||
go-version: 1.21.9
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
@ -73,7 +74,8 @@ jobs:
|
||||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
6
.github/workflows/validate.yml
vendored
6
.github/workflows/validate.yml
vendored
@ -16,7 +16,7 @@ on:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
# check that the generated Markdown and the checked-in files match
|
||||
validate-md:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
fi
|
||||
|
||||
validate-make:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@ -4,12 +4,12 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.6
|
||||
ARG XX_VERSION=1.2.1
|
||||
ARG GO_VERSION=1.21.9
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.12.1
|
||||
ARG COMPOSE_VERSION=v2.24.0
|
||||
ARG COMPOSE_VERSION=v2.24.3
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
@ -123,8 +123,14 @@ COPY --link . .
|
||||
FROM scratch AS plugins
|
||||
COPY --from=build-plugins /out .
|
||||
|
||||
FROM scratch AS bin-image
|
||||
FROM scratch AS bin-image-linux
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-darwin
|
||||
COPY --from=build /out/docker /docker
|
||||
FROM scratch AS bin-image-windows
|
||||
COPY --from=build /out/docker /docker.exe
|
||||
|
||||
FROM bin-image-${TARGETOS} AS bin-image
|
||||
|
||||
FROM scratch AS binary
|
||||
COPY --from=build /out .
|
||||
|
||||
2
Makefile
2
Makefile
@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
||||
.PHONY: fmt
|
||||
fmt: ## run gofumpt (if present) or gofmt
|
||||
@if command -v gofumpt > /dev/null; then \
|
||||
gofumpt -w -d -lang=1.19 . ; \
|
||||
gofumpt -w -d -lang=1.21 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
|
||||
@ -2,11 +2,14 @@ package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,6 +33,10 @@ const (
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
|
||||
)
|
||||
|
||||
var pluginCommandStubsOnce sync.Once
|
||||
@ -98,3 +105,44 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
dockerCliAttributePrefix = attribute.Key("docker.cli")
|
||||
|
||||
cobraCommandPath = attribute.Key("cobra.command_path")
|
||||
)
|
||||
|
||||
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
|
||||
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
|
||||
if commandPath == "" {
|
||||
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
|
||||
}
|
||||
|
||||
attrSet := attribute.NewSet(
|
||||
cobraCommandPath.String(commandPath),
|
||||
)
|
||||
|
||||
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
|
||||
for iter := attrSet.Iter(); iter.Next(); {
|
||||
attr := iter.Attribute()
|
||||
kvs = append(kvs, attribute.KeyValue{
|
||||
Key: dockerCliAttributePrefix + "." + attr.Key,
|
||||
Value: attr.Value,
|
||||
})
|
||||
}
|
||||
return attribute.NewSet(kvs...)
|
||||
}
|
||||
|
||||
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
|
||||
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
|
||||
// values in environment variables need to be in baggage format
|
||||
// otel/baggage package can be used after update to v1.22, currently it encodes incorrectly
|
||||
attrsSlice := make([]string, attrs.Len())
|
||||
for iter := attrs.Iter(); iter.Next(); {
|
||||
i, v := iter.IndexedAttribute()
|
||||
attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString())
|
||||
}
|
||||
env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ","))
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
@ -17,11 +17,17 @@ import (
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
const (
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||
|
||||
// ResourceAttributesEnvvar is the name of the envvar that includes additional
|
||||
// resource attributes for OTEL.
|
||||
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
|
||||
)
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
@ -236,6 +242,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
)
|
||||
|
||||
// EnvKey represents the well-known environment variable used to pass the plugin being
|
||||
@ -17,7 +17,7 @@ const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
|
||||
// and update the conn pointer, and returns the listener for the socket (which the caller
|
||||
// is responsible for closing when it's no longer needed).
|
||||
func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
|
||||
listener, err := listen("docker_cli_" + uuid.Generate().String())
|
||||
listener, err := listen("docker_cli_" + randomID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -27,6 +27,14 @@ func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func randomID() string {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func accept(listener *net.UnixListener, conn **net.UnixConn) {
|
||||
go func() {
|
||||
for {
|
||||
@ -65,6 +73,7 @@ func ConnectAndWait(cb func()) {
|
||||
_, err := conn.Read(b)
|
||||
if errors.Is(err, io.EOF) {
|
||||
cb()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//go:build !darwin
|
||||
//go:build !darwin && !openbsd
|
||||
|
||||
package socket
|
||||
|
||||
@ -15,5 +15,6 @@ func listen(socketname string) (*net.UnixListener, error) {
|
||||
|
||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
||||
// do nothing
|
||||
// while on darwin we would unlink here; on non-darwin the socket is abstract and not present on the filesystem
|
||||
// while on darwin and OpenBSD we would unlink here;
|
||||
// on non-darwin the socket is abstract and not present on the filesystem
|
||||
}
|
||||
|
||||
19
cli-plugins/socket/socket_openbsd.go
Normal file
19
cli-plugins/socket/socket_openbsd.go
Normal file
@ -0,0 +1,19 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func listen(socketname string) (*net.UnixListener, error) {
|
||||
return net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: filepath.Join(os.TempDir(), socketname),
|
||||
Net: "unix",
|
||||
})
|
||||
}
|
||||
|
||||
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
|
||||
syscall.Unlink(listener.Addr().String())
|
||||
}
|
||||
133
cli-plugins/socket/socket_test.go
Normal file
133
cli-plugins/socket/socket_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package socket
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/poll"
|
||||
)
|
||||
|
||||
func TestSetupConn(t *testing.T) {
|
||||
t.Run("updates conn when connected", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
|
||||
pollConnNotNil(t, &conn)
|
||||
})
|
||||
|
||||
t.Run("allows reconnects", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
|
||||
otherConn, err := net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
|
||||
otherConn.Close()
|
||||
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to redial listener")
|
||||
})
|
||||
|
||||
t.Run("does not leak sockets to local directory", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, listener != nil, "returned nil listener but no error")
|
||||
checkDirNoPluginSocket(t)
|
||||
|
||||
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
|
||||
assert.NilError(t, err, "failed to resolve listener address")
|
||||
_, err = net.DialUnix("unix", nil, addr)
|
||||
assert.NilError(t, err, "failed to dial returned listener")
|
||||
checkDirNoPluginSocket(t)
|
||||
})
|
||||
}
|
||||
|
||||
func checkDirNoPluginSocket(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
files, err := os.ReadDir(".")
|
||||
assert.NilError(t, err, "failed to list files in dir to check for leaked sockets")
|
||||
|
||||
for _, f := range files {
|
||||
info, err := f.Info()
|
||||
assert.NilError(t, err, "failed to check file info")
|
||||
// check for a socket with `docker_cli_` in the name (from `SetupConn()`)
|
||||
if strings.Contains(f.Name(), "docker_cli_") && info.Mode().Type() == fs.ModeSocket {
|
||||
t.Fatal("found socket in a local directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectAndWait(t *testing.T) {
|
||||
t.Run("calls cancel func on EOF", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err, "failed to setup listener")
|
||||
|
||||
done := make(chan struct{})
|
||||
t.Setenv(EnvKey, listener.Addr().String())
|
||||
cancelFunc := func() {
|
||||
done <- struct{}{}
|
||||
}
|
||||
ConnectAndWait(cancelFunc)
|
||||
pollConnNotNil(t, &conn)
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
t.Fatal("cancel function not closed after 10ms")
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: this test cannot be executed with `t.Parallel()`, due to
|
||||
// relying on goroutine numbers to ensure correct behaviour
|
||||
t.Run("connect goroutine exits after EOF", func(t *testing.T) {
|
||||
var conn *net.UnixConn
|
||||
listener, err := SetupConn(&conn)
|
||||
assert.NilError(t, err, "failed to setup listener")
|
||||
t.Setenv(EnvKey, listener.Addr().String())
|
||||
numGoroutines := runtime.NumGoroutine()
|
||||
|
||||
ConnectAndWait(func() {})
|
||||
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
|
||||
|
||||
pollConnNotNil(t, &conn)
|
||||
conn.Close()
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if runtime.NumGoroutine() > numGoroutines+1 {
|
||||
return poll.Continue("waiting for connect goroutine to exit")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||
})
|
||||
}
|
||||
|
||||
func pollConnNotNil(t *testing.T, conn **net.UnixConn) {
|
||||
t.Helper()
|
||||
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if *conn == nil {
|
||||
return poll.Continue("waiting for conn to not be nil")
|
||||
}
|
||||
return poll.Success()
|
||||
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
|
||||
}
|
||||
@ -470,7 +470,7 @@ Common Commands:
|
||||
Management Commands:
|
||||
|
||||
{{- range managementSubCommands . }}
|
||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
|
||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
@ -479,7 +479,7 @@ Management Commands:
|
||||
Swarm Commands:
|
||||
|
||||
{{- range orchestratorSubCommands . }}
|
||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
|
||||
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
|
||||
{{- end}}
|
||||
|
||||
{{- end}}
|
||||
|
||||
20
cli/command/builder/client_test.go
Normal file
20
cli/command/builder/client_test.go
Normal file
@ -0,0 +1,20 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
builderPruneFunc func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||
if c.builderPruneFunc != nil {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@ -66,8 +66,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
if options.all {
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
|
||||
|
||||
28
cli/command/builder/prune_test.go
Normal file
28
cli/command/builder/prune_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
builderPruneFunc: func(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
||||
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
@ -35,7 +35,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
|
||||
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
|
||||
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -11,19 +11,11 @@ import (
|
||||
"github.com/moby/term"
|
||||
)
|
||||
|
||||
// CLIOption applies a modification on a DockerCli.
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
// options can be passed to [NewDockerCli] to initialize a new CLI, or
|
||||
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
|
||||
type CLIOption func(cli *DockerCli) error
|
||||
|
||||
// DockerCliOption applies a modification on a DockerCli.
|
||||
//
|
||||
// Deprecated: use [CLIOption] instead.
|
||||
type DockerCliOption = CLIOption
|
||||
|
||||
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
|
||||
//
|
||||
// Deprecated: use [CLIOption] instead.
|
||||
type InitializeOpt = CLIOption
|
||||
|
||||
// WithStandardStreams sets a cli in, out and err streams with the standard streams.
|
||||
func WithStandardStreams() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -17,7 +18,7 @@ type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]s
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCli command.Cli) ValidArgsFn {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{})
|
||||
list, err := dockerCli.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp)
|
||||
flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
|
||||
@ -43,22 +43,21 @@ func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClie
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts AttachOptions
|
||||
var ctr string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "attach [OPTIONS] CONTAINER",
|
||||
Short: "Attach local standard input, output, and error streams to a running container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctr = args[0]
|
||||
return RunAttach(cmd.Context(), dockerCli, ctr, &opts)
|
||||
containerID := args[0]
|
||||
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container attach, docker attach",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr types.Container) bool {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr types.Container) bool {
|
||||
return ctr.State != "paused"
|
||||
}),
|
||||
}
|
||||
@ -71,13 +70,13 @@ func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// RunAttach executes an `attach` command
|
||||
func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *AttachOptions) error {
|
||||
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
resultC, errC := apiClient.ContainerWait(ctx, target, "")
|
||||
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, target)
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -106,11 +105,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *
|
||||
|
||||
if opts.Proxy && !c.Config.Tty {
|
||||
sigc := notifyAllSignals()
|
||||
go ForwardAllSignals(ctx, apiClient, target, sigc)
|
||||
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
resp, errAttach := apiClient.ContainerAttach(ctx, target, options)
|
||||
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
@ -124,13 +123,13 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *
|
||||
// the container and not exit.
|
||||
//
|
||||
// Recheck the container's state to avoid attach block.
|
||||
_, err = inspectContainerAndCheckState(ctx, apiClient, target)
|
||||
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
|
||||
resizeTTY(ctx, dockerCLI, target)
|
||||
resizeTTY(ctx, dockerCLI, containerID)
|
||||
}
|
||||
|
||||
streamer := hijackedIOStreamer{
|
||||
|
||||
@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
@ -23,7 +25,7 @@ type fakeClient struct {
|
||||
platform *specs.Platform,
|
||||
containerName string) (container.CreateResponse, error)
|
||||
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||
imageCreateFunc func(parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (system.Info, error)
|
||||
containerStatPathFunc func(containerID, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
|
||||
@ -34,6 +36,7 @@ type fakeClient struct {
|
||||
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
|
||||
Version string
|
||||
}
|
||||
|
||||
@ -90,7 +93,7 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
if f.imageCreateFunc != nil {
|
||||
return f.imageCreateFunc(parentReference, options)
|
||||
}
|
||||
@ -163,3 +166,10 @@ func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal stri
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
if f.containerPruneFunc != nil {
|
||||
return f.containerPruneFunc(ctx, pruneFilters)
|
||||
}
|
||||
return types.ContainersPruneReport{}, nil
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@ import (
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
@ -119,7 +119,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
return err
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, types.ImageCreateOptions{
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platform: options.platform,
|
||||
})
|
||||
|
||||
@ -15,8 +15,8 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@ -134,7 +134,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
return container.CreateResponse{ID: containerID}, nil
|
||||
}
|
||||
},
|
||||
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
imageCreateFunc: func(parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
defer func() { pullCounter++ }()
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
|
||||
@ -66,12 +66,12 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
flags.StringVarP(&options.DetachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container")
|
||||
flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
flags.BoolVarP(&options.Interactive, "interactive", "i", false, "Keep STDIN open even if not attached")
|
||||
flags.BoolVarP(&options.TTY, "tty", "t", false, "Allocate a pseudo-TTY")
|
||||
flags.BoolVarP(&options.Detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
|
||||
flags.BoolVarP(&options.Privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.Env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
|
||||
|
||||
@ -55,7 +55,7 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
|
||||
flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)")
|
||||
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
|
||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
|
||||
@ -53,8 +53,10 @@ Are you sure you want to continue?`
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)
|
||||
|
||||
29
cli/command/container/prune_test.go
Normal file
29
cli/command/container/prune_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestContainerPrunePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
return types.ContainersPruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
@ -28,13 +28,6 @@ type StartOptions struct {
|
||||
Containers []string
|
||||
}
|
||||
|
||||
// NewStartOptions creates a new StartOptions.
|
||||
//
|
||||
// Deprecated: create a new [StartOptions] directly.
|
||||
func NewStartOptions() StartOptions {
|
||||
return StartOptions{}
|
||||
}
|
||||
|
||||
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||
func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts StartOptions
|
||||
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -106,15 +107,6 @@ var acceptedStatsFilters = map[string]bool{
|
||||
func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// Get the daemonOSType if not set already
|
||||
if daemonOSType == "" {
|
||||
sv, err := apiClient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
daemonOSType = sv.Os
|
||||
}
|
||||
|
||||
// waitFirst is a WaitGroup to wait first stat data's reach for each container
|
||||
waitFirst := &sync.WaitGroup{}
|
||||
// closeChan is a non-buffered channel used to collect errors from goroutines.
|
||||
@ -138,9 +130,9 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
return err
|
||||
}
|
||||
|
||||
eh := command.InitEventHandler()
|
||||
eh := newEventHandler()
|
||||
if options.All {
|
||||
eh.Handle(events.ActionCreate, func(e events.Message) {
|
||||
eh.setHandler(events.ActionCreate, func(e events.Message) {
|
||||
s := NewStats(e.Actor.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
@ -149,7 +141,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
})
|
||||
}
|
||||
|
||||
eh.Handle(events.ActionStart, func(e events.Message) {
|
||||
eh.setHandler(events.ActionStart, func(e events.Message) {
|
||||
s := NewStats(e.Actor.ID[:12])
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
@ -158,7 +150,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
})
|
||||
|
||||
if !options.All {
|
||||
eh.Handle(events.ActionDie, func(e events.Message) {
|
||||
eh.setHandler(events.ActionDie, func(e events.Message) {
|
||||
cStats.remove(e.Actor.ID[:12])
|
||||
})
|
||||
}
|
||||
@ -195,7 +187,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
|
||||
eventChan := make(chan events.Message)
|
||||
go eh.Watch(eventChan)
|
||||
go eh.watch(eventChan)
|
||||
stopped := make(chan struct{})
|
||||
go monitorContainerEvents(started, eventChan, stopped)
|
||||
defer close(stopped)
|
||||
@ -267,6 +259,12 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
if daemonOSType == "" {
|
||||
// Get the daemonOSType if not set already. The daemonOSType variable
|
||||
// should already be set when collecting stats as part of "collect()",
|
||||
// so we unlikely hit this code in practice.
|
||||
daemonOSType = dockerCLI.ServerInfo().OSType
|
||||
}
|
||||
statsCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewStatsFormat(format, daemonOSType),
|
||||
@ -316,3 +314,31 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newEventHandler initializes and returns an eventHandler
|
||||
func newEventHandler() *eventHandler {
|
||||
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
||||
}
|
||||
|
||||
// eventHandler allows for registering specific events to setHandler.
|
||||
type eventHandler struct {
|
||||
handlers map[events.Action]func(events.Message)
|
||||
}
|
||||
|
||||
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
|
||||
eh.handlers[action] = handler
|
||||
}
|
||||
|
||||
// watch ranges over the passed in event chan and processes the events based on the
|
||||
// handlers created for a given action.
|
||||
// To stop watching, close the event chan.
|
||||
func (eh *eventHandler) watch(c <-chan events.Message) {
|
||||
for e := range c {
|
||||
h, exists := eh.handlers[e.Action]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("event handler: received event: %v", e)
|
||||
go h(e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,12 +9,16 @@ import (
|
||||
|
||||
// EventHandler is abstract interface for user to customize
|
||||
// own handle functions of each type of events
|
||||
//
|
||||
// Deprecated: EventHandler is no longer used, and will be removed in the next release.
|
||||
type EventHandler interface {
|
||||
Handle(action events.Action, h func(events.Message))
|
||||
Watch(c <-chan events.Message)
|
||||
}
|
||||
|
||||
// InitEventHandler initializes and returns an EventHandler
|
||||
//
|
||||
// Deprecated: InitEventHandler is no longer used, and will be removed in the next release.
|
||||
func InitEventHandler() EventHandler {
|
||||
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
||||
}
|
||||
|
||||
@ -129,7 +129,6 @@ func TestGetContextFromReaderString(t *testing.T) {
|
||||
tarReader := tar.NewReader(tarArchive)
|
||||
|
||||
_, err = tarReader.Next()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error when reading tar archive: %s", err)
|
||||
}
|
||||
|
||||
@ -17,15 +17,15 @@ type fakeClient struct {
|
||||
client.Client
|
||||
imageTagFunc func(string, string) error
|
||||
imageSaveFunc func(images []string) (io.ReadCloser, error)
|
||||
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
|
||||
imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
|
||||
imageRemoveFunc func(image string, options image.RemoveOptions) ([]image.DeleteResponse, error)
|
||||
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (system.Info, error)
|
||||
imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error)
|
||||
imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error)
|
||||
imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
|
||||
imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
|
||||
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
|
||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
|
||||
imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
|
||||
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
|
||||
}
|
||||
@ -45,7 +45,7 @@ func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadClo
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImageRemove(_ context.Context, img string,
|
||||
options types.ImageRemoveOptions,
|
||||
options image.RemoveOptions,
|
||||
) ([]image.DeleteResponse, error) {
|
||||
if cli.imageRemoveFunc != nil {
|
||||
return cli.imageRemoveFunc(img, options)
|
||||
@ -53,7 +53,7 @@ func (cli *fakeClient) ImageRemove(_ context.Context, img string,
|
||||
return []image.DeleteResponse{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
|
||||
func (cli *fakeClient) ImagePush(_ context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) {
|
||||
if cli.imagePushFunc != nil {
|
||||
return cli.imagePushFunc(ref, options)
|
||||
}
|
||||
@ -67,7 +67,7 @@ func (cli *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||
return system.Info{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
|
||||
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
if cli.imagePullFunc != nil {
|
||||
cli.imagePullFunc(ref, options)
|
||||
}
|
||||
@ -88,7 +88,7 @@ func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool)
|
||||
return types.ImageLoadResponse{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImageList(_ context.Context, options types.ImageListOptions) ([]image.Summary, error) {
|
||||
func (cli *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||
if cli.imageListFunc != nil {
|
||||
return cli.imageListFunc(options)
|
||||
}
|
||||
@ -103,7 +103,7 @@ func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, img string) (types
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string,
|
||||
options types.ImageImportOptions,
|
||||
options image.ImportOptions,
|
||||
) (io.ReadCloser, error) {
|
||||
if cli.imageImportFunc != nil {
|
||||
return cli.imageImportFunc(source, ref, options)
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -78,7 +79,7 @@ func runImport(ctx context.Context, dockerCli command.Cli, options importOptions
|
||||
}
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, types.ImageImportOptions{
|
||||
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, image.ImportOptions{
|
||||
Message: options.message,
|
||||
Changes: options.changes.GetAll(),
|
||||
Platform: options.platform,
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -17,7 +18,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
|
||||
}{
|
||||
{
|
||||
name: "wrong-args",
|
||||
@ -28,7 +29,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
name: "import-failed",
|
||||
args: []string{"testdata/import-command-success.input.txt"},
|
||||
expectedError: "something went wrong",
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
||||
return nil, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
@ -52,7 +53,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
@ -65,7 +66,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "double",
|
||||
args: []string{"-", "image:local"},
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal("image:local", ref))
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
@ -73,7 +74,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "message",
|
||||
args: []string{"--message", "test message", "-"},
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal("test message", options.Message))
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
@ -81,7 +82,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "change",
|
||||
args: []string{"--change", "ENV DEBUG=true", "-"},
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal("ENV DEBUG=true", options.Changes[0]))
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
@ -89,7 +90,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "change legacy syntax",
|
||||
args: []string{"--change", "ENV DEBUG true", "-"},
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
|
||||
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
|
||||
assert.Check(t, is.Equal("ENV DEBUG true", options.Changes[0]))
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
|
||||
@ -2,13 +2,15 @@ package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -21,10 +23,11 @@ type imagesOptions struct {
|
||||
showDigests bool
|
||||
format string
|
||||
filter opts.FilterOpt
|
||||
calledAs string
|
||||
}
|
||||
|
||||
// NewImagesCommand creates a new `docker images` command
|
||||
func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := imagesOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -35,7 +38,11 @@ func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if len(args) > 0 {
|
||||
options.matchName = args[0]
|
||||
}
|
||||
return runImages(cmd.Context(), dockerCli, options)
|
||||
// Pass through how the command was invoked. We use this to print
|
||||
// warnings when an ambiguous argument was passed when using the
|
||||
// legacy (top-level) "docker images" subcommand.
|
||||
options.calledAs = cmd.CalledAs()
|
||||
return runImages(cmd.Context(), dockerCLI, options)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"category-top": "7",
|
||||
@ -55,33 +62,31 @@ func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := *NewImagesCommand(dockerCli)
|
||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := *NewImagesCommand(dockerCLI)
|
||||
cmd.Aliases = []string{"list"}
|
||||
cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]"
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func runImages(ctx context.Context, dockerCli command.Cli, options imagesOptions) error {
|
||||
func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions) error {
|
||||
filters := options.filter.Value()
|
||||
if options.matchName != "" {
|
||||
filters.Add("reference", options.matchName)
|
||||
}
|
||||
|
||||
listOptions := types.ImageListOptions{
|
||||
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
|
||||
All: options.all,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
images, err := dockerCli.Client().ImageList(ctx, listOptions)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !options.quiet {
|
||||
format = dockerCli.ConfigFile().ImagesFormat
|
||||
if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet {
|
||||
format = dockerCLI.ConfigFile().ImagesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
@ -89,11 +94,50 @@ func runImages(ctx context.Context, dockerCli command.Cli, options imagesOptions
|
||||
|
||||
imageCtx := formatter.ImageContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Output: dockerCLI.Out(),
|
||||
Format: formatter.NewImageFormat(format, options.quiet, options.showDigests),
|
||||
Trunc: !options.noTrunc,
|
||||
},
|
||||
Digest: options.showDigests,
|
||||
}
|
||||
return formatter.ImageWrite(imageCtx, images)
|
||||
if err := formatter.ImageWrite(imageCtx, images); err != nil {
|
||||
return err
|
||||
}
|
||||
if options.matchName != "" && len(images) == 0 && options.calledAs == "images" {
|
||||
printAmbiguousHint(dockerCLI.Err(), options.matchName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// printAmbiguousHint prints an informational warning if the provided filter
|
||||
// argument is ambiguous.
|
||||
//
|
||||
// The "docker images" top-level subcommand predates the "docker <object> <verb>"
|
||||
// convention (e.g. "docker image ls"), but accepts a positional argument to
|
||||
// search/filter images by name (globbing). It's common for users to accidentally
|
||||
// mistake these commands, and to use (e.g.) "docker images ls", expecting
|
||||
// to see all images, but ending up with an empty list because no image named
|
||||
// "ls" was found.
|
||||
//
|
||||
// Disallowing these search-terms would be a breaking change, but we can print
|
||||
// and informational message to help the users correct their mistake.
|
||||
func printAmbiguousHint(stdErr io.Writer, matchName string) {
|
||||
switch matchName {
|
||||
// List of subcommands for "docker image" and their aliases (see "docker image --help"):
|
||||
case "build",
|
||||
"history",
|
||||
"import",
|
||||
"inspect",
|
||||
"list",
|
||||
"load",
|
||||
"ls",
|
||||
"prune",
|
||||
"pull",
|
||||
"push",
|
||||
"rm",
|
||||
"save",
|
||||
"tag":
|
||||
|
||||
_, _ = fmt.Fprintf(stdErr, "\nNo images found matching %q: did you mean \"docker image %[1]s\"?\n", matchName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -20,7 +19,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
|
||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||
}{
|
||||
{
|
||||
name: "wrong-args",
|
||||
@ -30,7 +29,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "failed-list",
|
||||
expectedError: "something went wrong",
|
||||
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
return []image.Summary{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
@ -48,7 +47,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
imageFormat string
|
||||
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
|
||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
@ -65,7 +64,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "match-name",
|
||||
args: []string{"image"},
|
||||
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
assert.Check(t, is.Equal("image", options.Filters.Get("reference")[0]))
|
||||
return []image.Summary{}, nil
|
||||
},
|
||||
@ -73,7 +72,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "filters",
|
||||
args: []string{"--filter", "name=value"},
|
||||
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
assert.Check(t, is.Equal("value", options.Filters.Get("name")[0]))
|
||||
return []image.Summary{}, nil
|
||||
},
|
||||
@ -96,3 +95,17 @@ func TestNewListCommandAlias(t *testing.T) {
|
||||
assert.Check(t, cmd.HasAlias("list"))
|
||||
assert.Check(t, !cmd.HasAlias("other"))
|
||||
}
|
||||
|
||||
func TestNewListCommandAmbiguous(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := NewImagesCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
|
||||
// Set the Use field to mimic that the command was called as "docker images",
|
||||
// not "docker image ls".
|
||||
cmd.Use = "images"
|
||||
cmd.SetArgs([]string{"ls"})
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.ErrBuffer().String(), "list-command-ambiguous.golden")
|
||||
}
|
||||
|
||||
@ -67,8 +67,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
if options.all {
|
||||
warning = allImageWarning
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -101,3 +103,19 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrunePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
|
||||
return types.ImagesPruneReport{}, errors.New("fakeClient imagesPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@ -69,7 +69,7 @@ func TestNewPullCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
|
||||
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
|
||||
},
|
||||
@ -111,7 +111,7 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
|
||||
imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), fmt.Errorf("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
@ -80,7 +80,7 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||
return err
|
||||
}
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push")
|
||||
options := types.ImagePushOptions{
|
||||
options := image.PushOptions{
|
||||
All: opts.all,
|
||||
RegistryAuth: encodedAuth,
|
||||
PrivilegeFunc: requestPrivilege,
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
@ -16,7 +16,7 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
|
||||
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
|
||||
}{
|
||||
{
|
||||
name: "wrong-args",
|
||||
@ -32,7 +32,7 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
name: "push-failed",
|
||||
args: []string{"image:repo"},
|
||||
expectedError: "Failed to push",
|
||||
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
|
||||
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push")
|
||||
},
|
||||
},
|
||||
@ -67,7 +67,7 @@ func TestNewPushCommandSuccess(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
|
||||
imagePushFunc: func(ref string, options image.PushOptions) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -52,7 +52,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions, images []string) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
options := types.ImageRemoveOptions{
|
||||
options := image.RemoveOptions{
|
||||
Force: opts.force,
|
||||
PruneChildren: !opts.noPrune,
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -36,7 +35,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
|
||||
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "wrong args",
|
||||
@ -46,7 +45,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
name: "ImageRemove fail with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
expectedError: "error removing image",
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
assert.Check(t, is.Equal("image1", img))
|
||||
return []image.DeleteResponse{}, errors.Errorf("error removing image")
|
||||
},
|
||||
@ -55,7 +54,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
name: "ImageRemove fail",
|
||||
args: []string{"arg1"},
|
||||
expectedError: "error removing image",
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
assert.Check(t, !options.Force)
|
||||
assert.Check(t, options.PruneChildren)
|
||||
return []image.DeleteResponse{}, errors.Errorf("error removing image")
|
||||
@ -78,13 +77,13 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
|
||||
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
|
||||
expectedStderr string
|
||||
}{
|
||||
{
|
||||
name: "Image Deleted",
|
||||
args: []string{"image1"},
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
assert.Check(t, is.Equal("image1", img))
|
||||
return []image.DeleteResponse{{Deleted: img}}, nil
|
||||
},
|
||||
@ -92,7 +91,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "Image not found with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
assert.Check(t, is.Equal("image1", img))
|
||||
assert.Check(t, is.Equal(true, options.Force))
|
||||
return []image.DeleteResponse{}, notFound{"image1"}
|
||||
@ -103,7 +102,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "Image Untagged",
|
||||
args: []string{"image1"},
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
assert.Check(t, is.Equal("image1", img))
|
||||
return []image.DeleteResponse{{Untagged: img}}, nil
|
||||
},
|
||||
@ -111,7 +110,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
{
|
||||
name: "Image Deleted and Untagged",
|
||||
args: []string{"image1", "image2"},
|
||||
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
|
||||
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
|
||||
if img == "image1" {
|
||||
return []image.DeleteResponse{{Untagged: img}}, nil
|
||||
}
|
||||
|
||||
@ -5,9 +5,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
@ -29,9 +26,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
|
||||
@ -5,9 +5,6 @@
|
||||
"RepoDigests": null,
|
||||
"Parent": "",
|
||||
"Comment": "",
|
||||
"Created": "",
|
||||
"Container": "",
|
||||
"ContainerConfig": null,
|
||||
"DockerVersion": "",
|
||||
"Author": "",
|
||||
"Config": null,
|
||||
|
||||
2
cli/command/image/testdata/list-command-ambiguous.golden
vendored
Normal file
2
cli/command/image/testdata/list-command-ambiguous.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
No images found matching "ls": did you mean "docker image ls"?
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
@ -30,7 +31,7 @@ type target struct {
|
||||
}
|
||||
|
||||
// TrustedPush handles content trust pushing of an image
|
||||
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options types.ImagePushOptions) error {
|
||||
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options image.PushOptions) error {
|
||||
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -267,7 +268,7 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
|
||||
return err
|
||||
}
|
||||
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull")
|
||||
responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), types.ImagePullOptions{
|
||||
responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), image.PullOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
PrivilegeFunc: requestPrivilege,
|
||||
All: opts.all,
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
@ -15,6 +16,8 @@ type fakeClient struct {
|
||||
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
|
||||
networkRemoveFunc func(ctx context.Context, networkID string) error
|
||||
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
|
||||
networkPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error)
|
||||
networkInspectFunc func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
@ -52,6 +55,16 @@ func (c *fakeClient) NetworkRemove(ctx context.Context, networkID string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkInspectWithRaw(context.Context, string, types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
|
||||
func (c *fakeClient) NetworkInspectWithRaw(ctx context.Context, networkID string, opts types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
|
||||
if c.networkInspectFunc != nil {
|
||||
return c.networkInspectFunc(ctx, networkID, opts)
|
||||
}
|
||||
return types.NetworkResource{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) {
|
||||
if c.networkPruneFunc != nil {
|
||||
return c.networkPruneFunc(ctx, pruneFilter)
|
||||
}
|
||||
return types.NetworksPruneReport{}, nil
|
||||
}
|
||||
|
||||
@ -49,8 +49,10 @@ Are you sure you want to continue?`
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (output string, err error) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return "", nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)
|
||||
|
||||
29
cli/command/network/prune_test.go
Normal file
29
cli/command/network/prune_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNetworkPrunePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
|
||||
return types.NetworksPruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
@ -46,10 +46,15 @@ func runRemove(ctx context.Context, dockerCli command.Cli, networks []string, op
|
||||
status := 0
|
||||
|
||||
for _, name := range networks {
|
||||
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{}); err == nil &&
|
||||
nw.Ingress &&
|
||||
!command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) {
|
||||
continue
|
||||
nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{})
|
||||
if err == nil && nw.Ingress {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), ingressWarning)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !r {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := client.NetworkRemove(ctx, name); err != nil {
|
||||
if opts.force && errdefs.IsNotFound(err) {
|
||||
|
||||
@ -5,7 +5,9 @@ import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
@ -94,3 +96,27 @@ func TestNetworkRemoveForce(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkRemovePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
networkRemoveFunc: func(ctx context.Context, networkID string) error {
|
||||
return errors.New("fakeClient networkRemoveFunc should not be called")
|
||||
},
|
||||
networkInspectFunc: func(ctx context.Context, networkID string, options types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
|
||||
return types.NetworkResource{
|
||||
ID: "existing-network",
|
||||
Name: "existing-network",
|
||||
Ingress: true,
|
||||
}, nil, nil
|
||||
},
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"existing-network"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ type fakeClient struct {
|
||||
pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
|
||||
pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error)
|
||||
pluginInspectFunc func(name string) (*types.Plugin, []byte, error)
|
||||
pluginUpgradeFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) PluginCreate(_ context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
|
||||
@ -75,3 +76,10 @@ func (c *fakeClient) PluginInspectWithRaw(_ context.Context, name string) (*type
|
||||
func (c *fakeClient) Info(context.Context) (system.Info, error) {
|
||||
return system.Info{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
||||
if c.pluginUpgradeFunc != nil {
|
||||
return c.pluginUpgradeFunc(name, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -114,7 +114,6 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateO
|
||||
createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{
|
||||
Compression: compression,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -142,6 +142,7 @@ func acceptPrivileges(dockerCli command.Cli, name string) func(privileges types.
|
||||
for _, privilege := range privileges {
|
||||
fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value)
|
||||
}
|
||||
return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil
|
||||
ctx := context.TODO()
|
||||
return command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?")
|
||||
}
|
||||
}
|
||||
|
||||
2
cli/command/plugin/testdata/plugin-upgrade-terminate.golden
vendored
Normal file
2
cli/command/plugin/testdata/plugin-upgrade-terminate.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Upgrading plugin foo/bar from localhost:5000/foo/bar:v0.1.0 to localhost:5000/foo/bar:v1.0.0
|
||||
Plugin images do not match, are you sure? [y/N]
|
||||
@ -63,7 +63,10 @@ func runUpgrade(ctx context.Context, dockerCli command.Cli, opts pluginOptions)
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||
if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?"); !r || err != nil {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "canceling upgrade request")
|
||||
}
|
||||
return errors.New("canceling upgrade request")
|
||||
}
|
||||
}
|
||||
|
||||
42
cli/command/plugin/upgrade_test.go
Normal file
42
cli/command/plugin/upgrade_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestUpgradePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
pluginUpgradeFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
|
||||
return nil, errors.New("should not be called")
|
||||
},
|
||||
pluginInspectFunc: func(name string) (*types.Plugin, []byte, error) {
|
||||
return &types.Plugin{
|
||||
ID: "5724e2c8652da337ab2eedd19fc6fc0ec908e4bd907c7421bf6a8dfc70c4c078",
|
||||
Name: "foo/bar",
|
||||
Enabled: false,
|
||||
PluginReference: "localhost:5000/foo/bar:v0.1.0",
|
||||
}, []byte{}, nil
|
||||
},
|
||||
})
|
||||
cmd := newUpgradeCommand(cli)
|
||||
// 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"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
|
||||
}
|
||||
@ -55,7 +55,7 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags.StringVarP(&opts.user, "username", "u", "", "Username")
|
||||
flags.StringVarP(&opts.password, "password", "p", "", "Password")
|
||||
flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
|
||||
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the password from stdin")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ func newSecretListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs")
|
||||
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
|
||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
|
||||
|
||||
return cmd
|
||||
|
||||
@ -137,7 +137,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||
return nil
|
||||
}
|
||||
|
||||
return waitOnService(ctx, dockerCli, response.ID, opts.quiet)
|
||||
return WaitOnService(ctx, dockerCli, response.ID, opts.quiet)
|
||||
}
|
||||
|
||||
// setConfigs does double duty: it both sets the ConfigReferences of the
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
)
|
||||
|
||||
// waitOnService waits for the service to converge. It outputs a progress bar,
|
||||
// WaitOnService waits for the service to converge. It outputs a progress bar,
|
||||
// if appropriate based on the CLI flags.
|
||||
func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error {
|
||||
func WaitOnService(ctx context.Context, dockerCli command.Cli, serviceID string, quiet bool) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
|
||||
@ -143,7 +143,7 @@ func ServiceProgress(ctx context.Context, apiClient client.APIClient, serviceID
|
||||
if converged && time.Since(convergedAt) >= monitor {
|
||||
progressOut.WriteProgress(progress.Progress{
|
||||
ID: "verify",
|
||||
Action: "Service converged",
|
||||
Action: fmt.Sprintf("Service %s converged", serviceID),
|
||||
})
|
||||
if message != nil {
|
||||
progressOut.WriteProgress(*message)
|
||||
|
||||
@ -62,5 +62,5 @@ func runRollback(ctx context.Context, dockerCli command.Cli, options *serviceOpt
|
||||
return nil
|
||||
}
|
||||
|
||||
return waitOnService(ctx, dockerCli, serviceID, options.quiet)
|
||||
return WaitOnService(ctx, dockerCli, serviceID, options.quiet)
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ func runScale(ctx context.Context, dockerCli command.Cli, options *scaleOptions,
|
||||
if len(serviceIDs) > 0 {
|
||||
if !options.detach && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.29") {
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := waitOnService(ctx, dockerCli, serviceID, false); err != nil {
|
||||
if err := WaitOnService(ctx, dockerCli, serviceID, false); err != nil {
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,7 +249,7 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||
return nil
|
||||
}
|
||||
|
||||
return waitOnService(ctx, dockerCli, serviceID, options.quiet)
|
||||
return WaitOnService(ctx, dockerCli, serviceID, options.quiet)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
|
||||
@ -26,7 +26,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return swarm.RunDeploy(cmd.Context(), dockerCli, opts, config)
|
||||
return swarm.RunDeploy(cmd.Context(), dockerCli, cmd.Flags(), &opts, config)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||
@ -42,5 +42,7 @@ func newDeployCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&opts.ResolveImage, "resolve-image", swarm.ResolveImageAlways,
|
||||
`Query the registry to resolve image digest and supported platforms ("`+swarm.ResolveImageAlways+`", "`+swarm.ResolveImageChanged+`", "`+swarm.ResolveImageNever+`")`)
|
||||
flags.SetAnnotation("resolve-image", "version", []string{"1.30"})
|
||||
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Exit immediately instead of waiting for the stack services to converge")
|
||||
flags.BoolVarP(&opts.Quiet, "quiet", "q", false, "Suppress progress output")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ type Deploy struct {
|
||||
ResolveImage string
|
||||
SendRegistryAuth bool
|
||||
Prune bool
|
||||
Detach bool
|
||||
Quiet bool
|
||||
}
|
||||
|
||||
// Config holds docker stack config options
|
||||
@ -36,6 +38,7 @@ type PS struct {
|
||||
// Remove holds docker stack remove options
|
||||
type Remove struct {
|
||||
Namespaces []string
|
||||
Detach bool
|
||||
}
|
||||
|
||||
// Services holds docker stack services options
|
||||
|
||||
@ -27,5 +27,8 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return completeNames(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.Detach, "detach", "d", true, "Do not wait for stack removal")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -44,3 +44,7 @@ func getStackSecrets(ctx context.Context, apiclient client.APIClient, namespace
|
||||
func getStackConfigs(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Config, error) {
|
||||
return apiclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
func getStackTasks(ctx context.Context, apiclient client.APIClient, namespace string) ([]swarm.Task, error) {
|
||||
return apiclient.TaskList(ctx, types.TaskListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Resolve image constants
|
||||
@ -22,8 +23,8 @@ const (
|
||||
)
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(ctx context.Context, dockerCli command.Cli, opts options.Deploy, cfg *composetypes.Config) error {
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
func RunDeploy(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts *options.Deploy, cfg *composetypes.Config) error {
|
||||
if err := validateResolveImageFlag(opts); err != nil {
|
||||
return err
|
||||
}
|
||||
// client side image resolution should not be done when the supported
|
||||
@ -32,6 +33,11 @@ func RunDeploy(ctx context.Context, dockerCli command.Cli, opts options.Deploy,
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
if opts.Detach && !flags.Changed("detach") {
|
||||
fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+
|
||||
"In a future release, --detach=false will become the default.")
|
||||
}
|
||||
|
||||
return deployCompose(ctx, dockerCli, opts, cfg)
|
||||
}
|
||||
|
||||
|
||||
@ -2,9 +2,11 @@ package swarm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
servicecli "github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack/options"
|
||||
"github.com/docker/cli/cli/compose/convert"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
@ -13,10 +15,9 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Deploy, config *composetypes.Config) error {
|
||||
func deployCompose(ctx context.Context, dockerCli command.Cli, opts *options.Deploy, config *composetypes.Config) error {
|
||||
if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -60,7 +61,17 @@ func deployCompose(ctx context.Context, dockerCli command.Cli, opts options.Depl
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
|
||||
serviceIDs, err := deployServices(ctx, dockerCli, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.Detach {
|
||||
return nil
|
||||
}
|
||||
|
||||
return waitOnServices(ctx, dockerCli, serviceIDs, opts.Quiet)
|
||||
}
|
||||
|
||||
func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} {
|
||||
@ -87,11 +98,11 @@ func validateExternalNetworks(ctx context.Context, client apiclient.NetworkAPICl
|
||||
network, err := client.NetworkInspect(ctx, networkName, types.NetworkInspectOptions{})
|
||||
switch {
|
||||
case errdefs.IsNotFound(err):
|
||||
return errors.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
|
||||
return fmt.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName)
|
||||
case err != nil:
|
||||
return err
|
||||
case network.Scope != "swarm":
|
||||
return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
|
||||
return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of \"swarm\"", networkName, network.Scope)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@ -106,13 +117,13 @@ func createSecrets(ctx context.Context, dockerCli command.Cli, secrets []swarm.S
|
||||
case err == nil:
|
||||
// secret already exists, then we update that
|
||||
if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update secret %s", secretSpec.Name)
|
||||
return fmt.Errorf("failed to update secret %s: %w", secretSpec.Name, err)
|
||||
}
|
||||
case errdefs.IsNotFound(err):
|
||||
// secret does not exist, then we create a new one.
|
||||
fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name)
|
||||
if _, err := client.SecretCreate(ctx, secretSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to create secret %s", secretSpec.Name)
|
||||
return fmt.Errorf("failed to create secret %s: %w", secretSpec.Name, err)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
@ -130,13 +141,13 @@ func createConfigs(ctx context.Context, dockerCli command.Cli, configs []swarm.C
|
||||
case err == nil:
|
||||
// config already exists, then we update that
|
||||
if err := client.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to update config %s", configSpec.Name)
|
||||
return fmt.Errorf("failed to update config %s: %w", configSpec.Name, err)
|
||||
}
|
||||
case errdefs.IsNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
fmt.Fprintf(dockerCli.Out(), "Creating config %s\n", configSpec.Name)
|
||||
if _, err := client.ConfigCreate(ctx, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
return fmt.Errorf("failed to create config %s: %w", configSpec.Name, err)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
@ -169,19 +180,19 @@ func createNetworks(ctx context.Context, dockerCli command.Cli, namespace conver
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name)
|
||||
if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||
return errors.Wrapf(err, "failed to create network %s", name)
|
||||
return fmt.Errorf("failed to create network %s: %w", name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deployServices(ctx context.Context, dockerCli command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) error {
|
||||
func deployServices(ctx context.Context, dockerCli command.Cli, services map[string]swarm.ServiceSpec, namespace convert.Namespace, sendAuth bool, resolveImage string) ([]string, error) {
|
||||
apiClient := dockerCli.Client()
|
||||
out := dockerCli.Out()
|
||||
|
||||
existingServices, err := getStackServices(ctx, apiClient, namespace.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existingServiceMap := make(map[string]swarm.Service)
|
||||
@ -189,6 +200,8 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
|
||||
existingServiceMap[service.Spec.Name] = service
|
||||
}
|
||||
|
||||
var serviceIDs []string
|
||||
|
||||
for internalName, serviceSpec := range services {
|
||||
var (
|
||||
name = namespace.Scope(internalName)
|
||||
@ -200,7 +213,7 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
|
||||
// Retrieve encoded auth token from the image reference
|
||||
encodedAuth, err = command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), image)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,12 +254,14 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
|
||||
|
||||
response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, serviceSpec, updateOpts)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to update service %s", name)
|
||||
return nil, fmt.Errorf("failed to update service %s: %w", name, err)
|
||||
}
|
||||
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintln(dockerCli.Err(), warning)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, service.ID)
|
||||
} else {
|
||||
fmt.Fprintf(out, "Creating service %s\n", name)
|
||||
|
||||
@ -257,10 +272,29 @@ func deployServices(ctx context.Context, dockerCli command.Cli, services map[str
|
||||
createOpts.QueryRegistry = true
|
||||
}
|
||||
|
||||
if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil {
|
||||
return errors.Wrapf(err, "failed to create service %s", name)
|
||||
response, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service %s: %w", name, err)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
return serviceIDs, nil
|
||||
}
|
||||
|
||||
func waitOnServices(ctx context.Context, dockerCli command.Cli, serviceIDs []string, quiet bool) error {
|
||||
var errs []error
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := servicecli.WaitOnService(ctx, dockerCli, serviceID, quiet); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
|
||||
_, err := deployServices(ctx, client, spec, namespace, false, ResolveImageChanged)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(receivedOptions.QueryRegistry, tc.expectedQueryRegistry))
|
||||
assert.Check(t, is.Equal(receivedService.TaskTemplate.ContainerSpec.Image, tc.expectedImage))
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -58,6 +59,14 @@ func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove)
|
||||
|
||||
if hasError {
|
||||
errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace))
|
||||
continue
|
||||
}
|
||||
|
||||
if !opts.Detach {
|
||||
err = waitOnTasks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("Failed to wait on tasks of stack: %s: %s", namespace, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,3 +146,45 @@ func removeConfigs(
|
||||
}
|
||||
return hasError
|
||||
}
|
||||
|
||||
var numberedStates = map[swarm.TaskState]int64{
|
||||
swarm.TaskStateNew: 1,
|
||||
swarm.TaskStateAllocated: 2,
|
||||
swarm.TaskStatePending: 3,
|
||||
swarm.TaskStateAssigned: 4,
|
||||
swarm.TaskStateAccepted: 5,
|
||||
swarm.TaskStatePreparing: 6,
|
||||
swarm.TaskStateReady: 7,
|
||||
swarm.TaskStateStarting: 8,
|
||||
swarm.TaskStateRunning: 9,
|
||||
swarm.TaskStateComplete: 10,
|
||||
swarm.TaskStateShutdown: 11,
|
||||
swarm.TaskStateFailed: 12,
|
||||
swarm.TaskStateRejected: 13,
|
||||
}
|
||||
|
||||
func terminalState(state swarm.TaskState) bool {
|
||||
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
|
||||
}
|
||||
|
||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error {
|
||||
terminalStatesReached := 0
|
||||
for {
|
||||
tasks, err := getStackTasks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
if terminalState(task.Status.State) {
|
||||
terminalStatesReached++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if terminalStatesReached == len(tasks) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ func equalCIDR(c1 net.IPNet, c2 net.IPNet) bool {
|
||||
|
||||
func setUpIPNetFlagSet(ipsp *[]net.IPNet) *pflag.FlagSet {
|
||||
f := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
f.VarP(newIPNetSliceValue([]net.IPNet{}, ipsp), "cidrs", "", "Command separated list!")
|
||||
f.Var(newIPNetSliceValue([]net.IPNet{}, ipsp), "cidrs", "Command separated list!")
|
||||
return f
|
||||
}
|
||||
|
||||
|
||||
@ -5,15 +5,18 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
|
||||
version string
|
||||
serverVersion func(ctx context.Context) (types.Version, error)
|
||||
eventsFn func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
|
||||
version string
|
||||
serverVersion func(ctx context.Context) (types.Version, error)
|
||||
eventsFn func(context.Context, types.EventsOptions) (<-chan events.Message, <-chan error)
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error)
|
||||
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
@ -27,3 +30,17 @@ func (cli *fakeClient) ClientVersion() string {
|
||||
func (cli *fakeClient) Events(ctx context.Context, opts types.EventsOptions) (<-chan events.Message, <-chan error) {
|
||||
return cli.eventsFn(ctx, opts)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
if cli.containerPruneFunc != nil {
|
||||
return cli.containerPruneFunc(ctx, pruneFilters)
|
||||
}
|
||||
return types.ContainersPruneReport{}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Args) (types.NetworksPruneReport, error) {
|
||||
if cli.networkPruneFunc != nil {
|
||||
return cli.networkPruneFunc(ctx, pruneFilter)
|
||||
}
|
||||
return types.NetworksPruneReport{}, nil
|
||||
}
|
||||
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
@ -380,7 +379,10 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) []error {
|
||||
}
|
||||
|
||||
fprintln(output)
|
||||
printServerWarnings(streams.Err(), info)
|
||||
for _, w := range info.Warnings {
|
||||
fprintln(streams.Err(), w)
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@ -454,84 +456,6 @@ func printSwarmInfo(output io.Writer, info system.Info) {
|
||||
}
|
||||
}
|
||||
|
||||
func printServerWarnings(stdErr io.Writer, info *dockerInfo) {
|
||||
if versions.LessThan(info.ClientInfo.APIVersion, "1.42") {
|
||||
printSecurityOptionsWarnings(stdErr, *info.Info)
|
||||
}
|
||||
if len(info.Warnings) > 0 {
|
||||
fprintln(stdErr, strings.Join(info.Warnings, "\n"))
|
||||
return
|
||||
}
|
||||
// daemon didn't return warnings. Fallback to old behavior
|
||||
printServerWarningsLegacy(stdErr, *info.Info)
|
||||
}
|
||||
|
||||
// printSecurityOptionsWarnings prints warnings based on the security options
|
||||
// returned by the daemon.
|
||||
//
|
||||
// Deprecated: warnings are now generated by the daemon, and returned in
|
||||
// info.Warnings. This function is used to provide backward compatibility with
|
||||
// daemons that do not provide these warnings. No new warnings should be added
|
||||
// here.
|
||||
func printSecurityOptionsWarnings(stdErr io.Writer, info system.Info) {
|
||||
if info.OSType == "windows" {
|
||||
return
|
||||
}
|
||||
kvs, _ := system.DecodeSecurityOptions(info.SecurityOptions)
|
||||
for _, so := range kvs {
|
||||
if so.Name != "seccomp" {
|
||||
continue
|
||||
}
|
||||
for _, o := range so.Options {
|
||||
if o.Key == "profile" && o.Value != "default" && o.Value != "builtin" {
|
||||
_, _ = fmt.Fprintln(stdErr, "WARNING: You're not using the default seccomp profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// printServerWarningsLegacy generates warnings based on information returned by the daemon.
|
||||
//
|
||||
// Deprecated: warnings are now generated by the daemon, and returned in
|
||||
// info.Warnings. This function is used to provide backward compatibility with
|
||||
// daemons that do not provide these warnings. No new warnings should be added
|
||||
// here.
|
||||
func printServerWarningsLegacy(stdErr io.Writer, info system.Info) {
|
||||
if info.OSType == "windows" {
|
||||
return
|
||||
}
|
||||
if !info.MemoryLimit {
|
||||
fprintln(stdErr, "WARNING: No memory limit support")
|
||||
}
|
||||
if !info.SwapLimit {
|
||||
fprintln(stdErr, "WARNING: No swap limit support")
|
||||
}
|
||||
if !info.OomKillDisable && info.CgroupVersion != "2" {
|
||||
fprintln(stdErr, "WARNING: No oom kill disable support")
|
||||
}
|
||||
if !info.CPUCfsQuota {
|
||||
fprintln(stdErr, "WARNING: No cpu cfs quota support")
|
||||
}
|
||||
if !info.CPUCfsPeriod {
|
||||
fprintln(stdErr, "WARNING: No cpu cfs period support")
|
||||
}
|
||||
if !info.CPUShares {
|
||||
fprintln(stdErr, "WARNING: No cpu shares support")
|
||||
}
|
||||
if !info.CPUSet {
|
||||
fprintln(stdErr, "WARNING: No cpuset support")
|
||||
}
|
||||
if !info.IPv4Forwarding {
|
||||
fprintln(stdErr, "WARNING: IPv4 forwarding is disabled")
|
||||
}
|
||||
if !info.BridgeNfIptables {
|
||||
fprintln(stdErr, "WARNING: bridge-nf-call-iptables is disabled")
|
||||
}
|
||||
if !info.BridgeNfIP6tables {
|
||||
fprintln(stdErr, "WARNING: bridge-nf-call-ip6tables is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func formatInfo(output io.Writer, info dockerInfo, format string) error {
|
||||
if format == formatter.JSONFormatKey {
|
||||
format = formatter.JSONFormat
|
||||
|
||||
@ -333,23 +333,6 @@ func TestPrettyPrintInfo(t *testing.T) {
|
||||
prettyGolden: "docker-info-with-swarm",
|
||||
jsonGolden: "docker-info-with-swarm",
|
||||
},
|
||||
{
|
||||
doc: "info with legacy warnings",
|
||||
dockerInfo: dockerInfo{
|
||||
Info: &infoWithWarningsLinux,
|
||||
ClientInfo: &clientInfo{
|
||||
clientVersion: clientVersion{
|
||||
Platform: &platformInfo{Name: "Docker Engine - Community"},
|
||||
Version: "24.0.0",
|
||||
Context: "default",
|
||||
},
|
||||
Debug: true,
|
||||
},
|
||||
},
|
||||
prettyGolden: "docker-info-no-swarm",
|
||||
warningsGolden: "docker-info-warnings",
|
||||
jsonGolden: "docker-info-legacy-warnings",
|
||||
},
|
||||
{
|
||||
doc: "info with daemon warnings",
|
||||
dockerInfo: dockerInfo{
|
||||
|
||||
@ -74,8 +74,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
if options.pruneVolumes && options.filter.Value().Contains("until") {
|
||||
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)) {
|
||||
return nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options)); !r || err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pruneFuncs := []func(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
|
||||
container.RunPrune,
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@ -49,3 +54,23 @@ func TestPrunePromptFilters(t *testing.T) {
|
||||
Are you sure you want to continue? [y/N] `
|
||||
assert.Check(t, is.Equal(expected, cli.OutBuffer().String()))
|
||||
}
|
||||
|
||||
func TestSystemPrunePromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.ContainersPruneReport, error) {
|
||||
return types.ContainersPruneReport{}, errors.New("fakeClient containerPruneFunc should not be called")
|
||||
},
|
||||
networkPruneFunc: func(ctx context.Context, pruneFilters filters.Args) (types.NetworksPruneReport, error) {
|
||||
return types.NetworksPruneReport{}, errors.New("fakeClient networkPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
|
||||
cmd := newPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":false,"SwapLimit":false,"CpuCfsPeriod":false,"CpuCfsQuota":false,"CPUShares":false,"CPUSet":false,"PidsLimit":false,"IPv4Forwarding":false,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"Debug":true,"NFd":33,"OomKillDisable":false,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
notaryfake "github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/theupdateframework/notary"
|
||||
@ -36,7 +37,7 @@ func (c *fakeClient) ImageInspectWithRaw(context.Context, string) (types.ImageIn
|
||||
return types.ImageInspect{}, []byte{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ImagePush(context.Context, string, types.ImagePushOptions) (io.ReadCloser, error) {
|
||||
func (c *fakeClient) ImagePush(context.Context, string, image.PushOptions) (io.ReadCloser, error) {
|
||||
return &utils.NoopCloser{Reader: bytes.NewBuffer([]byte{})}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ package trust
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -44,7 +43,11 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
|
||||
return fmt.Errorf("cannot use a digest reference for IMAGE:TAG")
|
||||
}
|
||||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||
deleteRemote := command.PromptForConfirmation(os.Stdin, dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Please confirm you would like to delete all signature data for %s?", remote))
|
||||
if err != nil {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return errors.Wrap(err, "aborting action")
|
||||
}
|
||||
if !deleteRemote {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return nil
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package trust
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
@ -12,6 +14,7 @@ import (
|
||||
"github.com/theupdateframework/notary/trustpinning"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestTrustRevokeCommandErrors(t *testing.T) {
|
||||
@ -148,3 +151,18 @@ func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) {
|
||||
err = getSignableRolesForTargetAndRemove(target, notaryRepo)
|
||||
assert.Error(t, err, "client is offline")
|
||||
}
|
||||
|
||||
func TestRevokeTrustPromptTermination(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := newRevokeCommand(cli)
|
||||
cmd.SetArgs([]string{"example/trust-demo"})
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, func(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
})
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
golden.Assert(t, cli.OutBuffer().String(), "trust-revoke-prompt-termination.golden")
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/api/types"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
@ -98,7 +98,7 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options := types.ImagePushOptions{
|
||||
options := imagetypes.PushOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
PrivilegeFunc: requestPrivilege,
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package trust
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
@ -76,6 +75,22 @@ func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSi
|
||||
return counter < releasesRoleWithSigs.Threshold, nil
|
||||
}
|
||||
|
||||
func maybePromptForSignerRemoval(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, isLastSigner, forceYes bool) (bool, error) {
|
||||
if isLastSigner && !forceYes {
|
||||
message := fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
|
||||
"Removing this signer will make %s unpullable. "+
|
||||
"Are you sure you want to continue?",
|
||||
signerName, repoName, repoName,
|
||||
)
|
||||
removeSigner, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return removeSigner, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// removeSingleSigner attempts to remove a single signer and returns whether signer removal happened.
|
||||
// The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation.
|
||||
func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, forceYes bool) (bool, error) {
|
||||
@ -110,28 +125,26 @@ func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, si
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if ok, err := isLastSignerForReleases(role, allRoles); ok && !forceYes {
|
||||
removeSigner := command.PromptForConfirmation(os.Stdin, dockerCLI.Out(), fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
|
||||
"Removing this signer will make %s unpullable. "+
|
||||
"Are you sure you want to continue?",
|
||||
signerName, repoName, repoName,
|
||||
))
|
||||
|
||||
if !removeSigner {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return false, nil
|
||||
}
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
|
||||
isLastSigner, err := isLastSignerForReleases(role, allRoles)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err = notaryRepo.Publish(); err != nil {
|
||||
ok, err := maybePromptForSignerRemoval(ctx, dockerCLI, repoName, signerName, isLastSigner, forceYes)
|
||||
if err != nil || !ok {
|
||||
fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := notaryRepo.Publish(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
||||
@ -111,7 +111,8 @@ func TestIsLastSignerForReleases(t *testing.T) {
|
||||
releaserole.Name = releasesRoleTUFName
|
||||
releaserole.Threshold = 1
|
||||
allrole := []client.RoleWithSignatures{releaserole}
|
||||
lastsigner, _ := isLastSignerForReleases(role, allrole)
|
||||
lastsigner, err := isLastSignerForReleases(role, allrole)
|
||||
assert.Error(t, err, "all signed tags are currently revoked, use docker trust sign to fix")
|
||||
assert.Check(t, is.Equal(false, lastsigner))
|
||||
|
||||
role.KeyIDs = []string{"deadbeef"}
|
||||
@ -120,13 +121,15 @@ func TestIsLastSignerForReleases(t *testing.T) {
|
||||
releaserole.Signatures = []data.Signature{sig}
|
||||
releaserole.Threshold = 1
|
||||
allrole = []client.RoleWithSignatures{releaserole}
|
||||
lastsigner, _ = isLastSignerForReleases(role, allrole)
|
||||
lastsigner, err = isLastSignerForReleases(role, allrole)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(true, lastsigner))
|
||||
|
||||
sig.KeyID = "8badf00d"
|
||||
releaserole.Signatures = []data.Signature{sig}
|
||||
releaserole.Threshold = 1
|
||||
allrole = []client.RoleWithSignatures{releaserole}
|
||||
lastsigner, _ = isLastSignerForReleases(role, allrole)
|
||||
lastsigner, err = isLastSignerForReleases(role, allrole)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(false, lastsigner))
|
||||
}
|
||||
|
||||
2
cli/command/trust/testdata/trust-revoke-prompt-termination.golden
vendored
Normal file
2
cli/command/trust/testdata/trust-revoke-prompt-termination.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Please confirm you would like to delete all signature data for example/trust-demo? [y/N]
|
||||
Aborting action.
|
||||
@ -5,12 +5,15 @@ package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
@ -72,12 +75,21 @@ func PrettyPrint(i any) string {
|
||||
}
|
||||
}
|
||||
|
||||
// PromptForConfirmation requests and checks confirmation from user.
|
||||
// This will display the provided message followed by ' [y/N] '. If
|
||||
// the user input 'y' or 'Y' it returns true other false. If no
|
||||
// message is provided "Are you sure you want to proceed? [y/N] "
|
||||
// will be used instead.
|
||||
func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool {
|
||||
type PromptError error
|
||||
|
||||
var ErrPromptTerminated = PromptError(errors.New("prompt terminated"))
|
||||
|
||||
// PromptForConfirmation requests and checks confirmation from the user.
|
||||
// This will display the provided message followed by ' [y/N] '. If the user
|
||||
// input 'y' or 'Y' it returns true otherwise false. If no message is provided,
|
||||
// "Are you sure you want to proceed? [y/N] " will be used instead.
|
||||
//
|
||||
// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is
|
||||
// active, the prompt will return false with an ErrPromptTerminated error.
|
||||
// When the prompt returns an error, the caller should propagate the error up
|
||||
// the stack and close the io.Reader used for the prompt which will prevent the
|
||||
// background goroutine from blocking indefinitely.
|
||||
func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) {
|
||||
if message == "" {
|
||||
message = "Are you sure you want to proceed?"
|
||||
}
|
||||
@ -90,9 +102,31 @@ func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool {
|
||||
ins = streams.NewIn(os.Stdin)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(ins)
|
||||
answer, _, _ := reader.ReadLine()
|
||||
return strings.ToLower(string(answer)) == "y"
|
||||
result := make(chan bool)
|
||||
|
||||
// Catch the termination signal and exit the prompt gracefully.
|
||||
// The caller is responsible for properly handling the termination.
|
||||
notifyCtx, notifyCancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer notifyCancel()
|
||||
|
||||
go func() {
|
||||
var res bool
|
||||
scanner := bufio.NewScanner(ins)
|
||||
if scanner.Scan() {
|
||||
answer := strings.TrimSpace(scanner.Text())
|
||||
if strings.EqualFold(answer, "y") {
|
||||
res = true
|
||||
}
|
||||
}
|
||||
result <- res
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-notifyCtx.Done():
|
||||
return false, ErrPromptTerminated
|
||||
case r := <-result:
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
|
||||
// PruneFilters returns consolidated prune filters obtained from config.json and cli
|
||||
|
||||
@ -1,36 +1,46 @@
|
||||
package command
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/pkg/errors"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestStringSliceReplaceAt(t *testing.T) {
|
||||
out, ok := StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, -1)
|
||||
out, ok := command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, -1)
|
||||
assert.Assert(t, ok)
|
||||
assert.DeepEqual(t, []string{"abc", "baz", "bax"}, out)
|
||||
|
||||
out, ok = StringSliceReplaceAt([]string{"foo"}, []string{"foo", "bar"}, []string{"baz"}, -1)
|
||||
out, ok = command.StringSliceReplaceAt([]string{"foo"}, []string{"foo", "bar"}, []string{"baz"}, -1)
|
||||
assert.Assert(t, !ok)
|
||||
assert.DeepEqual(t, []string{"foo"}, out)
|
||||
|
||||
out, ok = StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
|
||||
out, ok = command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
|
||||
assert.Assert(t, !ok)
|
||||
assert.DeepEqual(t, []string{"abc", "foo", "bar", "bax"}, out)
|
||||
|
||||
out, ok = StringSliceReplaceAt([]string{"foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
|
||||
out, ok = command.StringSliceReplaceAt([]string{"foo", "bar", "bax"}, []string{"foo", "bar"}, []string{"baz"}, 0)
|
||||
assert.Assert(t, ok)
|
||||
assert.DeepEqual(t, []string{"baz", "bax"}, out)
|
||||
|
||||
out, ok = StringSliceReplaceAt([]string{"abc", "foo", "bar", "baz"}, []string{"foo", "bar"}, nil, -1)
|
||||
out, ok = command.StringSliceReplaceAt([]string{"abc", "foo", "bar", "baz"}, []string{"foo", "bar"}, nil, -1)
|
||||
assert.Assert(t, ok)
|
||||
assert.DeepEqual(t, []string{"abc", "baz"}, out)
|
||||
|
||||
out, ok = StringSliceReplaceAt([]string{"foo"}, nil, []string{"baz"}, -1)
|
||||
out, ok = command.StringSliceReplaceAt([]string{"foo"}, nil, []string{"baz"}, -1)
|
||||
assert.Assert(t, !ok)
|
||||
assert.DeepEqual(t, []string{"foo"}, out)
|
||||
}
|
||||
@ -59,7 +69,7 @@ func TestValidateOutputPath(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.path, func(t *testing.T) {
|
||||
err := ValidateOutputPath(testcase.path)
|
||||
err := command.ValidateOutputPath(testcase.path)
|
||||
if testcase.err == nil {
|
||||
assert.NilError(t, err)
|
||||
} else {
|
||||
@ -68,3 +78,177 @@ func TestValidateOutputPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptForConfirmation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
type promptResult struct {
|
||||
result bool
|
||||
err error
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
var (
|
||||
promptWriter *io.PipeWriter
|
||||
promptReader *io.PipeReader
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if promptWriter != nil {
|
||||
promptWriter.Close()
|
||||
}
|
||||
if promptReader != nil {
|
||||
promptReader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
f func(*testing.T, context.Context, chan promptResult)
|
||||
}{
|
||||
{"SIGINT", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after SIGINT")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.ErrorContains(t, r.err, "prompt terminated")
|
||||
}
|
||||
}},
|
||||
{"no", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
_, err := fmt.Fprint(promptWriter, "n\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `n`")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"yes", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
_, err := fmt.Fprint(promptWriter, "y\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `y`")
|
||||
case r := <-c:
|
||||
assert.Check(t, r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"any", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
_, err := fmt.Fprint(promptWriter, "a\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input `a`")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"with space", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
_, err := fmt.Fprint(promptWriter, " y\n")
|
||||
assert.NilError(t, err)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after user input ` y`")
|
||||
case r := <-c:
|
||||
assert.Check(t, r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
{"reader closed", func(t *testing.T, ctx context.Context, c chan promptResult) {
|
||||
t.Helper()
|
||||
|
||||
assert.NilError(t, promptReader.Close())
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("PromptForConfirmation did not return after promptReader was closed")
|
||||
case r := <-c:
|
||||
assert.Check(t, !r.result)
|
||||
assert.NilError(t, r.err)
|
||||
}
|
||||
}},
|
||||
} {
|
||||
t.Run("case="+tc.desc, func(t *testing.T) {
|
||||
buf.Reset()
|
||||
promptReader, promptWriter = io.Pipe()
|
||||
|
||||
wroteHook := make(chan struct{}, 1)
|
||||
defer close(wroteHook)
|
||||
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
wroteHook <- struct{}{}
|
||||
})
|
||||
|
||||
result := make(chan promptResult, 1)
|
||||
defer close(result)
|
||||
go func() {
|
||||
r, err := command.PromptForConfirmation(ctx, promptReader, promptOut, "")
|
||||
result <- promptResult{r, err}
|
||||
}()
|
||||
|
||||
// wait for the Prompt to write to the buffer
|
||||
pollForPromptOutput(ctx, t, wroteHook)
|
||||
drainChannel(ctx, wroteHook)
|
||||
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, strings.TrimSpace(buf.String()), "Are you sure you want to proceed? [y/N]")
|
||||
|
||||
resultCtx, resultCancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer resultCancel()
|
||||
|
||||
tc.f(t, resultCtx, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func pollForPromptOutput(ctx context.Context, t *testing.T, wroteHook <-chan struct{}) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Prompt output was not written to before ctx was cancelled")
|
||||
return
|
||||
case <-wroteHook:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,6 +32,10 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
|
||||
if err != nil {
|
||||
if errdefs.IsCancelled(err) {
|
||||
fmt.Fprintln(dockerCli.Out(), output)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if output != "" {
|
||||
@ -76,8 +80,10 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
// API < v1.42 removes all volumes (anonymous and named) by default.
|
||||
warning = allVolumesWarning
|
||||
}
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
if !options.force {
|
||||
if r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning); !r || err != nil {
|
||||
return 0, "", errdefs.Cancelled(errors.New("user cancelled operation"))
|
||||
}
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().VolumesPrune(ctx, pruneFilters)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
@ -73,13 +74,15 @@ func TestVolumePruneSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
input string
|
||||
volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
args: []string{"--all"},
|
||||
name: "all",
|
||||
args: []string{"--all"},
|
||||
input: "y",
|
||||
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
|
||||
assert.Check(t, is.Equal([]string{"true"}, pruneFilter.Get("all")))
|
||||
assert.Check(t, is.DeepEqual([]string{"true"}, pruneFilter.Get("all")))
|
||||
return types.VolumesPruneReport{}, nil
|
||||
},
|
||||
},
|
||||
@ -91,10 +94,11 @@ func TestVolumePruneSuccess(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "label-filter",
|
||||
args: []string{"--filter", "label=foobar"},
|
||||
name: "label-filter",
|
||||
args: []string{"--filter", "label=foobar"},
|
||||
input: "y",
|
||||
volumePruneFunc: func(pruneFilter filters.Args) (types.VolumesPruneReport, error) {
|
||||
assert.Check(t, is.Equal([]string{"foobar"}, pruneFilter.Get("label")))
|
||||
assert.Check(t, is.DeepEqual([]string{"foobar"}, pruneFilter.Get("label")))
|
||||
return types.VolumesPruneReport{}, nil
|
||||
},
|
||||
},
|
||||
@ -104,6 +108,9 @@ func TestVolumePruneSuccess(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.volumePruneFunc})
|
||||
cmd := NewPruneCommand(cli)
|
||||
if tc.input != "" {
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(tc.input))))
|
||||
}
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
@ -177,3 +184,19 @@ func simplePruneFunc(filters.Args) (types.VolumesPruneReport, error) {
|
||||
SpaceReclaimed: 2000,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestVolumePrunePromptTerminate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
volumePruneFunc: func(filter filters.Args) (types.VolumesPruneReport, error) {
|
||||
return types.VolumesPruneReport{}, errors.New("fakeClient volumePruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
|
||||
cmd := NewPruneCommand(cli)
|
||||
test.TerminatePrompt(ctx, t, cmd, cli, nil)
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-terminate.golden")
|
||||
}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
WARNING! This will remove anonymous local volumes not used by at least one container.
|
||||
Are you sure you want to continue? [y/N] Total reclaimed space: 0B
|
||||
Are you sure you want to continue? [y/N]
|
||||
|
||||
2
cli/command/volume/testdata/volume-prune-terminate.golden
vendored
Normal file
2
cli/command/volume/testdata/volume-prune-terminate.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
WARNING! This will remove anonymous local volumes not used by at least one container.
|
||||
Are you sure you want to continue? [y/N]
|
||||
@ -328,7 +328,7 @@ func createTransformHook(additionalTransformers ...Transformer) mapstructure.Dec
|
||||
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
||||
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeOf(types.HostsList{}): transformListOrMappingFunc(":", false),
|
||||
reflect.TypeOf(types.HostsList{}): transformHostsList,
|
||||
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
||||
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
|
||||
reflect.TypeOf(types.Duration(0)): transformStringToDuration,
|
||||
@ -808,28 +808,58 @@ var transformStringList TransformerFunc = func(data any) (any, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
|
||||
return func(data any) (any, error) {
|
||||
return transformMappingOrList(data, sep, allowNil), nil
|
||||
var transformHostsList TransformerFunc = func(data any) (any, error) {
|
||||
hl := transformListOrMapping(data, ":", false, []string{"=", ":"})
|
||||
|
||||
// Remove brackets from IP addresses if present (for example "[::1]" -> "::1").
|
||||
result := make([]string, 0, len(hl))
|
||||
for _, hip := range hl {
|
||||
host, ip, _ := strings.Cut(hip, ":")
|
||||
if len(ip) > 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
result = append(result, fmt.Sprintf("%s:%s", host, ip))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func transformListOrMappingFunc(sep string, allowNil bool) TransformerFunc {
|
||||
return func(data any) (any, error) {
|
||||
return transformListOrMapping(data, sep, allowNil), nil
|
||||
}
|
||||
}
|
||||
|
||||
func transformListOrMapping(listOrMapping any, sep string, allowNil bool) any {
|
||||
// transformListOrMapping transforms pairs of strings that may be represented as
|
||||
// a map, or a list of '=' or ':' separated strings, into a list of ':' separated
|
||||
// strings.
|
||||
func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowSeps []string) []string {
|
||||
switch value := listOrMapping.(type) {
|
||||
case map[string]any:
|
||||
return toStringList(value, sep, allowNil)
|
||||
case []any:
|
||||
return listOrMapping
|
||||
result := make([]string, 0, len(value))
|
||||
for _, entry := range value {
|
||||
for i, allowSep := range allowSeps {
|
||||
entry := fmt.Sprint(entry)
|
||||
k, v, ok := strings.Cut(entry, allowSep)
|
||||
if ok {
|
||||
// Entry uses this allowed separator. Add it to the result, using
|
||||
// sep as a separator.
|
||||
result = append(result, fmt.Sprintf("%s%s%s", k, sep, v))
|
||||
break
|
||||
} else if i == len(allowSeps)-1 {
|
||||
// No more separators to try, keep the entry if allowNil.
|
||||
if allowNil {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
panic(errors.Errorf("expected a map or a list, got %T: %#v", listOrMapping, listOrMapping))
|
||||
}
|
||||
|
||||
func transformMappingOrListFunc(sep string, allowNil bool) TransformerFunc {
|
||||
return func(data any) (any, error) {
|
||||
return transformMappingOrList(data, sep, allowNil), nil
|
||||
}
|
||||
}
|
||||
|
||||
func transformMappingOrList(mappingOrList any, sep string, allowNil bool) any {
|
||||
switch values := mappingOrList.(type) {
|
||||
case map[string]any:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user