Compare commits

..

13 Commits

Author SHA1 Message Date
29cf629222 Merge pull request #4819 from dvdksn/25.0_backport_docs_host-gateway-ip_daemonjson
Some checks failed
build / prepare (push) Has been cancelled
build / build (push) Has been cancelled
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / e2e (19.03-dind, non-experimental) (push) Has been cancelled
e2e / e2e (alpine, stable-dind, connhelper-ssh) (push) Has been cancelled
e2e / e2e (alpine, stable-dind, experimental) (push) Has been cancelled
e2e / e2e (alpine, stable-dind, non-experimental) (push) Has been cancelled
e2e / e2e (debian, stable-dind, connhelper-ssh) (push) Has been cancelled
e2e / e2e (debian, stable-dind, experimental) (push) Has been cancelled
e2e / e2e (debian, stable-dind, non-experimental) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-12) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
[25.0 backport] docs: update host-gateway-ip to use daemon.json instead of cli flag
2024-01-23 16:11:10 +01:00
4caf4de039 docs: update host-gateway-ip to use daemon.json instead of cli flag
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit ec0a62436e)
2024-01-23 16:04:58 +01:00
950ecd42fd Merge pull request #4815 from thaJeztah/25.0_backport_update_compose
[25.0 backport] Dockerfile: update docker compose to v2.24.2
2024-01-23 11:04:43 +01:00
6ab4781bd0 Dockerfile: update docker compose to v2.24.2
Update the version of compose used in CI to the latest version.

- full diff: docker/compose@v2.24.0...v2.24.2
- release notes: https://github.com/docker/compose/releases/tag/v2.24.1
- release notes: https://github.com/docker/compose/releases/tag/v2.24.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 091af560ca)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-23 10:34:14 +01:00
e8852e8ed2 Merge pull request #4806 from thaJeztah/25.0_backport_socket-eof-return
[25.0 backport] socket: return from loop after EOF
2024-01-22 14:10:02 +01:00
4e097c643d socket: return from loop after EOF
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 8cd3b00420)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-20 13:45:39 +01:00
01f9332618 Merge pull request #4803 from thaJeztah/25.0_backport_update_engine
[25.0 backport] vendor: github.com/docker/docker v25.0.0
2024-01-19 15:31:35 +01:00
4cd8d5cf47 Merge pull request #4804 from dvdksn/backport_25.0_docs_fix_cli_broken_alias_links
[25.0 backport] docs: fix broken links to alias pages
2024-01-19 15:18:55 +01:00
21c12847bf docs: move base command to docker.md
CLI reference for the base command was generated to cli.md

Changed it to docker.md to handle broken links.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit d633890f91)
2024-01-19 15:09:15 +01:00
22e1f2cbfa docs: fix broken links to alias pages
Alias pages redirect to the canonical names, but these pages still
linked to the aliases, causing broken links when building the docs site.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit aff4649cb7)
2024-01-19 15:09:07 +01:00
68abf14c15 vendor: github.com/docker/docker v25.0.0
full diff: https://github.com/docker/docker/compare/v25.0.0-rc.3...v25.0.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 337dd82d8b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-19 15:07:25 +01:00
85a5ee4cb0 Merge pull request #4801 from dvdksn/backport_25.0_docs_cdi
[25.0 backport] docs: add documentation for CDI
2024-01-19 15:05:59 +01:00
9e1e07657a docs: add documentation for CDI
- Add section about cdi-spec-dirs daemon configuration
- Add subsection about cdi in --device section for docker run
- Update `docker info` output example

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 852d198bb5)
2024-01-19 14:58:18 +01:00
832 changed files with 22062 additions and 56339 deletions

19
.circleci/config.yml Normal file
View File

@ -0,0 +1,19 @@
# 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

View File

@ -22,13 +22,9 @@ Please provide the following information:
**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog.
It must be placed inside the below triple backticks section:
pull request for inclusion in the changelog:
-->
```markdown changelog
```
**- A picture of a cute animal (not mandatory but encouraged)**

View File

@ -19,7 +19,7 @@ on:
jobs:
prepare:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
@ -37,7 +37,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }}
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
needs:
- prepare
strategy:
@ -77,20 +77,20 @@ jobs:
platformPair=${platform//\//-}
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
if [ -z "${{ matrix.use_glibc }}" ]; then
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${{ matrix.target }}" >> $GITHUB_ENV
else
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
echo "ARTIFACT_NAME=${{ matrix.target }}-glibc" >> $GITHUB_ENV
fi
-
name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/*
if-no-files-found: error
bin-image:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.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-22.04
runs-on: ubuntu-20.04
outputs:
matrix: ${{ steps.platforms.outputs.matrix }}
steps:
@ -152,7 +152,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }}
plugins:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
needs:
- prepare-plugins
strategy:

View File

@ -26,8 +26,6 @@ jobs:
codeql:
runs-on: 'ubuntu-latest'
timeout-minutes: 360
env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions:
actions: read
contents: read
@ -54,16 +52,6 @@ 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

View File

@ -16,7 +16,7 @@ on:
jobs:
e2e:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:
@ -28,11 +28,11 @@ jobs:
- alpine
- debian
engine-version:
- 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
# - 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
steps:
-
name: Checkout
@ -55,11 +55,10 @@ jobs:
make -f docker.Makefile test-e2e-${{ matrix.target }}
env:
BASE_VARIANT: ${{ matrix.base }}
ENGINE_VERSION: ${{ matrix.engine-version }}
E2E_ENGINE_VERSION: ${{ matrix.engine-version }}
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -16,7 +16,7 @@ on:
jobs:
ctn:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
-
name: Checkout
@ -31,10 +31,9 @@ jobs:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
with:
file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
host:
runs-on: ${{ matrix.os }}
@ -64,7 +63,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21.11
go-version: 1.21.6
-
name: Test
run: |
@ -74,8 +73,7 @@ jobs:
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
with:
file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,62 +0,0 @@
name: validate-pr
on:
pull_request:
types: [opened, edited, labeled, unlabeled]
jobs:
check-area-label:
runs-on: ubuntu-20.04
steps:
- name: Missing `area/` label
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1
- name: OK
run: exit 0
check-changelog:
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
runs-on: ubuntu-20.04
env:
PR_BODY: |
${{ github.event.pull_request.body }}
steps:
- name: Check changelog description
run: |
# Extract the `markdown changelog` note code block
block=$(echo -n "$PR_BODY" | tr -d '\r' | awk '/^```markdown changelog$/{flag=1;next}/^```$/{flag=0}flag')
# Strip empty lines
desc=$(echo "$block" | awk NF)
if [ -z "$desc" ]; then
echo "::error::Changelog section is empty. Please provide a description for the changelog."
exit 1
fi
len=$(echo -n "$desc" | wc -c)
if [[ $len -le 6 ]]; then
echo "::error::Description looks too short: $desc"
exit 1
fi
echo "This PR will be included in the release notes with the following note:"
echo "$desc"
check-pr-branch:
runs-on: ubuntu-20.04
env:
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
# Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch
- name: Get branch from PR title
id: title_branch
run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
- name: Check release branch
if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '')
run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1

View File

@ -16,7 +16,7 @@ on:
jobs:
validate:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.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-22.04
runs-on: ubuntu-20.04
steps:
-
name: Checkout
@ -57,7 +57,7 @@ jobs:
fi
validate-make:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy:
fail-fast: false
matrix:

View File

@ -84,7 +84,7 @@ use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contr
<tr>
<td>Community Slack</td>
<td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>.
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/slack" target="_blank">with this link</a>.
</td>
</tr>
<tr>

View File

@ -1,15 +1,15 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
ARG ALPINE_VERSION=3.18
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.21.11
ARG XX_VERSION=1.4.0
ARG GO_VERSION=1.21.6
ARG XX_VERSION=1.2.1
ARG GOVERSIONINFO_VERSION=v1.3.0
ARG GOTESTSUM_VERSION=v1.10.0
ARG BUILDX_VERSION=0.12.1
ARG COMPOSE_VERSION=v2.24.3
ARG COMPOSE_VERSION=v2.24.2
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@ -123,14 +123,8 @@ COPY --link . .
FROM scratch AS plugins
COPY --from=build-plugins /out .
FROM scratch AS bin-image-linux
FROM scratch AS bin-image
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 .

View File

@ -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.21 . ; \
gofumpt -w -d -lang=1.19 . ; \
else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi

View File

@ -1,18 +0,0 @@
package hooks
import (
"fmt"
"io"
"github.com/morikuni/aec"
)
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages {
_, _ = fmt.Fprintf(out, " %s\n", n)
}
}

View File

@ -1,38 +0,0 @@
package hooks
import (
"bytes"
"testing"
"github.com/morikuni/aec"
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
testCases := []struct {
messages []string
expectedOutput string
}{
{
messages: []string{},
expectedOutput: "",
},
{
messages: []string{"Bork!"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Bork!\n",
},
{
messages: []string{"Foo", "bar"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
" Foo\n" +
" bar\n",
},
}
for _, tc := range testCases {
w := bytes.Buffer{}
PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
}
}

View File

@ -1,116 +0,0 @@
package hooks
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/spf13/cobra"
)
type HookType int
const (
NextSteps = iota
)
// HookMessage represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a json
// representation of this type when their hook subcommand
// is invoked.
type HookMessage struct {
Type HookType
Template string
}
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran the subcommand: run`
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string
// that will be replaced by the flags value.
//
// Example:
//
// "you ran a container named: " + TemplateReplaceFlagValue("name")
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran a container named: my-container`
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument, after processing the passed flags.
//
// Example:
//
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
//
// when being executed after the command:
// `docker pull alpine`
// will result in the message:
// "Run this image with `docker run alpine`"
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
}
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate)
if err != nil {
return nil, err
}
b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd)
if err != nil {
return nil, err
}
return strings.Split(b.String(), "\n"), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
const (
hookTemplateCommandName = "{{.Name}}"
hookTemplateFlagValue = `{{flag . "%s"}}`
hookTemplateArg = "{{arg . %s}}"
)
var commandFunctions = template.FuncMap{
"flag": getFlagValue,
"arg": getArgValue,
}
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
cmdFlag := cmd.Flag(flag)
if cmdFlag == nil {
return "", ErrHookTemplateParse
}
return cmdFlag.Value.String(), nil
}
func getArgValue(cmd *cobra.Command, i int) (string, error) {
flags := cmd.Flags()
if flags == nil {
return "", ErrHookTemplateParse
}
return flags.Arg(i), nil
}

View File

@ -1,86 +0,0 @@
package hooks
import (
"testing"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
testCases := []struct {
template string
flags []testFlag
args []string
expectedOutput []string
}{
{
template: "",
expectedOutput: []string{""},
},
{
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
template: TemplateReplaceFlagValue("tag"),
flags: []testFlag{
{
name: "tag",
value: "my-tag",
},
},
expectedOutput: []string{"my-tag"},
},
{
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
flags: []testFlag{
{
name: "test-one",
value: "value",
},
{
name: "test2",
value: "value2",
},
},
expectedOutput: []string{"value value2"},
},
{
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
template: "You just pulled " + TemplateReplaceArg(0),
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range testCases {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
out, err := ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
}

View File

@ -2,14 +2,11 @@ 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 (
@ -33,10 +30,6 @@ 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
@ -105,44 +98,3 @@ 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
}

View File

@ -41,9 +41,6 @@ func (e *pluginError) MarshalText() (text []byte, err error) {
// wrapAsPluginError wraps an error in a pluginError with an
// additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error {
if err == nil {
return nil
}
return &pluginError{cause: errors.Wrap(err, msg)}
}

View File

@ -1,191 +0,0 @@
package manager
import (
"encoding/json"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type HookPluginData struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for `docker context` is
// configured and the user executes `docker context ls`, the plugin will
// be invoked with `context`.
RootCmd string
Flags map[string]string
CommandError string
}
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
// a main CLI command was executed. It calls the hook subcommand for all
// present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses.
func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand)
runHooks(dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
// RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI.
func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) {
commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "")
}
func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage)
hooks.PrintNextSteps(dockerCli.Err(), nextSteps)
}
func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
pluginsCfg := dockerCli.ConfigFile().Plugins
if pluginsCfg == nil {
return nil
}
nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, cfg := range pluginsCfg {
match, ok := pluginMatch(cfg, subCmdStr)
if !ok {
continue
}
p, err := GetPlugin(pluginName, dockerCli, rootCmd)
if err != nil {
continue
}
hookReturn, err := p.RunHook(HookPluginData{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
// skip misbehaving plugins, but don't halt execution
continue
}
var hookMessageData hooks.HookMessage
err = json.Unmarshal(hookReturn, &hookMessageData)
if err != nil {
continue
}
// currently the only hook type
if hookMessageData.Type != hooks.NextSteps {
continue
}
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
if err != nil {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
if !appended {
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
}
}
return nextSteps
}
// appendNextSteps appends the processed hook output to the nextSteps slice.
// If the processed hook output is empty, it is not appended.
// Empty lines are not stripped if there's at least one non-empty line.
func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
empty := true
for _, l := range processed {
if strings.TrimSpace(l) != "" {
empty = false
break
}
}
if empty {
return nextSteps, false
}
return append(nextSteps, processed...), true
}
// pluginMatch takes a plugin configuration and a string representing the
// command being executed (such as 'image ls' the root 'docker' is omitted)
// and, if the configuration includes a hook for the invoked command, returns
// the configured hook string.
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
configuredPluginHooks, ok := pluginCfg["hooks"]
if !ok || configuredPluginHooks == "" {
return "", false
}
commands := strings.Split(configuredPluginHooks, ",")
for _, hookCmd := range commands {
if hookMatch(hookCmd, subCmd) {
return hookCmd, true
}
}
return "", false
}
func hookMatch(hookCmd, subCmd string) bool {
hookCmdTokens := strings.Split(hookCmd, " ")
subCmdTokens := strings.Split(subCmd, " ")
if len(hookCmdTokens) > len(subCmdTokens) {
return false
}
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
}
func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) {
var fValue string
if f.Value.Type() == "bool" {
fValue = f.Value.String()
}
flags[f.Name] = fValue
})
return flags
}
// getNaiveFlags string-matches argv and parses them into a map.
// This is used when calling hooks after a plugin command, since
// in this case we can't rely on the cobra command tree to parse
// flags in this case. In this case, no values are ever passed,
// since we don't have enough information to process them.
func getNaiveFlags(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
flags[arg[2:]] = ""
continue
}
if strings.HasPrefix(arg, "-") {
flags[arg[1:]] = ""
}
}
return flags
}

View File

@ -1,143 +0,0 @@
package manager
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestGetNaiveFlags(t *testing.T) {
testCases := []struct {
args []string
expectedFlags map[string]string
}{
{
args: []string{"docker"},
expectedFlags: map[string]string{},
},
{
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
expectedFlags: map[string]string{
"q": "",
"file": "",
},
},
{
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
expectedFlags: map[string]string{
"context": "",
"q": "",
"progress": "",
},
},
}
for _, tc := range testCases {
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
}
}
func TestPluginMatch(t *testing.T) {
testCases := []struct {
commandString string
pluginConfig map[string]string
expectedMatch string
expectedOk bool
}{
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "build",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "context ls",
},
expectedMatch: "context ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image ls,image",
},
expectedMatch: "image ls",
expectedOk: true,
},
{
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image i",
},
expectedMatch: "",
expectedOk: false,
},
{
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
}
for _, tc := range testCases {
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
}
}
func TestAppendNextSteps(t *testing.T) {
testCases := []struct {
processed []string
expectedOut []string
}{
{
processed: []string{},
expectedOut: []string{},
},
{
processed: []string{"", ""},
expectedOut: []string{},
},
{
processed: []string{"Some hint", "", "Some other hint"},
expectedOut: []string{"Some hint", "", "Some other hint"},
},
{
processed: []string{"Hint 1", "Hint 2"},
expectedOut: []string{"Hint 1", "Hint 2"},
},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
got, appended := appendNextSteps([]string{}, tc.processed)
assert.Check(t, is.DeepEqual(got, tc.expectedOut))
assert.Check(t, is.Equal(appended, len(got) > 0))
})
}
}

View File

@ -17,17 +17,11 @@ import (
"golang.org/x/sync/errgroup"
)
const (
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
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"
)
// 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"
// errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string
@ -240,8 +234,8 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0])
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
return cmd, nil
}

View File

@ -8,11 +8,6 @@ const (
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
)
// Metadata provided by the plugin.

View File

@ -2,8 +2,6 @@ package manager
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
@ -102,22 +100,3 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
}
return p, nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes))
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
}

View File

@ -12,17 +12,15 @@ import (
"github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
)
// PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended
// to use `PersistentPreRunE` to enable the error to be
// to use `PersistenPreRunE` to enable the error to be
// returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been
// called.
@ -52,24 +50,6 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
opts = append(opts, withPluginClientConn(plugin.Name()))
}
err = tcmd.Initialize(opts...)
ogRunE := cmd.RunE
if ogRunE == nil {
ogRun := cmd.Run
// necessary because error will always be nil here
// see: https://github.com/golangci/golangci-lint/issues/1379
//nolint:unparam
ogRunE = func(cmd *cobra.Command, args []string) error {
ogRun(cmd, args)
return nil
}
cmd.Run = nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
err := ogRunE(cmd, args)
stopInstrumentation(err)
return err
}
})
return err
}
@ -86,8 +66,6 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCli, err := command.NewDockerCli()
if err != nil {
fmt.Fprintln(os.Stderr, err)

View File

@ -1,128 +1,42 @@
package socket
import (
"crypto/rand"
"encoding/hex"
"errors"
"io"
"net"
"os"
"runtime"
"sync"
"github.com/docker/distribution/uuid"
)
// EnvKey represents the well-known environment variable used to pass the
// plugin being executed the socket name it should listen on to coordinate with
// the host CLI.
// EnvKey represents the well-known environment variable used to pass the plugin being
// executed the socket name it should listen on to coordinate with the host CLI.
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
// NewPluginServer creates a plugin server that listens on a new Unix domain
// socket. h is called for each new connection to the socket in a goroutine.
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) {
// Listen on a Unix socket, with the address being platform-dependent.
// When a non-abstract address is used, Go will unlink(2) the socket
// for us once the listener is closed, as documented in
// [net.UnixListener.SetUnlinkOnClose].
l, err := net.ListenUnix("unix", &net.UnixAddr{
Name: socketName("docker_cli_" + randomID()),
Net: "unix",
})
// SetupConn sets up a Unix socket listener, establishes a goroutine to handle connections
// 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())
if err != nil {
return nil, err
}
if h == nil {
h = func(net.Conn) {}
}
accept(listener, conn)
pl := &PluginServer{
l: l,
h: h,
}
return listener, nil
}
func accept(listener *net.UnixListener, conn **net.UnixConn) {
go func() {
defer pl.Close()
for {
err := pl.accept()
if err != nil {
return
}
// ignore error here, if we failed to accept a connection,
// conn is nil and we fallback to previous behavior
*conn, _ = listener.AcceptUnix()
// perform any platform-specific actions on accept (e.g. unlink non-abstract sockets)
onAccept(*conn, listener)
}
}()
return pl, nil
}
type PluginServer struct {
mu sync.Mutex
conns []net.Conn
l *net.UnixListener
h func(net.Conn)
closed bool
}
func (pl *PluginServer) accept() error {
conn, err := pl.l.Accept()
if err != nil {
return err
}
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
// Handle potential race between Close and accept.
conn.Close()
return errors.New("plugin server is closed")
}
pl.conns = append(pl.conns, conn)
go pl.h(conn)
return nil
}
// Addr returns the [net.Addr] of the underlying [net.Listener].
func (pl *PluginServer) Addr() net.Addr {
return pl.l.Addr()
}
// Close ensures that the server is no longer accepting new connections and
// closes all existing connections. Existing connections will receive [io.EOF].
//
// The error value is that of the underlying [net.Listner.Close] call.
func (pl *PluginServer) Close() error {
// Close connections first to ensure the connections get io.EOF instead
// of a connection reset.
pl.closeAllConns()
// Try to ensure that any active connections have a chance to receive
// io.EOF.
runtime.Gosched()
return pl.l.Close()
}
func (pl *PluginServer) closeAllConns() {
pl.mu.Lock()
defer pl.mu.Unlock()
// Prevent new connections from being accepted.
pl.closed = true
for _, conn := range pl.conns {
conn.Close()
}
pl.conns = 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)
}
// ConnectAndWait connects to the socket passed via well-known env var,

View File

@ -1,9 +0,0 @@
//go:build windows || linux
package socket
func socketName(basename string) string {
// Address of an abstract socket -- this socket can be opened by name,
// but is not present in the filesystem.
return "@" + basename
}

View 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())
}

View File

@ -1,14 +0,0 @@
//go:build !windows && !linux
package socket
import (
"os"
"path/filepath"
)
func socketName(basename string) string {
// Because abstract sockets are unavailable, use a socket path in the
// system temporary directory.
return filepath.Join(os.TempDir(), basename)
}

View File

@ -0,0 +1,19 @@
//go:build !darwin
package socket
import (
"net"
)
func listen(socketname string) (*net.UnixListener, error) {
return net.ListenUnix("unix", &net.UnixAddr{
Name: "@" + socketname,
Net: "unix",
})
}
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
}

View File

@ -1,189 +0,0 @@
package socket
import (
"errors"
"io"
"io/fs"
"net"
"os"
"runtime"
"strings"
"sync/atomic"
"testing"
"time"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
)
func TestPluginServer(t *testing.T) {
t.Run("connection closes with EOF when server closes", func(t *testing.T) {
called := make(chan struct{})
srv, err := NewPluginServer(func(_ net.Conn) { close(called) })
assert.NilError(t, err)
assert.Assert(t, srv != nil, "returned nil server but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
defer conn.Close()
done := make(chan error, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
done <- err
}()
select {
case <-called:
case <-time.After(10 * time.Millisecond):
t.Fatal("handler not called")
}
srv.Close()
select {
case err := <-done:
if !errors.Is(err, io.EOF) {
t.Fatalf("exepcted EOF error, got: %v", err)
}
case <-time.After(10 * time.Millisecond):
}
})
t.Run("allows reconnects", func(t *testing.T) {
var calls int32
h := func(_ net.Conn) {
atomic.AddInt32(&calls, 1)
}
srv, err := NewPluginServer(h)
assert.NilError(t, err)
defer srv.Close()
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
waitForCalls := func(n int) {
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if atomic.LoadInt32(&calls) == int32(n) {
return poll.Success()
}
return poll.Continue("waiting for handler to be called")
})
}
otherConn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
otherConn.Close()
waitForCalls(1)
conn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn.Close()
waitForCalls(2)
// and again but don't close the existing connection
conn2, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn2.Close()
waitForCalls(3)
srv.Close()
// now make sure we get EOF on the existing connections
buf := make([]byte, 1)
_, err = conn.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
_, err = conn2.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
})
t.Run("does not leak sockets to local directory", func(t *testing.T) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err)
assert.Check(t, srv != nil, "returned nil server but no error")
checkDirNoNewPluginServer(t)
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
_, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server")
checkDirNoNewPluginServer(t)
})
}
func checkDirNoNewPluginServer(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) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
done := make(chan struct{})
t.Setenv(EnvKey, srv.Addr().String())
cancelFunc := func() {
done <- struct{}{}
}
ConnectAndWait(cancelFunc)
select {
case <-done:
t.Fatal("unexpectedly done")
default:
}
srv.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) {
srv, err := NewPluginServer(nil)
assert.NilError(t, err, "failed to setup server")
defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String())
numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
srv.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))
})
}

View File

@ -470,7 +470,7 @@ Common Commands:
Management Commands:
{{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}}
{{- end}}
@ -479,7 +479,7 @@ Management Commands:
Swarm Commands:
{{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}}
{{- end}}

View File

@ -1,20 +0,0 @@
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
}

View File

@ -2,7 +2,6 @@ package builder
import (
"context"
"errors"
"fmt"
"strings"
@ -11,7 +10,6 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
@ -68,14 +66,8 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.all {
warning = allCacheWarning
}
if !options.force {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("builder prune has been cancelled"))
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}
report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{

View File

@ -1,23 +0,0 @@
package builder
import (
"context"
"errors"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
)
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)
}

View File

@ -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.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -30,7 +30,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
}
flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory")
flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd
}

View File

@ -65,7 +65,6 @@ type Cli interface {
ContextStore() store.Store
CurrentContext() string
DockerEndpoint() docker.Endpoint
TelemetryClient
}
// DockerCli is an instance the docker command line client.
@ -86,7 +85,6 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
initTimeout time.Duration
res telemetryResource
// baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that
@ -189,36 +187,6 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
return cli.ServerInfo().OSType != "windows", nil
}
// HooksEnabled returns whether plugin hooks are enabled.
func (cli *DockerCli) HooksEnabled() bool {
// legacy support DOCKER_CLI_HINTS env var
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// use DOCKER_CLI_HOOKS env var value if set and not empty
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
featuresMap := cli.ConfigFile().Features
if v, ok := featuresMap["hooks"]; ok {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// default to false
return false
}
// ManifestStore returns a store for local manifests
func (cli *DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file
@ -273,11 +241,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
},
}
// TODO(krissetto): pass ctx to the funcs instead of using this
cli.createGlobalMeterProvider(cli.baseCtx)
cli.createGlobalTracerProvider(cli.baseCtx)
return nil
}

View File

@ -11,11 +11,19 @@ import (
"github.com/moby/term"
)
// 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].
// CLIOption applies a modification on a DockerCli.
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 {

View File

@ -307,56 +307,3 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
})))
assert.Check(t, cli.ContextStore() != nil)
}
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, cli.HooksEnabled())
})
t.Run("env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HOOKS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
t.Run("legacy env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HINTS", "false")
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
defer dir.Remove()
cli, err := NewDockerCli()
assert.NilError(t, err)
config.SetDir(dir.Path())
assert.Check(t, !cli.HooksEnabled())
})
}

View File

@ -7,7 +7,6 @@ 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"
)
@ -18,7 +17,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(), image.ListOptions{})
list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}

View File

@ -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.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp)
flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp)
flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -43,21 +43,22 @@ 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 {
containerID := args[0]
return RunAttach(cmd.Context(), dockerCLI, containerID, &opts)
ctr = args[0]
return RunAttach(cmd.Context(), dockerCli, ctr, &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"
}),
}
@ -70,13 +71,13 @@ func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
}
// RunAttach executes an `attach` command
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
func RunAttach(ctx context.Context, dockerCLI command.Cli, target string, opts *AttachOptions) error {
apiClient := dockerCLI.Client()
// request channel to wait for client
resultC, errC := apiClient.ContainerWait(ctx, containerID, "")
resultC, errC := apiClient.ContainerWait(ctx, target, "")
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
c, err := inspectContainerAndCheckState(ctx, apiClient, target)
if err != nil {
return err
}
@ -105,11 +106,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
if opts.Proxy && !c.Config.Tty {
sigc := notifyAllSignals()
go ForwardAllSignals(ctx, apiClient, containerID, sigc)
go ForwardAllSignals(ctx, apiClient, target, sigc)
defer signal.StopCatch(sigc)
}
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
resp, errAttach := apiClient.ContainerAttach(ctx, target, options)
if errAttach != nil {
return errAttach
}
@ -123,13 +124,13 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
// the container and not exit.
//
// Recheck the container's state to avoid attach block.
_, err = inspectContainerAndCheckState(ctx, apiClient, containerID)
_, err = inspectContainerAndCheckState(ctx, apiClient, target)
if err != nil {
return err
}
if c.Config.Tty && dockerCLI.Out().IsTerminal() {
resizeTTY(ctx, dockerCLI, containerID)
resizeTTY(ctx, dockerCLI, target)
}
streamer := hijackedIOStreamer{

View File

@ -6,8 +6,6 @@ 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"
@ -25,7 +23,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 image.CreateOptions) (io.ReadCloser, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (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)
@ -36,7 +34,6 @@ 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
}
@ -93,7 +90,7 @@ func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, op
return nil
}
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
func (f *fakeClient) ImageCreate(_ context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
@ -166,10 +163,3 @@ 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
}

View File

@ -7,7 +7,7 @@ import (
"os"
"regexp"
"github.com/containerd/platforms"
"github.com/containerd/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -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, imagetypes.CreateOptions{
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, types.ImageCreateOptions{
RegistryAuth: encodedAuth,
Platform: options.platform,
})
@ -239,7 +239,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
p, err := platforms.Parse(options.platform)
if err != nil {
return "", errors.Wrap(errdefs.InvalidParameter(err), "error parsing specified platform")
return "", errors.Wrap(err, "error parsing specified platform")
}
platform = &p
}

View File

@ -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 image.CreateOptions) (io.ReadCloser, error) {
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
defer func() { pullCounter++ }()
return io.NopCloser(strings.NewReader("")), nil
},

View File

@ -66,12 +66,12 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags()
flags.SetInterspersed(false)
flags.StringVar(&options.DetachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
flags.StringVarP(&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.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
flags.BoolVarP(&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")

View File

@ -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.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -8,9 +8,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -55,14 +53,8 @@ Are you sure you want to continue?`
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
if !options.force {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("container prune has been cancelled"))
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}
report, err := dockerCli.Client().ContainersPrune(ctx, pruneFilters)

View File

@ -1,24 +0,0 @@
package container
import (
"context"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
)
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)
}

View File

@ -186,11 +186,7 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
defer closeFn()
}
// New context here because we don't to cancel waiting on container exit/remove
// when we cancel attach, etc.
statusCtx, cancelStatusCtx := context.WithCancel(context.WithoutCancel(ctx))
defer cancelStatusCtx()
statusChan := waitExitOrRemoved(statusCtx, apiClient, containerID, copts.autoRemove)
statusChan := waitExitOrRemoved(ctx, apiClient, containerID, copts.autoRemove)
// start the container
if err := apiClient.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil {

View File

@ -28,6 +28,13 @@ 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

View File

@ -18,7 +18,6 @@ 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"
)
@ -107,6 +106,15 @@ 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.
@ -130,9 +138,9 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
return err
}
eh := newEventHandler()
eh := command.InitEventHandler()
if options.All {
eh.setHandler(events.ActionCreate, func(e events.Message) {
eh.Handle(events.ActionCreate, func(e events.Message) {
s := NewStats(e.Actor.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
@ -141,7 +149,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
})
}
eh.setHandler(events.ActionStart, func(e events.Message) {
eh.Handle(events.ActionStart, func(e events.Message) {
s := NewStats(e.Actor.ID[:12])
if cStats.add(s) {
waitFirst.Add(1)
@ -150,7 +158,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
})
if !options.All {
eh.setHandler(events.ActionDie, func(e events.Message) {
eh.Handle(events.ActionDie, func(e events.Message) {
cStats.remove(e.Actor.ID[:12])
})
}
@ -187,7 +195,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)
@ -259,12 +267,6 @@ 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),
@ -314,31 +316,3 @@ 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)
}
}

View File

@ -2,7 +2,6 @@ package container
import (
"context"
"errors"
"strconv"
"github.com/docker/docker/api/types"
@ -36,10 +35,7 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe
statusC := make(chan int)
go func() {
defer close(statusC)
select {
case <-ctx.Done():
return
case result := <-resultC:
if result.Error != nil {
logrus.Errorf("Error waiting for container: %v", result.Error.Message)
@ -48,9 +44,6 @@ func waitExitOrRemoved(ctx context.Context, apiClient client.APIClient, containe
statusC <- int(result.StatusCode)
}
case err := <-errC:
if errors.Is(err, context.Canceled) {
return
}
logrus.Errorf("error waiting for container: %v", err)
statusC <- 125
}

View File

@ -24,10 +24,6 @@ type CreateOptions struct {
Description string
Docker map[string]string
From string
// Additional Metadata to store in the context. This option is not
// currently exposed to the user.
metaData map[string]any
}
func longCreateDescription() string {
@ -98,8 +94,7 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error {
docker.DockerEndpoint: dockerEP,
},
Metadata: command.DockerContext{
Description: o.Description,
AdditionalFields: o.metaData,
Description: o.Description,
},
Name: o.Name,
}

View File

@ -8,18 +8,14 @@ import (
"path/filepath"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestExportImportWithFile(t *testing.T) {
contextFile := filepath.Join(t.TempDir(), "exported")
cli := makeFakeCli(t)
createTestContext(t, cli, "test", map[string]any{
"MyCustomMetadata": t.Name(),
})
createTestContext(t, cli, "test")
cli.ErrBuffer().Reset()
assert.NilError(t, RunExport(cli, &ExportOptions{
ContextName: "test",
@ -33,26 +29,18 @@ func TestExportImportWithFile(t *testing.T) {
assert.NilError(t, err)
context2, err := cli.ContextStore().GetMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
Description: "description of test",
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
}))
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
assert.Check(t, is.Equal("test", context1.Name))
assert.Check(t, is.Equal("test2", context2.Name))
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportImportPipe(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "test", map[string]any{
"MyCustomMetadata": t.Name(),
})
createTestContext(t, cli, "test")
cli.ErrBuffer().Reset()
cli.OutBuffer().Reset()
assert.NilError(t, RunExport(cli, &ExportOptions{
@ -68,19 +56,13 @@ func TestExportImportPipe(t *testing.T) {
assert.NilError(t, err)
context2, err := cli.ContextStore().GetMetadata("test2")
assert.NilError(t, err)
assert.DeepEqual(t, context1.Endpoints, context2.Endpoints)
assert.DeepEqual(t, context1.Metadata, context2.Metadata)
assert.Equal(t, "test", context1.Name)
assert.Equal(t, "test2", context2.Name)
assert.Check(t, is.DeepEqual(context1.Metadata, command.DockerContext{
Description: "description of test",
AdditionalFields: map[string]any{"MyCustomMetadata": t.Name()},
}))
assert.Check(t, is.DeepEqual(context1.Endpoints, context2.Endpoints))
assert.Check(t, is.DeepEqual(context1.Metadata, context2.Metadata))
assert.Check(t, is.Equal("test", context1.Name))
assert.Check(t, is.Equal("test2", context2.Name))
assert.Check(t, is.Equal("test2\n", cli.OutBuffer().String()))
assert.Check(t, is.Equal("Successfully imported context \"test2\"\n", cli.ErrBuffer().String()))
assert.Equal(t, "test2\n", cli.OutBuffer().String())
assert.Equal(t, "Successfully imported context \"test2\"\n", cli.ErrBuffer().String())
}
func TestExportExistingFile(t *testing.T) {

View File

@ -10,9 +10,7 @@ import (
func TestInspect(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", map[string]any{
"MyCustomMetadata": "MyCustomMetadataValue",
})
createTestContext(t, cli, "current")
cli.OutBuffer().Reset()
assert.NilError(t, runInspect(cli, inspectOptions{
refs: []string{"current"},

View File

@ -66,8 +66,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Name: rawMeta.Name,
Current: isCurrent,
Error: err.Error(),
ContextType: getContextType(nil, opts.format),
})
continue
}
@ -82,8 +80,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Description: meta.Description,
DockerEndpoint: dockerEndpoint.Host,
Error: errMsg,
ContextType: getContextType(meta.AdditionalFields, opts.format),
}
contexts = append(contexts, &desc)
}
@ -100,8 +96,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
Name: curContext,
Current: true,
Error: errMsg,
ContextType: getContextType(nil, opts.format),
})
}
sort.Slice(contexts, func(i, j int) bool {
@ -117,30 +111,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
return nil
}
// getContextType sets the LegacyContextType field for compatibility with
// Visual Studio, which depends on this field from the "cloud integration"
// wrapper.
//
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/contextmetadata.go#L28-L34
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/store.go#L34-L51
//
// TODO(thaJeztah): remove this and [ClientContext.ContextType] once Visual Studio is updated to no longer depend on this.
func getContextType(meta map[string]any, format string) string {
if format != formatter.JSONFormat && format != formatter.JSONFormatKey {
// We only need the ContextType field when formatting as JSON,
// which is the format-string used by Visual Studio to detect the
// context-type.
return ""
}
if ct, ok := meta["Type"]; ok {
// If the context on-disk has a context-type (ecs, aci), return it.
return ct.(string)
}
// Use the default context-type.
return "moby"
}
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
contextCtx := formatter.Context{
Output: dockerCli.Out(),

View File

@ -4,70 +4,36 @@ import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"gotest.tools/v3/assert"
"gotest.tools/v3/golden"
)
func createTestContexts(t *testing.T, cli command.Cli, name ...string) {
t.Helper()
for _, n := range name {
createTestContext(t, cli, n, nil)
}
}
func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) {
func createTestContext(t *testing.T, cli command.Cli, name string) {
t.Helper()
err := RunCreate(cli, &CreateOptions{
Name: name,
Description: "description of " + name,
Docker: map[string]string{keyHost: "https://someswarmserver.example.com"},
metaData: metaData,
})
assert.NilError(t, err)
}
func TestList(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other", "unset")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
createTestContext(t, cli, "unset")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{}))
golden.Assert(t, cli.OutBuffer().String(), "list.golden")
}
func TestListJSON(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", nil)
createTestContext(t, cli, "context1", map[string]any{"Type": "aci"})
createTestContext(t, cli, "context2", map[string]any{"Type": "ecs"})
createTestContext(t, cli, "context3", map[string]any{"Type": "moby"})
cli.SetCurrentContext("current")
t.Run("format={{json .}}", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormat}))
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
})
t.Run("format=json", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: formatter.JSONFormatKey}))
golden.Assert(t, cli.OutBuffer().String(), "list-json.golden")
})
t.Run("format={{ json .Name }}", func(t *testing.T) {
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{format: `{{ json .Name }}`}))
golden.Assert(t, cli.OutBuffer().String(), "list-json-name.golden")
})
}
func TestListQuiet(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()
assert.NilError(t, runList(cli, &listOptions{quiet: true}))

View File

@ -13,7 +13,8 @@ import (
func TestRemove(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"}))
_, err := cli.ContextStore().GetMetadata("current")
assert.NilError(t, err)
@ -23,7 +24,8 @@ func TestRemove(t *testing.T) {
func TestRemoveNotAContext(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"})
assert.ErrorContains(t, err, `context "not-a-context" does not exist`)
@ -33,7 +35,8 @@ func TestRemoveNotAContext(t *testing.T) {
func TestRemoveCurrent(t *testing.T) {
cli := makeFakeCli(t)
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
err := RunRemove(cli, RemoveOptions{}, []string{"current"})
assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`)
@ -47,7 +50,8 @@ func TestRemoveCurrentForce(t *testing.T) {
assert.NilError(t, testCfg.Save())
cli := makeFakeCli(t, withCliConfig(testCfg))
createTestContexts(t, cli, "current", "other")
createTestContext(t, cli, "current")
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"}))
reloadedConfig, err := config.Load(configDir)
@ -57,7 +61,7 @@ func TestRemoveCurrentForce(t *testing.T) {
func TestRemoveDefault(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "other", nil)
createTestContext(t, cli, "other")
cli.SetCurrentContext("current")
err := RunRemove(cli, RemoveOptions{}, []string{"default"})
assert.ErrorContains(t, err, `default: context "default" cannot be removed`)

View File

@ -8,7 +8,7 @@ import (
func TestShow(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "current", nil)
createTestContext(t, cli, "current")
cli.SetCurrentContext("current")
cli.OutBuffer().Reset()

View File

@ -2,8 +2,7 @@
{
"Name": "current",
"Metadata": {
"Description": "description of current",
"MyCustomMetadata": "MyCustomMetadataValue"
"Description": "description of current"
},
"Endpoints": {
"docker": {

View File

@ -1,5 +0,0 @@
"context1"
"context2"
"context3"
"current"
"default"

View File

@ -1,5 +0,0 @@
{"Name":"context1","Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"aci"}
{"Name":"context2","Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"ecs"}
{"Name":"context3","Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"moby"}
{"Name":"current","Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Current":true,"Error":"","ContextType":"moby"}
{"Name":"default","Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Current":false,"Error":"","ContextType":"moby"}

View File

@ -6,7 +6,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/docker"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/assert/cmp"
)
func TestUpdateDescriptionOnly(t *testing.T) {
@ -34,7 +34,7 @@ func TestUpdateDescriptionOnly(t *testing.T) {
func TestUpdateDockerOnly(t *testing.T) {
cli := makeFakeCli(t)
createTestContext(t, cli, "test", nil)
createTestContext(t, cli, "test")
assert.NilError(t, RunUpdate(cli, &UpdateOptions{
Name: "test",
Docker: map[string]string{
@ -46,7 +46,7 @@ func TestUpdateDockerOnly(t *testing.T) {
dc, err := command.GetDockerContext(c)
assert.NilError(t, err)
assert.Equal(t, dc.Description, "description of test")
assert.Check(t, is.Contains(c.Endpoints, docker.DockerEndpoint))
assert.Check(t, cmp.Contains(c.Endpoints, docker.DockerEndpoint))
assert.Equal(t, c.Endpoints[docker.DockerEndpoint].(docker.EndpointMeta).Host, "tcp://some-host")
}

View File

@ -9,16 +9,12 @@ 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))}
}

View File

@ -1,7 +1,5 @@
package formatter
import "encoding/json"
const (
// ClientContextTableFormat is the default client context format.
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
@ -30,13 +28,6 @@ type ClientContext struct {
DockerEndpoint string
Current bool
Error string
// ContextType is a temporary field for compatibility with
// Visual Studio, which depends on this from the "cloud integration"
// wrapper.
//
// Deprecated: this type is only for backward-compatibility. Do not use.
ContextType string `json:"ContextType,omitempty"`
}
// ClientContextWrite writes formatted contexts using the Context
@ -69,13 +60,6 @@ func newClientContextContext() *clientContextContext {
}
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
if c.c.ContextType != "" {
// We only have ContextType set for plain "json" or "{{json .}}" formatting,
// so we should be able to just use the default json.Marshal with no
// special handling.
return json.Marshal(c.c)
}
// FIXME(thaJeztah): why do we need a special marshal function here?
return MarshalJSON(c)
}

View File

@ -129,6 +129,7 @@ func TestGetContextFromReaderString(t *testing.T) {
tarReader := tar.NewReader(tarArchive)
_, err = tarReader.Next()
if err != nil {
t.Fatalf("Error when reading tar archive: %s", err)
}

View File

@ -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 image.RemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error)
imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error)
imagePullFunc func(ref string, options types.ImagePullOptions) (io.ReadCloser, error)
imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (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 image.RemoveOptions,
options types.ImageRemoveOptions,
) ([]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 image.PushOptions) (io.ReadCloser, error) {
func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (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 image.PullOptions) (io.ReadCloser, error) {
func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (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 image.ListOptions) ([]image.Summary, error) {
func (cli *fakeClient) ImageList(_ context.Context, options types.ImageListOptions) ([]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 image.ImportOptions,
options types.ImageImportOptions,
) (io.ReadCloser, error) {
if cli.imageImportFunc != nil {
return cli.imageImportFunc(source, ref, options)

View File

@ -9,7 +9,6 @@ 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"
)
@ -79,7 +78,7 @@ func runImport(ctx context.Context, dockerCli command.Cli, options importOptions
}
}
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, image.ImportOptions{
responseBody, err := dockerCli.Client().ImageImport(ctx, source, options.reference, types.ImageImportOptions{
Message: options.message,
Changes: options.changes.GetAll(),
Platform: options.platform,

View File

@ -7,7 +7,6 @@ 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"
@ -18,7 +17,7 @@ func TestNewImportCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
}{
{
name: "wrong-args",
@ -29,7 +28,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 image.ImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
return nil, errors.Errorf("something went wrong")
},
},
@ -53,7 +52,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
imageImportFunc func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
}{
{
name: "simple",
@ -66,7 +65,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "double",
args: []string{"-", "image:local"},
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("image:local", ref))
return io.NopCloser(strings.NewReader("")), nil
},
@ -74,7 +73,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "message",
args: []string{"--message", "test message", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("test message", options.Message))
return io.NopCloser(strings.NewReader("")), nil
},
@ -82,7 +81,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "change",
args: []string{"--change", "ENV DEBUG=true", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("ENV DEBUG=true", options.Changes[0]))
return io.NopCloser(strings.NewReader("")), nil
},
@ -90,7 +89,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
{
name: "change legacy syntax",
args: []string{"--change", "ENV DEBUG true", "-"},
imageImportFunc: func(source types.ImageImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) {
imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
assert.Check(t, is.Equal("ENV DEBUG true", options.Changes[0]))
return io.NopCloser(strings.NewReader("")), nil
},

View File

@ -2,15 +2,13 @@ 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/image"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
)
@ -23,11 +21,10 @@ 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{
@ -38,11 +35,7 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command {
if len(args) > 0 {
options.matchName = args[0]
}
// Pass through how the command was invoked. We use this to print
// warnings when an ambiguous argument was passed when using the
// legacy (top-level) "docker images" subcommand.
options.calledAs = cmd.CalledAs()
return runImages(cmd.Context(), dockerCLI, options)
return runImages(cmd.Context(), dockerCli, options)
},
Annotations: map[string]string{
"category-top": "7",
@ -62,31 +55,33 @@ 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)
}
images, err := dockerCLI.Client().ImageList(ctx, image.ListOptions{
listOptions := types.ImageListOptions{
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
}
@ -94,50 +89,11 @@ 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,
}
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)
}
return formatter.ImageWrite(imageCtx, images)
}

View File

@ -7,6 +7,7 @@ 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"
@ -19,7 +20,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
}{
{
name: "wrong-args",
@ -29,7 +30,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
{
name: "failed-list",
expectedError: "something went wrong",
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
return []image.Summary{}, errors.Errorf("something went wrong")
},
},
@ -47,7 +48,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
name string
args []string
imageFormat string
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
imageListFunc func(options types.ImageListOptions) ([]image.Summary, error)
}{
{
name: "simple",
@ -64,7 +65,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
{
name: "match-name",
args: []string{"image"},
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("image", options.Filters.Get("reference")[0]))
return []image.Summary{}, nil
},
@ -72,7 +73,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
{
name: "filters",
args: []string{"--filter", "name=value"},
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
imageListFunc: func(options types.ImageListOptions) ([]image.Summary, error) {
assert.Check(t, is.Equal("value", options.Filters.Get("name")[0]))
return []image.Summary{}, nil
},
@ -95,17 +96,3 @@ 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")
}

View File

@ -10,9 +10,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts"
"github.com/docker/docker/errdefs"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -69,14 +67,8 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
if options.all {
warning = allImageWarning
}
if !options.force {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return 0, "", err
}
if !r {
return 0, "", errdefs.Cancelled(errors.New("image prune has been cancelled"))
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}
report, err := dockerCli.Client().ImagesPrune(ctx, pruneFilters)

View File

@ -1,13 +1,10 @@
package image
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -95,30 +92,12 @@ func TestNewPruneCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
// when prompted, answer "Y" to confirm the prune.
// will not be prompted if --force is used.
cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader("Y\n"))))
cmd := NewPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
})
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
cmd := NewPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NilError(t, err)
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)
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types"
"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 image.PullOptions) (io.ReadCloser, error) {
imagePullFunc: func(ref string, options types.ImagePullOptions) (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 image.PullOptions) (io.ReadCloser, error) {
imagePullFunc: func(ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), fmt.Errorf("shouldn't try to pull image")
},
}, test.EnableContentTrust)

View File

@ -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/image"
"github.com/docker/docker/api/types"
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 := image.PushOptions{
options := types.ImagePushOptions{
All: opts.all,
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,

View File

@ -6,7 +6,7 @@ import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types"
"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 image.PushOptions) (io.ReadCloser, error)
imagePushFunc func(ref string, options types.ImagePushOptions) (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 image.PushOptions) (io.ReadCloser, error) {
imagePushFunc: func(ref string, options types.ImagePushOptions) (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 image.PushOptions) (io.ReadCloser, error) {
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
},
})

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types"
"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 := image.RemoveOptions{
options := types.ImageRemoveOptions{
Force: opts.force,
PruneChildren: !opts.noPrune,
}

View File

@ -6,6 +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"
@ -35,7 +36,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name string
args []string
expectedError string
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
}{
{
name: "wrong args",
@ -45,7 +46,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 image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{}, errors.Errorf("error removing image")
},
@ -54,7 +55,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
name: "ImageRemove fail",
args: []string{"arg1"},
expectedError: "error removing image",
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, !options.Force)
assert.Check(t, options.PruneChildren)
return []image.DeleteResponse{}, errors.Errorf("error removing image")
@ -77,13 +78,13 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
testCases := []struct {
name string
args []string
imageRemoveFunc func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error)
imageRemoveFunc func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error)
expectedStderr string
}{
{
name: "Image Deleted",
args: []string{"image1"},
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{{Deleted: img}}, nil
},
@ -91,7 +92,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image not found with force option",
args: []string{"-f", "image1"},
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
assert.Check(t, is.Equal(true, options.Force))
return []image.DeleteResponse{}, notFound{"image1"}
@ -102,7 +103,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image Untagged",
args: []string{"image1"},
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
assert.Check(t, is.Equal("image1", img))
return []image.DeleteResponse{{Untagged: img}}, nil
},
@ -110,7 +111,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
{
name: "Image Deleted and Untagged",
args: []string{"image1", "image2"},
imageRemoveFunc: func(img string, options image.RemoveOptions) ([]image.DeleteResponse, error) {
imageRemoveFunc: func(img string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) {
if img == "image1" {
return []image.DeleteResponse{{Untagged: img}}, nil
}

View File

@ -5,6 +5,9 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",
"Author": "",
"Config": null,
@ -26,6 +29,9 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",
"Author": "",
"Config": null,

View File

@ -5,6 +5,9 @@
"RepoDigests": null,
"Parent": "",
"Comment": "",
"Created": "",
"Container": "",
"ContainerConfig": null,
"DockerVersion": "",
"Author": "",
"Config": null,

View File

@ -1,2 +0,0 @@
No images found matching "ls": did you mean "docker image ls"?

View File

@ -13,7 +13,6 @@ 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"
@ -31,7 +30,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 image.PushOptions) error {
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options types.ImagePushOptions) error {
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
if err != nil {
return err
@ -268,7 +267,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()), image.PullOptions{
responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), types.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: opts.all,

View File

@ -4,7 +4,6 @@ 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"
)
@ -16,8 +15,6 @@ 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) {
@ -55,16 +52,6 @@ func (c *fakeClient) NetworkRemove(ctx context.Context, networkID string) error
return nil
}
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)
}
func (c *fakeClient) NetworkInspectWithRaw(context.Context, string, types.NetworkInspectOptions) (types.NetworkResource, []byte, error) {
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
}

View File

@ -7,8 +7,6 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
@ -51,14 +49,8 @@ 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 {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
if err != nil {
return "", err
}
if !r {
return "", errdefs.Cancelled(errors.New("network prune cancelled has been cancelled"))
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return "", nil
}
report, err := dockerCli.Client().NetworksPrune(ctx, pruneFilters)

View File

@ -1,24 +0,0 @@
package network
import (
"context"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/pkg/errors"
)
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)
}

View File

@ -46,15 +46,10 @@ func runRemove(ctx context.Context, dockerCli command.Cli, networks []string, op
status := 0
for _, name := range networks {
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 nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{}); err == nil &&
nw.Ingress &&
!command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) {
continue
}
if err := client.NetworkRemove(ctx, name); err != nil {
if opts.force && errdefs.IsNotFound(err) {

View File

@ -6,7 +6,6 @@ import (
"testing"
"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"
@ -95,24 +94,3 @@ 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)
}

View File

@ -19,7 +19,6 @@ 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 {
@ -76,10 +75,3 @@ 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
}

View File

@ -114,6 +114,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateO
createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{
Compression: compression,
})
if err != nil {
return err
}

View File

@ -142,7 +142,6 @@ 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)
}
ctx := context.TODO()
return command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?")
return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil
}
}

View File

@ -1,2 +0,0 @@
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]

View File

@ -8,7 +8,6 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/pkg/errors"
"github.com/spf13/cobra"
@ -64,12 +63,8 @@ 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() {
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?")
if err != nil {
return err
}
if !r {
return errdefs.Cancelled(errors.New("plugin upgrade has been cancelled"))
if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") {
return errors.New("canceling upgrade request")
}
}

View File

@ -1,37 +0,0 @@
package plugin
import (
"context"
"io"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"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)
golden.Assert(t, cli.OutBuffer().String(), "plugin-upgrade-terminate.golden")
}

View File

@ -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.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the password from stdin")
flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
return cmd
}

View File

@ -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.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.StringVarP(&options.format, "format", "", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd

View File

@ -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

View File

@ -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()

View File

@ -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: fmt.Sprintf("Service %s converged", serviceID),
Action: "Service converged",
})
if message != nil {
progressOut.WriteProgress(*message)

View File

@ -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)
}

Some files were not shown because too many files have changed in this diff Show More