mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -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/"
|
||||
|
||||
@@ -7,3 +7,5 @@ updates:
|
||||
labels:
|
||||
- "area/testing"
|
||||
- "status/2-code-review"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
+12
-12
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -63,10 +63,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
fi
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
@@ -101,20 +101,20 @@ jobs:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # 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@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
with:
|
||||
images: dockereng/cli-bin
|
||||
tags: |
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
-
|
||||
name: Build and push image
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -165,10 +165,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 2
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
@@ -61,19 +61,20 @@ jobs:
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
go-version: "1.25.8"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 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@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -30,15 +30,15 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -60,24 +60,28 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version: "1.25.5"
|
||||
go-version: "1.25.8"
|
||||
cache: false
|
||||
-
|
||||
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
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@v6
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Generate
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
|
||||
+1
-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.5"
|
||||
go: "1.25.8"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
+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.5
|
||||
ARG GO_VERSION=1.25.8
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -132,7 +132,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -177,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
|
||||
@@ -188,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
|
||||
@@ -212,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
|
||||
@@ -221,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
|
||||
}
|
||||
}
|
||||
@@ -248,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")
|
||||
}
|
||||
|
||||
@@ -286,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)
|
||||
}
|
||||
@@ -319,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,
|
||||
@@ -347,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,
|
||||
@@ -371,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 {
|
||||
@@ -428,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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FIXME(vvoland): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
// 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
|
||||
@@ -795,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
|
||||
}
|
||||
}
|
||||
@@ -902,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)
|
||||
@@ -947,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,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)
|
||||
@@ -172,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")
|
||||
|
||||
@@ -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 (
|
||||
@@ -60,7 +63,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)
|
||||
|
||||
@@ -389,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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 progress
|
||||
|
||||
import (
|
||||
@@ -662,10 +665,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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +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 volume
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"maps"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -124,10 +127,10 @@ func TestVolumeCreateWithFlags(t *testing.T) {
|
||||
if options.Driver != expectedDriver {
|
||||
return client.VolumeCreateResult{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, options.Driver)
|
||||
}
|
||||
if !reflect.DeepEqual(options.DriverOpts, expectedOpts) {
|
||||
if !maps.Equal(options.DriverOpts, expectedOpts) {
|
||||
return client.VolumeCreateResult{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, options.DriverOpts)
|
||||
}
|
||||
if !reflect.DeepEqual(options.Labels, expectedLabels) {
|
||||
if !maps.Equal(options.Labels, expectedLabels) {
|
||||
return client.VolumeCreateResult{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, options.Labels)
|
||||
}
|
||||
return client.VolumeCreateResult{
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// 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 convert
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -174,7 +179,11 @@ func Service(
|
||||
}
|
||||
|
||||
func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference {
|
||||
result := []swarm.PlacementPreference{}
|
||||
if len(preferences) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]swarm.PlacementPreference, 0, len(preferences))
|
||||
for _, preference := range preferences {
|
||||
spreadDescriptor := preference.Spread
|
||||
result = append(result, swarm.PlacementPreference{
|
||||
@@ -198,7 +207,7 @@ func convertServiceNetworks(
|
||||
}
|
||||
}
|
||||
|
||||
nets := []swarm.NetworkAttachmentConfig{}
|
||||
nets := make([]swarm.NetworkAttachmentConfig, 0, len(networks))
|
||||
for networkName, nw := range networks {
|
||||
networkConfig, ok := networkConfigs[networkName]
|
||||
if !ok && networkName != defaultNetwork {
|
||||
@@ -241,8 +250,6 @@ func convertServiceSecrets(
|
||||
secrets []composetypes.ServiceSecretConfig,
|
||||
secretSpecs map[string]composetypes.SecretConfig,
|
||||
) ([]*swarm.SecretReference, error) {
|
||||
refs := []*swarm.SecretReference{}
|
||||
|
||||
lookup := func(key string) (composetypes.FileObjectConfig, error) {
|
||||
secretSpec, exists := secretSpecs[key]
|
||||
if !exists {
|
||||
@@ -250,6 +257,8 @@ func convertServiceSecrets(
|
||||
}
|
||||
return composetypes.FileObjectConfig(secretSpec), nil
|
||||
}
|
||||
|
||||
refs := make([]*swarm.SecretReference, 0, len(secrets))
|
||||
for _, secret := range secrets {
|
||||
obj, err := convertFileObject(namespace, composetypes.FileReferenceConfig(secret), lookup)
|
||||
if err != nil {
|
||||
@@ -564,20 +573,17 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement
|
||||
}
|
||||
|
||||
func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec {
|
||||
portConfigs := []swarm.PortConfig{}
|
||||
portConfigs := make([]swarm.PortConfig, 0, len(source))
|
||||
for _, port := range source {
|
||||
portConfig := swarm.PortConfig{
|
||||
portConfigs = append(portConfigs, swarm.PortConfig{
|
||||
Protocol: network.IPProtocol(port.Protocol),
|
||||
TargetPort: port.Target,
|
||||
PublishedPort: port.Published,
|
||||
PublishMode: swarm.PortConfigPublishMode(port.Mode),
|
||||
}
|
||||
portConfigs = append(portConfigs, portConfig)
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(portConfigs, func(i, j int) bool {
|
||||
return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort
|
||||
})
|
||||
slices.SortFunc(portConfigs, swarm.PortConfig.Compare)
|
||||
|
||||
return &swarm.EndpointSpec{
|
||||
Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)),
|
||||
@@ -698,28 +704,22 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec
|
||||
}
|
||||
|
||||
func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*container.Ulimit {
|
||||
newUlimits := make(map[string]*container.Ulimit)
|
||||
ulimits := make([]*container.Ulimit, 0, len(origUlimits))
|
||||
for name, u := range origUlimits {
|
||||
soft, hard := int64(u.Soft), int64(u.Hard)
|
||||
if u.Single != 0 {
|
||||
newUlimits[name] = &container.Ulimit{
|
||||
Name: name,
|
||||
Soft: int64(u.Single),
|
||||
Hard: int64(u.Single),
|
||||
}
|
||||
} else {
|
||||
newUlimits[name] = &container.Ulimit{
|
||||
Name: name,
|
||||
Soft: int64(u.Soft),
|
||||
Hard: int64(u.Hard),
|
||||
}
|
||||
soft, hard = int64(u.Single), int64(u.Single)
|
||||
}
|
||||
|
||||
ulimits = append(ulimits, &container.Ulimit{
|
||||
Name: name,
|
||||
Soft: soft,
|
||||
Hard: hard,
|
||||
})
|
||||
}
|
||||
ulimits := make([]*container.Ulimit, 0, len(newUlimits))
|
||||
for _, ulimit := range newUlimits {
|
||||
ulimits = append(ulimits, ulimit)
|
||||
}
|
||||
sort.SliceStable(ulimits, func(i, j int) bool {
|
||||
return ulimits[i].Name < ulimits[j].Name
|
||||
|
||||
slices.SortFunc(ulimits, func(a, b *container.Ulimit) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
return ulimits
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package loader
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -249,9 +250,7 @@ func GetDeprecatedProperties(configDicts ...map[string]any) map[string]string {
|
||||
|
||||
for _, configDict := range configDicts {
|
||||
deprecatedProperties := getProperties(getServices(configDict), types.DeprecatedProperties)
|
||||
for key, value := range deprecatedProperties {
|
||||
deprecated[key] = value
|
||||
}
|
||||
maps.Copy(deprecated, deprecatedProperties)
|
||||
}
|
||||
|
||||
return deprecated
|
||||
@@ -322,26 +321,26 @@ type Transformer struct {
|
||||
|
||||
func createTransformHook(additionalTransformers ...Transformer) mapstructure.DecodeHookFuncType {
|
||||
transforms := map[reflect.Type]func(any) (any, error){
|
||||
reflect.TypeOf(types.External{}): transformExternal,
|
||||
reflect.TypeOf(types.HealthCheckTest{}): transformHealthCheckTest,
|
||||
reflect.TypeOf(types.ShellCommand{}): transformShellCommand,
|
||||
reflect.TypeOf(types.StringList{}): transformStringList,
|
||||
reflect.TypeOf(map[string]string{}): transformMapStringString,
|
||||
reflect.TypeOf(types.UlimitsConfig{}): transformUlimits,
|
||||
reflect.TypeOf(types.UnitBytes(0)): transformSize,
|
||||
reflect.TypeOf([]types.ServicePortConfig{}): transformServicePort,
|
||||
reflect.TypeOf(types.ServiceSecretConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(types.ServiceConfigObjConfig{}): transformStringSourceMap,
|
||||
reflect.TypeOf(types.StringOrNumberList{}): transformStringOrNumberList,
|
||||
reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): transformServiceNetworkMap,
|
||||
reflect.TypeOf(types.Mapping{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(types.MappingWithEquals{}): transformMappingOrListFunc("=", true),
|
||||
reflect.TypeOf(types.Labels{}): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeOf(types.MappingWithColon{}): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeOf(types.HostsList{}): transformHostsList,
|
||||
reflect.TypeOf(types.ServiceVolumeConfig{}): transformServiceVolumeConfig,
|
||||
reflect.TypeOf(types.BuildConfig{}): transformBuildConfig,
|
||||
reflect.TypeOf(types.Duration(0)): transformStringToDuration,
|
||||
reflect.TypeFor[types.External](): transformExternal,
|
||||
reflect.TypeFor[types.HealthCheckTest](): transformHealthCheckTest,
|
||||
reflect.TypeFor[types.ShellCommand](): transformShellCommand,
|
||||
reflect.TypeFor[types.StringList](): transformStringList,
|
||||
reflect.TypeFor[map[string]string](): transformMapStringString,
|
||||
reflect.TypeFor[types.UlimitsConfig](): transformUlimits,
|
||||
reflect.TypeFor[types.UnitBytes](): transformSize,
|
||||
reflect.TypeFor[[]types.ServicePortConfig](): transformServicePort,
|
||||
reflect.TypeFor[types.ServiceSecretConfig](): transformStringSourceMap,
|
||||
reflect.TypeFor[types.ServiceConfigObjConfig](): transformStringSourceMap,
|
||||
reflect.TypeFor[types.StringOrNumberList](): transformStringOrNumberList,
|
||||
reflect.TypeFor[map[string]*types.ServiceNetworkConfig](): transformServiceNetworkMap,
|
||||
reflect.TypeFor[types.Mapping](): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeFor[types.MappingWithEquals](): transformMappingOrListFunc("=", true),
|
||||
reflect.TypeFor[types.Labels](): transformMappingOrListFunc("=", false),
|
||||
reflect.TypeFor[types.MappingWithColon](): transformMappingOrListFunc(":", false),
|
||||
reflect.TypeFor[types.HostsList](): transformHostsList,
|
||||
reflect.TypeFor[types.ServiceVolumeConfig](): transformServiceVolumeConfig,
|
||||
reflect.TypeFor[types.BuildConfig](): transformBuildConfig,
|
||||
reflect.TypeFor[types.Duration](): transformStringToDuration,
|
||||
}
|
||||
|
||||
for _, transformer := range additionalTransformers {
|
||||
|
||||
+62
-78
@@ -4,8 +4,11 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"dario.cat/mergo"
|
||||
@@ -26,62 +29,73 @@ func (s *specials) Transformer(t reflect.Type) func(dst, src reflect.Value) erro
|
||||
func merge(configs []*types.Config) (*types.Config, error) {
|
||||
base := configs[0]
|
||||
for _, override := range configs[1:] {
|
||||
var err error
|
||||
base.Services, err = mergeServices(base.Services, override.Services)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("cannot merge services from %s: %w", override.Filename, err)
|
||||
var errs []error
|
||||
if services, err := mergeServices(base.Services, override.Services); err != nil {
|
||||
errs = append(errs, fmt.Errorf("cannot merge services: %w", err))
|
||||
} else {
|
||||
base.Services = services
|
||||
}
|
||||
base.Volumes, err = mergeVolumes(base.Volumes, override.Volumes)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("cannot merge volumes from %s: %w", override.Filename, err)
|
||||
if err := mergo.Map(&base.Volumes, &override.Volumes, mergo.WithOverride); err != nil {
|
||||
errs = append(errs, fmt.Errorf("cannot merge volumes: %w", err))
|
||||
}
|
||||
base.Networks, err = mergeNetworks(base.Networks, override.Networks)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("cannot merge networks from %s: %w", override.Filename, err)
|
||||
if err := mergo.Map(&base.Networks, &override.Networks, mergo.WithOverride); err != nil {
|
||||
errs = append(errs, fmt.Errorf("cannot merge networks: %w", err))
|
||||
}
|
||||
base.Secrets, err = mergeSecrets(base.Secrets, override.Secrets)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("cannot merge secrets from %s: %w", override.Filename, err)
|
||||
if err := mergo.Map(&base.Secrets, &override.Secrets, mergo.WithOverride); err != nil {
|
||||
errs = append(errs, fmt.Errorf("cannot merge secrets: %w", err))
|
||||
}
|
||||
base.Configs, err = mergeConfigs(base.Configs, override.Configs)
|
||||
if err != nil {
|
||||
return base, fmt.Errorf("cannot merge configs from %s: %w", override.Filename, err)
|
||||
if err := mergo.Map(&base.Configs, &override.Configs, mergo.WithOverride); err != nil {
|
||||
errs = append(errs, fmt.Errorf("cannot merge configs: %w", err))
|
||||
}
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return nil, errors.Join(fmt.Errorf("failed to merge file %s", override.Filename), err)
|
||||
}
|
||||
}
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) {
|
||||
baseServices := mapByName(base)
|
||||
overrideServices := mapByName(override)
|
||||
specials := &specials{
|
||||
m: map[reflect.Type]func(dst, src reflect.Value) error{
|
||||
reflect.TypeOf(&types.LoggingConfig{}): safelyMerge(mergeLoggingConfig),
|
||||
reflect.TypeOf([]types.ServicePortConfig{}): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
|
||||
reflect.TypeOf([]types.ServiceSecretConfig{}): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
|
||||
reflect.TypeOf([]types.ServiceConfigObjConfig{}): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
|
||||
reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig,
|
||||
reflect.TypeOf([]types.ServiceVolumeConfig{}): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
|
||||
reflect.TypeOf(types.ShellCommand{}): mergeShellCommand,
|
||||
reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
|
||||
reflect.PointerTo(reflect.TypeOf(uint64(1))): mergeUint64,
|
||||
},
|
||||
mergeOpts := []func(*mergo.Config){
|
||||
mergo.WithAppendSlice,
|
||||
mergo.WithOverride,
|
||||
mergo.WithTransformers(&specials{m: map[reflect.Type]func(dst, src reflect.Value) error{
|
||||
reflect.PointerTo(reflect.TypeFor[types.LoggingConfig]()): safelyMerge(mergeLoggingConfig),
|
||||
reflect.TypeFor[[]types.ServicePortConfig](): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice),
|
||||
reflect.TypeFor[[]types.ServiceSecretConfig](): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice),
|
||||
reflect.TypeFor[[]types.ServiceConfigObjConfig](): mergeSlice(toServiceConfigObjConfigsMap, toSServiceConfigObjConfigsSlice),
|
||||
reflect.PointerTo(reflect.TypeFor[types.UlimitsConfig]()): mergeUlimitsConfig,
|
||||
reflect.TypeFor[[]types.ServiceVolumeConfig](): mergeSlice(toServiceVolumeConfigsMap, toServiceVolumeConfigsSlice),
|
||||
reflect.TypeFor[types.ShellCommand](): mergeShellCommand,
|
||||
reflect.PointerTo(reflect.TypeFor[types.ServiceNetworkConfig]()): mergeServiceNetworkConfig,
|
||||
reflect.PointerTo(reflect.TypeFor[uint64]()): mergeUint64,
|
||||
}}),
|
||||
}
|
||||
for name, overrideService := range overrideServices {
|
||||
if baseService, ok := baseServices[name]; ok {
|
||||
if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(specials)); err != nil {
|
||||
return base, fmt.Errorf("cannot merge service %s: %w", name, err)
|
||||
|
||||
baseServices := make(map[string]types.ServiceConfig, len(base))
|
||||
for _, s := range base {
|
||||
baseServices[s.Name] = s
|
||||
}
|
||||
|
||||
for _, overrideService := range override {
|
||||
if baseService, ok := baseServices[overrideService.Name]; ok {
|
||||
if err := mergo.Merge(&baseService, &overrideService, mergeOpts...); err != nil {
|
||||
return nil, fmt.Errorf("cannot merge service %s: %w", overrideService.Name, err)
|
||||
}
|
||||
baseServices[name] = baseService
|
||||
baseServices[overrideService.Name] = baseService
|
||||
continue
|
||||
}
|
||||
baseServices[name] = overrideService
|
||||
baseServices[overrideService.Name] = overrideService
|
||||
}
|
||||
services := []types.ServiceConfig{}
|
||||
|
||||
services := make([]types.ServiceConfig, 0, len(baseServices))
|
||||
for _, baseService := range baseServices {
|
||||
services = append(services, baseService)
|
||||
}
|
||||
sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name })
|
||||
|
||||
slices.SortFunc(services, func(a, b types.ServiceConfig) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
@@ -134,7 +148,7 @@ func toServiceVolumeConfigsMap(s any) (map[any]any, error) {
|
||||
}
|
||||
|
||||
func toServiceSecretConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
s := []types.ServiceSecretConfig{}
|
||||
s := make([]types.ServiceSecretConfig, 0, len(m))
|
||||
for _, v := range m {
|
||||
s = append(s, v.(types.ServiceSecretConfig))
|
||||
}
|
||||
@@ -144,7 +158,7 @@ func toServiceSecretConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
}
|
||||
|
||||
func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
s := []types.ServiceConfigObjConfig{}
|
||||
s := make([]types.ServiceConfigObjConfig, 0, len(m))
|
||||
for _, v := range m {
|
||||
s = append(s, v.(types.ServiceConfigObjConfig))
|
||||
}
|
||||
@@ -154,7 +168,7 @@ func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
}
|
||||
|
||||
func toServicePortConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
s := []types.ServicePortConfig{}
|
||||
s := make([]types.ServicePortConfig, 0, len(m))
|
||||
for _, v := range m {
|
||||
s = append(s, v.(types.ServicePortConfig))
|
||||
}
|
||||
@@ -164,7 +178,7 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
}
|
||||
|
||||
func toServiceVolumeConfigsSlice(dst reflect.Value, m map[any]any) error {
|
||||
s := []types.ServiceVolumeConfig{}
|
||||
s := make([]types.ServiceVolumeConfig, 0, len(m))
|
||||
for _, v := range m {
|
||||
s = append(s, v.(types.ServiceVolumeConfig))
|
||||
}
|
||||
@@ -217,11 +231,13 @@ func sliceToMap(tomap tomapFn, v reflect.Value) (map[any]any, error) {
|
||||
}
|
||||
|
||||
func mergeLoggingConfig(dst, src reflect.Value) error {
|
||||
dstDriver := dst.Elem().FieldByName("Driver").String()
|
||||
srcDriver := src.Elem().FieldByName("Driver").String()
|
||||
|
||||
// Same driver, merging options
|
||||
if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) ||
|
||||
getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" {
|
||||
if getLoggingDriver(dst.Elem()) == "" {
|
||||
dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem()))
|
||||
if dstDriver == srcDriver || dstDriver == "" || srcDriver == "" {
|
||||
if dstDriver == "" {
|
||||
dst.Elem().FieldByName("Driver").SetString(srcDriver)
|
||||
}
|
||||
dstOptions := dst.Elem().FieldByName("Options").Interface().(map[string]string)
|
||||
srcOptions := src.Elem().FieldByName("Options").Interface().(map[string]string)
|
||||
@@ -269,35 +285,3 @@ func mergeUint64(dst, src reflect.Value) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoggingDriver(v reflect.Value) string {
|
||||
return v.FieldByName("Driver").String()
|
||||
}
|
||||
|
||||
func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig {
|
||||
m := map[string]types.ServiceConfig{}
|
||||
for _, service := range services {
|
||||
m[service.Name] = service
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) {
|
||||
err := mergo.Map(&base, &override, mergo.WithOverride)
|
||||
return base, err
|
||||
}
|
||||
|
||||
func mergeNetworks(base, override map[string]types.NetworkConfig) (map[string]types.NetworkConfig, error) {
|
||||
err := mergo.Map(&base, &override, mergo.WithOverride)
|
||||
return base, err
|
||||
}
|
||||
|
||||
func mergeSecrets(base, override map[string]types.SecretConfig) (map[string]types.SecretConfig, error) {
|
||||
err := mergo.Map(&base, &override, mergo.WithOverride)
|
||||
return base, err
|
||||
}
|
||||
|
||||
func mergeConfigs(base, override map[string]types.ConfigObjConfig) (map[string]types.ConfigObjConfig, error) {
|
||||
err := mergo.Map(&base, &override, mergo.WithOverride)
|
||||
return base, err
|
||||
}
|
||||
|
||||
@@ -1153,7 +1153,7 @@ func TestLoadMultipleServiceVolumes(t *testing.T) {
|
||||
func TestMergeUlimitsConfig(t *testing.T) {
|
||||
specials := &specials{
|
||||
m: map[reflect.Type]func(dst, src reflect.Value) error{
|
||||
reflect.TypeOf(&types.UlimitsConfig{}): mergeUlimitsConfig,
|
||||
reflect.TypeFor[*types.UlimitsConfig](): mergeUlimitsConfig,
|
||||
},
|
||||
}
|
||||
base := map[string]*types.UlimitsConfig{
|
||||
@@ -1189,7 +1189,7 @@ func TestMergeUlimitsConfig(t *testing.T) {
|
||||
func TestMergeServiceNetworkConfig(t *testing.T) {
|
||||
specials := &specials{
|
||||
m: map[reflect.Type]func(dst, src reflect.Value) error{
|
||||
reflect.TypeOf(&types.ServiceNetworkConfig{}): mergeServiceNetworkConfig,
|
||||
reflect.TypeFor[*types.ServiceNetworkConfig](): mergeServiceNetworkConfig,
|
||||
},
|
||||
}
|
||||
base := map[string]*types.ServiceNetworkConfig{
|
||||
|
||||
@@ -5,6 +5,7 @@ package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -140,9 +141,7 @@ func recurseExtract(value any, pattern regexper) map[string]string {
|
||||
case map[string]any:
|
||||
for _, elem := range val {
|
||||
submap := recurseExtract(elem, pattern)
|
||||
for k, v := range submap {
|
||||
m[k] = v
|
||||
}
|
||||
maps.Copy(m, submap)
|
||||
}
|
||||
|
||||
case []any:
|
||||
|
||||
@@ -6,6 +6,7 @@ package types
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -129,9 +130,7 @@ func (c Config) MarshalJSON() ([]byte, error) {
|
||||
if len(c.Configs) > 0 {
|
||||
m["configs"] = c.Configs
|
||||
}
|
||||
for k, v := range c.Extras {
|
||||
m[k] = v
|
||||
}
|
||||
maps.Copy(m, c.Extras)
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
@@ -352,7 +351,7 @@ func (u UnitBytes) MarshalYAML() (any, error) {
|
||||
|
||||
// MarshalJSON makes UnitBytes implement json.Marshaler
|
||||
func (u UnitBytes) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf(`"%d"`, u)), nil
|
||||
return fmt.Appendf(nil, `"%d"`, u), nil
|
||||
}
|
||||
|
||||
// RestartPolicy the service restart policy
|
||||
@@ -562,7 +561,7 @@ func (e External) MarshalJSON() ([]byte, error) {
|
||||
if e.Name == "" {
|
||||
return []byte(strconv.FormatBool(e.External)), nil
|
||||
}
|
||||
return []byte(fmt.Sprintf(`{"name": %q}`, e.Name)), nil
|
||||
return fmt.Appendf(nil, `{"name": %q}`, e.Name), nil
|
||||
}
|
||||
|
||||
// CredentialSpecConfig for credential spec on Windows
|
||||
|
||||
@@ -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 configfile
|
||||
|
||||
import (
|
||||
@@ -6,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -374,9 +378,7 @@ func getConfiguredCredentialStore(c *ConfigFile, registryHostname string) string
|
||||
func (configFile *ConfigFile) GetAllCredentials() (map[string]types.AuthConfig, error) {
|
||||
auths := make(map[string]types.AuthConfig)
|
||||
addAll := func(from map[string]types.AuthConfig) {
|
||||
for reg, ac := range from {
|
||||
auths[reg] = ac
|
||||
}
|
||||
maps.Copy(auths, from)
|
||||
}
|
||||
|
||||
defaultStore := configFile.GetCredentialsStore("")
|
||||
|
||||
@@ -73,10 +73,10 @@ func (m *mockCommand) Output() ([]byte, error) {
|
||||
return []byte("program failed"), errCommandExited
|
||||
}
|
||||
case "list":
|
||||
return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>")), nil
|
||||
return fmt.Appendf(nil, `{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>"), nil
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited
|
||||
return fmt.Appendf(nil, "unknown argument %q with %q", m.arg, inS), errCommandExited
|
||||
}
|
||||
|
||||
// Input sets the input to send to a remote credentials helper.
|
||||
|
||||
@@ -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 connhelper provides helpers for connecting to a remote daemon host with custom logic.
|
||||
package connhelper
|
||||
|
||||
@@ -6,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper/commandconn"
|
||||
@@ -89,10 +93,8 @@ func addSSHTimeout(sshFlags []string) []string {
|
||||
// disablePseudoTerminalAllocation disables pseudo-terminal allocation to
|
||||
// prevent SSH from executing as a login shell
|
||||
func disablePseudoTerminalAllocation(sshFlags []string) []string {
|
||||
for _, flag := range sshFlags {
|
||||
if flag == "-T" {
|
||||
return sshFlags
|
||||
}
|
||||
if slices.Contains(sshFlags, "-T") {
|
||||
return sshFlags
|
||||
}
|
||||
return append(sshFlags, "-T")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package connhelper
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -27,7 +26,8 @@ func TestSSHFlags(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.DeepEqual(t, addSSHTimeout(tc.in), tc.out)
|
||||
result := addSSHTimeout(tc.in)
|
||||
assert.DeepEqual(t, result, tc.out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,9 +57,7 @@ func TestDisablePseudoTerminalAllocation(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := disablePseudoTerminalAllocation(tc.sshFlags)
|
||||
if !reflect.DeepEqual(result, tc.expected) {
|
||||
t.Errorf("expected %v, got %v", tc.expected, result)
|
||||
}
|
||||
assert.DeepEqual(t, result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ func quoteCommand(commandAndArgs ...string) (string, error) {
|
||||
quotedCmd = a
|
||||
continue
|
||||
}
|
||||
quotedCmd += " " + a //nolint:perfsprint // ignore "concat-loop"; no need to use a string-builder for this.
|
||||
quotedCmd += " " + a
|
||||
}
|
||||
// each part is quoted appropriately, so now we'll have a full
|
||||
// shell command to pass off to "ssh"
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
go.opentelemetry.io/otel v1.40.0
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
|
||||
@@ -57,19 +57,19 @@ require (
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
|
||||
|
||||
+16
-16
@@ -213,8 +213,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
@@ -252,26 +252,26 @@ github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc h1:zkGwegkOW709y0oiAr
|
||||
github.com/zmap/zcrypto v0.0.0-20210511125630-18f1e0152cfc/go.mod h1:FM4U1E3NzlNMRnSUTU3P1UdukWhYGifqEsjk9fn7BCk=
|
||||
github.com/zmap/zlint/v3 v3.1.0 h1:WjVytZo79m/L1+/Mlphl09WBob6YTGljN5IGWZFpAv0=
|
||||
github.com/zmap/zlint/v3 v3.1.0/go.mod h1:L7t8s3sEKkb0A2BxGy1IWrxt1ZATa1R4QfJZaQOD3zU=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -302,8 +302,8 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
|
||||
@@ -22,8 +22,7 @@ import (
|
||||
var pluginFilename = "docker-buildx"
|
||||
|
||||
func TestBuildWithBuilder(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
@@ -131,8 +130,7 @@ func (*fakeClient) Ping(context.Context, client.PingOptions) (client.PingResult,
|
||||
}
|
||||
|
||||
func TestBuildkitDisabled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
t.Setenv("DOCKER_BUILDKIT", "0")
|
||||
|
||||
@@ -172,8 +170,7 @@ func TestBuildkitDisabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuilderBroken(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithFile(pluginFilename, `#!/bin/sh exit 1`, fs.WithMode(0o777)),
|
||||
@@ -212,8 +209,7 @@ func TestBuilderBroken(t *testing.T) {
|
||||
|
||||
func TestBuilderBrokenEnforced(t *testing.T) {
|
||||
t.Setenv("DOCKER_BUILDKIT", "1")
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithFile(pluginFilename, `#!/bin/sh exit 1`, fs.WithMode(0o777)),
|
||||
|
||||
+26
-13
@@ -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 main
|
||||
|
||||
import (
|
||||
@@ -7,6 +10,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -115,6 +119,20 @@ func getExitCode(err error) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// cmdErrorMessage extracts an error message suitable for passing to plugin
|
||||
// hooks. If the error's message is empty (e.g. a StatusError with only an
|
||||
// exit code), it falls back to a generic message so that hooks can still
|
||||
// detect that the command failed.
|
||||
func cmdErrorMessage(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
if msg := err.Error(); msg != "" {
|
||||
return msg
|
||||
}
|
||||
return fmt.Sprintf("exited with code %d", getExitCode(err))
|
||||
}
|
||||
|
||||
func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
|
||||
var (
|
||||
opts *cliflags.ClientOptions
|
||||
@@ -410,7 +428,7 @@ func forceExitAfter3TerminationSignals(ctx context.Context, streams command.Stre
|
||||
signal.Notify(sig, platformsignals.TerminationSignals...)
|
||||
|
||||
// once we have received a total of 3 signals we force exit the cli
|
||||
for i := 0; i < 2; i++ {
|
||||
for range 2 {
|
||||
<-sig
|
||||
}
|
||||
_, _ = fmt.Fprint(streams.Err(), "\ngot 3 SIGTERM/SIGINTs, forcefully exiting\n")
|
||||
@@ -476,10 +494,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
subCommand = ccmd
|
||||
if err != nil || pluginmanager.IsPluginCommand(ccmd) {
|
||||
err := tryPluginRun(ctx, dockerCli, cmd, args[0], envs)
|
||||
if ccmd != nil && dockerCli.Out().IsTerminal() && dockerCli.HooksEnabled() && !errdefs.IsNotFound(err) {
|
||||
errMessage := cmdErrorMessage(err)
|
||||
pluginmanager.RunPluginHooks(ctx, dockerCli, cmd, ccmd, args, errMessage)
|
||||
}
|
||||
if err == nil {
|
||||
if ccmd != nil && dockerCli.Out().IsTerminal() && dockerCli.HooksEnabled() {
|
||||
pluginmanager.RunPluginHooks(ctx, dockerCli, cmd, ccmd, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !errdefs.IsNotFound(err) {
|
||||
@@ -503,11 +522,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
||||
// If the command is being executed in an interactive terminal
|
||||
// and hook are enabled, run the plugin hooks.
|
||||
if subCommand != nil && dockerCli.Out().IsTerminal() && dockerCli.HooksEnabled() {
|
||||
var errMessage string
|
||||
if err != nil {
|
||||
errMessage = err.Error()
|
||||
}
|
||||
pluginmanager.RunCLICommandHooks(ctx, dockerCli, cmd, subCommand, errMessage)
|
||||
pluginmanager.RunCLICommandHooks(ctx, dockerCli, cmd, subCommand, cmdErrorMessage(err))
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -617,10 +632,8 @@ func findCommand(cmd *cobra.Command, cmds []string) bool {
|
||||
if cmd == nil {
|
||||
return false
|
||||
}
|
||||
for _, c := range cmds {
|
||||
if c == cmd.Name() {
|
||||
return true
|
||||
}
|
||||
if slices.Contains(cmds, cmd.Name()) {
|
||||
return true
|
||||
}
|
||||
return findCommand(cmd.Parent(), cmds)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/commands"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
@@ -38,8 +40,7 @@ func TestDisableFlagsInUseLineIsSet(t *testing.T) {
|
||||
|
||||
func TestClientDebugEnabled(t *testing.T) {
|
||||
defer debug.Disable()
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
ctx := t.Context()
|
||||
|
||||
cli, err := command.NewDockerCli(command.WithBaseContext(ctx))
|
||||
assert.NilError(t, err)
|
||||
@@ -127,6 +128,66 @@ func TestUserTerminatedError(t *testing.T) {
|
||||
assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 143)
|
||||
}
|
||||
|
||||
func TestGetExitCode(t *testing.T) {
|
||||
t.Run("nil error returns 0", func(t *testing.T) {
|
||||
assert.Equal(t, getExitCode(nil), 0)
|
||||
})
|
||||
|
||||
t.Run("generic error returns 1", func(t *testing.T) {
|
||||
assert.Equal(t, getExitCode(errors.New("some failure")), 1)
|
||||
})
|
||||
|
||||
t.Run("StatusError with code", func(t *testing.T) {
|
||||
err := dockercli.StatusError{StatusCode: 42}
|
||||
assert.Equal(t, getExitCode(err), 42)
|
||||
})
|
||||
|
||||
t.Run("StatusError with zero code falls back to 1", func(t *testing.T) {
|
||||
err := dockercli.StatusError{StatusCode: 0, Status: "something went wrong"}
|
||||
assert.Equal(t, getExitCode(err), 1)
|
||||
})
|
||||
|
||||
t.Run("wrapped StatusError", func(t *testing.T) {
|
||||
err := fmt.Errorf("wrapper: %w", dockercli.StatusError{StatusCode: 99})
|
||||
assert.Equal(t, getExitCode(err), 99)
|
||||
})
|
||||
|
||||
t.Run("SIGINT returns 130", func(t *testing.T) {
|
||||
err := errCtxSignalTerminated{signal: syscall.SIGINT}
|
||||
assert.Equal(t, getExitCode(err), 130)
|
||||
})
|
||||
|
||||
t.Run("SIGTERM returns 143", func(t *testing.T) {
|
||||
err := errCtxSignalTerminated{signal: syscall.SIGTERM}
|
||||
assert.Equal(t, getExitCode(err), 143)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCmdErrorMessage(t *testing.T) {
|
||||
t.Run("nil error returns empty string", func(t *testing.T) {
|
||||
assert.Equal(t, cmdErrorMessage(nil), "")
|
||||
})
|
||||
|
||||
t.Run("generic error returns error message", func(t *testing.T) {
|
||||
assert.Equal(t, cmdErrorMessage(errors.New("something broke")), "something broke")
|
||||
})
|
||||
|
||||
t.Run("StatusError with Status field", func(t *testing.T) {
|
||||
err := dockercli.StatusError{Status: "build failed", StatusCode: 1}
|
||||
assert.Equal(t, cmdErrorMessage(err), "build failed")
|
||||
})
|
||||
|
||||
t.Run("StatusError with only exit code falls back to generic message", func(t *testing.T) {
|
||||
err := dockercli.StatusError{StatusCode: 42}
|
||||
assert.Equal(t, cmdErrorMessage(err), "exited with code 42")
|
||||
})
|
||||
|
||||
t.Run("wrapped error preserves message", func(t *testing.T) {
|
||||
err := fmt.Errorf("wrapper: %w", errors.New("inner failure"))
|
||||
assert.Equal(t, cmdErrorMessage(err), "wrapper: inner failure")
|
||||
})
|
||||
}
|
||||
|
||||
func TestVisitAll(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{Use: "sub1"}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.25.5"
|
||||
default = null
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ mddocs: build_docker_image ## generate markdown files from go source
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: build_docker_image ## generate documentation YAML files consumed by docs repo
|
||||
$(DOCKER_RUN) -it $(DEV_DOCKER_IMAGE_NAME) make yamldocs
|
||||
$(DOCKER_RUN) $(DEV_DOCKER_IMAGE_NAME) make yamldocs
|
||||
|
||||
.PHONY: test ## run unit and e2e tests
|
||||
test: test-unit test-e2e
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository.
|
||||
ARG ALPINE_VERSION=3.22
|
||||
ARG ALPINE_VERSION=3.23
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS gen
|
||||
RUN apk add --no-cache bash git
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.25.5
|
||||
ARG GO_VERSION=1.25.8
|
||||
|
||||
# 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
|
||||
|
||||
# BUILDX_VERSION sets the version of buildx to install in the dev container.
|
||||
# It must be a valid 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
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
@@ -18,7 +18,7 @@ ENV GOTOOLCHAIN=local
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
FROM golang AS gofumpt
|
||||
ARG GOFUMPT_VERSION=v0.7.0
|
||||
ARG GOFUMPT_VERSION=v0.9.2
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=tmpfs,target=/go/src/ \
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user