Compare commits
53 Commits
v28.3.0-rc
...
v28.3.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 980b856816 | |||
| 9c256146ac | |||
| bc01f8489d | |||
| ea2a0c3b8a | |||
| 3d985799d4 | |||
| f5f3b027e8 | |||
| 143f36133a | |||
| d7181e47e2 | |||
| 8b6436ecee | |||
| 7d574b816d | |||
| 0f2b709c7c | |||
| 7668b683d2 | |||
| 53d02ece89 | |||
| 3600ebca76 | |||
| 9b047a501f | |||
| e0f4bc699c | |||
| 1264a59779 | |||
| e6b8cc1c7d | |||
| 50fa436c21 | |||
| 0be687acc0 | |||
| c69d8bde4a | |||
| 8eac03d5fa | |||
| 578ccf607d | |||
| 0c5e258f8a | |||
| 30cad385b6 | |||
| 9bcc88611f | |||
| 3302212263 | |||
| ccd5bd8d57 | |||
| dec07e6fdf | |||
| 28f19a9d65 | |||
| 219e5ca4f2 | |||
| 7e040d91ef | |||
| 76524e7d0e | |||
| 3262107821 | |||
| 8403869122 | |||
| 1fc7194554 | |||
| fa2a7f1536 | |||
| 350b3a6e25 | |||
| 4ea6fbf538 | |||
| 74a896f18c | |||
| 94f097da28 | |||
| e7e238eb4b | |||
| 2ba7cb8b44 | |||
| 52e1e4fb21 | |||
| 7cbee73f19 | |||
| ae6f8d0021 | |||
| 70867e7067 | |||
| 38b7060a21 | |||
| 2d46d162c1 | |||
| 88d1133224 | |||
| 82eda48066 | |||
| 52d2a9b5ae | |||
| 64a9a6d0c8 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@ -121,6 +121,8 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
-
|
||||
name: Build and push image
|
||||
uses: docker/bake-action@v6
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
go-version: "1.24.5"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -66,7 +66,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24.4"
|
||||
go-version: "1.24.5"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
@ -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.24.4"
|
||||
go: "1.24.5"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@ -1,19 +1,30 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
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.21
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.5
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.12.3
|
||||
|
||||
# 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.24.0
|
||||
ARG COMPOSE_VERSION=v2.36.2
|
||||
ARG BUILDX_VERSION=0.25.0
|
||||
|
||||
# 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.38.2
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -16,7 +16,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
@ -398,8 +398,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
}
|
||||
|
||||
options := container.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: false,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
if copyConfig.quiet {
|
||||
|
||||
@ -248,15 +248,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
// 1. Mount the actual docker socket.
|
||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
||||
|
||||
socket := dockerCli.DockerEndpoint().Host
|
||||
if !strings.HasPrefix(socket, "unix://") {
|
||||
return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
|
||||
if dockerCli.ServerInfo().OSType == "windows" {
|
||||
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
|
||||
}
|
||||
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
|
||||
|
||||
// hard-code engine socket path until https://github.com/moby/moby/pull/43459 gives us a discovery mechanism
|
||||
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: socket,
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
BindOptions: &mount.BindOptions{},
|
||||
})
|
||||
|
||||
@ -7,25 +7,17 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type diffOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts diffOptions
|
||||
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
Short: "Inspect changes to files or directories on a container's filesystem",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
return runDiff(cmd.Context(), dockerCli, &opts)
|
||||
return runDiff(cmd.Context(), dockerCli, args[0])
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container diff, docker diff",
|
||||
@ -34,16 +26,13 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
|
||||
if opts.container == "" {
|
||||
return errors.New("Container name cannot be empty")
|
||||
}
|
||||
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
|
||||
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
|
||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
diffCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Output: dockerCLI.Out(),
|
||||
Format: NewDiffFormat("{{.Type}} {{.Path}}"),
|
||||
}
|
||||
return DiffFormatWrite(diffCtx, changes)
|
||||
|
||||
@ -77,17 +77,3 @@ func TestRunDiffClientError(t *testing.T) {
|
||||
err := cmd.Execute()
|
||||
assert.ErrorIs(t, err, clientError)
|
||||
}
|
||||
|
||||
func TestRunDiffEmptyContainerError(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
|
||||
cmd := NewDiffCommand(cli)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
|
||||
containerID := ""
|
||||
cmd.SetArgs([]string{containerID})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "Container name cannot be empty")
|
||||
}
|
||||
|
||||
@ -5,8 +5,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -176,7 +175,7 @@ func (c *statsContext) Name() string {
|
||||
|
||||
func (c *statsContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.s.ID)
|
||||
return formatter.TruncateID(c.s.ID)
|
||||
}
|
||||
return c.s.ID
|
||||
}
|
||||
|
||||
@ -5,13 +5,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestContainerStatsContext(t *testing.T) {
|
||||
containerID := stringid.GenerateRandomID()
|
||||
containerID := test.RandomID()
|
||||
|
||||
var ctx statsContext
|
||||
tt := []struct {
|
||||
|
||||
@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/moby/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -19,6 +18,18 @@ import (
|
||||
// TODO: This could be moved to `pkg/term`.
|
||||
var defaultEscapeKeys = []byte{16, 17}
|
||||
|
||||
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
|
||||
// It calls the given callback function when closed.
|
||||
type readCloserWrapper struct {
|
||||
io.Reader
|
||||
closer func() error
|
||||
}
|
||||
|
||||
// Close calls back the passed closer function
|
||||
func (r *readCloserWrapper) Close() error {
|
||||
return r.closer()
|
||||
}
|
||||
|
||||
// A hijackedIOStreamer handles copying input to and output from streams to the
|
||||
// connection.
|
||||
type hijackedIOStreamer struct {
|
||||
@ -100,7 +111,10 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
}
|
||||
}
|
||||
|
||||
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
|
||||
h.inputStream = &readCloserWrapper{
|
||||
Reader: term.NewEscapeProxy(h.inputStream, escapeKeys),
|
||||
closer: h.inputStream.Close,
|
||||
}
|
||||
|
||||
return restore, nil
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
@ -400,17 +399,14 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
tmpfs[k] = v
|
||||
}
|
||||
|
||||
var (
|
||||
runCmd strslice.StrSlice
|
||||
entrypoint strslice.StrSlice
|
||||
)
|
||||
var runCmd, entrypoint []string
|
||||
|
||||
if len(copts.Args) > 0 {
|
||||
runCmd = copts.Args
|
||||
}
|
||||
|
||||
if copts.entrypoint != "" {
|
||||
entrypoint = strslice.StrSlice{copts.entrypoint}
|
||||
entrypoint = []string{copts.entrypoint}
|
||||
} else if flags.Changed("entrypoint") {
|
||||
// if `--entrypoint=` is parsed then Entrypoint is reset
|
||||
entrypoint = []string{""}
|
||||
@ -551,9 +547,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
if haveHealthSettings {
|
||||
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
|
||||
}
|
||||
healthConfig = &container.HealthConfig{Test: strslice.StrSlice{"NONE"}}
|
||||
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
|
||||
} else if haveHealthSettings {
|
||||
var probe strslice.StrSlice
|
||||
var probe []string
|
||||
if copts.healthCmd != "" {
|
||||
probe = []string{"CMD-SHELL", copts.healthCmd}
|
||||
}
|
||||
@ -675,8 +671,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
UTSMode: utsMode,
|
||||
UsernsMode: usernsMode,
|
||||
CgroupnsMode: cgroupnsMode,
|
||||
CapAdd: strslice.StrSlice(copts.capAdd.GetSlice()),
|
||||
CapDrop: strslice.StrSlice(copts.capDrop.GetSlice()),
|
||||
CapAdd: copts.capAdd.GetSlice(),
|
||||
CapDrop: copts.capDrop.GetSlice(),
|
||||
GroupAdd: copts.groupAdd.GetSlice(),
|
||||
RestartPolicy: restartPolicy,
|
||||
SecurityOpt: securityOpts,
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
@ -115,7 +114,7 @@ func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
|
||||
func (c *buildCacheContext) ID() string {
|
||||
id := c.v.ID
|
||||
if c.trunc {
|
||||
id = stringid.TruncateID(c.v.ID)
|
||||
id = TruncateID(c.v.ID)
|
||||
}
|
||||
if c.v.InUse {
|
||||
return id + "*"
|
||||
@ -131,7 +130,7 @@ func (c *buildCacheContext) Parent() string {
|
||||
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
|
||||
}
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(parent)
|
||||
return TruncateID(parent)
|
||||
}
|
||||
return parent
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ import (
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
@ -135,7 +134,7 @@ func (c *ContainerContext) MarshalJSON() ([]byte, error) {
|
||||
// option being set, the full or truncated ID is returned.
|
||||
func (c *ContainerContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.c.ID)
|
||||
return TruncateID(c.c.ID)
|
||||
}
|
||||
return c.c.ID
|
||||
}
|
||||
@ -172,7 +171,7 @@ func (c *ContainerContext) Image() string {
|
||||
return "<no image>"
|
||||
}
|
||||
if c.trunc {
|
||||
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
|
||||
if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) {
|
||||
return trunc
|
||||
}
|
||||
// truncate digest if no-trunc option was not selected
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -21,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
func TestContainerPsContext(t *testing.T) {
|
||||
containerID := stringid.GenerateRandomID()
|
||||
containerID := test.RandomID()
|
||||
unix := time.Now().Add(-65 * time.Second).Unix()
|
||||
|
||||
var ctx ContainerContext
|
||||
@ -34,7 +33,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||
{
|
||||
container: container.Summary{ID: containerID},
|
||||
trunc: true,
|
||||
expValue: stringid.TruncateID(containerID),
|
||||
expValue: TruncateID(containerID),
|
||||
call: ctx.ID,
|
||||
},
|
||||
{
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -27,6 +27,25 @@ func charWidth(r rune) int {
|
||||
}
|
||||
}
|
||||
|
||||
const shortLen = 12
|
||||
|
||||
// TruncateID returns a shorthand version of a string identifier for presentation,
|
||||
// after trimming digest algorithm prefix (if any).
|
||||
//
|
||||
// This function is a copy of [stringid.TruncateID] for presentation / formatting
|
||||
// purposes.
|
||||
//
|
||||
// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19
|
||||
func TruncateID(id string) string {
|
||||
if i := strings.IndexRune(id, ':'); i >= 0 {
|
||||
id = id[i+1:]
|
||||
}
|
||||
if len(id) > shortLen {
|
||||
id = id[:shortLen]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
|
||||
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
|
||||
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.
|
||||
|
||||
@ -7,6 +7,49 @@ import (
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestTruncateID(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc, id, expected string
|
||||
}{
|
||||
{
|
||||
doc: "empty ID",
|
||||
id: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
// IDs are expected to be 12 (short) or 64 characters, and not be numeric only,
|
||||
// but TruncateID should handle these gracefully.
|
||||
doc: "invalid ID",
|
||||
id: "1234",
|
||||
expected: "1234",
|
||||
},
|
||||
{
|
||||
doc: "full ID",
|
||||
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
|
||||
expected: "90435eec5c4e",
|
||||
},
|
||||
{
|
||||
doc: "digest",
|
||||
id: "sha256:90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
|
||||
expected: "90435eec5c4e",
|
||||
},
|
||||
{
|
||||
doc: "very long ID",
|
||||
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a290435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
|
||||
expected: "90435eec5c4e",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
actual := TruncateID(tc.id)
|
||||
if actual != tc.expected {
|
||||
t.Errorf("expected: %q, got: %q", tc.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEllipsis(t *testing.T) {
|
||||
testcases := []struct {
|
||||
source string
|
||||
|
||||
@ -6,8 +6,7 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -216,7 +215,7 @@ func (c *imageContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (c *imageContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.i.ID)
|
||||
return TruncateID(c.i.ID)
|
||||
}
|
||||
return c.i.ID
|
||||
}
|
||||
|
||||
@ -9,13 +9,12 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestImageContext(t *testing.T) {
|
||||
imageID := stringid.GenerateRandomID()
|
||||
imageID := test.RandomID()
|
||||
unix := time.Now().Unix()
|
||||
zeroTime := int64(-62135596800)
|
||||
|
||||
@ -27,7 +26,7 @@ func TestImageContext(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
imageCtx: imageContext{i: image.Summary{ID: imageID}, trunc: true},
|
||||
expValue: stringid.TruncateID(imageID),
|
||||
expValue: TruncateID(imageID),
|
||||
call: ctx.ID,
|
||||
},
|
||||
{
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -12,13 +12,12 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestVolumeContext(t *testing.T) {
|
||||
volumeName := stringid.GenerateRandomID()
|
||||
volumeName := test.RandomID()
|
||||
|
||||
var ctx volumeContext
|
||||
cases := []struct {
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -15,10 +17,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/builder/remotecontext/git"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/patternmatcher"
|
||||
@ -108,7 +108,7 @@ func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool,
|
||||
return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err)
|
||||
}
|
||||
|
||||
return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil
|
||||
return newReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil
|
||||
}
|
||||
|
||||
// WriteTempDockerfile writes a Dockerfile stream to a temporary file with a
|
||||
@ -169,7 +169,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return ioutils.NewReadCloserWrapper(tarArchive, func() error {
|
||||
return newReadCloserWrapper(tarArchive, func() error {
|
||||
err := tarArchive.Close()
|
||||
os.RemoveAll(dockerfileDir)
|
||||
return err
|
||||
@ -227,7 +227,7 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
|
||||
// Pass the response body through a progress reader.
|
||||
progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", "Downloading build context from remote url: "+remoteURL)
|
||||
|
||||
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
|
||||
return GetContextFromReader(newReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
|
||||
}
|
||||
|
||||
// getWithStatusError does an http.Get() and returns an error if the
|
||||
@ -379,7 +379,7 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
|
||||
return nil, "", err
|
||||
}
|
||||
now := time.Now()
|
||||
randomName := ".dockerfile." + stringid.GenerateRandomID()[:20]
|
||||
randomName := ".dockerfile." + randomSuffix()
|
||||
|
||||
buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{
|
||||
// Add the dockerfile with a random filename
|
||||
@ -422,6 +422,15 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
|
||||
return buildCtx, randomName, nil
|
||||
}
|
||||
|
||||
// randomSuffix returns a unique, 20-character ID consisting of a-z, 0-9.
|
||||
func randomSuffix() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
return hex.EncodeToString(b)[:20]
|
||||
}
|
||||
|
||||
// Compress the build context for sending to the API
|
||||
func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
@ -444,3 +453,25 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
|
||||
|
||||
return pipeReader, nil
|
||||
}
|
||||
|
||||
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
|
||||
// It calls the given callback function when closed. It should be constructed
|
||||
// with [newReadCloserWrapper].
|
||||
type readCloserWrapper struct {
|
||||
io.Reader
|
||||
closer func() error
|
||||
}
|
||||
|
||||
// Close calls back the passed closer function
|
||||
func (r *readCloserWrapper) Close() error {
|
||||
return r.closer()
|
||||
}
|
||||
|
||||
// newReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser.
|
||||
// It calls the given callback function when closed.
|
||||
func newReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
|
||||
return &readCloserWrapper{
|
||||
Reader: r,
|
||||
closer: closer,
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,8 +2,7 @@ package build
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/pkg/longpath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getContextRoot(srcPath string) (string, error) {
|
||||
@ -11,5 +10,27 @@ func getContextRoot(srcPath string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return longpath.AddPrefix(cr), nil
|
||||
return addPrefix(cr), nil
|
||||
}
|
||||
|
||||
// longPathPrefix is the longpath prefix for Windows file paths.
|
||||
const longPathPrefix = `\\?\`
|
||||
|
||||
// addPrefix adds the Windows long path prefix to the path provided if
|
||||
// it does not already have it.
|
||||
//
|
||||
// See https://github.com/moby/moby/pull/15898
|
||||
//
|
||||
// This is a copy of [longpath.AddPrefix].
|
||||
//
|
||||
// [longpath.AddPrefix]:https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/pkg/longpath#AddPrefix
|
||||
func addPrefix(path string) string {
|
||||
if strings.HasPrefix(path, longPathPrefix) {
|
||||
return path
|
||||
}
|
||||
if strings.HasPrefix(path, `\\`) {
|
||||
// This is a UNC path, so we need to add 'UNC' to the path as well.
|
||||
return longPathPrefix + `UNC` + path[1:]
|
||||
}
|
||||
return longPathPrefix + path
|
||||
}
|
||||
|
||||
22
cli/command/image/build/context_windows_test.go
Normal file
22
cli/command/image/build/context_windows_test.go
Normal file
@ -0,0 +1,22 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStandardLongPath(t *testing.T) {
|
||||
c := `C:\simple\path`
|
||||
longC := addPrefix(c)
|
||||
if !strings.EqualFold(longC, `\\?\C:\simple\path`) {
|
||||
t.Errorf("Wrong long path returned. Original = %s ; Long = %s", c, longC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUNCLongPath(t *testing.T) {
|
||||
c := `\\server\share\path`
|
||||
longC := addPrefix(c)
|
||||
if !strings.EqualFold(longC, `\\?\UNC\server\share\path`) {
|
||||
t.Errorf("Wrong UNC long path returned. Original = %s ; Long = %s", c, longC)
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -72,7 +71,7 @@ func (c *historyContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (c *historyContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.h.ID)
|
||||
return formatter.TruncateID(c.h.ID)
|
||||
}
|
||||
return c.h.ID
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -21,7 +20,7 @@ type historyCase struct {
|
||||
}
|
||||
|
||||
func TestHistoryContext_ID(t *testing.T) {
|
||||
id := stringid.GenerateRandomID()
|
||||
id := test.RandomID()
|
||||
|
||||
var ctx historyContext
|
||||
cases := []historyCase{
|
||||
@ -35,7 +34,7 @@ func TestHistoryContext_ID(t *testing.T) {
|
||||
historyContext{
|
||||
h: image.HistoryResponseItem{ID: id},
|
||||
trunc: true,
|
||||
}, stringid.TruncateID(id), ctx.ID,
|
||||
}, formatter.TruncateID(id), ctx.ID,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -12,10 +12,10 @@ import (
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/opencontainers/go-digest"
|
||||
@ -222,7 +222,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
|
||||
Align: alignLeft,
|
||||
Width: 12,
|
||||
DetailsValue: func(d *imageDetails) string {
|
||||
return stringid.TruncateID(d.ID)
|
||||
return formatter.TruncateID(d.ID)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -73,7 +72,7 @@ func (c *networkContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (c *networkContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.n.ID)
|
||||
return formatter.TruncateID(c.n.ID)
|
||||
}
|
||||
return c.n.ID
|
||||
}
|
||||
|
||||
@ -14,13 +14,12 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestNetworkContext(t *testing.T) {
|
||||
networkID := stringid.GenerateRandomID()
|
||||
networkID := test.RandomID()
|
||||
|
||||
var ctx networkContext
|
||||
cases := []struct {
|
||||
@ -35,7 +34,7 @@ func TestNetworkContext(t *testing.T) {
|
||||
{networkContext{
|
||||
n: network.Summary{ID: networkID},
|
||||
trunc: true,
|
||||
}, stringid.TruncateID(networkID), ctx.ID},
|
||||
}, formatter.TruncateID(networkID), ctx.ID},
|
||||
{networkContext{
|
||||
n: network.Summary{Name: "network_name"},
|
||||
}, "network_name", ctx.Name},
|
||||
|
||||
@ -10,7 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -14,13 +14,12 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestNodeContext(t *testing.T) {
|
||||
nodeID := stringid.GenerateRandomID()
|
||||
nodeID := test.RandomID()
|
||||
|
||||
var ctx nodeContext
|
||||
cases := []struct {
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -66,7 +65,7 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (c *pluginContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.p.ID)
|
||||
return formatter.TruncateID(c.p.ID)
|
||||
}
|
||||
return c.p.ID
|
||||
}
|
||||
|
||||
@ -12,13 +12,12 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestPluginContext(t *testing.T) {
|
||||
pluginID := stringid.GenerateRandomID()
|
||||
pluginID := test.RandomID()
|
||||
|
||||
var ctx pluginContext
|
||||
cases := []struct {
|
||||
@ -33,7 +32,7 @@ func TestPluginContext(t *testing.T) {
|
||||
{pluginContext{
|
||||
p: types.Plugin{ID: pluginID},
|
||||
trunc: true,
|
||||
}, stringid.TruncateID(pluginID), ctx.ID},
|
||||
}, formatter.TruncateID(pluginID), ctx.ID},
|
||||
{pluginContext{
|
||||
p: types.Plugin{Name: "plugin_name"},
|
||||
}, "plugin_name", ctx.Name},
|
||||
|
||||
@ -110,6 +110,9 @@ func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) err
|
||||
if err := verifyLoginOptions(dockerCLI, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
maybePrintEnvAuthWarning(dockerCLI)
|
||||
|
||||
var (
|
||||
serverAddress string
|
||||
msg string
|
||||
|
||||
@ -36,6 +36,8 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string) error {
|
||||
maybePrintEnvAuthWarning(dockerCLI)
|
||||
|
||||
var isDefaultRegistry bool
|
||||
|
||||
if serverAddress == "" {
|
||||
|
||||
18
cli/command/registry/warning.go
Normal file
18
cli/command/registry/warning.go
Normal file
@ -0,0 +1,18 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
)
|
||||
|
||||
// maybePrintEnvAuthWarning if the `DOCKER_AUTH_CONFIG` environment variable is
|
||||
// set this function will output a warning to stdErr
|
||||
func maybePrintEnvAuthWarning(out command.Streams) {
|
||||
if os.Getenv(configfile.DockerEnvConfigKey) != "" {
|
||||
tui.NewOutput(out.Err()).
|
||||
PrintWarning("%[1]s is set and takes precedence.\nUnset %[1]s to restore the CLI auth behaviour.\n", configfile.DockerEnvConfigKey)
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@ -14,8 +14,7 @@ import (
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -645,7 +644,7 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (c *serviceContext) ID() string {
|
||||
return stringid.TruncateID(c.service.ID)
|
||||
return formatter.TruncateID(c.service.ID)
|
||||
}
|
||||
|
||||
func (c *serviceContext) Name() string {
|
||||
|
||||
@ -13,13 +13,13 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/internal/logdetails"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -220,7 +220,7 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string,
|
||||
if f.opts.noTrunc {
|
||||
taskName += "." + task.ID
|
||||
} else {
|
||||
taskName += "." + stringid.TruncateID(task.ID)
|
||||
taskName += "." + formatter.TruncateID(task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,12 +11,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -505,7 +505,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
|
||||
|
||||
if task.Status.Err != "" {
|
||||
u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: stringid.TruncateID(task.NodeID),
|
||||
ID: formatter.TruncateID(task.NodeID),
|
||||
Action: truncError(task.Status.Err),
|
||||
})
|
||||
return
|
||||
@ -513,7 +513,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
|
||||
|
||||
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
|
||||
u.progressOut.WriteProgress(progress.Progress{
|
||||
ID: stringid.TruncateID(task.NodeID),
|
||||
ID: formatter.TruncateID(task.NodeID),
|
||||
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
|
||||
Current: numberedStates[task.Status.State],
|
||||
Total: maxProgress,
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/go-units"
|
||||
)
|
||||
|
||||
@ -79,7 +78,7 @@ func (c *taskContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
func (c *taskContext) ID() string {
|
||||
if c.trunc {
|
||||
return stringid.TruncateID(c.task.ID)
|
||||
return formatter.TruncateID(c.task.ID)
|
||||
}
|
||||
return c.task.ID
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -119,7 +118,7 @@ func (c *signerInfoContext) Keys() string {
|
||||
truncatedKeys := []string{}
|
||||
if c.trunc {
|
||||
for _, keyID := range c.s.Keys {
|
||||
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
|
||||
truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID))
|
||||
}
|
||||
return strings.Join(truncatedKeys, ", ")
|
||||
}
|
||||
|
||||
@ -5,13 +5,13 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestTrustTag(t *testing.T) {
|
||||
digest := stringid.GenerateRandomID()
|
||||
digest := test.RandomID()
|
||||
trustedTag := "tag"
|
||||
|
||||
var ctx trustTagContext
|
||||
|
||||
@ -21,7 +21,7 @@ import (
|
||||
"github.com/docker/cli/opts/swarmopts"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/go-connections/nat"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/google/shlex"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@ -56,7 +56,7 @@ type configEnv struct {
|
||||
AuthConfigs map[string]configEnvAuth `json:"auths"`
|
||||
}
|
||||
|
||||
// dockerEnvConfig is an environment variable that contains a JSON encoded
|
||||
// DockerEnvConfigKey is an environment variable that contains a JSON encoded
|
||||
// credential config. It only supports storing the credentials as a base64
|
||||
// encoded string in the format base64("username:pat").
|
||||
//
|
||||
@ -71,7 +71,7 @@ type configEnv struct {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
const dockerEnvConfig = "DOCKER_AUTH_CONFIG"
|
||||
const DockerEnvConfigKey = "DOCKER_AUTH_CONFIG"
|
||||
|
||||
// ProxyConfig contains proxy configuration settings
|
||||
type ProxyConfig struct {
|
||||
@ -296,7 +296,7 @@ func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) crede
|
||||
store = newNativeStore(configFile, helper)
|
||||
}
|
||||
|
||||
envConfig := os.Getenv(dockerEnvConfig)
|
||||
envConfig := os.Getenv(DockerEnvConfigKey)
|
||||
if envConfig == "" {
|
||||
return store
|
||||
}
|
||||
|
||||
@ -47,14 +47,19 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
|
||||
}
|
||||
sshFlags = addSSHTimeout(sshFlags)
|
||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||
|
||||
remoteCommand := []string{"docker", "system", "dial-stdio"}
|
||||
socketPath := sp.Path
|
||||
if strings.Trim(sp.Path, "/") != "" {
|
||||
remoteCommand = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
|
||||
}
|
||||
sshArgs, err := sp.Command(sshFlags, remoteCommand...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
args := []string{"docker"}
|
||||
if sp.Path != "" {
|
||||
args = append(args, "--host", "unix://"+sp.Path)
|
||||
}
|
||||
args = append(args, "system", "dial-stdio")
|
||||
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
|
||||
return commandconn.New(ctx, "ssh", sshArgs...)
|
||||
},
|
||||
Host: "http://docker.example.com",
|
||||
}, nil
|
||||
|
||||
27
cli/connhelper/internal/syntax/LICENSE
Normal file
27
cli/connhelper/internal/syntax/LICENSE
Normal file
@ -0,0 +1,27 @@
|
||||
Copyright (c) 2016, Daniel Martí. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
13
cli/connhelper/internal/syntax/doc.go
Normal file
13
cli/connhelper/internal/syntax/doc.go
Normal file
@ -0,0 +1,13 @@
|
||||
// Package syntax is a fork of [mvdan.cc/sh/v3@v3.10.0/syntax].
|
||||
//
|
||||
// Copyright (c) 2016, Daniel Martí. All rights reserved.
|
||||
//
|
||||
// It is a reduced set of the package to only provide the [Quote] function,
|
||||
// and contains the [LICENSE], [quote.go] and [parser.go] files at the given
|
||||
// revision.
|
||||
//
|
||||
// [quote.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/quote.go
|
||||
// [parser.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/parser.go
|
||||
// [LICENSE]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/LICENSE
|
||||
// [mvdan.cc/sh/v3@v3.10.0/syntax]: https://pkg.go.dev/mvdan.cc/sh/v3@v3.10.0/syntax
|
||||
package syntax
|
||||
95
cli/connhelper/internal/syntax/parser.go
Normal file
95
cli/connhelper/internal/syntax/parser.go
Normal file
@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
|
||||
// See LICENSE for licensing information
|
||||
|
||||
package syntax
|
||||
|
||||
// LangVariant describes a shell language variant to use when tokenizing and
|
||||
// parsing shell code. The zero value is [LangBash].
|
||||
type LangVariant int
|
||||
|
||||
const (
|
||||
// LangBash corresponds to the GNU Bash language, as described in its
|
||||
// manual at https://www.gnu.org/software/bash/manual/bash.html.
|
||||
//
|
||||
// We currently follow Bash version 5.2.
|
||||
//
|
||||
// Its string representation is "bash".
|
||||
LangBash LangVariant = iota
|
||||
|
||||
// LangPOSIX corresponds to the POSIX Shell language, as described at
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.
|
||||
//
|
||||
// Its string representation is "posix" or "sh".
|
||||
LangPOSIX
|
||||
|
||||
// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as
|
||||
// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.
|
||||
// Note that it shares some features with Bash, due to the shared
|
||||
// ancestry that is ksh.
|
||||
//
|
||||
// We currently follow mksh version 59.
|
||||
//
|
||||
// Its string representation is "mksh".
|
||||
LangMirBSDKorn
|
||||
|
||||
// LangBats corresponds to the Bash Automated Testing System language,
|
||||
// as described at https://github.com/bats-core/bats-core. Note that
|
||||
// it's just a small extension of the Bash language.
|
||||
//
|
||||
// Its string representation is "bats".
|
||||
LangBats
|
||||
|
||||
// LangAuto corresponds to automatic language detection,
|
||||
// commonly used by end-user applications like shfmt,
|
||||
// which can guess a file's language variant given its filename or shebang.
|
||||
//
|
||||
// At this time, [Variant] does not support LangAuto.
|
||||
LangAuto
|
||||
)
|
||||
|
||||
func (l LangVariant) String() string {
|
||||
switch l {
|
||||
case LangBash:
|
||||
return "bash"
|
||||
case LangPOSIX:
|
||||
return "posix"
|
||||
case LangMirBSDKorn:
|
||||
return "mksh"
|
||||
case LangBats:
|
||||
return "bats"
|
||||
case LangAuto:
|
||||
return "auto"
|
||||
}
|
||||
return "unknown shell language variant"
|
||||
}
|
||||
|
||||
// IsKeyword returns true if the given word is part of the language keywords.
|
||||
func IsKeyword(word string) bool {
|
||||
// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
|
||||
switch word {
|
||||
case
|
||||
"!",
|
||||
"[[", // only if COND_COMMAND is defined
|
||||
"]]", // only if COND_COMMAND is defined
|
||||
"case",
|
||||
"coproc", // only if COPROCESS_SUPPORT is defined
|
||||
"do",
|
||||
"done",
|
||||
"else",
|
||||
"esac",
|
||||
"fi",
|
||||
"for",
|
||||
"function",
|
||||
"if",
|
||||
"in",
|
||||
"select", // only if SELECT_COMMAND is defined
|
||||
"then",
|
||||
"time", // only if COMMAND_TIMING is defined
|
||||
"until",
|
||||
"while",
|
||||
"{",
|
||||
"}":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
187
cli/connhelper/internal/syntax/quote.go
Normal file
187
cli/connhelper/internal/syntax/quote.go
Normal file
@ -0,0 +1,187 @@
|
||||
// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
|
||||
// See LICENSE for licensing information
|
||||
|
||||
package syntax
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type QuoteError struct {
|
||||
ByteOffset int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e QuoteError) Error() string {
|
||||
return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message)
|
||||
}
|
||||
|
||||
const (
|
||||
quoteErrNull = "shell strings cannot contain null bytes"
|
||||
quoteErrPOSIX = "POSIX shell lacks escape sequences"
|
||||
quoteErrRange = "rune out of range"
|
||||
quoteErrMksh = "mksh cannot escape codepoints above 16 bits"
|
||||
)
|
||||
|
||||
// Quote returns a quoted version of the input string,
|
||||
// so that the quoted version is expanded or interpreted
|
||||
// as the original string in the given language variant.
|
||||
//
|
||||
// Quoting is necessary when using arbitrary literal strings
|
||||
// as words in a shell script or command.
|
||||
// Without quoting, one can run into syntax errors,
|
||||
// as well as the possibility of running unintended code.
|
||||
//
|
||||
// An error is returned when a string cannot be quoted for a variant.
|
||||
// For instance, POSIX lacks escape sequences for non-printable characters,
|
||||
// and no language variant can represent a string containing null bytes.
|
||||
// In such cases, the returned error type will be *QuoteError.
|
||||
//
|
||||
// The quoting strategy is chosen on a best-effort basis,
|
||||
// to minimize the amount of extra bytes necessary.
|
||||
//
|
||||
// Some strings do not require any quoting and are returned unchanged.
|
||||
// Those strings can be directly surrounded in single quotes as well.
|
||||
//
|
||||
//nolint:gocyclo // ignore "cyclomatic complexity 35 of func `Quote` is high (> 16) (gocyclo)"
|
||||
func Quote(s string, lang LangVariant) (string, error) {
|
||||
if s == "" {
|
||||
// Special case; an empty string must always be quoted,
|
||||
// as otherwise it expands to zero fields.
|
||||
return "''", nil
|
||||
}
|
||||
shellChars := false
|
||||
nonPrintable := false
|
||||
offs := 0
|
||||
for rem := s; len(rem) > 0; {
|
||||
r, size := utf8.DecodeRuneInString(rem)
|
||||
switch r {
|
||||
// Like regOps; token characters.
|
||||
case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
|
||||
// Whitespace; might result in multiple fields.
|
||||
' ', '\t', '\r', '\n',
|
||||
// Escape sequences would be expanded.
|
||||
'\\',
|
||||
// Would start a comment unless quoted.
|
||||
'#',
|
||||
// Might result in brace expansion.
|
||||
'{',
|
||||
// Might result in tilde expansion.
|
||||
'~',
|
||||
// Might result in globbing.
|
||||
'*', '?', '[',
|
||||
// Might result in an assignment.
|
||||
'=':
|
||||
shellChars = true
|
||||
case '\x00':
|
||||
return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull}
|
||||
}
|
||||
if r == utf8.RuneError || !unicode.IsPrint(r) {
|
||||
if lang == LangPOSIX {
|
||||
return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}
|
||||
}
|
||||
nonPrintable = true
|
||||
}
|
||||
rem = rem[size:]
|
||||
offs += size
|
||||
}
|
||||
if !shellChars && !nonPrintable && !IsKeyword(s) {
|
||||
// Nothing to quote; avoid allocating.
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Single quotes are usually best,
|
||||
// as they don't require any escaping of characters.
|
||||
// If we have any invalid utf8 or non-printable runes,
|
||||
// use $'' so that we can escape them.
|
||||
// Note that we can't use double quotes for those.
|
||||
var b strings.Builder
|
||||
if nonPrintable {
|
||||
b.WriteString("$'")
|
||||
lastRequoteIfHex := false
|
||||
offs = 0
|
||||
for rem := s; len(rem) > 0; {
|
||||
nextRequoteIfHex := false
|
||||
r, size := utf8.DecodeRuneInString(rem)
|
||||
switch {
|
||||
case r == '\'', r == '\\':
|
||||
b.WriteByte('\\')
|
||||
b.WriteRune(r)
|
||||
case unicode.IsPrint(r) && r != utf8.RuneError:
|
||||
if lastRequoteIfHex && isHex(r) {
|
||||
b.WriteString("'$'")
|
||||
}
|
||||
b.WriteRune(r)
|
||||
case r == '\a':
|
||||
b.WriteString(`\a`)
|
||||
case r == '\b':
|
||||
b.WriteString(`\b`)
|
||||
case r == '\f':
|
||||
b.WriteString(`\f`)
|
||||
case r == '\n':
|
||||
b.WriteString(`\n`)
|
||||
case r == '\r':
|
||||
b.WriteString(`\r`)
|
||||
case r == '\t':
|
||||
b.WriteString(`\t`)
|
||||
case r == '\v':
|
||||
b.WriteString(`\v`)
|
||||
case r < utf8.RuneSelf, r == utf8.RuneError && size == 1:
|
||||
// \xXX, fixed at two hexadecimal characters.
|
||||
fmt.Fprintf(&b, "\\x%02x", rem[0])
|
||||
// Unfortunately, mksh allows \x to consume more hex characters.
|
||||
// Ensure that we don't allow it to read more than two.
|
||||
if lang == LangMirBSDKorn {
|
||||
nextRequoteIfHex = true
|
||||
}
|
||||
case r > utf8.MaxRune:
|
||||
// Not a valid Unicode code point?
|
||||
return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange}
|
||||
case lang == LangMirBSDKorn && r > 0xFFFD:
|
||||
// From the CAVEATS section in R59's man page:
|
||||
//
|
||||
// mksh currently uses OPTU-16 internally, which is the same as
|
||||
// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
|
||||
return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}
|
||||
case r < 0x10000:
|
||||
// \uXXXX, fixed at four hexadecimal characters.
|
||||
fmt.Fprintf(&b, "\\u%04x", r)
|
||||
default:
|
||||
// \UXXXXXXXX, fixed at eight hexadecimal characters.
|
||||
fmt.Fprintf(&b, "\\U%08x", r)
|
||||
}
|
||||
rem = rem[size:]
|
||||
lastRequoteIfHex = nextRequoteIfHex
|
||||
offs += size
|
||||
}
|
||||
b.WriteString("'")
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// Single quotes without any need for escaping.
|
||||
if !strings.Contains(s, "'") {
|
||||
return "'" + s + "'", nil
|
||||
}
|
||||
|
||||
// The string contains single quotes,
|
||||
// so fall back to double quotes.
|
||||
b.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"', '\\', '`', '$':
|
||||
b.WriteByte('\\')
|
||||
}
|
||||
b.WriteRune(r)
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func isHex(r rune) bool {
|
||||
return (r >= '0' && r <= '9') ||
|
||||
(r >= 'a' && r <= 'f') ||
|
||||
(r >= 'A' && r <= 'F')
|
||||
}
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper/internal/syntax"
|
||||
)
|
||||
|
||||
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if
|
||||
@ -76,16 +78,106 @@ type Spec struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// Args returns args except "ssh" itself combined with optional additional command args
|
||||
func (sp *Spec) Args(add ...string) []string {
|
||||
// Args returns args except "ssh" itself combined with optional additional
|
||||
// command and args to be executed on the remote host. It attempts to quote
|
||||
// the given arguments to account for ssh executing the remote command in a
|
||||
// shell. It returns nil when unable to quote the remote command.
|
||||
func (sp *Spec) Args(remoteCommandAndArgs ...string) []string {
|
||||
// Format the remote command to run using the ssh connection, quoting
|
||||
// values where needed because ssh executes these in a POSIX shell.
|
||||
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sshArgs, err := sp.args()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if remoteCommand != "" {
|
||||
sshArgs = append(sshArgs, remoteCommand)
|
||||
}
|
||||
return sshArgs
|
||||
}
|
||||
|
||||
func (sp *Spec) args(sshFlags ...string) ([]string, error) {
|
||||
var args []string
|
||||
if sp.Host == "" {
|
||||
return nil, errors.New("no host specified")
|
||||
}
|
||||
if sp.User != "" {
|
||||
args = append(args, "-l", sp.User)
|
||||
// Quote user, as it's obtained from the URL.
|
||||
usr, err := syntax.Quote(sp.User, syntax.LangPOSIX)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user: %w", err)
|
||||
}
|
||||
args = append(args, "-l", usr)
|
||||
}
|
||||
if sp.Port != "" {
|
||||
args = append(args, "-p", sp.Port)
|
||||
// Quote port, as it's obtained from the URL.
|
||||
port, err := syntax.Quote(sp.Port, syntax.LangPOSIX)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid port: %w", err)
|
||||
}
|
||||
args = append(args, "-p", port)
|
||||
}
|
||||
args = append(args, "--", sp.Host)
|
||||
args = append(args, add...)
|
||||
return args
|
||||
|
||||
// We consider "sshFlags" to be "trusted", and set from code only,
|
||||
// as they are not parsed from the DOCKER_HOST URL.
|
||||
args = append(args, sshFlags...)
|
||||
|
||||
host, err := syntax.Quote(sp.Host, syntax.LangPOSIX)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid host: %w", err)
|
||||
}
|
||||
|
||||
return append(args, "--", host), nil
|
||||
}
|
||||
|
||||
// Command returns the ssh flags and arguments to execute a command
|
||||
// (remoteCommandAndArgs) on the remote host. Where needed, it quotes
|
||||
// values passed in remoteCommandAndArgs to account for ssh executing
|
||||
// the remote command in a shell. It returns an error if no remote command
|
||||
// is passed, or when unable to quote the remote command.
|
||||
//
|
||||
// Important: to preserve backward-compatibility, Command does not currently
|
||||
// perform sanitization or quoting on the sshFlags and callers are expected
|
||||
// to sanitize this argument.
|
||||
func (sp *Spec) Command(sshFlags []string, remoteCommandAndArgs ...string) ([]string, error) {
|
||||
if len(remoteCommandAndArgs) == 0 {
|
||||
return nil, errors.New("no remote command specified")
|
||||
}
|
||||
sshArgs, err := sp.args(sshFlags...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if remoteCommand != "" {
|
||||
sshArgs = append(sshArgs, remoteCommand)
|
||||
}
|
||||
return sshArgs, nil
|
||||
}
|
||||
|
||||
// quoteCommand returns the remote command to run using the ssh connection
|
||||
// as a single string, quoting values where needed because ssh executes
|
||||
// these in a POSIX shell.
|
||||
func quoteCommand(commandAndArgs ...string) (string, error) {
|
||||
var quotedCmd string
|
||||
for i, arg := range commandAndArgs {
|
||||
a, err := syntax.Quote(arg, syntax.LangPOSIX)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid argument: %w", err)
|
||||
}
|
||||
if i == 0 {
|
||||
quotedCmd = a
|
||||
continue
|
||||
}
|
||||
quotedCmd += " " + a
|
||||
}
|
||||
// each part is quoted appropriately, so now we'll have a full
|
||||
// shell command to pass off to "ssh"
|
||||
return quotedCmd, nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
@ -26,6 +27,28 @@ func TestParseURL(t *testing.T) {
|
||||
Host: "example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bare ssh URL with trailing slash",
|
||||
url: "ssh://example.com/",
|
||||
expectedArgs: []string{
|
||||
"--", "example.com",
|
||||
},
|
||||
expectedSpec: Spec{
|
||||
Host: "example.com",
|
||||
Path: "/",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bare ssh URL with trailing slashes",
|
||||
url: "ssh://example.com//",
|
||||
expectedArgs: []string{
|
||||
"--", "example.com",
|
||||
},
|
||||
expectedSpec: Spec{
|
||||
Host: "example.com",
|
||||
Path: "//",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bare ssh URL and remote command",
|
||||
url: "ssh://example.com",
|
||||
@ -34,7 +57,7 @@ func TestParseURL(t *testing.T) {
|
||||
},
|
||||
expectedArgs: []string{
|
||||
"--", "example.com",
|
||||
"docker", "system", "dial-stdio",
|
||||
`docker system dial-stdio`,
|
||||
},
|
||||
expectedSpec: Spec{
|
||||
Host: "example.com",
|
||||
@ -48,7 +71,7 @@ func TestParseURL(t *testing.T) {
|
||||
},
|
||||
expectedArgs: []string{
|
||||
"--", "example.com",
|
||||
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
|
||||
`docker --host unix:///var/run/docker.sock system dial-stdio`,
|
||||
},
|
||||
expectedSpec: Spec{
|
||||
Host: "example.com",
|
||||
@ -84,6 +107,25 @@ func TestParseURL(t *testing.T) {
|
||||
Path: "/var/run/docker.sock",
|
||||
},
|
||||
},
|
||||
{
|
||||
// This test is only to verify the behavior of ParseURL to
|
||||
// pass through the Path as-is. Neither Spec.Args, nor
|
||||
// Spec.Command use the Path field directly, and it should
|
||||
// likely be deprecated.
|
||||
doc: "bad path",
|
||||
url: `ssh://example.com/var/run/docker.sock '$(echo hello > /hello.txt)'`,
|
||||
remoteCommand: []string{
|
||||
"docker", "--host", `unix:///var/run/docker.sock '$(echo hello > /hello.txt)'`, "system", "dial-stdio",
|
||||
},
|
||||
expectedArgs: []string{
|
||||
"--", "example.com",
|
||||
`docker --host "unix:///var/run/docker.sock '\$(echo hello > /hello.txt)'" system dial-stdio`,
|
||||
},
|
||||
expectedSpec: Spec{
|
||||
Host: "example.com",
|
||||
Path: `/var/run/docker.sock '$(echo hello > /hello.txt)'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "malformed URL",
|
||||
url: "malformed %%url",
|
||||
@ -123,6 +165,21 @@ func TestParseURL(t *testing.T) {
|
||||
url: "https://example.com",
|
||||
expectedError: `invalid SSH URL: incorrect scheme: https`,
|
||||
},
|
||||
{
|
||||
doc: "invalid URL with NUL character",
|
||||
url: "ssh://example.com/var/run/\x00docker.sock",
|
||||
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
|
||||
},
|
||||
{
|
||||
doc: "invalid URL with newline character",
|
||||
url: "ssh://example.com/var/run/docker.sock\n",
|
||||
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
|
||||
},
|
||||
{
|
||||
doc: "invalid URL with control character",
|
||||
url: "ssh://example.com/var/run/\x1bdocker.sock",
|
||||
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
@ -139,3 +196,122 @@ func TestParseURL(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
url string
|
||||
sshFlags []string
|
||||
customCmd []string
|
||||
expectedCmd []string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
doc: "bare ssh URL",
|
||||
url: "ssh://example.com",
|
||||
expectedCmd: []string{
|
||||
"--", "example.com",
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bare ssh URL with trailing slash",
|
||||
url: "ssh://example.com/",
|
||||
expectedCmd: []string{
|
||||
"--", "example.com",
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bare ssh URL with custom ssh flags",
|
||||
url: "ssh://example.com",
|
||||
sshFlags: []string{"-T", "-o", "ConnectTimeout=30", "-oStrictHostKeyChecking=no"},
|
||||
expectedCmd: []string{
|
||||
"-T",
|
||||
"-o", "ConnectTimeout=30",
|
||||
"-oStrictHostKeyChecking=no",
|
||||
"--", "example.com",
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "ssh URL with all options",
|
||||
url: "ssh://me@example.com:10022/var/run/docker.sock",
|
||||
sshFlags: []string{"-T", "-o ConnectTimeout=30"},
|
||||
expectedCmd: []string{
|
||||
"-l", "me",
|
||||
"-p", "10022",
|
||||
"-T",
|
||||
"-o ConnectTimeout=30",
|
||||
"--", "example.com",
|
||||
"docker '--host=unix:///var/run/docker.sock' system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bad ssh flags",
|
||||
url: "ssh://example.com",
|
||||
sshFlags: []string{"-T", "-o", `ConnectTimeout=30 $(echo hi > /hi.txt)`},
|
||||
expectedCmd: []string{
|
||||
"-T",
|
||||
"-o", `ConnectTimeout=30 $(echo hi > /hi.txt)`,
|
||||
"--", "example.com",
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bad username",
|
||||
url: `ssh://$(shutdown)me@example.com`,
|
||||
expectedCmd: []string{
|
||||
"-l", `'$(shutdown)me'`,
|
||||
"--", "example.com",
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bad hostname",
|
||||
url: `ssh://$(shutdown)example.com`,
|
||||
expectedCmd: []string{
|
||||
"--", `'$(shutdown)example.com'`,
|
||||
"docker system dial-stdio",
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "bad path",
|
||||
url: `ssh://example.com/var/run/docker.sock '$(echo hello > /hello.txt)'`,
|
||||
expectedCmd: []string{
|
||||
"--", "example.com",
|
||||
`docker "--host=unix:///var/run/docker.sock '\$(echo hello > /hello.txt)'" system dial-stdio`,
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "missing command",
|
||||
url: "ssh://example.com",
|
||||
customCmd: []string{},
|
||||
expectedError: "no remote command specified",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
sp, err := ParseURL(tc.url)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var commandAndArgs []string
|
||||
if tc.customCmd == nil {
|
||||
socketPath := sp.Path
|
||||
commandAndArgs = []string{"docker", "system", "dial-stdio"}
|
||||
if strings.Trim(socketPath, "/") != "" {
|
||||
commandAndArgs = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
|
||||
}
|
||||
}
|
||||
|
||||
actualCmd, err := sp.Command(tc.sshFlags, commandAndArgs...)
|
||||
if tc.expectedError == "" {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(actualCmd, tc.expectedCmd), "%+#v", actualCmd)
|
||||
} else {
|
||||
assert.Check(t, is.Error(err, tc.expectedError))
|
||||
assert.Check(t, is.Nil(actualCmd))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,5 +33,8 @@ func IsEnabled() bool {
|
||||
// The default is to log to the debug level which is only
|
||||
// enabled when debugging is enabled.
|
||||
var OTELErrorHandler otel.ErrorHandler = otel.ErrorHandlerFunc(func(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logrus.WithError(err).Debug("otel error")
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.24.4"
|
||||
default = "1.24.5"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# 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.21
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS gen
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.5
|
||||
|
||||
# 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.21
|
||||
|
||||
# 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.24.0
|
||||
ARG BUILDX_VERSION=0.25.0
|
||||
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
|
||||
@ -22,7 +26,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
&& gofumpt --version
|
||||
|
||||
FROM golang AS gotestsum
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.12.3
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=tmpfs,target=/go/src/ \
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.5
|
||||
|
||||
# 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.21
|
||||
ARG GOLANGCI_LINT_VERSION=v2.1.5
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.5
|
||||
|
||||
# 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.21
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
|
||||
@ -937,15 +937,14 @@ PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.micro
|
||||
|
||||
#### CDI devices
|
||||
|
||||
> [!NOTE]
|
||||
> The CDI feature is experimental, and potentially subject to change.
|
||||
> CDI is currently only supported for Linux containers.
|
||||
|
||||
[Container Device Interface
|
||||
(CDI)](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
|
||||
is a standardized mechanism for container runtimes to create containers which
|
||||
are able to interact with third party devices.
|
||||
|
||||
CDI is currently only supported for Linux containers and is enabled by default
|
||||
since Docker Engine 28.3.0.
|
||||
|
||||
With CDI, device configurations are declaratively defined using a JSON or YAML
|
||||
file. In addition to enabling the container to interact with the device node,
|
||||
it also lets you specify additional configuration for the device, such as
|
||||
@ -966,7 +965,7 @@ This starts an `ubuntu` container with access to the specified CDI device,
|
||||
available on the system running the daemon, in one of the configured CDI
|
||||
specification directories.
|
||||
- The CDI feature has been enabled in the daemon; see [Enable CDI
|
||||
devices](https://docs.docker.com/reference/cli/dockerd/#enable-cdi-devices).
|
||||
devices](https://docs.docker.com/reference/cli/dockerd/#configure-cdi-devices).
|
||||
|
||||
### <a name="attach"></a> Attach to STDIN/STDOUT/STDERR (-a, --attach)
|
||||
|
||||
|
||||
@ -840,42 +840,49 @@ $ docker run -it --add-host host.docker.internal:host-gateway \
|
||||
PING host.docker.internal (2001:db8::1111): 56 data bytes
|
||||
```
|
||||
|
||||
### Enable CDI devices
|
||||
|
||||
> [!NOTE]
|
||||
> This is experimental feature and as such doesn't represent a stable API.
|
||||
>
|
||||
> This feature isn't enabled by default. To this feature, set `features.cdi` to
|
||||
> `true` in the `daemon.json` configuration file.
|
||||
### Configure CDI devices
|
||||
|
||||
Container Device Interface (CDI) is a
|
||||
[standardized](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
|
||||
mechanism for container runtimes to create containers which are able to
|
||||
interact with third party devices.
|
||||
|
||||
CDI is currently only supported for Linux containers and is enabled by default
|
||||
since Docker Engine 28.3.0.
|
||||
|
||||
The Docker daemon supports running containers with CDI devices if the requested
|
||||
device specifications are available on the filesystem of the daemon.
|
||||
|
||||
The default specification directors are:
|
||||
The default specification directories are:
|
||||
|
||||
- `/etc/cdi/` for static CDI Specs
|
||||
- `/var/run/cdi` for generated CDI Specs
|
||||
|
||||
Alternatively, you can set custom locations for CDI specifications using the
|
||||
#### Set custom locations
|
||||
|
||||
To set custom locations for CDI specifications, use the
|
||||
`cdi-spec-dirs` option in the `daemon.json` configuration file, or the
|
||||
`--cdi-spec-dir` flag for the `dockerd` CLI.
|
||||
`--cdi-spec-dir` flag for the `dockerd` CLI:
|
||||
|
||||
```json
|
||||
{
|
||||
"features": {
|
||||
"cdi": true
|
||||
},
|
||||
"cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"]
|
||||
}
|
||||
```
|
||||
|
||||
When CDI is enabled for a daemon, you can view the configured CDI specification
|
||||
directories using the `docker info` command.
|
||||
You can view the configured CDI specification directories using the `docker info` command.
|
||||
|
||||
#### Disable CDI devices
|
||||
|
||||
The feature in enabled by default. To disable it, use the `cdi` options in the `deamon.json` file:
|
||||
|
||||
```json
|
||||
"features": {
|
||||
"cdi": false
|
||||
},
|
||||
```
|
||||
|
||||
To check the status of the CDI devices, run `docker info`.
|
||||
|
||||
#### Daemon logging format {#log-format}
|
||||
|
||||
|
||||
@ -240,7 +240,7 @@ func TestPromptExitCode(t *testing.T) {
|
||||
case <-writeDone:
|
||||
buf.Reset()
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, buf.String(), "\n", "expected a new line after the process exits from SIGINT")
|
||||
assert.Assert(t, strings.HasSuffix(buf.String(), "\n"), "expected a new line after the process exits from SIGINT")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
2
e2e/testdata/Dockerfile.gencerts
vendored
2
e2e/testdata/Dockerfile.gencerts
vendored
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.24.4
|
||||
ARG GO_VERSION=1.24.5
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS generated
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
15
internal/test/randomid.go
Normal file
15
internal/test/randomid.go
Normal file
@ -0,0 +1,15 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// RandomID returns a unique, 64-character ID consisting of a-z, 0-9.
|
||||
func RandomID() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
@ -15,19 +15,39 @@ var InfoHeader = Str{
|
||||
Fancy: aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")),
|
||||
}
|
||||
|
||||
func (o Output) PrintNote(format string, args ...any) {
|
||||
type options struct {
|
||||
header Str
|
||||
}
|
||||
|
||||
type noteOptions func(o *options)
|
||||
|
||||
func withHeader(header Str) noteOptions {
|
||||
return func(o *options) {
|
||||
o.header = header
|
||||
}
|
||||
}
|
||||
|
||||
func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) {
|
||||
if o.isTerminal {
|
||||
// TODO: Handle all flags
|
||||
format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform"))
|
||||
}
|
||||
|
||||
header := o.Sprint(InfoHeader)
|
||||
opt := &options{
|
||||
header: InfoHeader,
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprint(o, "\n", header)
|
||||
for _, override := range opts {
|
||||
override(opt)
|
||||
}
|
||||
|
||||
h := o.Sprint(opt.header)
|
||||
|
||||
_, _ = fmt.Fprint(o, "\n", h)
|
||||
s := fmt.Sprintf(format, args...)
|
||||
for idx, line := range strings.Split(s, "\n") {
|
||||
if idx > 0 {
|
||||
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(header)))
|
||||
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(h)))
|
||||
}
|
||||
|
||||
l := line
|
||||
@ -37,3 +57,16 @@ func (o Output) PrintNote(format string, args ...any) {
|
||||
_, _ = fmt.Fprintln(o, l)
|
||||
}
|
||||
}
|
||||
|
||||
func (o Output) PrintNote(format string, args ...any) {
|
||||
o.printNoteWithOptions(format, args, withHeader(InfoHeader))
|
||||
}
|
||||
|
||||
var warningHeader = Str{
|
||||
Plain: " Warn -> ",
|
||||
Fancy: aec.Bold.Apply(aec.LightYellowB.Apply(aec.BlackF.Apply("w")) + " " + ColorWarning.Apply("Warn → ")),
|
||||
}
|
||||
|
||||
func (o Output) PrintWarning(format string, args ...any) {
|
||||
o.printNoteWithOptions(format, args, withHeader(warningHeader))
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ require (
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.10.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible // v28.x branch / v28.3.0-rc.2
|
||||
github.com/docker/docker v28.3.1+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.9.3
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
|
||||
@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09f
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible h1:aIFvT3snnLS3DAg38wPrGS9WEcV86fTeONAO/W6N+Y8=
|
||||
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
|
||||
github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
|
||||
44
vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go
generated
vendored
44
vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go
generated
vendored
@ -1,44 +0,0 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
)
|
||||
|
||||
// NewAtomicFileWriter returns WriteCloser so that writing to it writes to a
|
||||
// temporary file and closing it atomically changes the temporary file to
|
||||
// destination path. Writing and closing concurrently is not allowed.
|
||||
// NOTE: umask is not considered for the file's permissions.
|
||||
//
|
||||
// Deprecated: use [atomicwriter.New] instead.
|
||||
func NewAtomicFileWriter(filename string, perm os.FileMode) (io.WriteCloser, error) {
|
||||
return atomicwriter.New(filename, perm)
|
||||
}
|
||||
|
||||
// AtomicWriteFile atomically writes data to a file named by filename and with the specified permission bits.
|
||||
// NOTE: umask is not considered for the file's permissions.
|
||||
//
|
||||
// Deprecated: use [atomicwriter.WriteFile] instead.
|
||||
func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||
return atomicwriter.WriteFile(filename, data, perm)
|
||||
}
|
||||
|
||||
// AtomicWriteSet is used to atomically write a set
|
||||
// of files and ensure they are visible at the same time.
|
||||
// Must be committed to a new directory.
|
||||
//
|
||||
// Deprecated: use [atomicwriter.WriteSet] instead.
|
||||
type AtomicWriteSet = atomicwriter.WriteSet
|
||||
|
||||
// NewAtomicWriteSet creates a new atomic write set to
|
||||
// atomically create a set of files. The given directory
|
||||
// is used as the base directory for storing files before
|
||||
// commit. If no temporary directory is given the system
|
||||
// default is used.
|
||||
//
|
||||
// Deprecated: use [atomicwriter.NewWriteSet] instead.
|
||||
func NewAtomicWriteSet(tmpDir string) (*atomicwriter.WriteSet, error) {
|
||||
return atomicwriter.NewWriteSet(tmpDir)
|
||||
}
|
||||
118
vendor/github.com/docker/docker/pkg/ioutils/readers.go
generated
vendored
118
vendor/github.com/docker/docker/pkg/ioutils/readers.go
generated
vendored
@ -1,118 +0,0 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/containerd/log"
|
||||
)
|
||||
|
||||
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
|
||||
// It calls the given callback function when closed. It should be constructed
|
||||
// with NewReadCloserWrapper
|
||||
type readCloserWrapper struct {
|
||||
io.Reader
|
||||
closer func() error
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// Close calls back the passed closer function
|
||||
func (r *readCloserWrapper) Close() error {
|
||||
if !r.closed.CompareAndSwap(false, true) {
|
||||
subsequentCloseWarn("ReadCloserWrapper")
|
||||
return nil
|
||||
}
|
||||
return r.closer()
|
||||
}
|
||||
|
||||
// NewReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser.
|
||||
// It calls the given callback function when closed.
|
||||
func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
|
||||
return &readCloserWrapper{
|
||||
Reader: r,
|
||||
closer: closer,
|
||||
}
|
||||
}
|
||||
|
||||
// cancelReadCloser wraps an io.ReadCloser with a context for cancelling read
|
||||
// operations.
|
||||
type cancelReadCloser struct {
|
||||
cancel func()
|
||||
pR *io.PipeReader // Stream to read from
|
||||
pW *io.PipeWriter
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
// NewCancelReadCloser creates a wrapper that closes the ReadCloser when the
|
||||
// context is cancelled. The returned io.ReadCloser must be closed when it is
|
||||
// no longer needed.
|
||||
func NewCancelReadCloser(ctx context.Context, in io.ReadCloser) io.ReadCloser {
|
||||
pR, pW := io.Pipe()
|
||||
|
||||
// Create a context used to signal when the pipe is closed
|
||||
doneCtx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
p := &cancelReadCloser{
|
||||
cancel: cancel,
|
||||
pR: pR,
|
||||
pW: pW,
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, err := io.Copy(pW, in)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// If the context was closed, p.closeWithError
|
||||
// was already called. Calling it again would
|
||||
// change the error that Read returns.
|
||||
default:
|
||||
p.closeWithError(err)
|
||||
}
|
||||
in.Close()
|
||||
}()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
p.closeWithError(ctx.Err())
|
||||
case <-doneCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Read wraps the Read method of the pipe that provides data from the wrapped
|
||||
// ReadCloser.
|
||||
func (p *cancelReadCloser) Read(buf []byte) (int, error) {
|
||||
return p.pR.Read(buf)
|
||||
}
|
||||
|
||||
// closeWithError closes the wrapper and its underlying reader. It will
|
||||
// cause future calls to Read to return err.
|
||||
func (p *cancelReadCloser) closeWithError(err error) {
|
||||
_ = p.pW.CloseWithError(err)
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
// Close closes the wrapper its underlying reader. It will cause
|
||||
// future calls to Read to return io.EOF.
|
||||
func (p *cancelReadCloser) Close() error {
|
||||
if !p.closed.CompareAndSwap(false, true) {
|
||||
subsequentCloseWarn("cancelReadCloser")
|
||||
return nil
|
||||
}
|
||||
p.closeWithError(io.EOF)
|
||||
return nil
|
||||
}
|
||||
|
||||
func subsequentCloseWarn(name string) {
|
||||
log.G(context.TODO()).Error("subsequent attempt to close " + name)
|
||||
if log.GetLevel() >= log.DebugLevel {
|
||||
log.G(context.TODO()).Errorf("stack trace: %s", string(debug.Stack()))
|
||||
}
|
||||
}
|
||||
96
vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go
generated
vendored
96
vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go
generated
vendored
@ -1,96 +0,0 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// WriteFlusher wraps the Write and Flush operation ensuring that every write
|
||||
// is a flush. In addition, the Close method can be called to intercept
|
||||
// Read/Write calls if the targets lifecycle has already ended.
|
||||
type WriteFlusher struct {
|
||||
w io.Writer
|
||||
flusher flusher
|
||||
flushed chan struct{}
|
||||
flushedOnce sync.Once
|
||||
closed chan struct{}
|
||||
closeLock sync.Mutex
|
||||
}
|
||||
|
||||
type flusher interface {
|
||||
Flush()
|
||||
}
|
||||
|
||||
func (wf *WriteFlusher) Write(b []byte) (int, error) {
|
||||
select {
|
||||
case <-wf.closed:
|
||||
return 0, io.EOF
|
||||
default:
|
||||
}
|
||||
|
||||
n, err := wf.w.Write(b)
|
||||
wf.Flush() // every write is a flush.
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Flush the stream immediately.
|
||||
func (wf *WriteFlusher) Flush() {
|
||||
select {
|
||||
case <-wf.closed:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
wf.flushedOnce.Do(func() {
|
||||
close(wf.flushed)
|
||||
})
|
||||
wf.flusher.Flush()
|
||||
}
|
||||
|
||||
// Flushed returns the state of flushed.
|
||||
// If it's flushed, return true, or else it return false.
|
||||
func (wf *WriteFlusher) Flushed() bool {
|
||||
// BUG(stevvooe): Remove this method. Its use is inherently racy. Seems to
|
||||
// be used to detect whether or a response code has been issued or not.
|
||||
// Another hook should be used instead.
|
||||
var flushed bool
|
||||
select {
|
||||
case <-wf.flushed:
|
||||
flushed = true
|
||||
default:
|
||||
}
|
||||
return flushed
|
||||
}
|
||||
|
||||
// Close closes the write flusher, disallowing any further writes to the
|
||||
// target. After the flusher is closed, all calls to write or flush will
|
||||
// result in an error.
|
||||
func (wf *WriteFlusher) Close() error {
|
||||
wf.closeLock.Lock()
|
||||
defer wf.closeLock.Unlock()
|
||||
|
||||
select {
|
||||
case <-wf.closed:
|
||||
return io.EOF
|
||||
default:
|
||||
close(wf.closed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nopFlusher represents a type which flush operation is nop.
|
||||
type nopFlusher struct{}
|
||||
|
||||
// Flush is a nop operation.
|
||||
func (f *nopFlusher) Flush() {}
|
||||
|
||||
// NewWriteFlusher returns a new WriteFlusher.
|
||||
func NewWriteFlusher(w io.Writer) *WriteFlusher {
|
||||
var fl flusher
|
||||
if f, ok := w.(flusher); ok {
|
||||
fl = f
|
||||
} else {
|
||||
fl = &nopFlusher{}
|
||||
}
|
||||
return &WriteFlusher{w: w, flusher: fl, closed: make(chan struct{}), flushed: make(chan struct{})}
|
||||
}
|
||||
28
vendor/github.com/docker/docker/pkg/ioutils/writers.go
generated
vendored
28
vendor/github.com/docker/docker/pkg/ioutils/writers.go
generated
vendored
@ -1,28 +0,0 @@
|
||||
package ioutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type writeCloserWrapper struct {
|
||||
io.Writer
|
||||
closer func() error
|
||||
closed atomic.Bool
|
||||
}
|
||||
|
||||
func (r *writeCloserWrapper) Close() error {
|
||||
if !r.closed.CompareAndSwap(false, true) {
|
||||
subsequentCloseWarn("WriteCloserWrapper")
|
||||
return nil
|
||||
}
|
||||
return r.closer()
|
||||
}
|
||||
|
||||
// NewWriteCloserWrapper returns a new io.WriteCloser.
|
||||
func NewWriteCloserWrapper(r io.Writer, closer func() error) io.WriteCloser {
|
||||
return &writeCloserWrapper{
|
||||
Writer: r,
|
||||
closer: closer,
|
||||
}
|
||||
}
|
||||
42
vendor/github.com/docker/docker/pkg/longpath/longpath.go
generated
vendored
42
vendor/github.com/docker/docker/pkg/longpath/longpath.go
generated
vendored
@ -1,42 +0,0 @@
|
||||
// Package longpath introduces some constants and helper functions for handling
|
||||
// long paths in Windows.
|
||||
//
|
||||
// Long paths are expected to be prepended with "\\?\" and followed by either a
|
||||
// drive letter, a UNC server\share, or a volume identifier.
|
||||
package longpath
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// longPathPrefix is the longpath prefix for Windows file paths.
|
||||
const longPathPrefix = `\\?\`
|
||||
|
||||
// AddPrefix adds the Windows long path prefix to the path provided if
|
||||
// it does not already have it.
|
||||
func AddPrefix(path string) string {
|
||||
if strings.HasPrefix(path, longPathPrefix) {
|
||||
return path
|
||||
}
|
||||
if strings.HasPrefix(path, `\\`) {
|
||||
// This is a UNC path, so we need to add 'UNC' to the path as well.
|
||||
return longPathPrefix + `UNC` + path[1:]
|
||||
}
|
||||
return longPathPrefix + path
|
||||
}
|
||||
|
||||
// MkdirTemp is the equivalent of [os.MkdirTemp], except that on Windows
|
||||
// the result is in Windows longpath format. On Unix systems it is
|
||||
// equivalent to [os.MkdirTemp].
|
||||
func MkdirTemp(dir, prefix string) (string, error) {
|
||||
tempDir, err := os.MkdirTemp(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
return tempDir, nil
|
||||
}
|
||||
return AddPrefix(tempDir), nil
|
||||
}
|
||||
63
vendor/github.com/docker/docker/pkg/stringid/stringid.go
generated
vendored
63
vendor/github.com/docker/docker/pkg/stringid/stringid.go
generated
vendored
@ -1,63 +0,0 @@
|
||||
// Package stringid provides helper functions for dealing with string identifiers
|
||||
package stringid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
shortLen = 12
|
||||
fullLen = 64
|
||||
)
|
||||
|
||||
// TruncateID returns a shorthand version of a string identifier for convenience.
|
||||
// A collision with other shorthands is very unlikely, but possible.
|
||||
// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller
|
||||
// will need to use a longer prefix, or the full-length Id.
|
||||
func TruncateID(id string) string {
|
||||
if i := strings.IndexRune(id, ':'); i >= 0 {
|
||||
id = id[i+1:]
|
||||
}
|
||||
if len(id) > shortLen {
|
||||
id = id[:shortLen]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// GenerateRandomID returns a unique, 64-character ID consisting of a-z, 0-9.
|
||||
// It guarantees that the ID, when truncated ([TruncateID]) does not consist
|
||||
// of numbers only, so that the truncated ID can be used as hostname for
|
||||
// containers.
|
||||
func GenerateRandomID() string {
|
||||
b := make([]byte, 32)
|
||||
for {
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err) // This shouldn't happen
|
||||
}
|
||||
id := hex.EncodeToString(b)
|
||||
|
||||
// make sure that the truncated ID does not consist of only numeric
|
||||
// characters, as it's used as default hostname for containers.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/moby/moby/issues/3869
|
||||
// - https://bugzilla.redhat.com/show_bug.cgi?id=1059122
|
||||
if allNum(id[:shortLen]) {
|
||||
// all numbers; try again
|
||||
continue
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// allNum checks whether id consists of only numbers (0-9).
|
||||
func allNum(id string) bool {
|
||||
for _, c := range []byte(id) {
|
||||
if c > '9' || c < '0' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
5
vendor/modules.txt
vendored
5
vendor/modules.txt
vendored
@ -65,7 +65,7 @@ github.com/docker/distribution/registry/client/transport
|
||||
github.com/docker/distribution/registry/storage/cache
|
||||
github.com/docker/distribution/registry/storage/cache/memory
|
||||
github.com/docker/distribution/uuid
|
||||
# github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible
|
||||
# github.com/docker/docker v28.3.1+incompatible
|
||||
## explicit
|
||||
github.com/docker/docker/api
|
||||
github.com/docker/docker/api/types
|
||||
@ -95,14 +95,11 @@ github.com/docker/docker/client
|
||||
github.com/docker/docker/internal/lazyregexp
|
||||
github.com/docker/docker/internal/multierror
|
||||
github.com/docker/docker/pkg/homedir
|
||||
github.com/docker/docker/pkg/ioutils
|
||||
github.com/docker/docker/pkg/jsonmessage
|
||||
github.com/docker/docker/pkg/longpath
|
||||
github.com/docker/docker/pkg/process
|
||||
github.com/docker/docker/pkg/progress
|
||||
github.com/docker/docker/pkg/stdcopy
|
||||
github.com/docker/docker/pkg/streamformatter
|
||||
github.com/docker/docker/pkg/stringid
|
||||
github.com/docker/docker/registry
|
||||
# github.com/docker/docker-credential-helpers v0.9.3
|
||||
## explicit; go 1.21
|
||||
|
||||
Reference in New Issue
Block a user