mirror of
https://github.com/docker/cli.git
synced 2026-08-25 10:05:26 -05:00
Compare commits
@@ -1,11 +1,17 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "Contributing to Docker"
|
||||
- name: "🗃️ Docker Sandboxes: report an issue"
|
||||
about: "Issues with Docker Sandboxes should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
||||
url: "https://github.com/docker/desktop-feedback/issues"
|
||||
- name: "🖥️ Docker Desktop: report an issue"
|
||||
about: "General Docker Desktop issues (installation, upgrades, UI, networking on macOS/Windows, WSL2, etc.) should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
||||
url: "https://github.com/docker/desktop-feedback/issues"
|
||||
- name: "🧑💻 Contributing to Docker"
|
||||
about: "Read guidelines and tips about contributing to Docker."
|
||||
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
|
||||
- name: "Security and Vulnerabilities"
|
||||
- name: "🔒 Security and Vulnerabilities"
|
||||
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
|
||||
url: "https://github.com/moby/moby/security/policy"
|
||||
- name: "General Support"
|
||||
- name: "💬 General Support"
|
||||
about: "Get the help you need to build, share, and run your Docker applications"
|
||||
url: "https://www.docker.com/support/"
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
fi
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -101,16 +101,16 @@ jobs:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.4"
|
||||
go-version: "1.25.7"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
|
||||
@@ -37,14 +37,14 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- 29-rc # latest rc
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
- 25 # mirantis lts
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@v6
|
||||
@@ -60,18 +60,21 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.25.4"
|
||||
go-version: "1.25.7"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
||||
# run in go modules mode to prevent traversing to nested modules
|
||||
ln -s vendor.mod go.mod
|
||||
ln -s vendor.sum go.sum
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '^github.com/docker/cli/e2e/')
|
||||
go tool cover -func=/tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
shell: bash
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Generate
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
|
||||
+3
-1
@@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.25.4"
|
||||
go: "1.25.7"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@@ -94,6 +94,8 @@ linters:
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
- pkg: "gopkg.in/yaml.v3"
|
||||
desc: Use go.yaml.in/yaml/v3 instead.
|
||||
|
||||
forbidigo:
|
||||
forbid:
|
||||
|
||||
+5
-5
@@ -5,14 +5,14 @@ ARG BASE_VARIANT=alpine
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.25.4
|
||||
ARG GO_VERSION=1.25.7
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.7.0
|
||||
ARG XX_VERSION=1.9.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.29.1
|
||||
ARG BUILDX_VERSION=0.31.1
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v2.40.0
|
||||
ARG COMPOSE_VERSION=v5.1.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestValidateCandidate(t *testing.T) {
|
||||
assert.ErrorContains(t, err, tc.err)
|
||||
case tc.invalid != "":
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeFor[*pluginError]()))
|
||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||
default:
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -57,7 +57,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Delegate completion to plugin
|
||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
|
||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things.
|
||||
cargs = append(cargs, args...)
|
||||
cargs = append(cargs, toComplete)
|
||||
os.Args = cargs
|
||||
|
||||
@@ -28,7 +28,7 @@ func (e *pluginError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// MarshalText marshalls the pluginError into a textual form.
|
||||
// MarshalText marshals the pluginError into a textual form.
|
||||
func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
return []byte(e.cause.Error()), nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
@@ -39,11 +42,11 @@ func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd,
|
||||
|
||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||||
// after a plugin command was just executed by the CLI.
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
|
||||
commandName := strings.Join(args, " ")
|
||||
flags := getNaiveFlags(args)
|
||||
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "")
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
@@ -67,7 +70,7 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr)
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
@@ -135,14 +138,34 @@ func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
// 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 == "" {
|
||||
//
|
||||
// Plugins can declare two types of hooks in their configuration:
|
||||
// - "hooks": fires on every command invocation (success or failure)
|
||||
// - "error-hooks": fires only when a command fails (cmdErrorMessage is non-empty)
|
||||
func pluginMatch(pluginCfg map[string]string, subCmd string, cmdErrorMessage string) (string, bool) {
|
||||
// Check "hooks" first — these always fire regardless of command outcome.
|
||||
if match, ok := matchHookConfig(pluginCfg["hooks"], subCmd); ok {
|
||||
return match, true
|
||||
}
|
||||
|
||||
// Check "error-hooks" — these only fire when there was an error.
|
||||
if cmdErrorMessage != "" {
|
||||
if match, ok := matchHookConfig(pluginCfg["error-hooks"], subCmd); ok {
|
||||
return match, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// matchHookConfig checks if a comma-separated hook configuration string
|
||||
// contains a prefix match for the given subcommand.
|
||||
func matchHookConfig(configuredHooks string, subCmd string) (string, bool) {
|
||||
if configuredHooks == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
commands := strings.Split(configuredPluginHooks, ",")
|
||||
for _, hookCmd := range commands {
|
||||
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
|
||||
if hookMatch(hookCmd, subCmd) {
|
||||
return hookCmd, true
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type fakeConfigProvider struct {
|
||||
cfg *configfile.ConfigFile
|
||||
}
|
||||
|
||||
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
|
||||
return f.cfg
|
||||
}
|
||||
|
||||
func TestGetNaiveFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
@@ -40,12 +51,15 @@ func TestGetNaiveFlags(t *testing.T) {
|
||||
|
||||
func TestPluginMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
commandString string
|
||||
pluginConfig map[string]string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
doc string
|
||||
commandString string
|
||||
pluginConfig map[string]string
|
||||
cmdErrorMessage string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
}{
|
||||
{
|
||||
doc: "hooks prefix match",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
@@ -54,6 +68,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks no match",
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
@@ -62,6 +77,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks exact match",
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "context ls",
|
||||
@@ -70,6 +86,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks first match wins",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image ls,image",
|
||||
@@ -78,6 +95,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks empty string",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "",
|
||||
@@ -86,6 +104,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks partial token no match",
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image i",
|
||||
@@ -94,6 +113,7 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks prefix token match",
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
@@ -101,12 +121,140 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks match on error",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks no match on success",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks prefix match on error",
|
||||
commandString: "compose up",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "compose",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "compose",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks no match for wrong command",
|
||||
commandString: "pull",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks takes precedence over error-hooks",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks fires on success even with error-hooks configured",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks with multiple commands",
|
||||
commandString: "compose up",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build,compose up,pull",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "compose up",
|
||||
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)
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchHookConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
configuredHooks string
|
||||
subCmd string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
}{
|
||||
{
|
||||
doc: "empty config",
|
||||
configuredHooks: "",
|
||||
subCmd: "build",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "exact match",
|
||||
configuredHooks: "build",
|
||||
subCmd: "build",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "prefix match",
|
||||
configuredHooks: "image",
|
||||
subCmd: "image ls",
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "comma-separated match",
|
||||
configuredHooks: "pull,build,push",
|
||||
subCmd: "build",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "no match",
|
||||
configuredHooks: "pull,push",
|
||||
subCmd: "build",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
match, ok := matchHookConfig(tc.configuredHooks, tc.subCmd)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,3 +289,87 @@ func TestAppendNextSteps(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"hooks": "build"},
|
||||
}
|
||||
provider := &fakeConfigProvider{cfg: cfg}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// Should not panic with empty error message (success case)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
||||
|
||||
// Should not panic with non-empty error message (failure case)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
||||
}
|
||||
|
||||
func TestRunPluginHooksErrorHooks(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"error-hooks": "build"},
|
||||
}
|
||||
provider := &fakeConfigProvider{cfg: cfg}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// Should not panic — error-hooks with error message
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
||||
|
||||
// Should not panic — error-hooks with no error (should be skipped)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"nonexistent": {"error-hooks": "build"},
|
||||
}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"hooks": "build"},
|
||||
}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root, sub,
|
||||
"build", map[string]string{}, "exit status 1",
|
||||
)
|
||||
assert.Check(t, is.Nil(result))
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||
}
|
||||
|
||||
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conflicts.
|
||||
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
||||
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
||||
fs.WithDir("plugins",
|
||||
|
||||
@@ -16,6 +16,5 @@ import (
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
var defaultSystemPluginDirs = []string{
|
||||
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func (pl *PluginServer) Addr() net.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.
|
||||
// The error value is that of the underlying [net.Listener.Close] call.
|
||||
func (pl *PluginServer) Close() error {
|
||||
if pl == nil {
|
||||
return nil
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestPluginServer(t *testing.T) {
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("exepcted EOF error, got: %v", err)
|
||||
t.Fatalf("expected EOF error, got: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
|
||||
+2
-3
@@ -60,6 +60,7 @@ type Cli interface {
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
options *cliflags.ClientOptions
|
||||
clientOpts []client.Opt
|
||||
in *streams.In
|
||||
out *streams.Out
|
||||
err *streams.Out
|
||||
@@ -72,7 +73,6 @@ type DockerCli struct {
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig *store.Config
|
||||
initTimeout time.Duration
|
||||
userAgent string
|
||||
res telemetryResource
|
||||
|
||||
// baseCtx is the base context used for internal operations. In the future
|
||||
@@ -533,8 +533,7 @@ func (cli *DockerCli) initialize() error {
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,16 @@ func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error))
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIClientOptions configures additional [client.Opt] to use when
|
||||
// initializing the API client. These options have no effect if a custom
|
||||
// client is set (through [WithAPIClient] or [WithInitializeClient]).
|
||||
func WithAPIClientOptions(c ...client.Opt) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.clientOpts = append(cli.clientOpts, c...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders is the name of the environment-variable that can be
|
||||
// used to set custom HTTP headers to be sent by the client. This environment
|
||||
// variable is the equivalent to the HttpHeaders field in the configuration
|
||||
@@ -221,7 +231,7 @@ func WithUserAgent(userAgent string) CLIOption {
|
||||
if userAgent == "" {
|
||||
return errors.New("user agent cannot be blank")
|
||||
}
|
||||
cli.userAgent = userAgent
|
||||
cli.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
+26
-2
@@ -120,8 +120,8 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
const customVersion = "v3.3.3"
|
||||
const expectedVersion = "3.3.3"
|
||||
const customVersion = "v3.3"
|
||||
const expectedVersion = "3.3"
|
||||
t.Setenv("DOCKER_API_VERSION", customVersion)
|
||||
t.Setenv("DOCKER_HOST", ":2375")
|
||||
|
||||
@@ -358,6 +358,7 @@ func TestSetGoDebug(t *testing.T) {
|
||||
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
|
||||
})
|
||||
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
|
||||
t.Setenv("GODEBUG", "")
|
||||
meta := store.Metadata{
|
||||
Metadata: DockerContext{
|
||||
AdditionalFields: map[string]any{
|
||||
@@ -392,3 +393,26 @@ func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
|
||||
var received string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.UserAgent()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithAPIClientOptions(client.WithUserAgent("fake-agent/0.0.1")),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
cli.currentContext = DefaultContextName
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(t.Context(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
@@ -70,6 +70,14 @@ 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 {
|
||||
detachKeys := opts.DetachKeys
|
||||
if detachKeys == "" {
|
||||
detachKeys = dockerCLI.ConfigFile().DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(detachKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
@@ -85,11 +93,6 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
return err
|
||||
}
|
||||
|
||||
detachKeys := dockerCLI.ConfigFile().DetachKeys
|
||||
if opts.DetachKeys != "" {
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := client.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
||||
|
||||
@@ -27,6 +27,14 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-detach-keys",
|
||||
args: []string{"--detach-keys", "shift-b", "5cb5bb5e4a3b"},
|
||||
expectedError: "invalid detach keys (shift-b):",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
|
||||
@@ -232,3 +232,7 @@ func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, opt
|
||||
|
||||
return client.ContainerPauseResult{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) Ping(_ context.Context, _ client.PingOptions) (client.PingResult, error) {
|
||||
return client.PingResult{}, nil
|
||||
}
|
||||
|
||||
@@ -95,14 +95,14 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
if err := validatePullOpt(options.pull); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
proxyConfig := dockerCLI.ConfigFile().ParseProxyConfig(dockerCLI.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
newEnv := make([]string, 0, len(proxyConfig))
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
@@ -112,24 +112,28 @@ func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet,
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
serverInfo, err := dockerCLI.Client().Ping(ctx, client.PingOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerCfg, err := parse(flags, copts, serverInfo.OSType)
|
||||
if err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
||||
id, err := createContainer(ctx, dockerCLI, containerCfg, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), id)
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
|
||||
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,7 +143,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
// Already validated.
|
||||
ociPlatforms = append(ociPlatforms, platforms.MustParse(options.platform))
|
||||
}
|
||||
resp, err := dockerCli.Client().ImagePull(ctx, img, client.ImagePullOptions{
|
||||
resp, err := dockerCLI.Client().ImagePull(ctx, img, client.ImagePullOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platforms: ociPlatforms,
|
||||
})
|
||||
@@ -150,7 +154,7 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
|
||||
_ = resp.Close()
|
||||
}()
|
||||
|
||||
out := dockerCli.Err()
|
||||
out := dockerCLI.Err()
|
||||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
}
|
||||
@@ -172,8 +176,8 @@ func (cid *cidFile) Close() error {
|
||||
if cid.written {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(cid.path); err != nil {
|
||||
return fmt.Errorf("failed to remove the CID file '%s': %w", cid.path, err)
|
||||
if err := os.Remove(cid.path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to remove the CID file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -183,8 +187,8 @@ func (cid *cidFile) Write(id string) error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return fmt.Errorf("failed to write the container ID to the file: %w", err)
|
||||
if _, err := cid.file.WriteString(id); err != nil {
|
||||
return fmt.Errorf("failed to write the container ID (%s) to file: %w", id, err)
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
@@ -207,7 +211,7 @@ func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
|
||||
func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, _ error) {
|
||||
config := containerCfg.Config
|
||||
hostConfig := containerCfg.HostConfig
|
||||
networkingConfig := containerCfg.NetworkingConfig
|
||||
@@ -216,8 +220,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
|
||||
// TODO(thaJeztah): add a platform option-type / flag-type.
|
||||
if options.platform != "" {
|
||||
_, err = platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
if _, err := platforms.Parse(options.platform); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -243,10 +246,11 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
|
||||
if options.useAPISocket {
|
||||
// We'll create two new mounts to handle this flag:
|
||||
//
|
||||
// 1. Mount the actual docker socket.
|
||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
||||
// 2. A synthesized ~/.docker/config.json with resolved tokens.
|
||||
|
||||
if dockerCli.ServerInfo().OSType == "windows" {
|
||||
if dockerCLI.ServerInfo().OSType == "windows" {
|
||||
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
|
||||
}
|
||||
|
||||
@@ -281,18 +285,18 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
})
|
||||
*/
|
||||
|
||||
var envvarPresent bool
|
||||
for _, envvar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
|
||||
envvarPresent = true
|
||||
var envVarPresent bool
|
||||
for _, envVar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envVar, "DOCKER_CONFIG=") {
|
||||
envVarPresent = true
|
||||
}
|
||||
}
|
||||
|
||||
// If the DOCKER_CONFIG env var is already present, we assume the client knows
|
||||
// what they're doing and don't inject the creds.
|
||||
if !envvarPresent {
|
||||
if !envVarPresent {
|
||||
// Resolve this here for later, ensuring we error our before we create the container.
|
||||
creds, err := readCredentials(dockerCli)
|
||||
creds, err := readCredentials(dockerCLI)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving credentials failed: %w", err)
|
||||
}
|
||||
@@ -314,22 +318,15 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
platform = &p
|
||||
}
|
||||
|
||||
pullAndTagImage := func() error {
|
||||
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.pull == PullImageAlways {
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
if err := pullImage(ctx, dockerCLI, config.Image, options); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCLI.Out().GetTtySize()
|
||||
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
response, err := dockerCLI.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
@@ -342,15 +339,15 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||
if !options.quiet {
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
_, _ = fmt.Fprintf(dockerCLI.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
}
|
||||
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
if err := pullImage(ctx, dockerCLI, config.Image, options); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var retryErr error
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
response, retryErr = dockerCLI.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
@@ -366,24 +363,20 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
}
|
||||
}
|
||||
|
||||
containerID = response.ID
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
}
|
||||
err = containerIDFile.Write(containerID)
|
||||
|
||||
if options.useAPISocket && len(apiSocketCreds) > 0 {
|
||||
// Create a new config file with just the auth.
|
||||
newConfig := &configfile.ConfigFile{
|
||||
if err := copyDockerConfigIntoContainer(ctx, dockerCLI.Client(), response.ID, dockerConfigPathInContainer, &configfile.ConfigFile{
|
||||
AuthConfigs: apiSocketCreds,
|
||||
}
|
||||
|
||||
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
|
||||
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
|
||||
}); err != nil {
|
||||
response.Warnings = append(response.Warnings, fmt.Sprintf("injecting docker config.json into container failed: %v", err))
|
||||
}
|
||||
}
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING:", w)
|
||||
}
|
||||
|
||||
return containerID, err
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return response.ID, err
|
||||
}
|
||||
|
||||
func validatePullOpt(val string) error {
|
||||
@@ -423,6 +416,7 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
|
||||
})
|
||||
|
||||
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
|
||||
_ = tarWriter.Close()
|
||||
return fmt.Errorf("writing config to tar file for config copy: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -42,17 +43,31 @@ func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithNoWrite(t *testing.T) {
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
// Closing should remove the file if it was not written to.
|
||||
t.Run("closing should remove file", func(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "cidfile-1")
|
||||
file, err := newCIDFile(filename)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, filename))
|
||||
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, path))
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(filename)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
})
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
// Closing (and removing) the file should not produce an error if the file no longer exists.
|
||||
t.Run("close should remove file", func(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "cidfile-2")
|
||||
file, err := newCIDFile(filename)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, filename))
|
||||
|
||||
assert.NilError(t, os.Remove(filename))
|
||||
_, err = os.Stat(filename)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
})
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithWrite(t *testing.T) {
|
||||
|
||||
@@ -248,5 +248,8 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client
|
||||
} else {
|
||||
execOptions.DetachKeys = configFile.DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(execOpts.DetachKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return execOptions, nil
|
||||
}
|
||||
|
||||
@@ -92,21 +92,21 @@ TWO=2
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
DetachKeys: "ctrl-d,e",
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Detach: true,
|
||||
DetachKeys: "ab",
|
||||
DetachKeys: "ctrl-a,b",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
|
||||
expected: client.ExecCreateOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
DetachKeys: "ctrl-a,b",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -147,13 +147,23 @@ TWO=2
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExecNoSuchFile(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
assert.Check(t, execConfig == nil)
|
||||
func TestParseExecErrors(t *testing.T) {
|
||||
t.Run("missing env-file", func(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
assert.Check(t, execConfig == nil)
|
||||
})
|
||||
t.Run("invalid detach keys", func(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{
|
||||
DetachKeys: "shift-a",
|
||||
})
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.Check(t, is.ErrorContains(err, "invalid detach keys (shift-a):"))
|
||||
assert.Check(t, is.Nil(execConfig))
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunExec(t *testing.T) {
|
||||
|
||||
@@ -30,6 +30,16 @@ func (r *readCloserWrapper) Close() error {
|
||||
return r.closer()
|
||||
}
|
||||
|
||||
func validateDetachKeys(keys string) error {
|
||||
if keys == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := term.ToBytes(keys); err != nil {
|
||||
return invalidParameter(fmt.Errorf("invalid detach keys (%s): %w", keys, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A hijackedIOStreamer handles copying input to and output from streams to the
|
||||
// connection.
|
||||
type hijackedIOStreamer struct {
|
||||
@@ -82,13 +92,15 @@ func (h *hijackedIOStreamer) stream(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
func (h *hijackedIOStreamer) setupInput() (restore func(), _ error) {
|
||||
if h.inputStream == nil || !h.tty {
|
||||
// No need to setup input TTY.
|
||||
// The restore func is a nop.
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
if err := validateDetachKeys(h.detachKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setRawTerminal(h.streams); err != nil {
|
||||
return nil, fmt.Errorf("unable to set IO streams as raw terminal: %s", err)
|
||||
}
|
||||
@@ -103,11 +115,11 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
// Use default escape keys if an invalid sequence is given.
|
||||
escapeKeys := defaultEscapeKeys
|
||||
if h.detachKeys != "" {
|
||||
customEscapeKeys, err := term.ToBytes(h.detachKeys)
|
||||
var err error
|
||||
escapeKeys, err = term.ToBytes(h.detachKeys)
|
||||
if err != nil {
|
||||
logrus.Warnf("invalid detach escape keys, using default: %s", err)
|
||||
} else {
|
||||
escapeKeys = customEscapeKeys
|
||||
restore()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -426,6 +426,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// short syntax ([ip:]public:private[/proto])
|
||||
//
|
||||
// TODO(thaJeztah): we need an equivalent that handles the "ip-address" part without depending on the nat package.
|
||||
ports, natPortBindings, err := nat.ParsePortSpecs(convertedOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -792,7 +795,7 @@ func parseNetworkOpts(copts *containerOptions) (map[string]*network.EndpointSett
|
||||
// and only a single network is specified, omit the endpoint-configuration
|
||||
// on the client (the daemon will still create it when creating the container)
|
||||
if i == 0 && len(copts.netMode.Value()) == 1 {
|
||||
if ep == nil || reflect.DeepEqual(*ep, network.EndpointSettings{}) {
|
||||
if ep == nil || reflect.ValueOf(*ep).IsZero() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -899,7 +902,7 @@ func convertToStandardNotation(ports []string) ([]string, error) {
|
||||
for _, publish := range ports {
|
||||
if strings.Contains(publish, "=") {
|
||||
params := map[string]string{"protocol": "tcp"}
|
||||
for _, param := range strings.Split(publish, ",") {
|
||||
for param := range strings.SplitSeq(publish, ",") {
|
||||
k, v, ok := strings.Cut(param, "=")
|
||||
if !ok || k == "" {
|
||||
return optsList, fmt.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
|
||||
@@ -944,11 +947,11 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) {
|
||||
if err != nil {
|
||||
return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %w", v, err)
|
||||
}
|
||||
b := bytes.NewBuffer(nil)
|
||||
if err := json.Compact(b, f); err != nil {
|
||||
var b bytes.Buffer
|
||||
if err := json.Compact(&b, f); err != nil {
|
||||
return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %w", v, err)
|
||||
}
|
||||
securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes())
|
||||
securityOpts[key] = "seccomp=" + b.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -994,9 +997,11 @@ func parseDevice(device, serverOS string) (container.DeviceMapping, error) {
|
||||
case "linux":
|
||||
return parseLinuxDevice(device)
|
||||
case "windows":
|
||||
return parseWindowsDevice(device)
|
||||
// Windows doesn't support mapping, so passing the given value as-is.
|
||||
return container.DeviceMapping{PathOnHost: device}, nil
|
||||
default:
|
||||
return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
return container.DeviceMapping{}, fmt.Errorf("unknown server OS: %s", serverOS)
|
||||
}
|
||||
|
||||
// parseLinuxDevice parses a device mapping string to a container.DeviceMapping struct
|
||||
@@ -1027,18 +1032,11 @@ func parseLinuxDevice(device string) (container.DeviceMapping, error) {
|
||||
dst = src
|
||||
}
|
||||
|
||||
deviceMapping := container.DeviceMapping{
|
||||
return container.DeviceMapping{
|
||||
PathOnHost: src,
|
||||
PathInContainer: dst,
|
||||
CgroupPermissions: permissions,
|
||||
}
|
||||
return deviceMapping, nil
|
||||
}
|
||||
|
||||
// parseWindowsDevice parses a device mapping string to a container.DeviceMapping struct
|
||||
// knowing that the target is a Windows daemon
|
||||
func parseWindowsDevice(device string) (container.DeviceMapping, error) {
|
||||
return container.DeviceMapping{PathOnHost: device}, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateDeviceCgroupRule validates a device cgroup rule string format
|
||||
|
||||
@@ -84,14 +84,14 @@ func newRunCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
||||
func runRun(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
||||
if err := validatePullOpt(ropts.pull); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "run").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
proxyConfig := dockerCLI.ConfigFile().ParseProxyConfig(dockerCLI.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
@@ -101,7 +101,12 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
serverInfo, err := dockerCLI.Client().Ping(ctx, client.PingOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerCfg, err := parse(flags, copts, serverInfo.OSType)
|
||||
// just in case the parse does not exit
|
||||
if err != nil {
|
||||
return cli.StatusError{
|
||||
@@ -109,7 +114,7 @@ func runRun(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, ro
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
return runContainer(ctx, dockerCli, ropts, copts, containerCfg)
|
||||
return runContainer(ctx, dockerCLI, ropts, copts, containerCfg)
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
@@ -135,6 +140,14 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
config.StdinOnce = false
|
||||
}
|
||||
|
||||
detachKeys := runOpts.detachKeys
|
||||
if detachKeys == "" {
|
||||
detachKeys = dockerCli.ConfigFile().DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(runOpts.detachKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerID, err := createContainer(ctx, dockerCli, containerCfg, &runOpts.createOptions)
|
||||
if err != nil {
|
||||
return toStatusError(err)
|
||||
@@ -167,11 +180,6 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
}()
|
||||
}
|
||||
if attach {
|
||||
detachKeys := dockerCli.ConfigFile().DetachKeys
|
||||
if runOpts.detachKeys != "" {
|
||||
detachKeys = runOpts.detachKeys
|
||||
}
|
||||
|
||||
// ctx should not be cancellable here, as this would kill the stream to the container
|
||||
// and we want to keep the stream open until the process in the container exits or until
|
||||
// the user forcefully terminates the CLI.
|
||||
|
||||
@@ -16,8 +16,6 @@ import (
|
||||
"github.com/moby/moby/api/types"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/moby/client/pkg/progress"
|
||||
"github.com/moby/moby/client/pkg/streamformatter"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -34,6 +32,11 @@ func TestRunValidateFlags(t *testing.T) {
|
||||
args: []string{"--attach", "stdin", "--detach", "myimage"},
|
||||
expectedErr: "conflicting options: cannot specify both --attach and --detach",
|
||||
},
|
||||
{
|
||||
name: "with invalid --detach-keys",
|
||||
args: []string{"--detach-keys", "shift-a", "myimage"},
|
||||
expectedErr: "invalid detach keys (shift-a):",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newRunCommand(test.NewFakeCli(&fakeClient{}))
|
||||
@@ -124,7 +127,7 @@ func TestRunAttach(t *testing.T) {
|
||||
}
|
||||
|
||||
// end stream from "container" so that we'll detach
|
||||
conn.Close()
|
||||
assert.NilError(t, conn.Close())
|
||||
|
||||
select {
|
||||
case cmdErr := <-cmdErrC:
|
||||
@@ -204,7 +207,7 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM))
|
||||
conn.Close()
|
||||
assert.NilError(t, conn.Close())
|
||||
|
||||
select {
|
||||
case <-killCh:
|
||||
@@ -240,20 +243,12 @@ func TestRunPullTermination(t *testing.T) {
|
||||
_ = server.Close()
|
||||
})
|
||||
go func() {
|
||||
id := test.RandomID()[:12] // short-ID
|
||||
progressOutput := streamformatter.NewJSONProgressOutput(server, true)
|
||||
for i := 0; i < 100; i++ {
|
||||
for range 100 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
assert.NilError(t, server.Close(), "failed to close imageCreateFunc server")
|
||||
return
|
||||
default:
|
||||
assert.NilError(t, progressOutput.WriteProgress(progress.Progress{
|
||||
ID: id,
|
||||
Message: "Downloading",
|
||||
Current: int64(i),
|
||||
Total: 100,
|
||||
}))
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func TestForwardSignals(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
called := make(chan struct{})
|
||||
apiClient := &fakeClient{containerKillFunc: func(ctx context.Context, container string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
|
||||
@@ -70,6 +70,14 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
ctx, cancelFun := context.WithCancel(ctx)
|
||||
defer cancelFun()
|
||||
|
||||
detachKeys := opts.DetachKeys
|
||||
if detachKeys == "" {
|
||||
detachKeys = dockerCli.ConfigFile().DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(detachKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case opts.Attach || opts.OpenStdin:
|
||||
// We're going to attach to a container.
|
||||
@@ -93,11 +101,6 @@ func RunStart(ctx context.Context, dockerCli command.Cli, opts *StartOptions) er
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
detachKeys := dockerCli.ConfigFile().DetachKeys
|
||||
if opts.DetachKeys != "" {
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := client.ContainerAttachOptions{
|
||||
Stream: true,
|
||||
Stdin: opts.OpenStdin && c.Container.Config.OpenStdin,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestStartValidateFlags(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "with invalid --detach-keys",
|
||||
args: []string{"--detach-keys", "shift-a", "myimage"},
|
||||
expectedErr: "invalid detach keys (shift-a):",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := newStartCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
|
||||
err := cmd.Execute()
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
|
||||
} else {
|
||||
assert.Check(t, is.Nil(err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
@@ -337,7 +340,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(statsTextBuffer.String(), "\n") {
|
||||
for line := range strings.SplitSeq(statsTextBuffer.String(), "\n") {
|
||||
// In case the new text is shorter than the one we are writing over,
|
||||
// we'll append the "erase line" escape sequence to clear the remaining text.
|
||||
_, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K")
|
||||
|
||||
@@ -60,7 +60,7 @@ func initTtySize(ctx context.Context, cli command.Cli, id string, isExec bool, r
|
||||
if err := rTTYfunc(ctx, cli, id, isExec); err != nil {
|
||||
go func() {
|
||||
var err error
|
||||
for retry := 0; retry < 10; retry++ {
|
||||
for retry := range 10 {
|
||||
time.Sleep(time.Duration(retry+1) * 10 * time.Millisecond)
|
||||
if err = rTTYfunc(ctx, cli, id, isExec); err == nil {
|
||||
break
|
||||
|
||||
@@ -6,6 +6,7 @@ package command
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"maps"
|
||||
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
)
|
||||
@@ -23,9 +24,7 @@ func (dc DockerContext) MarshalJSON() ([]byte, error) {
|
||||
s["Description"] = dc.Description
|
||||
}
|
||||
if dc.AdditionalFields != nil {
|
||||
for k, v := range dc.AdditionalFields {
|
||||
s[k] = v
|
||||
}
|
||||
maps.Copy(s, dc.AdditionalFields)
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
@@ -350,7 +350,7 @@ func DisplayablePorts(ports []container.PortSummary) string {
|
||||
last uint16
|
||||
}
|
||||
groupMap := make(map[string]*portGroup)
|
||||
var result []string //nolint:prealloc
|
||||
var result []string
|
||||
var hostMappings []string
|
||||
var groupMapKeys []string
|
||||
sort.Slice(ports, func(i, j int) bool {
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type dummy struct{}
|
||||
@@ -45,24 +47,18 @@ var dummyExpected = map[string]any{
|
||||
func TestMarshalMap(t *testing.T) {
|
||||
d := dummy{}
|
||||
m, err := marshalMap(&d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(dummyExpected, m) {
|
||||
t.Fatalf("expected %+v, got %+v",
|
||||
dummyExpected, m)
|
||||
}
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(m, dummyExpected))
|
||||
}
|
||||
|
||||
func TestMarshalMapBad(t *testing.T) {
|
||||
if _, err := marshalMap(nil); err == nil {
|
||||
t.Fatal("expected an error (argument is nil)")
|
||||
}
|
||||
if _, err := marshalMap(dummy{}); err == nil {
|
||||
t.Fatal("expected an error (argument is non-pointer)")
|
||||
}
|
||||
_, err := marshalMap(nil)
|
||||
assert.Check(t, is.Error(err, "expected a pointer to a struct, got invalid"), "expected an error (argument is nil)")
|
||||
|
||||
_, err = marshalMap(dummy{})
|
||||
assert.Check(t, is.Error(err, "expected a pointer to a struct, got struct"), "expected an error (argument is non-pointer)")
|
||||
|
||||
x := 42
|
||||
if _, err := marshalMap(&x); err == nil {
|
||||
t.Fatal("expected an error (argument is a pointer to non-struct)")
|
||||
}
|
||||
_, err = marshalMap(&x)
|
||||
assert.Check(t, is.Error(err, "expected a pointer to a struct, got a pointer to int"), "expected an error (argument is a pointer to non-struct)")
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (b *buffer) Write(buf []byte) (written int, err error) {
|
||||
m := len(buf)
|
||||
if n+m <= cap(b.a) {
|
||||
b.a = b.a[0 : n+m]
|
||||
for i := 0; i < m; i++ {
|
||||
for i := range m {
|
||||
b.a[n+i] = buf[i]
|
||||
}
|
||||
} else {
|
||||
@@ -669,7 +669,7 @@ func BenchmarkTable(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings
|
||||
// Write the line h times.
|
||||
for j := 0; j < h; j++ {
|
||||
for range h {
|
||||
w.Write(line)
|
||||
}
|
||||
w.Flush()
|
||||
@@ -681,7 +681,7 @@ func BenchmarkTable(b *testing.B) {
|
||||
w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Write the line h times.
|
||||
for j := 0; j < h; j++ {
|
||||
for range h {
|
||||
w.Write(line)
|
||||
}
|
||||
w.Flush()
|
||||
@@ -701,7 +701,7 @@ func BenchmarkPyramid(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings
|
||||
// Write increasing prefixes of that line.
|
||||
for j := 0; j < x; j++ {
|
||||
for j := range x {
|
||||
w.Write(line[:j*2])
|
||||
w.Write([]byte{'\n'})
|
||||
}
|
||||
@@ -723,7 +723,7 @@ func BenchmarkRagged(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings
|
||||
// Write the lines in turn h times.
|
||||
for j := 0; j < h; j++ {
|
||||
for j := range h {
|
||||
w.Write(lines[j%len(lines)])
|
||||
w.Write([]byte{'\n'})
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestResolveWithCache(t *testing.T) {
|
||||
idResolver := New(apiClient, false)
|
||||
|
||||
ctx := context.Background()
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
id, err := idResolver.Resolve(ctx, swarm.Node{}, "nodeID")
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("node-foo", id))
|
||||
|
||||
@@ -480,7 +480,7 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
compressWriter, err := compression.CompressStream(pipeWriter, archive.Gzip)
|
||||
compressWriter, err := compression.CompressStream(pipeWriter, compression.Gzip)
|
||||
if err != nil {
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func (f *fakeBuild) build(_ context.Context, buildContext io.Reader, options cli
|
||||
|
||||
func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
|
||||
t.Helper()
|
||||
headers := []*tar.Header{}
|
||||
var headers []*tar.Header
|
||||
for {
|
||||
hdr, err := f.context.Next()
|
||||
switch err {
|
||||
@@ -206,8 +206,9 @@ func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
|
||||
|
||||
func (f *fakeBuild) filenames(t *testing.T) []string {
|
||||
t.Helper()
|
||||
names := []string{}
|
||||
for _, header := range f.headers(t) {
|
||||
h := f.headers(t)
|
||||
names := make([]string, 0, len(h))
|
||||
for _, header := range h {
|
||||
names = append(names, header.Name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
+13
-13
@@ -65,7 +65,7 @@ func newImagesCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only show image IDs")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Show all images (default hides intermediate images)")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Show all images (default hides intermediate and dangling images)")
|
||||
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
|
||||
flags.BoolVar(&options.showDigests, "digests", false, "Show digests")
|
||||
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
|
||||
@@ -109,29 +109,29 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
|
||||
|
||||
images := res.Items
|
||||
if !options.all {
|
||||
if _, ok := filters["dangling"]; !ok {
|
||||
if dangling, ok := filters["dangling"]; !ok || dangling["false"] {
|
||||
images = slices.DeleteFunc(images, isDangling)
|
||||
}
|
||||
}
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet && !options.tree {
|
||||
format = dockerCLI.ConfigFile().ImagesFormat
|
||||
useTree = false
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
if useTree {
|
||||
return runTree(ctx, dockerCLI, treeOptions{
|
||||
images: images,
|
||||
all: options.all,
|
||||
filters: filters,
|
||||
expanded: options.tree,
|
||||
})
|
||||
}
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
if len(dockerCLI.ConfigFile().ImagesFormat) > 0 && !options.quiet {
|
||||
format = dockerCLI.ConfigFile().ImagesFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
imageCtx := formatter.ImageContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
@@ -176,7 +176,7 @@ func shouldUseTree(options imagesOptions) (bool, error) {
|
||||
|
||||
// isDangling is a copy of [formatter.isDangling].
|
||||
func isDangling(img image.Summary) bool {
|
||||
if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
|
||||
if len(img.RepoTags) == 0 {
|
||||
return true
|
||||
}
|
||||
return len(img.RepoTags) == 1 && img.RepoTags[0] == "<none>:<none>" && len(img.RepoDigests) == 1 && img.RepoDigests[0] == "<none>@<none>"
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
@@ -116,6 +118,86 @@ func TestNewListCommandAmbiguous(t *testing.T) {
|
||||
golden.Assert(t, cli.ErrBuffer().String(), "list-command-ambiguous.golden")
|
||||
}
|
||||
|
||||
func TestImagesFilterDangling(t *testing.T) {
|
||||
// Create test images with different states
|
||||
items := []image.Summary{
|
||||
{
|
||||
ID: "sha256:87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7",
|
||||
RepoTags: []string{"myimage:latest"},
|
||||
RepoDigests: []string{"myimage@sha256:abc123"},
|
||||
},
|
||||
{
|
||||
ID: "sha256:0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f",
|
||||
RepoTags: []string{},
|
||||
RepoDigests: []string{},
|
||||
},
|
||||
{
|
||||
ID: "sha256:a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478",
|
||||
RepoTags: []string{},
|
||||
RepoDigests: []string{"image@sha256:a3a5e715f0cc574a73c3f9bebb6bc24f32ffd5b67b387244c2c909da779a1478"},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
imageListFunc func(options client.ImageListOptions) (client.ImageListResult, error)
|
||||
}{
|
||||
{
|
||||
name: "dangling-true",
|
||||
args: []string{"-f", "dangling=true"},
|
||||
imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
// Verify the filter is passed to the API
|
||||
assert.Check(t, options.Filters["dangling"]["true"])
|
||||
// dangling=true is handled on the server side and returns only dangling images
|
||||
return client.ImageListResult{Items: []image.Summary{items[1], items[2]}}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dangling-false",
|
||||
args: []string{"-f", "dangling=false"},
|
||||
imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
// Verify the filter is passed to the API
|
||||
assert.Check(t, options.Filters["dangling"]["false"])
|
||||
// Return all images including dangling
|
||||
return client.ImageListResult{Items: slices.Clone(items)}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-dangling-filter",
|
||||
args: []string{},
|
||||
imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
// Verify no dangling filter is passed to the API
|
||||
_, exists := options.Filters["dangling"]
|
||||
assert.Check(t, !exists)
|
||||
// Return all images including dangling
|
||||
return client.ImageListResult{Items: slices.Clone(items)}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "all-flag",
|
||||
args: []string{"--all"},
|
||||
imageListFunc: func(options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
// Verify the All flag is set
|
||||
assert.Check(t, options.All)
|
||||
// Return all images including dangling
|
||||
return client.ImageListResult{Items: slices.Clone(items)}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
|
||||
cmd := newImagesCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-filter-dangling.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func nilToEmptySlice[T any](s []T) []T {
|
||||
if s == nil {
|
||||
return []T{}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
myimage:latest 87428fc52280 0B 0B
|
||||
<untagged> 0263829989b6 0B 0B
|
||||
<untagged> a3a5e715f0cc 0B 0B
|
||||
@@ -0,0 +1,2 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
myimage:latest 87428fc52280 0B 0B
|
||||
@@ -0,0 +1,3 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
<untagged> 0263829989b6 0B 0B
|
||||
<untagged> a3a5e715f0cc 0B 0B
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
myimage:latest 87428fc52280 0B 0B
|
||||
@@ -1,2 +1 @@
|
||||
Info -> U In Use
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
Info -> U In Use
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
Info -> U In Use
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
Info -> U In Use
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
multiplatform:latest aaaaaaaaaaaa 25.5 MB 20.2 MB U
|
||||
├─ linux/amd64 bbbbbbbbbbbb 12.1 MB 10.0 MB
|
||||
└─ linux/arm64 cccccccccccc 13.4 MB 10.2 MB U
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
app:v1
|
||||
app:latest 101010101010 30.5 MB 25.2 MB U
|
||||
└─ linux/amd64 202020202020 15.2 MB 12.6 MB U
|
||||
|
||||
<untagged> 303030303030 12.3 MB 10.1 MB
|
||||
└─ linux/arm/v7 404040404040 6.1 MB 5.0 MB
|
||||
|
||||
base:alpine 505050505050 5.5 MB 5.5 MB
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
<untagged> dddddddddddd 18.5 MB 15.2 MB
|
||||
├─ linux/amd64 eeeeeeeeeeee 9.2 MB 7.6 MB
|
||||
└─ linux/arm64 ffffffffffff 9.3 MB 7.6 MB
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
|
||||
a:1 111111111111 5.5 MB 2.5 MB
|
||||
<untagged> 222222222222 3.2 MB 1.6 MB
|
||||
short:v1 333333333333 7.1 MB 3.5 MB U
|
||||
+96
-51
@@ -22,9 +22,10 @@ import (
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
const untaggedName = "<untagged>"
|
||||
|
||||
type treeOptions struct {
|
||||
images []imagetypes.Summary
|
||||
all bool
|
||||
filters client.Filters
|
||||
expanded bool
|
||||
}
|
||||
@@ -111,6 +112,13 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) (int,
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sortedTags) == 0 {
|
||||
view.images = append(view.images, topImage{
|
||||
Details: topDetails,
|
||||
Children: children,
|
||||
created: img.Created,
|
||||
})
|
||||
}
|
||||
for _, tag := range sortedTags {
|
||||
view.images = append(view.images, topImage{
|
||||
Names: []string{tag},
|
||||
@@ -230,11 +238,11 @@ func printImageTree(outs command.Streams, view treeView) {
|
||||
}
|
||||
|
||||
out := tui.NewOutput(outs.Out())
|
||||
isTerm := out.IsTerminal()
|
||||
|
||||
_, width := out.GetTtySize()
|
||||
if width == 0 {
|
||||
width = 80
|
||||
}
|
||||
if width < 20 {
|
||||
limitWidth := width == 0
|
||||
if isTerm && width < 20 {
|
||||
width = 20
|
||||
}
|
||||
|
||||
@@ -243,16 +251,18 @@ func printImageTree(outs command.Streams, view treeView) {
|
||||
untaggedColor := out.Color(tui.ColorTertiary)
|
||||
titleColor := out.Color(tui.ColorTitle)
|
||||
|
||||
isTerm := out.IsTerminal()
|
||||
|
||||
out.Println(generateLegend(out, width))
|
||||
// Legend is right-aligned, so don't print it if the width is unlimited
|
||||
if !limitWidth {
|
||||
out.Println(generateLegend(out, width))
|
||||
}
|
||||
|
||||
possibleChips := getPossibleChips(view)
|
||||
columns := []imgColumn{
|
||||
{
|
||||
Title: "Image",
|
||||
Align: alignLeft,
|
||||
Width: 0,
|
||||
Title: "Image",
|
||||
Align: alignLeft,
|
||||
Width: 0,
|
||||
NoEllipsis: true,
|
||||
},
|
||||
{
|
||||
Title: "ID",
|
||||
@@ -284,7 +294,7 @@ func printImageTree(outs command.Streams, view treeView) {
|
||||
Width: func() int {
|
||||
maxChipsWidth := 0
|
||||
for _, chip := range possibleChips {
|
||||
s := chip.String(isTerm)
|
||||
s := out.Sprint(chip)
|
||||
l := tui.Width(s)
|
||||
maxChipsWidth += l
|
||||
}
|
||||
@@ -300,9 +310,9 @@ func printImageTree(outs command.Streams, view treeView) {
|
||||
var b strings.Builder
|
||||
for _, chip := range possibleChips {
|
||||
if chip.check(d) {
|
||||
b.WriteString(chip.String(isTerm))
|
||||
b.WriteString(out.Sprint(chip))
|
||||
} else {
|
||||
b.WriteString(chipPlaceholder.String(isTerm))
|
||||
b.WriteString(out.Sprint(chipPlaceholder))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
@@ -340,25 +350,27 @@ func printImageTree(outs command.Streams, view treeView) {
|
||||
// to display their content.
|
||||
func adjustColumns(width uint, columns []imgColumn, images []topImage) []imgColumn {
|
||||
nameWidth := int(width)
|
||||
for idx, h := range columns {
|
||||
if h.Width == 0 {
|
||||
continue
|
||||
if nameWidth > 0 {
|
||||
for idx, h := range columns {
|
||||
if h.Width == 0 {
|
||||
continue
|
||||
}
|
||||
d := h.Width
|
||||
if idx > 0 {
|
||||
d += columnSpacing
|
||||
}
|
||||
// If the first column gets too short, remove remaining columns
|
||||
if nameWidth-d < 12 {
|
||||
columns = columns[:idx]
|
||||
break
|
||||
}
|
||||
nameWidth -= d
|
||||
}
|
||||
d := h.Width
|
||||
if idx > 0 {
|
||||
d += columnSpacing
|
||||
}
|
||||
// If the first column gets too short, remove remaining columns
|
||||
if nameWidth-d < 12 {
|
||||
columns = columns[:idx]
|
||||
break
|
||||
}
|
||||
nameWidth -= d
|
||||
}
|
||||
|
||||
// Try to make the first column as narrow as possible
|
||||
widest := widestFirstColumnValue(columns, images)
|
||||
if nameWidth > widest {
|
||||
if width == 0 || nameWidth > widest {
|
||||
nameWidth = widest
|
||||
}
|
||||
columns[0].Width = nameWidth
|
||||
@@ -377,10 +389,7 @@ func generateLegend(out tui.Output, width uint) string {
|
||||
}
|
||||
legend += legendSb371.String()
|
||||
|
||||
r := int(width) - tui.Width(legend)
|
||||
if r < 0 {
|
||||
r = 0
|
||||
}
|
||||
r := max(int(width)-tui.Width(legend), 0)
|
||||
legend = strings.Repeat(" ", r) + legend
|
||||
return legend
|
||||
}
|
||||
@@ -422,17 +431,31 @@ func printChildren(out tui.Output, headers []imgColumn, img topImage, normalColo
|
||||
|
||||
func printNames(out tui.Output, headers []imgColumn, img topImage, color, untaggedColor aec.ANSI) {
|
||||
if len(img.Names) == 0 {
|
||||
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, "<untagged>"))
|
||||
_, _ = fmt.Fprint(out, headers[0].Print(untaggedColor, untaggedName))
|
||||
}
|
||||
|
||||
for nameIdx, name := range img.Names {
|
||||
// Don't limit first names to the column width because only the last
|
||||
// name will be printed alongside other columns.
|
||||
if nameIdx < len(img.Names)-1 {
|
||||
_, fullWidth := out.GetTtySize()
|
||||
_, _ = fmt.Fprintln(out, color.Apply(tui.Ellipsis(name, int(fullWidth))))
|
||||
} else {
|
||||
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
||||
nameWidth := tui.Width(name)
|
||||
lastName := nameIdx == len(img.Names)-1
|
||||
multiLine := nameWidth > headers[0].Width
|
||||
|
||||
_, _ = fmt.Fprint(out, headers[0].Print(color, name))
|
||||
|
||||
// Print each name on its own line, including the last,
|
||||
// unless the last name fits into the column.
|
||||
//
|
||||
// IMAGE ID ...
|
||||
// anImage 171e65262c80 ...
|
||||
// firstName
|
||||
// lastNameIsALongOne
|
||||
// eade5be814e8 ...
|
||||
// anotherLongName
|
||||
// bb747ca923a5 ...
|
||||
if !lastName || multiLine {
|
||||
_, _ = fmt.Fprintln(out)
|
||||
}
|
||||
if multiLine && lastName {
|
||||
_, _ = fmt.Fprint(out, strings.Repeat(" ", headers[0].Width))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -452,6 +475,7 @@ type imgColumn struct {
|
||||
|
||||
DetailsValue func(*imageDetails) string
|
||||
Color *aec.ANSI
|
||||
NoEllipsis bool
|
||||
}
|
||||
|
||||
func (h imgColumn) Print(clr aec.ANSI, s string) string {
|
||||
@@ -468,12 +492,16 @@ func (h imgColumn) Print(clr aec.ANSI, s string) string {
|
||||
func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
|
||||
ln := tui.Width(s)
|
||||
|
||||
if ln > h.Width {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
}
|
||||
|
||||
fill := h.Width - ln
|
||||
|
||||
if fill < 0 {
|
||||
if h.NoEllipsis {
|
||||
fill = 0
|
||||
} else {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
}
|
||||
}
|
||||
|
||||
l := fill / 2
|
||||
r := fill - l
|
||||
|
||||
@@ -482,27 +510,44 @@ func (h imgColumn) PrintC(clr aec.ANSI, s string) string {
|
||||
|
||||
func (h imgColumn) PrintL(clr aec.ANSI, s string) string {
|
||||
ln := tui.Width(s)
|
||||
if ln > h.Width {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
|
||||
fill := h.Width - ln
|
||||
|
||||
if fill < 0 {
|
||||
if h.NoEllipsis {
|
||||
fill = 0
|
||||
} else {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
}
|
||||
}
|
||||
|
||||
return clr.Apply(s) + strings.Repeat(" ", h.Width-ln)
|
||||
return clr.Apply(s) + strings.Repeat(" ", fill)
|
||||
}
|
||||
|
||||
func (h imgColumn) PrintR(clr aec.ANSI, s string) string {
|
||||
ln := tui.Width(s)
|
||||
if ln > h.Width {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
fill := h.Width - ln
|
||||
|
||||
if fill < 0 {
|
||||
if h.NoEllipsis {
|
||||
fill = 0
|
||||
} else {
|
||||
return clr.Apply(tui.Ellipsis(s, h.Width))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Repeat(" ", h.Width-ln) + clr.Apply(s)
|
||||
return strings.Repeat(" ", fill) + clr.Apply(s)
|
||||
}
|
||||
|
||||
// widestFirstColumnValue calculates the width needed to fully display the image names and platforms.
|
||||
func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
|
||||
width := len(headers[0].Title)
|
||||
for _, img := range images {
|
||||
for _, name := range img.Names {
|
||||
names := img.Names
|
||||
if len(names) == 0 {
|
||||
names = []string{untaggedName}
|
||||
}
|
||||
for _, name := range names {
|
||||
if len(name) > width {
|
||||
width = len(name)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestPrintImageTreeAnsiTty(t *testing.T) {
|
||||
@@ -15,6 +17,7 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
|
||||
stdoutTty bool
|
||||
stderrTty bool
|
||||
expectedAnsi bool
|
||||
noColorEnv bool
|
||||
}{
|
||||
{
|
||||
name: "non-terminal",
|
||||
@@ -80,6 +83,24 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
|
||||
|
||||
expectedAnsi: false,
|
||||
},
|
||||
{
|
||||
name: "no-color-env",
|
||||
stdinTty: false,
|
||||
stdoutTty: false,
|
||||
stderrTty: false,
|
||||
|
||||
noColorEnv: true,
|
||||
expectedAnsi: false,
|
||||
},
|
||||
{
|
||||
name: "no-color-env-terminal",
|
||||
stdinTty: true,
|
||||
stdoutTty: true,
|
||||
stderrTty: true,
|
||||
|
||||
noColorEnv: true,
|
||||
expectedAnsi: false,
|
||||
},
|
||||
}
|
||||
|
||||
mockView := treeView{
|
||||
@@ -115,6 +136,11 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
|
||||
cli.In().SetIsTerminal(tc.stdinTty)
|
||||
cli.Out().SetIsTerminal(tc.stdoutTty)
|
||||
cli.Err().SetIsTerminal(tc.stderrTty)
|
||||
if tc.noColorEnv {
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
} else {
|
||||
t.Setenv("NO_COLOR", "")
|
||||
}
|
||||
|
||||
printImageTree(cli, mockView)
|
||||
|
||||
@@ -123,10 +149,207 @@ func TestPrintImageTreeAnsiTty(t *testing.T) {
|
||||
|
||||
hasAnsi := strings.Contains(out, "\x1b[")
|
||||
if tc.expectedAnsi {
|
||||
assert.Check(t, hasAnsi, "Output should contain ANSI escape codes")
|
||||
assert.Check(t, hasAnsi, "Output should contain ANSI escape codes, output: %s", out)
|
||||
} else {
|
||||
assert.Check(t, !hasAnsi, "Output should not contain ANSI escape codes")
|
||||
assert.Check(t, !hasAnsi, "Output should not contain ANSI escape codes, output: %s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintImageTreeGolden(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
view treeView
|
||||
expanded bool
|
||||
}{
|
||||
{
|
||||
name: "width-calculation-untagged",
|
||||
expanded: false,
|
||||
view: treeView{
|
||||
images: []topImage{
|
||||
{
|
||||
Names: []string{"a:1"},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
DiskUsage: "5.5 MB",
|
||||
InUse: false,
|
||||
ContentSize: "2.5 MB",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Untagged image name is longer than "a:1"
|
||||
Names: []string{},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:2222222222222222222222222222222222222222222222222222222222222222",
|
||||
DiskUsage: "3.2 MB",
|
||||
InUse: false,
|
||||
ContentSize: "1.6 MB",
|
||||
},
|
||||
},
|
||||
{
|
||||
Names: []string{"short:v1"},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:3333333333333333333333333333333333333333333333333333333333333333",
|
||||
DiskUsage: "7.1 MB",
|
||||
InUse: true,
|
||||
ContentSize: "3.5 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
imageSpacing: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expanded-view-with-platforms",
|
||||
expanded: false,
|
||||
view: treeView{
|
||||
images: []topImage{
|
||||
{
|
||||
Names: []string{"multiplatform:latest"},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
DiskUsage: "25.5 MB",
|
||||
InUse: true,
|
||||
ContentSize: "20.2 MB",
|
||||
},
|
||||
Children: []subImage{
|
||||
{
|
||||
Platform: "linux/amd64",
|
||||
Available: true,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
DiskUsage: "12.1 MB",
|
||||
InUse: false,
|
||||
ContentSize: "10.0 MB",
|
||||
},
|
||||
},
|
||||
{
|
||||
Platform: "linux/arm64",
|
||||
Available: true,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
DiskUsage: "13.4 MB",
|
||||
InUse: true,
|
||||
ContentSize: "10.2 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
imageSpacing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "untagged-with-platforms",
|
||||
expanded: false,
|
||||
view: treeView{
|
||||
images: []topImage{
|
||||
{
|
||||
Names: []string{},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
DiskUsage: "18.5 MB",
|
||||
InUse: false,
|
||||
ContentSize: "15.2 MB",
|
||||
},
|
||||
Children: []subImage{
|
||||
{
|
||||
Platform: "linux/amd64",
|
||||
Available: true,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
||||
DiskUsage: "9.2 MB",
|
||||
InUse: false,
|
||||
ContentSize: "7.6 MB",
|
||||
},
|
||||
},
|
||||
{
|
||||
Platform: "linux/arm64",
|
||||
Available: false,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
DiskUsage: "9.3 MB",
|
||||
InUse: false,
|
||||
ContentSize: "7.6 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
imageSpacing: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed-tagged-untagged-with-children",
|
||||
expanded: false,
|
||||
view: treeView{
|
||||
images: []topImage{
|
||||
{
|
||||
Names: []string{"app:v1", "app:latest"},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:1010101010101010101010101010101010101010101010101010101010101010",
|
||||
DiskUsage: "30.5 MB",
|
||||
InUse: true,
|
||||
ContentSize: "25.2 MB",
|
||||
},
|
||||
Children: []subImage{
|
||||
{
|
||||
Platform: "linux/amd64",
|
||||
Available: true,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:2020202020202020202020202020202020202020202020202020202020202020",
|
||||
DiskUsage: "15.2 MB",
|
||||
InUse: true,
|
||||
ContentSize: "12.6 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Names: []string{},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:3030303030303030303030303030303030303030303030303030303030303030",
|
||||
DiskUsage: "12.3 MB",
|
||||
InUse: false,
|
||||
ContentSize: "10.1 MB",
|
||||
},
|
||||
Children: []subImage{
|
||||
{
|
||||
Platform: "linux/arm/v7",
|
||||
Available: true,
|
||||
Details: imageDetails{
|
||||
ID: "sha256:4040404040404040404040404040404040404040404040404040404040404040",
|
||||
DiskUsage: "6.1 MB",
|
||||
InUse: false,
|
||||
ContentSize: "5.0 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Names: []string{"base:alpine"},
|
||||
Details: imageDetails{
|
||||
ID: "sha256:5050505050505050505050505050505050505050505050505050505050505050",
|
||||
DiskUsage: "5.5 MB",
|
||||
InUse: false,
|
||||
ContentSize: "5.5 MB",
|
||||
},
|
||||
},
|
||||
},
|
||||
imageSpacing: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.Out().SetIsTerminal(false)
|
||||
|
||||
printImageTree(cli, tc.view)
|
||||
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("tree-command-success.%s.golden", tc.name))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package manifest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli"
|
||||
@@ -164,10 +168,8 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error {
|
||||
}
|
||||
|
||||
func appendIfUnique(list []string, str string) []string {
|
||||
for _, s := range list {
|
||||
if s == str {
|
||||
return list
|
||||
}
|
||||
if slices.Contains(list, str) {
|
||||
return list
|
||||
}
|
||||
return append(list, str)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
@@ -170,15 +170,23 @@ func (c *nodeContext) ManagerStatus() string {
|
||||
}
|
||||
|
||||
func (c *nodeContext) TLSStatus() string {
|
||||
if c.info.Cluster == nil || reflect.DeepEqual(c.info.Cluster.TLSInfo, swarm.TLSInfo{}) || reflect.DeepEqual(c.n.Description.TLSInfo, swarm.TLSInfo{}) {
|
||||
if c.info.Cluster == nil || isEmptyTLSInfo(c.info.Cluster.TLSInfo) || isEmptyTLSInfo(c.n.Description.TLSInfo) {
|
||||
return "Unknown"
|
||||
}
|
||||
if reflect.DeepEqual(c.n.Description.TLSInfo, c.info.Cluster.TLSInfo) {
|
||||
if equalTLSInfo(c.n.Description.TLSInfo, c.info.Cluster.TLSInfo) {
|
||||
return "Ready"
|
||||
}
|
||||
return "Needs Rotation"
|
||||
}
|
||||
|
||||
func isEmptyTLSInfo(t swarm.TLSInfo) bool {
|
||||
return t.TrustRoot == "" && len(t.CertIssuerSubject) == 0 && len(t.CertIssuerPublicKey) == 0
|
||||
}
|
||||
|
||||
func equalTLSInfo(t, o swarm.TLSInfo) bool {
|
||||
return t.TrustRoot == o.TrustRoot && bytes.Equal(t.CertIssuerSubject, o.CertIssuerSubject) && bytes.Equal(t.CertIssuerPublicKey, o.CertIssuerPublicKey)
|
||||
}
|
||||
|
||||
func (c *nodeContext) EngineVersion() string {
|
||||
return c.n.Description.Engine.EngineVersion
|
||||
}
|
||||
@@ -320,8 +328,7 @@ func (ctx *nodeInspectContext) EngineVersion() string {
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) HasTLSInfo() bool {
|
||||
tlsInfo := ctx.Node.Description.TLSInfo
|
||||
return !reflect.DeepEqual(tlsInfo, swarm.TLSInfo{})
|
||||
return !isEmptyTLSInfo(ctx.Node.Description.TLSInfo)
|
||||
}
|
||||
|
||||
func (ctx *nodeInspectContext) TLSInfoTrustRoot() string {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -99,9 +103,7 @@ func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error {
|
||||
}
|
||||
if flags.Changed(flagLabelAdd) {
|
||||
labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetSlice()
|
||||
for k, v := range opts.ConvertKVStringsToMap(labels) {
|
||||
spec.Annotations.Labels[k] = v
|
||||
}
|
||||
maps.Copy(spec.Annotations.Labels, opts.ConvertKVStringsToMap(labels))
|
||||
}
|
||||
if flags.Changed(flagLabelRemove) {
|
||||
keys := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetSlice()
|
||||
|
||||
@@ -144,8 +144,8 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
}
|
||||
}
|
||||
|
||||
argPassword = strings.TrimSpace(argPassword)
|
||||
if argPassword == "" {
|
||||
isEmpty := strings.TrimSpace(argPassword) == ""
|
||||
if isEmpty {
|
||||
restoreInput, err := prompt.DisableInputEcho(cli.In())
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
@@ -200,7 +200,7 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
encodedAuth, err := authconfig.Encode(registrytypes.AuthConfig{
|
||||
return authconfig.Encode(registrytypes.AuthConfig{
|
||||
Username: authConfig.Username,
|
||||
Password: authConfig.Password,
|
||||
ServerAddress: authConfig.ServerAddress,
|
||||
@@ -210,10 +210,6 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
IdentityToken: authConfig.IdentityToken,
|
||||
RegistryToken: authConfig.RegistryToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encodedAuth, nil
|
||||
}
|
||||
|
||||
// getAuthConfigKey special-cases using the full index address of the official
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -88,6 +89,38 @@ func verifyLoginFlags(flags *pflag.FlagSet, opts loginOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readSecretFromStdin reads the secret from r and returns it as a string.
|
||||
// It trims terminal line-endings (LF, CRLF, or CR), which may be added when
|
||||
// inputting interactively or piping input. The value is otherwise treated as
|
||||
// opaque, preserving any other whitespace, including newlines, per [NIST SP 800-63B §5.1.1.2].
|
||||
// Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]);
|
||||
//
|
||||
// > Verifiers **MAY** make limited allowances for mistyping (e.g., removing
|
||||
// > leading and trailing whitespace characters before verification, allowing
|
||||
// > the verification of passwords with differing cases for the leading character)
|
||||
//
|
||||
// [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver
|
||||
// [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver
|
||||
func readSecretFromStdin(r io.Reader) (string, error) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
for _, eol := range [][]byte{[]byte("\r\n"), []byte("\n"), []byte("\r")} {
|
||||
var ok bool
|
||||
b, ok = bytes.CutSuffix(b, eol)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
|
||||
if opts.password != "" {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
|
||||
@@ -97,14 +130,14 @@ func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
|
||||
if opts.user == "" {
|
||||
return errors.New("username is empty")
|
||||
}
|
||||
|
||||
contents, err := io.ReadAll(dockerCLI.In())
|
||||
p, err := readSecretFromStdin(dockerCLI.In())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.password = strings.TrimSuffix(string(contents), "\n")
|
||||
opts.password = strings.TrimSuffix(opts.password, "\r")
|
||||
if strings.TrimSpace(p) == "" {
|
||||
return errors.New("password is empty")
|
||||
}
|
||||
opts.password = p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -197,7 +230,7 @@ func loginUser(ctx context.Context, dockerCLI command.Cli, opts loginOptions, de
|
||||
// will hit this if you attempt docker login from mintty where stdin
|
||||
// is a pipe, not a character based console.
|
||||
if (opts.user == "" || opts.password == "") && !dockerCLI.In().IsTerminal() {
|
||||
return "", errors.New("error: cannot perform an interactive login from a non TTY device")
|
||||
return "", errors.New("error: cannot perform an interactive login from a non-TTY device")
|
||||
}
|
||||
|
||||
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
@@ -19,7 +22,6 @@ import (
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -71,8 +73,9 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
tmpDir := t.TempDir()
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.ConfigFile().Filename = filepath.Join(t.TempDir(), "config.json")
|
||||
cli.SetConfigFile(configfile.New(filepath.Join(tmpDir, "config.json")))
|
||||
for _, tc := range testCases {
|
||||
_, err := loginWithStoredCredentials(ctx, cli, tc.inputAuthConfig)
|
||||
if tc.expectedErrMsg != "" {
|
||||
@@ -91,6 +94,7 @@ func TestRunLogin(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
priorCredentials map[string]configtypes.AuthConfig
|
||||
stdIn string
|
||||
input loginOptions
|
||||
expectedCredentials map[string]configtypes.AuthConfig
|
||||
expectedErr string
|
||||
@@ -127,7 +131,7 @@ func TestRunLogin(t *testing.T) {
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
{
|
||||
doc: "store valid username and password",
|
||||
@@ -286,20 +290,125 @@ func TestRunLogin(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password stdin empty",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedErr: `password is empty`,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password stdin read error",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedErr: `TEST_READ_ERR`,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password stdin with line-endings",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
stdIn: "my password\r\n",
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "my password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password with leading and trailing spaces",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: " my password with spaces ",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: " my password with spaces ",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password stdin with line-endings",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
stdIn: " my password with spaces \r\n",
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: " my password with spaces ",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password stdin with multiple line-endings",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
stdIn: " my password\nwith spaces \r\n\r\n",
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: " my password\nwith spaces \r\n",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
tmpDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
cli.SetConfigFile(cfg)
|
||||
if tc.input.passwordStdin {
|
||||
if tc.expectedErr == "TEST_READ_ERR" {
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(iotest.ErrReader(errors.New(tc.expectedErr)))))
|
||||
} else {
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBufferString(tc.stdIn))))
|
||||
}
|
||||
}
|
||||
|
||||
for _, priorCred := range tc.priorCredentials {
|
||||
assert.NilError(t, configfile.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred))
|
||||
assert.NilError(t, cfg.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred))
|
||||
}
|
||||
storedCreds, err := configfile.GetAllCredentials()
|
||||
storedCreds, err := cfg.GetAllCredentials()
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, storedCreds, tc.priorCredentials)
|
||||
|
||||
@@ -310,7 +419,7 @@ func TestRunLogin(t *testing.T) {
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
|
||||
outputCreds, err := configfile.GetAllCredentials()
|
||||
outputCreds, err := cfg.GetAllCredentials()
|
||||
assert.Check(t, err)
|
||||
assert.DeepEqual(t, outputCreds, tc.expectedCredentials)
|
||||
})
|
||||
@@ -334,19 +443,19 @@ func TestLoginNonInteractive(t *testing.T) {
|
||||
doc: "error - w/o user w/o pass ",
|
||||
username: false,
|
||||
password: false,
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -356,11 +465,10 @@ func TestLoginNonInteractive(t *testing.T) {
|
||||
for _, registryAddr := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
tmpDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cfg := cli.ConfigFile()
|
||||
cfg.Filename = tmpFile.Path()
|
||||
cli.SetConfigFile(cfg)
|
||||
options := loginOptions{
|
||||
serverAddress: registryAddr,
|
||||
}
|
||||
@@ -403,13 +511,13 @@ func TestLoginNonInteractive(t *testing.T) {
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "error: cannot perform an interactive login from a non TTY device",
|
||||
expectedErr: "error: cannot perform an interactive login from a non-TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -419,11 +527,10 @@ func TestLoginNonInteractive(t *testing.T) {
|
||||
for _, registryAddr := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
tmpDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cfg := cli.ConfigFile()
|
||||
cfg.Filename = tmpFile.Path()
|
||||
cli.SetConfigFile(cfg)
|
||||
serverAddress := registryAddr
|
||||
if serverAddress == "" {
|
||||
serverAddress = "https://index.docker.io/v1/"
|
||||
@@ -465,17 +572,15 @@ func TestLoginTermination(t *testing.T) {
|
||||
_ = p.Close()
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
|
||||
cli := test.NewFakeCli(&fakeClient{}, func(fc *test.FakeCli) {
|
||||
fc.SetOut(streams.NewOut(tty))
|
||||
fc.SetIn(streams.NewIn(tty))
|
||||
})
|
||||
tmpFile := fs.NewFile(t, "test-login-termination")
|
||||
defer tmpFile.Remove()
|
||||
cli.SetConfigFile(cfg)
|
||||
|
||||
configFile := cli.ConfigFile()
|
||||
configFile.Filename = tmpFile.Path()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
runErr := make(chan error)
|
||||
|
||||
@@ -3,6 +3,7 @@ package command_test
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@@ -58,7 +59,9 @@ func TestGetDefaultAuthConfig(t *testing.T) {
|
||||
expectedAuthConfig: testAuthConfigs[1],
|
||||
},
|
||||
}
|
||||
cfg := configfile.New("filename")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "cli-config.json"))
|
||||
for _, authConfig := range testAuthConfigs {
|
||||
assert.Check(t, cfg.GetCredentialsStore(authConfig.ServerAddress).Store(configtypes.AuthConfig{
|
||||
Username: authConfig.Username,
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestServiceListServiceStatus(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
matrix := make([]testCase, 0)
|
||||
matrix := make([]testCase, 0, len(tests))
|
||||
for _, quiet := range []bool{false, true} {
|
||||
for _, tc := range tests {
|
||||
if quiet {
|
||||
@@ -191,10 +191,7 @@ func generateServices(t *testing.T, opts clusterOpts) client.ServiceListResult {
|
||||
t.Helper()
|
||||
|
||||
// Can't have more global tasks than nodes
|
||||
globalTasks := opts.runningTasks
|
||||
if globalTasks > opts.activeNodes {
|
||||
globalTasks = opts.activeNodes
|
||||
}
|
||||
globalTasks := min(opts.runningTasks, opts.activeNodes)
|
||||
return client.ServiceListResult{
|
||||
Items: []swarm.Service{
|
||||
*builders.Service(
|
||||
@@ -279,7 +276,7 @@ func generateNodes(t *testing.T, activeNodes uint64) client.NodeListResult {
|
||||
t.Helper()
|
||||
nodes := client.NodeListResult{}
|
||||
var i uint64
|
||||
for i = 0; i < activeNodes; i++ {
|
||||
for i = range activeNodes {
|
||||
nodes.Items = append(nodes.Items, swarm.Node{
|
||||
ID: fmt.Sprintf("node-ready-%d", i),
|
||||
Status: swarm.NodeStatus{State: swarm.NodeStateReady},
|
||||
|
||||
@@ -905,9 +905,9 @@ func addServiceFlags(flags *pflag.FlagSet, options *serviceOptions, defaultFlagV
|
||||
flags.Int64Var(&options.resources.limitPids, flagLimitPids, 0, "Limit maximum number of processes (default 0 = unlimited)")
|
||||
flags.SetAnnotation(flagLimitPids, "version", []string{"1.41"})
|
||||
flags.Var(&options.resources.swapBytes, flagSwapBytes, "Swap Bytes (-1 for unlimited)")
|
||||
flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"})
|
||||
flags.SetAnnotation(flagSwapBytes, "version", []string{"1.52"})
|
||||
flags.Int64Var(&options.resources.memSwappiness, flagMemSwappiness, -1, "Tune memory swappiness (0-100), -1 to reset to default")
|
||||
flags.SetAnnotation(flagLimitPids, "version", []string{"1.52"})
|
||||
flags.SetAnnotation(flagMemSwappiness, "version", []string{"1.52"})
|
||||
|
||||
flags.Var(&options.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)"))
|
||||
flags.Var(&options.replicas, flagReplicas, "Number of tasks")
|
||||
|
||||
@@ -662,10 +662,7 @@ func (u *replicatedJobProgressUpdater) writeOverallProgress(active, completed in
|
||||
})
|
||||
|
||||
// actualDesired is the lesser of MaxConcurrent, or the remaining tasks
|
||||
actualDesired := u.total - completed
|
||||
if actualDesired > u.concurrent {
|
||||
actualDesired = u.concurrent
|
||||
}
|
||||
actualDesired := min(u.total-completed, u.concurrent)
|
||||
|
||||
_ = u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: "active tasks",
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestReplicatedProgressUpdaterOneReplica(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task //nolint:prealloc // no need to over-complicate things.
|
||||
|
||||
ut.testUpdater(tasks, false,
|
||||
[]progress.Progress{
|
||||
@@ -193,7 +193,7 @@ func TestReplicatedProgressUpdaterManyReplicas(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task
|
||||
|
||||
// No per-task progress bars because there are too many replicas
|
||||
ut.testUpdater(tasks, false,
|
||||
@@ -248,7 +248,7 @@ func TestGlobalProgressUpdaterOneNode(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task //nolint:prealloc // no need to over-complicate things.
|
||||
|
||||
ut.testUpdater(tasks, false,
|
||||
[]progress.Progress{
|
||||
@@ -362,7 +362,7 @@ func TestGlobalProgressUpdaterManyNodes(t *testing.T) {
|
||||
ut.activeNodes[strconv.Itoa(i)] = struct{}{}
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task
|
||||
|
||||
ut.testUpdater(tasks, false,
|
||||
[]progress.Progress{
|
||||
@@ -459,9 +459,8 @@ func TestReplicatedJobProgressUpdaterSmall(t *testing.T) {
|
||||
})
|
||||
|
||||
// wipe the old tasks out of the list
|
||||
tasks = []swarm.Task{}
|
||||
tasks = append(tasks,
|
||||
swarm.Task{
|
||||
tasks = []swarm.Task{ //nolint:prealloc // no need to over-complicate things.
|
||||
{
|
||||
ID: "task1",
|
||||
Slot: 0,
|
||||
NodeID: "",
|
||||
@@ -469,7 +468,7 @@ func TestReplicatedJobProgressUpdaterSmall(t *testing.T) {
|
||||
Status: swarm.TaskStatus{State: swarm.TaskStateNew},
|
||||
JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
|
||||
},
|
||||
swarm.Task{
|
||||
{
|
||||
ID: "task2",
|
||||
Slot: 1,
|
||||
NodeID: "",
|
||||
@@ -477,7 +476,7 @@ func TestReplicatedJobProgressUpdaterSmall(t *testing.T) {
|
||||
Status: swarm.TaskStatus{State: swarm.TaskStateNew},
|
||||
JobIteration: &swarm.Version{Index: service.JobStatus.JobIteration.Index},
|
||||
},
|
||||
)
|
||||
}
|
||||
ut.testUpdater(tasks, false, []progress.Progress{
|
||||
{ID: "1/5", Action: "new ", Current: 1, Total: 10, HideCounts: true},
|
||||
{ID: "2/5", Action: "new ", Current: 1, Total: 10, HideCounts: true},
|
||||
@@ -644,7 +643,7 @@ func TestReplicatedJobProgressUpdaterLarge(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task
|
||||
|
||||
// see the comments in TestReplicatedJobProgressUpdaterSmall for why
|
||||
// we write this out twice.
|
||||
@@ -741,7 +740,7 @@ func TestGlobalJobProgressUpdaterSmall(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{
|
||||
tasks := []swarm.Task{ //nolint:prealloc // no need to over-complicate things.
|
||||
{
|
||||
ID: "oldtask1",
|
||||
DesiredState: swarm.TaskStateComplete,
|
||||
@@ -856,7 +855,7 @@ func TestGlobalJobProgressUpdaterLarge(t *testing.T) {
|
||||
}
|
||||
|
||||
activeNodes := map[string]struct{}{}
|
||||
for i := 0; i < 50; i++ {
|
||||
for i := range 50 {
|
||||
activeNodes[fmt.Sprintf("node%v", i)] = struct{}{}
|
||||
}
|
||||
|
||||
@@ -871,7 +870,7 @@ func TestGlobalJobProgressUpdaterLarge(t *testing.T) {
|
||||
service: service,
|
||||
}
|
||||
|
||||
tasks := []swarm.Task{}
|
||||
var tasks []swarm.Task //nolint:prealloc // no need to over-complicate things.
|
||||
for nodeID := range activeNodes {
|
||||
tasks = append(tasks, swarm.Task{
|
||||
ID: "task" + nodeID,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -551,12 +552,7 @@ func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) {
|
||||
}
|
||||
|
||||
func anyChanged(flags *pflag.FlagSet, fields ...string) bool {
|
||||
for _, flag := range fields {
|
||||
if flags.Changed(flag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return slices.ContainsFunc(fields, flags.Changed)
|
||||
}
|
||||
|
||||
func addGenericResources(flags *pflag.FlagSet, spec *swarm.TaskSpec) error {
|
||||
@@ -685,9 +681,7 @@ func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) {
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetSlice()
|
||||
for key, value := range opts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
maps.Copy((*field), opts.ConvertKVStringsToMap(values))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,9 +698,7 @@ func updateLabels(flags *pflag.FlagSet, field *map[string]string) {
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetSlice()
|
||||
for key, value := range opts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
maps.Copy((*field), opts.ConvertKVStringsToMap(values))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,9 +715,7 @@ func updateSysCtls(flags *pflag.FlagSet, field *map[string]string) {
|
||||
}
|
||||
|
||||
values := flags.Lookup(flagSysCtlAdd).Value.(*opts.ListOpts).GetSlice()
|
||||
for key, value := range opts.ConvertKVStringsToMap(values) {
|
||||
(*field)[key] = value
|
||||
}
|
||||
maps.Copy((*field), opts.ConvertKVStringsToMap(values))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1333,7 +1323,7 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
|
||||
}
|
||||
|
||||
existingNetworks := make(map[string]struct{})
|
||||
var newNetworks []swarm.NetworkAttachmentConfig //nolint:prealloc
|
||||
var newNetworks []swarm.NetworkAttachmentConfig
|
||||
for _, nw := range specNetworks {
|
||||
if _, exists := idsToRemove[nw.Target]; exists {
|
||||
continue
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -368,23 +368,23 @@ func TestUpdateHealthcheckTable(t *testing.T) {
|
||||
err: "--no-healthcheck conflicts with --health-* options",
|
||||
},
|
||||
}
|
||||
for i, c := range testCases {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
for _, flag := range c.flags {
|
||||
flags.Set(flag[0], flag[1])
|
||||
}
|
||||
cspec := &swarm.ContainerSpec{
|
||||
Healthcheck: c.initial,
|
||||
}
|
||||
err := updateHealthcheck(flags, cspec)
|
||||
if c.err != "" {
|
||||
assert.Error(t, err, c.err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
if !reflect.DeepEqual(cspec.Healthcheck, c.expected) {
|
||||
t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck)
|
||||
for i, tc := range testCases {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
flags := newUpdateCommand(nil).Flags()
|
||||
for _, flag := range tc.flags {
|
||||
assert.Check(t, flags.Set(flag[0], flag[1]))
|
||||
}
|
||||
}
|
||||
cspec := &swarm.ContainerSpec{
|
||||
Healthcheck: tc.initial,
|
||||
}
|
||||
err := updateHealthcheck(flags, cspec)
|
||||
if tc.err != "" {
|
||||
assert.Error(t, err, tc.err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(cspec.Healthcheck, tc.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
composeLoader "github.com/docker/cli/cli/compose/loader"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// configOptions holds docker stack config options
|
||||
|
||||
@@ -51,7 +51,12 @@ func newDeployCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringSliceVarP(&opts.composefiles, "compose-file", "c", []string{}, `Path to a Compose file, or "-" to read from stdin`)
|
||||
flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||
_ = flags.SetAnnotation("compose-file", "version", []string{"1.25"})
|
||||
// Provide tab-completion for filenames. On Bash, this is constrained to the
|
||||
// ".yaml" and ".yml" file-extensions, but this doesn't appear to be supported
|
||||
// by other shells.
|
||||
_ = cmd.MarkFlagFilename("compose-file", "yaml", "yml")
|
||||
|
||||
flags.BoolVar(&opts.sendRegistryAuth, "with-registry-auth", false, "Send registry authentication details to Swarm agents")
|
||||
flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced")
|
||||
flags.SetAnnotation("prune", "version", []string{"1.27"})
|
||||
|
||||
@@ -27,7 +27,6 @@ func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
config, err := loader.Load(configDetails)
|
||||
if err != nil {
|
||||
var fpe *loader.ForbiddenPropertiesError
|
||||
@@ -39,6 +38,7 @@ func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dicts := getDictsFrom(configDetails.ConfigFiles)
|
||||
unsupportedProperties := loader.GetUnsupportedProperties(dicts...)
|
||||
if len(unsupportedProperties) > 0 {
|
||||
_, _ = fmt.Fprintf(streams.Err(), "Ignoring unsupported options: %s\n\n",
|
||||
@@ -65,8 +65,7 @@ func loadComposeFile(streams command.Streams, opts deployOptions) (*composetypes
|
||||
}
|
||||
|
||||
func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]any {
|
||||
dicts := []map[string]any{}
|
||||
|
||||
dicts := make([]map[string]any, 0, len(configFiles))
|
||||
for _, configFile := range configFiles {
|
||||
dicts = append(dicts, configFile.Config)
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (*ExternalCAOption) Type() string {
|
||||
|
||||
// String returns a string repr of this option.
|
||||
func (m *ExternalCAOption) String() string {
|
||||
externalCAs := []string{}
|
||||
externalCAs := make([]string, 0, len(m.values))
|
||||
for _, externalCA := range m.values {
|
||||
repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL)
|
||||
externalCAs = append(externalCAs, repr)
|
||||
|
||||
@@ -133,12 +133,12 @@ func prettyPrintEvent(out io.Writer, event events.Message) error {
|
||||
_, _ = fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID)
|
||||
|
||||
if len(event.Actor.Attributes) > 0 {
|
||||
var attrs []string
|
||||
var keys []string
|
||||
keys := make([]string, 0, len(event.Actor.Attributes))
|
||||
for k := range event.Actor.Attributes {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
attrs := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
v := event.Actor.Attributes[k]
|
||||
attrs = append(attrs, k+"="+v)
|
||||
|
||||
@@ -397,6 +397,13 @@ func prettyPrintServerInfo(streams command.Streams, info *dockerInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
if info.NRI != nil {
|
||||
fprintln(output, " NRI:")
|
||||
for _, v := range info.NRI.Info {
|
||||
fprintf(output, " %s: %s\n", v[0], v[1])
|
||||
}
|
||||
}
|
||||
|
||||
fprintln(output)
|
||||
for _, w := range info.Warnings {
|
||||
fprintln(streams.Err(), w)
|
||||
|
||||
@@ -126,6 +126,12 @@ var sampleInfoNoSwarm = system.Info{
|
||||
{"ReloadedAt", "2025-07-16T16:59:14Z"},
|
||||
},
|
||||
},
|
||||
NRI: &system.NRIInfo{
|
||||
Info: [][2]string{
|
||||
{"plugin-path", "/usr/libexec/docker/nri-plugins"},
|
||||
{"plugin-config-path", "/etc/docker/nri/conf.d"},
|
||||
},
|
||||
},
|
||||
CDISpecDirs: []string{"/etc/cdi", "/var/run/cdi"},
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"","APIVersion":"","MinAPIVersion":"","Os":"","Arch":"","Experimental":false,"Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}]}}
|
||||
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"2018-07-09T22:38:38.000000000+00:00"}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"","APIVersion":"","MinAPIVersion":"","Os":"","Arch":"","Experimental":false,"Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}]}}
|
||||
{"Client":{"Version":"18.99.5-ce","ApiVersion":"1.38","DefaultAPIVersion":"1.38","GitCommit":"deadbeef","GoVersion":"go1.10.2","Os":"linux","Arch":"amd64","BuildTime":"Wed May 30 22:21:05 2018","Context":"my-context"},"Server":{"Platform":{"Name":"Docker Enterprise Edition (EE) 2.0"},"Version":"18.99.5-ce","ApiVersion":"1.30","MinAPIVersion":"1.12","Os":"linux","Arch":"amd64","Components":[{"Name":"Engine","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 9 23:38:38 2018","Experimental":"false","GitCommit":"64ddfa6","GoVersion":"go1.8.7","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"Universal Control Plane","Version":"17.06.2-ee-15","Details":{"ApiVersion":"1.30","Arch":"amd64","BuildTime":"Mon Jul 2 21:24:07 UTC 2018","GitCommit":"4513922","GoVersion":"go1.9.4","MinApiVersion":"1.20","Os":"linux","Version":"3.0.3-tp2"}},{"Name":"Kubernetes","Version":"1.8+","Details":{"buildDate":"2018-04-26T16:51:21Z","compiler":"gc","gitCommit":"8d637aedf46b9c21dde723e29c645b9f27106fa5","gitTreeState":"clean","gitVersion":"v1.8.11-docker-8d637ae","goVersion":"go1.8.3","major":"1","minor":"8+","platform":"linux/amd64"}},{"Name":"Calico","Version":"v3.0.8","Details":{"cni":"v2.0.6","kube-controllers":"v2.0.5","node":"v3.0.8"}}],"GitCommit":"64ddfa6","GoVersion":"go1.8.7","KernelVersion":"v1.0.0","BuildTime":"2018-07-09T22:38:38.000000000+00:00"}}
|
||||
|
||||
@@ -54,4 +54,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}
|
||||
|
||||
@@ -59,4 +59,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}}
|
||||
|
||||
@@ -64,4 +64,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}}
|
||||
|
||||
@@ -59,4 +59,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"Warnings":null}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":null}
|
||||
|
||||
@@ -56,4 +56,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -56,4 +56,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -80,4 +80,7 @@ Server:
|
||||
Base: 10.123.0.0/16, Size: 24
|
||||
Firewall Backend: nftables+firewalld
|
||||
ReloadedAt: 2025-07-16T16:59:14Z
|
||||
NRI:
|
||||
plugin-path: /usr/libexec/docker/nri-plugins
|
||||
plugin-config-path: /etc/docker/nri/conf.d
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}}
|
||||
{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"FirewallBackend":{"Driver":"nftables+firewalld","Info":[["ReloadedAt","2025-07-16T16:59:14Z"]]},"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"NRI":{"Info":[["plugin-path","/usr/libexec/docker/nri-plugins"],["plugin-config-path","/etc/docker/nri/conf.d"]]},"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
@@ -63,7 +64,7 @@ type versionOptions struct {
|
||||
// versionInfo contains version information of both the Client, and Server
|
||||
type versionInfo struct {
|
||||
Client clientVersion
|
||||
Server *client.ServerVersionResult
|
||||
Server *serverVersion
|
||||
}
|
||||
|
||||
type platformInfo struct {
|
||||
@@ -83,6 +84,26 @@ type clientVersion struct {
|
||||
Context string `json:"Context"`
|
||||
}
|
||||
|
||||
// serverVersion contains information about the Docker server host.
|
||||
// it's the client-side presentation of [client.ServerVersionResult].
|
||||
type serverVersion struct {
|
||||
Platform client.PlatformInfo `json:",omitempty"` // Platform is the platform (product name) the server is running on.
|
||||
Version string `json:"Version"` // Version is the version of the daemon.
|
||||
APIVersion string `json:"ApiVersion"` // APIVersion is the highest API version supported by the server.
|
||||
MinAPIVersion string `json:"MinAPIVersion,omitempty"` // MinAPIVersion is the minimum API version the server supports.
|
||||
Os string `json:"Os"` // Os is the operating system the server runs on.
|
||||
Arch string `json:"Arch"` // Arch is the hardware architecture the server runs on.
|
||||
Components []system.ComponentVersion `json:"Components,omitempty"` // Components contains version information for the components making up the server.
|
||||
|
||||
// The following fields are deprecated, they relate to the Engine component and are kept for backwards compatibility
|
||||
|
||||
GitCommit string `json:"GitCommit,omitempty"`
|
||||
GoVersion string `json:"GoVersion,omitempty"`
|
||||
KernelVersion string `json:"KernelVersion,omitempty"`
|
||||
Experimental bool `json:"Experimental,omitempty"`
|
||||
BuildTime string `json:"BuildTime,omitempty"`
|
||||
}
|
||||
|
||||
// newClientVersion constructs a new clientVersion. If a dockerCLI is
|
||||
// passed as argument, additional information is included (API version),
|
||||
// which may invoke an API connection. Pass nil to omit the additional
|
||||
@@ -107,6 +128,49 @@ func newClientVersion(contextName string, dockerCli command.Cli) clientVersion {
|
||||
return v
|
||||
}
|
||||
|
||||
func newServerVersion(sv client.ServerVersionResult) *serverVersion {
|
||||
out := &serverVersion{
|
||||
Platform: sv.Platform,
|
||||
Version: sv.Version,
|
||||
APIVersion: sv.APIVersion,
|
||||
MinAPIVersion: sv.MinAPIVersion,
|
||||
Os: sv.Os,
|
||||
Arch: sv.Arch,
|
||||
Experimental: sv.Experimental, //nolint:staticcheck // ignore deprecated field.
|
||||
Components: make([]system.ComponentVersion, 0, len(sv.Components)),
|
||||
}
|
||||
foundEngine := false
|
||||
for _, component := range sv.Components {
|
||||
if component.Name == "Engine" {
|
||||
foundEngine = true
|
||||
buildTime, ok := component.Details["BuildTime"]
|
||||
if ok {
|
||||
component.Details["BuildTime"] = reformatDate(buildTime)
|
||||
}
|
||||
out.GitCommit = component.Details["GitCommit"]
|
||||
out.GoVersion = component.Details["GoVersion"]
|
||||
out.KernelVersion = component.Details["KernelVersion"]
|
||||
out.Experimental = func() bool { b, _ := strconv.ParseBool(component.Details["Experimental"]); return b }()
|
||||
out.BuildTime = buildTime
|
||||
}
|
||||
out.Components = append(out.Components, component)
|
||||
}
|
||||
|
||||
if !foundEngine {
|
||||
out.Components = append(out.Components, system.ComponentVersion{
|
||||
Name: "Engine",
|
||||
Version: sv.Version,
|
||||
Details: map[string]string{
|
||||
"ApiVersion": sv.APIVersion,
|
||||
"MinAPIVersion": sv.MinAPIVersion,
|
||||
"Os": sv.Os,
|
||||
"Arch": sv.Arch,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// newVersionCommand creates a new cobra.Command for `docker version`
|
||||
func newVersionCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts versionOptions
|
||||
@@ -138,14 +202,14 @@ func reformatDate(buildTime string) string {
|
||||
}
|
||||
|
||||
func arch() string {
|
||||
arch := runtime.GOARCH
|
||||
out := runtime.GOARCH
|
||||
if rosetta.Enabled() {
|
||||
arch += " (rosetta)"
|
||||
out += " (rosetta)"
|
||||
}
|
||||
return arch
|
||||
return out
|
||||
}
|
||||
|
||||
func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions) error {
|
||||
func runVersion(ctx context.Context, dockerCLI command.Cli, opts *versionOptions) error {
|
||||
var err error
|
||||
tmpl, err := newVersionTemplate(opts.format)
|
||||
if err != nil {
|
||||
@@ -153,36 +217,13 @@ func runVersion(ctx context.Context, dockerCli command.Cli, opts *versionOptions
|
||||
}
|
||||
|
||||
vd := versionInfo{
|
||||
Client: newClientVersion(dockerCli.CurrentContext(), dockerCli),
|
||||
Client: newClientVersion(dockerCLI.CurrentContext(), dockerCLI),
|
||||
}
|
||||
sv, err := dockerCli.Client().ServerVersion(ctx, client.ServerVersionOptions{})
|
||||
sv, err := dockerCLI.Client().ServerVersion(ctx, client.ServerVersionOptions{})
|
||||
if err == nil {
|
||||
vd.Server = &sv
|
||||
foundEngine := false
|
||||
for _, component := range sv.Components {
|
||||
if component.Name == "Engine" {
|
||||
foundEngine = true
|
||||
buildTime, ok := component.Details["BuildTime"]
|
||||
if ok {
|
||||
component.Details["BuildTime"] = reformatDate(buildTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundEngine {
|
||||
vd.Server.Components = append(vd.Server.Components, system.ComponentVersion{
|
||||
Name: "Engine",
|
||||
Version: sv.Version,
|
||||
Details: map[string]string{
|
||||
"ApiVersion": sv.APIVersion,
|
||||
"MinAPIVersion": sv.MinAPIVersion,
|
||||
"Os": sv.Os,
|
||||
"Arch": sv.Arch,
|
||||
},
|
||||
})
|
||||
}
|
||||
vd.Server = newServerVersion(sv)
|
||||
}
|
||||
if err2 := prettyPrintVersion(dockerCli.Out(), vd, tmpl); err2 != nil && err == nil {
|
||||
if err2 := prettyPrintVersion(dockerCLI.Out(), vd, tmpl); err2 != nil && err == nil {
|
||||
err = err2
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -47,8 +47,18 @@ func TestVersionFormat(t *testing.T) {
|
||||
BuildTime: "Wed May 30 22:21:05 2018",
|
||||
Context: "my-context",
|
||||
},
|
||||
Server: &client.ServerVersionResult{
|
||||
Platform: client.PlatformInfo{Name: "Docker Enterprise Edition (EE) 2.0"},
|
||||
Server: &serverVersion{
|
||||
Platform: client.PlatformInfo{Name: "Docker Enterprise Edition (EE) 2.0"},
|
||||
Version: "18.99.5-ce",
|
||||
APIVersion: "1.30",
|
||||
MinAPIVersion: "1.12",
|
||||
Os: "linux",
|
||||
Arch: "amd64",
|
||||
GitCommit: "64ddfa6",
|
||||
GoVersion: "go1.8.7",
|
||||
KernelVersion: "v1.0.0",
|
||||
Experimental: false,
|
||||
BuildTime: "2018-07-09T22:38:38.000000000+00:00",
|
||||
Components: []system.ComponentVersion{
|
||||
{
|
||||
Name: "Engine",
|
||||
|
||||
@@ -155,11 +155,11 @@ func (e statusError) Error() string {
|
||||
// Note: The root command's name is excluded. If cmd is the root cmd, return ""
|
||||
func getCommandName(cmd *cobra.Command) string {
|
||||
fullCmdName := getFullCommandName(cmd)
|
||||
i := strings.Index(fullCmdName, " ")
|
||||
if i == -1 {
|
||||
_, after, ok := strings.Cut(fullCmdName, " ")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fullCmdName[i+1:]
|
||||
return after
|
||||
}
|
||||
|
||||
// getFullCommandName gets the full cobra command name in the format
|
||||
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func setupCobraCommands() (*cobra.Command, *cobra.Command, *cobra.Command) {
|
||||
@@ -139,7 +140,7 @@ func TestStdioAttributes(t *testing.T) {
|
||||
cli.Out().SetIsTerminal(tc.stdoutTty)
|
||||
actual := stdioAttributes(cli)
|
||||
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
assert.Check(t, is.DeepEqual(actual, tc.expected, cmpopts.EquateComparable(attribute.Value{})))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -179,7 +180,7 @@ func TestAttributesFromError(t *testing.T) {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := attributesFromError(tc.err)
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
assert.Check(t, is.DeepEqual(actual, tc.expected, cmpopts.EquateComparable(attribute.Value{})))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.24
|
||||
|
||||
package volume
|
||||
|
||||
import (
|
||||
@@ -166,7 +169,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options createOptions
|
||||
// each topology takes the form segment=value,segment=value
|
||||
// comma-separated list of equal separated maps
|
||||
segments := map[string]string{}
|
||||
for _, segment := range strings.Split(top, ",") {
|
||||
for segment := range strings.SplitSeq(top, ",") {
|
||||
// TODO(dperny): validate topology syntax
|
||||
k, v, _ := strings.Cut(segment, "=")
|
||||
segments[k] = v
|
||||
@@ -181,7 +184,7 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options createOptions
|
||||
// each topology takes the form segment=value,segment=value
|
||||
// comma-separated list of equal separated maps
|
||||
segments := map[string]string{}
|
||||
for _, segment := range strings.Split(top, ",") {
|
||||
for segment := range strings.SplitSeq(top, ",") {
|
||||
// TODO(dperny): validate topology syntax
|
||||
k, v, _ := strings.Cut(segment, "=")
|
||||
segments[k] = v
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user