diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30bb4e0b027a..b6b14d113f7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Create matrix id: platforms @@ -88,7 +88,7 @@ jobs: fi - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.ARTIFACT_NAME }} path: /tmp/out/* @@ -143,7 +143,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Create matrix id: platforms diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4ba63f3f030c..0cc0746a4a99 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 2 # CodeQL 2.16.4's auto-build added support for multi-module repositories, @@ -61,19 +61,19 @@ jobs: ln -s vendor.sum go.sum - name: Update Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: "1.24.5" + go-version: "1.24.9" - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 with: category: "/language:go" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 87433ec00704..1ba37f0318bc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -39,12 +39,11 @@ jobs: engine-version: - 28 # latest - 27 # latest - 1 - - 26 # github actions default - - 23 # mirantis lts + - 25 # mirantis lts steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Update daemon.json run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b84754248c7..cbbeeaec7e0e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,20 +53,21 @@ jobs: fail-fast: false matrix: os: - - macos-13 # macOS 13 on Intel - - macos-14 # macOS 14 on arm64 (Apple Silicon M1) + - macos-14 # macOS 14 on arm64 (Apple Silicon M1) + - macos-15-intel # macOS 15 on Intel + - macos-15 # macOS 15 on arm64 (Apple Silicon M1) # - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: ${{ env.GOPATH }}/src/github.com/docker/cli - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: "1.24.5" + go-version: "1.24.9" - name: Test run: | diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 9bd443c13d77..63b688bcc696 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -48,7 +48,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Generate shell: 'script --return --quiet --command "bash {0}"' @@ -74,7 +74,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Run shell: 'script --return --quiet --command "bash {0}"' diff --git a/.golangci.yml b/.golangci.yml index 0df356b2309d..8940b9accb7c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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.5" + go: "1.24.9" timeout: 5m @@ -86,6 +86,8 @@ linters: desc: Use github.com/moby/sys/userns instead. - pkg: "github.com/containerd/containerd/platforms" desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead. + - pkg: "github.com/docker/docker/errdefs" + desc: Use github.com/containerd/errdefs instead. - pkg: "github.com/docker/docker/pkg/system" desc: This package should not be used unless strictly necessary. - pkg: "github.com/docker/distribution/uuid" @@ -124,10 +126,9 @@ linters: no-unaliased: true alias: - # Enforce alias to prevent it accidentally being used instead of our - # own errdefs package (or vice-versa). - - pkg: github.com/containerd/errdefs - alias: cerrdefs + # Should no longer be aliased, because we no longer allow moby/docker errdefs. + - pkg: "github.com/docker/docker/errdefs" + alias: "" - pkg: github.com/opencontainers/image-spec/specs-go/v1 alias: ocispec # Enforce that gotest.tools/v3/assert/cmp is always aliased as "is" @@ -221,6 +222,14 @@ linters: linters: - staticcheck + # Ignore deprecation linting for cli/command/stack/*. + # + # FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389 + - text: '^(SA1019): ' + path: "cli/command/stack" + linters: + - staticcheck + # Log a warning if an exclusion rule is unused. # Default: false warn-unused: true diff --git a/Dockerfile b/Dockerfile index 6ea150890011..9acd85f1a478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,13 @@ ARG BASE_VARIANT=alpine ARG ALPINE_VERSION=3.21 ARG BASE_DEBIAN_DISTRO=bookworm -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.24.9 ARG XX_VERSION=1.6.1 ARG GOVERSIONINFO_VERSION=v1.4.1 # 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 +ARG GOTESTSUM_VERSION=v1.13.0 # BUILDX_VERSION sets the version of buildx to use for the e2e tests. # It must be a tag in the docker.io/docker/buildx-bin image repository @@ -31,7 +31,7 @@ FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine ENV GOTOOLCHAIN=local COPY --link --from=xx / / -RUN apk add --no-cache bash clang lld llvm file git +RUN apk add --no-cache bash clang lld llvm file git git-daemon WORKDIR /go/src/github.com/docker/cli FROM build-base-alpine AS build-alpine diff --git a/VERSION b/VERSION index af15bc84df1f..5440ff0cd4c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -28.3.0-dev +28.4.0-dev diff --git a/cli-plugins/manager/annotations.go b/cli-plugins/manager/annotations.go index 8fc76b73c9bc..abe47fe091a5 100644 --- a/cli-plugins/manager/annotations.go +++ b/cli-plugins/manager/annotations.go @@ -6,25 +6,35 @@ const ( // CommandAnnotationPlugin is added to every stub command added by // AddPluginCommandStubs with the value "true" and so can be // used to distinguish plugin stubs from regular commands. + // + // Deprecated: use [metadata.CommandAnnotationPlugin]. This alias will be removed in the next release. CommandAnnotationPlugin = metadata.CommandAnnotationPlugin // CommandAnnotationPluginVendor is added to every stub command // added by AddPluginCommandStubs and contains the vendor of // that plugin. + // + // Deprecated: use [metadata.CommandAnnotationPluginVendor]. This alias will be removed in the next release. CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor // CommandAnnotationPluginVersion is added to every stub command // added by AddPluginCommandStubs and contains the version of // that plugin. + // + // Deprecated: use [metadata.CommandAnnotationPluginVersion]. This alias will be removed in the next release. CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion // CommandAnnotationPluginInvalid is added to any stub command // added by AddPluginCommandStubs for an invalid command (that // is, one which failed it's candidate test) and contains the // reason for the failure. + // + // Deprecated: use [metadata.CommandAnnotationPluginInvalid]. This alias will be removed in the next release. CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid // CommandAnnotationPluginCommandPath is added to overwrite the // command path for a plugin invocation. + // + // Deprecated: use [metadata.CommandAnnotationPluginCommandPath]. This alias will be removed in the next release. CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath ) diff --git a/cli-plugins/manager/candidate.go b/cli-plugins/manager/candidate.go index d809926cf62b..a4253968dcca 100644 --- a/cli-plugins/manager/candidate.go +++ b/cli-plugins/manager/candidate.go @@ -6,12 +6,6 @@ import ( "github.com/docker/cli/cli-plugins/metadata" ) -// Candidate represents a possible plugin candidate, for mocking purposes -type Candidate interface { - Path() string - Metadata() ([]byte, error) -} - type candidate struct { path string } diff --git a/cli-plugins/manager/candidate_test.go b/cli-plugins/manager/candidate_test.go index e8df42909949..a0dbb0dd7b56 100644 --- a/cli-plugins/manager/candidate_test.go +++ b/cli-plugins/manager/candidate_test.go @@ -32,14 +32,12 @@ func (c *fakeCandidate) Metadata() ([]byte, error) { func TestValidateCandidate(t *testing.T) { const ( goodPluginName = metadata.NamePrefix + "goodplugin" + builtinName = metadata.NamePrefix + "builtin" + builtinAlias = metadata.NamePrefix + "alias" - builtinName = metadata.NamePrefix + "builtin" - builtinAlias = metadata.NamePrefix + "alias" - - badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" - badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" - goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName - metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}` + badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" + badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" + goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName ) fakeroot := &cobra.Command{Use: "docker"} @@ -51,31 +49,103 @@ func TestValidateCandidate(t *testing.T) { }) for _, tc := range []struct { - name string - c *fakeCandidate + name string + plugin *fakeCandidate // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). err string invalid string + expVer string }{ - /* Each failing one of the tests */ - {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"}, - {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)}, - {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"}, - {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`}, - {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`}, - {name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)}, - {name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"}, - {name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`}, - {name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`}, - {name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"}, - {name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"}, - // This one should work - {name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}}, - {name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}}, + // Invalid cases. + { + name: "empty path", + plugin: &fakeCandidate{path: ""}, + err: "plugin candidate path cannot be empty", + }, + { + name: "bad prefix", + plugin: &fakeCandidate{path: badPrefixPath}, + err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix), + }, + { + name: "bad path", + plugin: &fakeCandidate{path: badNamePath}, + invalid: "did not match", + }, + { + name: "builtin command", + plugin: &fakeCandidate{path: builtinName}, + invalid: `plugin "builtin" duplicates builtin command`, + }, + { + name: "builtin alias", + plugin: &fakeCandidate{path: builtinAlias}, + invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`, + }, + { + name: "fetch failure", + plugin: &fakeCandidate{path: goodPluginPath, exec: false}, + invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath), + }, + { + name: "metadata not json", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, + invalid: "invalid character", + }, + { + name: "empty schemaversion", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, + invalid: `plugin SchemaVersion version cannot be empty`, + }, + { + name: "invalid schemaversion", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, + invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be ..`, + }, + { + name: "invalid schemaversion major", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`}, + invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`, + }, + { + name: "no vendor", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, + invalid: "plugin metadata does not define a vendor", + }, + { + name: "empty vendor", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, + invalid: "plugin metadata does not define a vendor", + }, + + // Valid cases. + { + name: "valid", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}, + expVer: "0.1.0", + }, + { + // Including the deprecated "experimental" field should not break processing. + name: "with legacy experimental", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`}, + expVer: "0.1.0", + }, + { + // note that this may not be supported by older CLIs + name: "new minor schema version", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`}, + expVer: "0.2.0", + }, + { + // note that this may not be supported by older CLIs + name: "new major schema version", + plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`}, + expVer: "1.0.0", + }, } { t.Run(tc.name, func(t *testing.T) { - p, err := newPlugin(tc.c, fakeroot.Commands()) + p, err := newPlugin(tc.plugin, fakeroot.Commands()) switch { case tc.err != "": assert.ErrorContains(t, err, tc.err) @@ -86,7 +156,7 @@ func TestValidateCandidate(t *testing.T) { default: assert.NilError(t, err) assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) - assert.Equal(t, p.SchemaVersion, "0.1.0") + assert.Equal(t, p.SchemaVersion, tc.expVer) assert.Equal(t, p.Vendor, "e2e-testing") } }) diff --git a/cli-plugins/manager/cobra.go b/cli-plugins/manager/cobra.go index ddf067be1dbd..57ec4a7909ec 100644 --- a/cli-plugins/manager/cobra.go +++ b/cli-plugins/manager/cobra.go @@ -38,6 +38,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e rootCmd.AddCommand(&cobra.Command{ Use: p.Name, Short: p.ShortDescription, + Hidden: p.Hidden, Run: func(_ *cobra.Command, _ []string) {}, Annotations: annotations, DisableFlagParsing: true, diff --git a/cli-plugins/manager/error.go b/cli-plugins/manager/error.go index 1b5f4f60e54e..aaedae14fad3 100644 --- a/cli-plugins/manager/error.go +++ b/cli-plugins/manager/error.go @@ -23,11 +23,6 @@ func (e *pluginError) Error() string { return e.cause.Error() } -// Cause satisfies the errors.causer interface for pluginError. -func (e *pluginError) Cause() error { - return e.cause -} - // Unwrap provides compatibility for Go 1.13 error chains. func (e *pluginError) Unwrap() error { return e.cause @@ -41,14 +36,11 @@ func (e *pluginError) MarshalText() (text []byte, err error) { // wrapAsPluginError wraps an error in a pluginError with an // additional message. func wrapAsPluginError(err error, msg string) error { - if err == nil { - return nil - } return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)} } -// NewPluginError creates a new pluginError, analogous to +// newPluginError creates a new pluginError, analogous to // errors.Errorf. -func NewPluginError(msg string, args ...any) error { +func newPluginError(msg string, args ...any) error { return &pluginError{cause: fmt.Errorf(msg, args...)} } diff --git a/cli-plugins/manager/error_test.go b/cli-plugins/manager/error_test.go index c4cb19bd55d5..2e92001c20ed 100644 --- a/cli-plugins/manager/error_test.go +++ b/cli-plugins/manager/error_test.go @@ -10,7 +10,7 @@ import ( ) func TestPluginError(t *testing.T) { - err := NewPluginError("new error") + err := newPluginError("new error") assert.Check(t, is.Error(err, "new error")) inner := errors.New("testing") @@ -21,4 +21,7 @@ func TestPluginError(t *testing.T) { actual, err := json.Marshal(err) assert.Check(t, err) assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual))) + + err = wrapAsPluginError(nil, "wrapping") + assert.Check(t, is.Error(err, "wrapping: %!w()")) } diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index 25515bccb868..c8ea15de395b 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -2,6 +2,7 @@ package manager import ( "context" + "errors" "os" "os/exec" "path/filepath" @@ -9,9 +10,11 @@ import ( "strings" "sync" + "github.com/containerd/errdefs" "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/debug" "github.com/fvbommel/sortorder" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -22,13 +25,9 @@ const ( // used to originally invoke the docker CLI when executing a // plugin. Assuming $PATH and $CWD remain unchanged this should allow // the plugin to re-execute the original CLI. - ReexecEnvvar = metadata.ReexecEnvvar - - // ResourceAttributesEnvvar is the name of the envvar that includes additional - // resource attributes for OTEL. // - // Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release. - ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" + // Deprecated: use [metadata.ReexecEnvvar]. This alias will be removed in the next release. + ReexecEnvvar = metadata.ReexecEnvvar ) // errPluginNotFound is the error returned when a plugin could not be found. @@ -40,15 +39,11 @@ func (e errPluginNotFound) Error() string { return "Error: No such CLI plugin: " + string(e) } -type notFound interface{ NotFound() } - // IsNotFound is true if the given error is due to a plugin not being found. +// +// Deprecated: use [errdefs.IsNotFound]. func IsNotFound(err error) bool { - if e, ok := err.(*pluginError); ok { - err = e.Cause() - } - _, ok := err.(notFound) - return ok + return errdefs.IsNotFound(err) } // getPluginDirs returns the platform-specific locations to search for plugins @@ -81,9 +76,17 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) { return } for _, dentry := range dentries { - switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list - case 0, os.ModeSymlink: - // Regular file or symlink, keep going + switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list + case os.ModeSymlink: + if !debug.IsEnabled() { + // Skip broken symlinks unless debug is enabled. With debug + // enabled, this will print a warning in "docker info". + if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) { + continue + } + } + case 0: + // Regular file, keep going default: // Something else, ignore. continue @@ -127,7 +130,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi if err != nil { return nil, err } - if !IsNotFound(p.Err) { + if !errdefs.IsNotFound(p.Err) { p.ShadowedPaths = paths[1:] } return &p, nil @@ -164,7 +167,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e if err != nil { return err } - if !IsNotFound(p.Err) { + if !errdefs.IsNotFound(p.Err) { p.ShadowedPaths = paths[1:] mu.Lock() defer mu.Unlock() @@ -185,9 +188,9 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e return plugins, nil } -// PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin. +// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin. // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts. -// The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow. +// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow. func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { // This uses the full original args, not the args which may // have been provided by cobra to our caller. This is because diff --git a/cli-plugins/manager/manager_test.go b/cli-plugins/manager/manager_test.go index 64609fbbad39..16c43b29d338 100644 --- a/cli-plugins/manager/manager_test.go +++ b/cli-plugins/manager/manager_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" @@ -37,7 +38,7 @@ func TestListPluginCandidates(t *testing.T) { "plugins3-target", // Will be referenced as a symlink from below fs.WithFile("docker-plugin1", ""), fs.WithDir("ignored3"), - fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later) + fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ... fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should. ), @@ -71,9 +72,6 @@ func TestListPluginCandidates(t *testing.T) { "hardlink2": { dir.Join("plugins2", "docker-hardlink2"), }, - "brokensymlink": { - dir.Join("plugins3", "docker-brokensymlink"), - }, "symlinked": { dir.Join("plugins3", "docker-symlinked"), }, @@ -131,7 +129,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)), _, err = GetPlugin("ccc", cli, &cobra.Command{}) assert.Error(t, err, "Error: No such CLI plugin: ccc") - assert.Assert(t, IsNotFound(err)) + assert.Assert(t, errdefs.IsNotFound(err)) } func TestListPluginsIsSorted(t *testing.T) { @@ -166,8 +164,8 @@ func TestErrPluginNotFound(t *testing.T) { var err error = errPluginNotFound("test") err.(errPluginNotFound).NotFound() assert.Error(t, err, "Error: No such CLI plugin: test") - assert.Assert(t, IsNotFound(err)) - assert.Assert(t, !IsNotFound(nil)) + assert.Assert(t, errdefs.IsNotFound(err)) + assert.Assert(t, !errdefs.IsNotFound(nil)) } func TestGetPluginDirs(t *testing.T) { diff --git a/cli-plugins/manager/metadata.go b/cli-plugins/manager/metadata.go index 9bddb121422d..6fda7fa1af05 100644 --- a/cli-plugins/manager/metadata.go +++ b/cli-plugins/manager/metadata.go @@ -6,18 +6,26 @@ import ( const ( // NamePrefix is the prefix required on all plugin binary names + // + // Deprecated: use [metadata.NamePrefix]. This alias will be removed in a future release. NamePrefix = metadata.NamePrefix // MetadataSubcommandName is the name of the plugin subcommand // which must be supported by every plugin and returns the // plugin metadata. + // + // Deprecated: use [metadata.MetadataSubcommandName]. This alias will be removed in a future release. MetadataSubcommandName = metadata.MetadataSubcommandName // HookSubcommandName is the name of the plugin subcommand // which must be implemented by plugins declaring support // for hooks in their metadata. + // + // Deprecated: use [metadata.HookSubcommandName]. This alias will be removed in a future release. HookSubcommandName = metadata.HookSubcommandName ) // Metadata provided by the plugin. +// +// Deprecated: use [metadata.Metadata]. This alias will be removed in a future release. type Metadata = metadata.Metadata diff --git a/cli-plugins/manager/plugin.go b/cli-plugins/manager/plugin.go index fa846452b548..fd8936174cc3 100644 --- a/cli-plugins/manager/plugin.go +++ b/cli-plugins/manager/plugin.go @@ -2,12 +2,14 @@ package manager import ( "context" + "encoding" "encoding/json" "errors" "fmt" "os" "os/exec" "path/filepath" + "strconv" "strings" "github.com/docker/cli/cli-plugins/metadata" @@ -31,12 +33,34 @@ type Plugin struct { ShadowedPaths []string `json:",omitempty"` } +// MarshalJSON implements [json.Marshaler] to handle marshaling the +// [Plugin.Err] field (Go doesn't marshal errors by default). +func (p *Plugin) MarshalJSON() ([]byte, error) { + type Alias Plugin // avoid recursion + + cp := *p // shallow copy to avoid mutating original + + if cp.Err != nil { + if _, ok := cp.Err.(encoding.TextMarshaler); !ok { + cp.Err = &pluginError{cp.Err} + } + } + + return json.Marshal((*Alias)(&cp)) +} + +// pluginCandidate represents a possible plugin candidate, for mocking purposes. +type pluginCandidate interface { + Path() string + Metadata() ([]byte, error) +} + // newPlugin determines if the given candidate is valid and returns a // Plugin. If the candidate fails one of the tests then `Plugin.Err` // is set, and is always a `pluginError`, but the `Plugin` is still // returned with no error. An error is only returned due to a // non-recoverable error. -func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { +func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { path := c.Path() if path == "" { return Plugin{}, errors.New("plugin candidate path cannot be empty") @@ -63,7 +87,7 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { // Now apply the candidate tests, so these update p.Err. if !pluginNameRe.MatchString(p.Name) { - p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String()) + p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String()) return p, nil } @@ -75,11 +99,11 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { continue } if cmd.Name() == p.Name { - p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name) + p.Err = newPluginError("plugin %q duplicates builtin command", p.Name) return p, nil } if cmd.HasAlias(p.Name) { - p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name()) + p.Err = newPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name()) return p, nil } } @@ -95,17 +119,42 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { p.Err = wrapAsPluginError(err, "invalid metadata") return p, nil } - if p.Metadata.SchemaVersion != "0.1.0" { - p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion) + if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil { + p.Err = &pluginError{cause: err} return p, nil } if p.Metadata.Vendor == "" { - p.Err = NewPluginError("plugin metadata does not define a vendor") + p.Err = newPluginError("plugin metadata does not define a vendor") return p, nil } return p, nil } +// validateSchemaVersion validates if the plugin's schemaVersion is supported. +// +// The current schema-version is "0.1.0", but we don't want to break compatibility +// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0. +// +// Note that CLI versions before 28.4.1 may not support these versions as they were +// hard-coded to only accept "0.1.0". +func validateSchemaVersion(version string) error { + if version == "0.1.0" { + return nil + } + if version == "" { + return errors.New("plugin SchemaVersion version cannot be empty") + } + major, _, ok := strings.Cut(version, ".") + majorVersion, err := strconv.Atoi(major) + if !ok || err != nil { + return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be ..", version) + } + if majorVersion > 1 { + return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version) + } + return nil +} + // RunHook executes the plugin's hooks command // and returns its unprocessed output. func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) { diff --git a/cli-plugins/manager/plugin_test.go b/cli-plugins/manager/plugin_test.go new file mode 100644 index 000000000000..ace51c12c630 --- /dev/null +++ b/cli-plugins/manager/plugin_test.go @@ -0,0 +1,43 @@ +package manager + +import ( + "encoding/json" + "errors" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestPluginMarshal(t *testing.T) { + const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}` + const jsonNoError = `{"Name":"some-plugin"}` + + tests := []struct { + doc string + error error + expected string + }{ + { + doc: "no error", + expected: jsonNoError, + }, + { + doc: "regular error", + error: errors.New("something went wrong"), + expected: jsonWithError, + }, + { + doc: "custom error", + error: newPluginError("something went wrong"), + expected: jsonWithError, + }, + } + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error}) + assert.NilError(t, err) + assert.Check(t, is.Equal(string(actual), tc.expected)) + }) + } +} diff --git a/cli-plugins/metadata/metadata.go b/cli-plugins/metadata/metadata.go index 9d408c00b3dd..7061486a7056 100644 --- a/cli-plugins/metadata/metadata.go +++ b/cli-plugins/metadata/metadata.go @@ -33,4 +33,6 @@ type Metadata struct { ShortDescription string `json:",omitempty"` // URL is a pointer to the plugin's homepage. URL string `json:",omitempty"` + // Hidden hides the plugin in completion and help message output. + Hidden bool `json:",omitempty"` } diff --git a/cli-plugins/plugin/plugin.go b/cli-plugins/plugin/plugin.go index 39d6d8694342..6dca555a85bf 100644 --- a/cli-plugins/plugin/plugin.go +++ b/cli-plugins/plugin/plugin.go @@ -80,19 +80,23 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat return cmd.Execute() } -// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. -func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) { +// Run is the top-level entry point to the CLI plugin framework. It should +// be called from the plugin's "main()" function. It initializes a new +// [command.DockerCli] instance with the given options before calling +// makeCmd to construct the plugin command, then invokes the plugin command +// using [RunPlugin]. +func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) { otel.SetErrorHandler(debug.OTELErrorHandler) - dockerCli, err := command.NewDockerCli() + dockerCLI, err := command.NewDockerCli(ops...) if err != nil { - fmt.Fprintln(os.Stderr, err) + _, _ = fmt.Fprintln(os.Stderr, err) os.Exit(1) } - plugin := makeCmd(dockerCli) + plugin := makeCmd(dockerCLI) - if err := RunPlugin(dockerCli, plugin, meta); err != nil { + if err := RunPlugin(dockerCLI, plugin, meta); err != nil { var stErr cli.StatusError if errors.As(err, &stErr) { // StatusError should only be used for errors, and all errors should @@ -100,10 +104,10 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) { if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. stErr.StatusCode = 1 } - _, _ = fmt.Fprintln(dockerCli.Err(), stErr) + _, _ = fmt.Fprintln(dockerCLI.Err(), stErr) os.Exit(stErr.StatusCode) } - _, _ = fmt.Fprintln(dockerCli.Err(), err) + _, _ = fmt.Fprintln(dockerCLI.Err(), err) os.Exit(1) } } @@ -175,11 +179,24 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta newMetadataSubcommand(plugin, meta), ) - cli.DisableFlagsInUseLine(cmd) + visitAll(cmd, + // prevent adding "[flags]" to the end of the usage line. + func(c *cobra.Command) { c.DisableFlagsInUseLine = true }, + ) return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) } +// visitAll traverses all commands from the root. +func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) { + for _, cmd := range root.Commands() { + visitAll(cmd, fns...) + } + for _, fn := range fns { + fn(root) + } +} + func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command { if meta.ShortDescription == "" { meta.ShortDescription = plugin.Short diff --git a/cli-plugins/plugin/plugin_test.go b/cli-plugins/plugin/plugin_test.go new file mode 100644 index 000000000000..8b2b299fb1c0 --- /dev/null +++ b/cli-plugins/plugin/plugin_test.go @@ -0,0 +1,28 @@ +package plugin + +import ( + "slices" + "testing" + + "github.com/spf13/cobra" +) + +func TestVisitAll(t *testing.T) { + root := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub1sub1 := &cobra.Command{Use: "sub1sub1"} + sub1sub2 := &cobra.Command{Use: "sub1sub2"} + sub2 := &cobra.Command{Use: "sub2"} + + root.AddCommand(sub1, sub2) + sub1.AddCommand(sub1sub1, sub1sub2) + + var visited []string + visitAll(root, func(ccmd *cobra.Command) { + visited = append(visited, ccmd.Name()) + }) + expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"} + if !slices.Equal(expected, visited) { + t.Errorf("expected %#v, got %#v", expected, visited) + } +} diff --git a/cli/cobra.go b/cli/cobra.go index 7a14b6f483b9..793d6b0b1f08 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -168,34 +168,30 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error { } // VisitAll will traverse all commands from the root. -// This is different from the VisitAll of cobra.Command where only parents -// are checked. +// +// Deprecated: this utility was only used internally and will be removed in the next release. func VisitAll(root *cobra.Command, fn func(*cobra.Command)) { + visitAll(root, fn) +} + +func visitAll(root *cobra.Command, fn func(*cobra.Command)) { for _, cmd := range root.Commands() { - VisitAll(cmd, fn) + visitAll(cmd, fn) } fn(root) } // DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all // commands within the tree rooted at cmd. +// +// Deprecated: this utility was only used internally and will be removed in the next release. func DisableFlagsInUseLine(cmd *cobra.Command) { - VisitAll(cmd, func(ccmd *cobra.Command) { + visitAll(cmd, func(ccmd *cobra.Command) { // do not add a `[flags]` to the end of the usage line. ccmd.DisableFlagsInUseLine = true }) } -// HasCompletionArg returns true if a cobra completion arg request is found. -func HasCompletionArg(args []string) bool { - for _, arg := range args { - if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd { - return true - } - } - return false -} - var helpCommand = &cobra.Command{ Use: "help [command]", Short: "Help about the command", @@ -380,13 +376,10 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command { func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command { cmds := []*cobra.Command{} for _, sub := range cmd.Commands() { - if isPlugin(sub) { - if invalidPluginReason(sub) == "" { - cmds = append(cmds, sub) - } + if invalidPluginReason(sub) != "" { continue } - if sub.IsAvailableCommand() && sub.HasSubCommands() { + if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) { cmds = append(cmds, sub) } } diff --git a/cli/cobra_test.go b/cli/cobra_test.go index e1d4bcb2d25f..eb87c75d4184 100644 --- a/cli/cobra_test.go +++ b/cli/cobra_test.go @@ -10,28 +10,6 @@ import ( is "gotest.tools/v3/assert/cmp" ) -func TestVisitAll(t *testing.T) { - root := &cobra.Command{Use: "root"} - sub1 := &cobra.Command{Use: "sub1"} - sub1sub1 := &cobra.Command{Use: "sub1sub1"} - sub1sub2 := &cobra.Command{Use: "sub1sub2"} - sub2 := &cobra.Command{Use: "sub2"} - - root.AddCommand(sub1, sub2) - sub1.AddCommand(sub1sub1, sub1sub2) - - // Take the opportunity to test DisableFlagsInUseLine too - DisableFlagsInUseLine(root) - - var visited []string - VisitAll(root, func(ccmd *cobra.Command) { - visited = append(visited, ccmd.Name()) - assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name()) - }) - expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"} - assert.DeepEqual(t, expected, visited) -} - func TestVendorAndVersion(t *testing.T) { // Non plugin. assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "") @@ -78,6 +56,33 @@ func TestInvalidPlugin(t *testing.T) { assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{})) } +func TestHiddenPlugin(t *testing.T) { + root := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{ + Use: "sub1", + Hidden: true, + Annotations: map[string]string{ + metadata.CommandAnnotationPlugin: "true", + }, + Run: func(cmd *cobra.Command, args []string) {}, + } + + sub1sub1 := &cobra.Command{Use: "sub1sub1"} + sub1sub2 := &cobra.Command{Use: "sub1sub2"} + sub2 := &cobra.Command{ + Use: "sub2", + Annotations: map[string]string{ + metadata.CommandAnnotationPlugin: "true", + }, + Run: func(cmd *cobra.Command, args []string) {}, + } + + root.AddCommand(sub1, sub2) + sub1.AddCommand(sub1sub1, sub1sub2) + + assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{})) +} + func TestCommandAliases(t *testing.T) { root := &cobra.Command{Use: "root"} sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}} diff --git a/cli/command/builder/cmd.go b/cli/command/builder/cmd.go index ba2c069e8998..e65bdd3d394b 100644 --- a/cli/command/builder/cmd.go +++ b/cli/command/builder/cmd.go @@ -9,17 +9,25 @@ import ( ) // NewBuilderCommand returns a cobra command for `builder` subcommands -func NewBuilderCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewBuilderCommand(dockerCLI command.Cli) *cobra.Command { + return newBuilderCommand(dockerCLI) +} + +func newBuilderCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "builder", Short: "Manage builds", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.31"}, } cmd.AddCommand( - NewPruneCommand(dockerCli), - image.NewBuildCommand(dockerCli), + newPruneCommand(dockerCLI), + // we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function. + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) + image.NewBuildCommand(dockerCLI), ) return cmd } @@ -28,7 +36,13 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command { // This command is a placeholder / stub that is dynamically replaced by an // alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin // installed). +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command { + return newBakeStubCommand(dockerCLI) +} + +func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command { return &cobra.Command{ Use: "bake [OPTIONS] [TARGET...]", Short: "Build from a file", diff --git a/cli/command/builder/prune.go b/cli/command/builder/prune.go index 7a323a393941..e57534cbfb19 100644 --- a/cli/command/builder/prune.go +++ b/cli/command/builder/prune.go @@ -8,7 +8,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/build" @@ -24,7 +23,14 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for images +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewPruneCommand(dockerCli command.Cli) *cobra.Command { + return newPruneCommand(dockerCli) +} + +// newPruneCommand returns a new cobra prune command for images +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -32,18 +38,18 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove build cache", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options) + spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - fmt.Fprintln(dockerCli.Out(), output) + _, _ = fmt.Fprintln(dockerCLI.Out(), output) } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + _, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, Annotations: map[string]string{"version": "1.39"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -79,9 +85,12 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) } report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{ - All: options.all, - KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720 - Filters: pruneFilters, + All: options.all, + // TODO(austinvazquez): remove when updated to use github.com/moby/moby/client@v0.1.0 + // See https://github.com/moby/moby/pull/50772 for more details. + KeepStorage: options.keepStorage.Value(), + ReservedSpace: options.keepStorage.Value(), + Filters: pruneFilters, }) if err != nil { return 0, "", err diff --git a/cli/command/builder/prune_test.go b/cli/command/builder/prune_test.go index e0c3097b07e2..de1582cf6a77 100644 --- a/cli/command/builder/prune_test.go +++ b/cli/command/builder/prune_test.go @@ -19,7 +19,7 @@ func TestBuilderPromptTermination(t *testing.T) { return nil, errors.New("fakeClient builderPruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) test.TerminatePrompt(ctx, t, cmd, cli) diff --git a/cli/command/checkpoint/cmd.go b/cli/command/checkpoint/cmd.go index 2a698e74e5e2..14154b2627ab 100644 --- a/cli/command/checkpoint/cmd.go +++ b/cli/command/checkpoint/cmd.go @@ -7,12 +7,18 @@ import ( ) // NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) -func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewCheckpointCommand(dockerCLI command.Cli) *cobra.Command { + return newCheckpointCommand(dockerCLI) +} + +func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "checkpoint", Short: "Manage checkpoints", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "experimental": "", "ostype": "linux", @@ -20,9 +26,9 @@ func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command { }, } cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), + newCreateCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), ) return cmd } diff --git a/cli/command/checkpoint/create.go b/cli/command/checkpoint/create.go index 8455e979e64f..aa471bf3b83e 100644 --- a/cli/command/checkpoint/create.go +++ b/cli/command/checkpoint/create.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/checkpoint" "github.com/spf13/cobra" ) @@ -30,7 +29,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { opts.checkpoint = args[1] return runCreate(cmd.Context(), dockerCli, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/checkpoint/formatter.go b/cli/command/checkpoint/formatter.go index 47ee77635b71..4de82043aab3 100644 --- a/cli/command/checkpoint/formatter.go +++ b/cli/command/checkpoint/formatter.go @@ -11,7 +11,14 @@ const ( ) // NewFormat returns a format for use with a checkpoint Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string) formatter.Format { + return newFormat(source) +} + +// newFormat returns a format for use with a checkpointContext. +func newFormat(source string) formatter.Format { if source == formatter.TableFormatKey { return defaultCheckpointFormat } @@ -19,7 +26,14 @@ func NewFormat(source string) formatter.Format { } // FormatWrite writes formatted checkpoints using the Context -func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error { + return formatWrite(fmtCtx, checkpoints) +} + +// formatWrite writes formatted checkpoints using the Context +func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error { render := func(format func(subContext formatter.SubContext) error) error { for _, cp := range checkpoints { if err := format(&checkpointContext{c: cp}); err != nil { @@ -28,7 +42,7 @@ func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error } return nil } - return ctx.Write(newCheckpointContext(), render) + return fmtCtx.Write(newCheckpointContext(), render) } type checkpointContext struct { diff --git a/cli/command/checkpoint/formatter_test.go b/cli/command/checkpoint/formatter_test.go index 7f265294bab7..1cca11e24337 100644 --- a/cli/command/checkpoint/formatter_test.go +++ b/cli/command/checkpoint/formatter_test.go @@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) { expected string }{ { - formatter.Context{Format: NewFormat(defaultCheckpointFormat)}, + formatter.Context{Format: newFormat(defaultCheckpointFormat)}, `CHECKPOINT NAME checkpoint-1 checkpoint-2 @@ -23,14 +23,14 @@ checkpoint-3 `, }, { - formatter.Context{Format: NewFormat("{{.Name}}")}, + formatter.Context{Format: newFormat("{{.Name}}")}, `checkpoint-1 checkpoint-2 checkpoint-3 `, }, { - formatter.Context{Format: NewFormat("{{.Name}}:")}, + formatter.Context{Format: newFormat("{{.Name}}:")}, `checkpoint-1: checkpoint-2: checkpoint-3: @@ -41,7 +41,7 @@ checkpoint-3: for _, testcase := range cases { out := bytes.NewBufferString("") testcase.context.Output = out - err := FormatWrite(testcase.context, []checkpoint.Summary{ + err := formatWrite(testcase.context, []checkpoint.Summary{ {Name: "checkpoint-1"}, {Name: "checkpoint-2"}, {Name: "checkpoint-3"}, diff --git a/cli/command/checkpoint/list.go b/cli/command/checkpoint/list.go index 55344e08c9d5..0fe769686857 100644 --- a/cli/command/checkpoint/list.go +++ b/cli/command/checkpoint/list.go @@ -45,7 +45,7 @@ func runList(ctx context.Context, dockerCli command.Cli, container string, opts cpCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewFormat(formatter.TableFormatKey), + Format: newFormat(formatter.TableFormatKey), } - return FormatWrite(cpCtx, checkpoints) + return formatWrite(cpCtx, checkpoints) } diff --git a/cli/command/cli.go b/cli/command/cli.go index 1e042ec0e23b..e0f8cc28c313 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -48,9 +48,7 @@ type Cli interface { Apply(ops ...CLIOption) error config.Provider ServerInfo() ServerInfo - DefaultVersion() string CurrentVersion() string - ContentTrustEnabled() bool BuildKitEnabled() (bool, error) ContextStore() store.Store CurrentContext() string @@ -78,6 +76,7 @@ type DockerCli struct { dockerEndpoint docker.Endpoint contextStoreConfig *store.Config initTimeout time.Duration + userAgent string res telemetryResource // baseCtx is the base context used for internal operations. In the future @@ -89,6 +88,8 @@ type DockerCli struct { } // DefaultVersion returns [api.DefaultVersion]. +// +// Deprecated: this function is no longer used and will be removed in the next release. func (*DockerCli) DefaultVersion() string { return api.DefaultVersion } @@ -159,6 +160,8 @@ func (cli *DockerCli) ServerInfo() ServerInfo { // ContentTrustEnabled returns whether content trust has been enabled by an // environment variable. +// +// Deprecated: check the value of the DOCKER_CONTENT_TRUST environment variable to detect whether content-trust is enabled. func (cli *DockerCli) ContentTrustEnabled() bool { return cli.contentTrust } @@ -269,7 +272,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) cli.contextStore = &ContextStoreWithDefault{ Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(cli.options, *cli.contextStoreConfig) + return resolveDefaultContext(cli.options, *cli.contextStoreConfig) }, } @@ -282,6 +285,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) } filterResourceAttributesEnvvar() + // early return if GODEBUG is already set or the docker context is + // the default context, i.e. is a virtual context where we won't override + // any GODEBUG values. + if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" { + return nil + } + meta, err := cli.contextStore.GetMetadata(cli.currentContext) + if err == nil { + setGoDebug(meta) + } + return nil } @@ -295,17 +309,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile. contextStore := &ContextStoreWithDefault{ Store: store.New(config.ContextStoreDir(), storeConfig), Resolver: func() (*DefaultContext, error) { - return ResolveDefaultContext(opts, storeConfig) + return resolveDefaultContext(opts, storeConfig) }, } endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile)) if err != nil { return nil, errors.Wrap(err, "unable to resolve docker endpoint") } - return newAPIClientFromEndpoint(endpoint, configFile) + return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent())) } -func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) { +func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) { opts, err := ep.ClientOpts() if err != nil { return nil, err @@ -313,7 +327,14 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF if len(configFile.HTTPHeaders) > 0 { opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) } - opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent())) + withCustomHeaders, err := withCustomHeadersFromEnv() + if err != nil { + return nil, err + } + if withCustomHeaders != nil { + opts = append(opts, withCustomHeaders) + } + opts = append(opts, extraOpts...) return client.NewClientWithOpts(opts...) } @@ -475,6 +496,57 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) { return resolveDockerEndpoint(cli.contextStore, cn) } +// setGoDebug is an escape hatch that sets the GODEBUG environment +// variable value using docker context metadata. +// +// { +// "Name": "my-context", +// "Metadata": { "GODEBUG": "x509negativeserial=1" } +// } +// +// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept +// X.509 certificates with negative serial numbers. +// This behavior is deprecated and non-compliant with current security +// standards (RFC 5280). Accepting negative serial numbers can introduce +// serious security vulnerabilities, including the risk of certificate +// collision or bypass attacks. +// This option should only be used for legacy compatibility and never in +// production environments. +// Use at your own risk. +func setGoDebug(meta store.Metadata) { + fieldName := "GODEBUG" + godebugEnv := os.Getenv(fieldName) + // early return if GODEBUG is already set. We don't want to override what + // the user already sets. + if godebugEnv != "" { + return + } + + var cfg any + var ok bool + switch m := meta.Metadata.(type) { + case DockerContext: + cfg, ok = m.AdditionalFields[fieldName] + if !ok { + return + } + case map[string]any: + cfg, ok = m[fieldName] + if !ok { + return + } + default: + return + } + + v, ok := cfg.(string) + if !ok { + return + } + // set the GODEBUG environment variable with whatever was in the context + _ = os.Setenv(fieldName, v) +} + func (cli *DockerCli) initialize() error { cli.init.Do(func() { cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() @@ -483,7 +555,8 @@ func (cli *DockerCli) initialize() error { return } if cli.client == nil { - if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil { + ops := []client.Opt{client.WithUserAgent(cli.userAgent)} + if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil { return } } @@ -496,6 +569,8 @@ func (cli *DockerCli) initialize() error { } // Apply all the operation on the cli +// +// Deprecated: this method is no longer used and will be removed in the next release if there are no remaining users. func (cli *DockerCli) Apply(ops ...CLIOption) error { for _, op := range ops { if err := op(cli); err != nil { @@ -527,15 +602,18 @@ type ServerInfo struct { // environment. func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { defaultOps := []CLIOption{ - WithContentTrustFromEnv(), + withContentTrustFromEnv(), WithDefaultContextStoreConfig(), WithStandardStreams(), + WithUserAgent(UserAgent()), } ops = append(defaultOps, ops...) cli := &DockerCli{baseCtx: context.Background()} - if err := cli.Apply(ops...); err != nil { - return nil, err + for _, op := range ops { + if err := op(cli); err != nil { + return nil, err + } } return cli, nil } @@ -551,7 +629,7 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) { } } -// UserAgent returns the user agent string used for making API requests +// UserAgent returns the default user agent string used for making API requests. func UserAgent() string { return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" } diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go index dd3c9473369d..6af65e98e376 100644 --- a/cli/command/cli_options.go +++ b/cli/command/cli_options.go @@ -75,8 +75,8 @@ func WithErrorStream(err io.Writer) CLIOption { } } -// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. -func WithContentTrustFromEnv() CLIOption { +// withContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. +func withContentTrustFromEnv() CLIOption { return func(cli *DockerCli) error { cli.contentTrust = false if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { @@ -89,7 +89,16 @@ func WithContentTrustFromEnv() CLIOption { } } +// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. +// +// Deprecated: this option is no longer used, and will be removed in the next release. +func WithContentTrustFromEnv() CLIOption { + return withContentTrustFromEnv() +} + // WithContentTrust enables content trust on a cli. +// +// Deprecated: this option is no longer used, and will be removed in the next release. func WithContentTrust(enabled bool) CLIOption { return func(cli *DockerCli) error { cli.contentTrust = enabled @@ -180,61 +189,70 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS" // override headers with the same name). // // TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason. -func withCustomHeadersFromEnv() client.Opt { - return func(apiClient *client.Client) error { - value := os.Getenv(envOverrideHTTPHeaders) - if value == "" { - return nil - } - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return invalidParameter(errors.Errorf( - "failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", - envOverrideHTTPHeaders, +func withCustomHeadersFromEnv() (client.Opt, error) { + value := os.Getenv(envOverrideHTTPHeaders) + if value == "" { + return nil, nil + } + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return nil, invalidParameter(errors.Errorf( + "failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", + envOverrideHTTPHeaders, + )) + } + if len(fields) == 0 { + return nil, nil + } + + env := map[string]string{} + for _, kv := range fields { + k, v, hasValue := strings.Cut(kv, "=") + + // Only strip whitespace in keys; preserve whitespace in values. + k = strings.TrimSpace(k) + + if k == "" { + return nil, invalidParameter(errors.Errorf( + `failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, + envOverrideHTTPHeaders, kv, )) } - if len(fields) == 0 { - return nil - } - env := map[string]string{} - for _, kv := range fields { - k, v, hasValue := strings.Cut(kv, "=") - - // Only strip whitespace in keys; preserve whitespace in values. - k = strings.TrimSpace(k) + // We don't currently allow empty key=value pairs, and produce an error. + // This is something we could allow in future (e.g. to read value + // from an environment variable with the same name). In the meantime, + // produce an error to prevent users from depending on this. + if !hasValue { + return nil, invalidParameter(errors.Errorf( + `failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, + envOverrideHTTPHeaders, kv, + )) + } - if k == "" { - return invalidParameter(errors.Errorf( - `failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, - envOverrideHTTPHeaders, kv, - )) - } + env[http.CanonicalHeaderKey(k)] = v + } - // We don't currently allow empty key=value pairs, and produce an error. - // This is something we could allow in future (e.g. to read value - // from an environment variable with the same name). In the meantime, - // produce an error to prevent users from depending on this. - if !hasValue { - return invalidParameter(errors.Errorf( - `failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`, - envOverrideHTTPHeaders, kv, - )) - } + if len(env) == 0 { + // We should probably not hit this case, as we don't skip values + // (only return errors), but we don't want to discard existing + // headers with an empty set. + return nil, nil + } - env[http.CanonicalHeaderKey(k)] = v - } + // TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_ + // see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc) + return client.WithHTTPHeaders(env), nil +} - if len(env) == 0 { - // We should probably not hit this case, as we don't skip values - // (only return errors), but we don't want to discard existing - // headers with an empty set. - return nil +// WithUserAgent configures the User-Agent string for cli HTTP requests. +func WithUserAgent(userAgent string) CLIOption { + return func(cli *DockerCli) error { + if userAgent == "" { + return errors.New("user agent cannot be blank") } - - // TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_ - // see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc) - return client.WithHTTPHeaders(env)(apiClient) + cli.userAgent = userAgent + return nil } } diff --git a/cli/command/cli_options_test.go b/cli/command/cli_options_test.go index 45ac1d8a5773..946a94df7468 100644 --- a/cli/command/cli_options_test.go +++ b/cli/command/cli_options_test.go @@ -10,7 +10,7 @@ import ( func contentTrustEnabled(t *testing.T) bool { t.Helper() var cli DockerCli - assert.NilError(t, WithContentTrustFromEnv()(&cli)) + assert.NilError(t, withContentTrustFromEnv()(&cli)) return cli.contentTrust } diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index ea67d403632c..859d6865fa78 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -18,6 +18,7 @@ import ( "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/flags" "github.com/docker/docker/api" "github.com/docker/docker/api/types" @@ -188,16 +189,16 @@ func TestInitializeFromClient(t *testing.T) { for _, tc := range testcases { t.Run(tc.doc, func(t *testing.T) { - apiclient := &fakeClient{ + apiClient := &fakeClient{ pingFunc: tc.pingFunc, version: defaultVersion, } - cli := &DockerCli{client: apiclient} + cli := &DockerCli{client: apiClient} err := cli.Initialize(flags.NewClientOptions()) assert.NilError(t, err) assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) - assert.Equal(t, apiclient.negotiated, tc.negotiated) + assert.Equal(t, apiClient.negotiated, tc.negotiated) }) } } @@ -353,3 +354,46 @@ func TestHooksEnabled(t *testing.T) { assert.Check(t, !cli.HooksEnabled()) }) } + +func TestSetGoDebug(t *testing.T) { + t.Run("GODEBUG already set", func(t *testing.T) { + t.Setenv("GODEBUG", "val1,val2") + meta := store.Metadata{} + setGoDebug(meta) + assert.Equal(t, "val1,val2", os.Getenv("GODEBUG")) + }) + t.Run("GODEBUG in context metadata can set env", func(t *testing.T) { + meta := store.Metadata{ + Metadata: DockerContext{ + AdditionalFields: map[string]any{ + "GODEBUG": "val1,val2=1", + }, + }, + } + setGoDebug(meta) + assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG")) + }) +} + +func TestNewDockerCliWithCustomUserAgent(t *testing.T) { + var received string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + received = r.UserAgent() + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + host := strings.Replace(ts.URL, "http://", "tcp://", 1) + opts := &flags.ClientOptions{Hosts: []string{host}} + + cli, err := NewDockerCli( + WithUserAgent("fake-agent/0.0.1"), + ) + assert.NilError(t, err) + cli.currentContext = DefaultContextName + cli.options = opts + cli.configFile = &configfile.ConfigFile{} + + _, err = cli.Client().Ping(context.Background()) + assert.NilError(t, err) + assert.DeepEqual(t, received, "fake-agent/0.0.1") +} diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index d3929293999d..6e1d96b66564 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -29,69 +29,127 @@ import ( func AddCommands(cmd *cobra.Command, dockerCli command.Cli) { cmd.AddCommand( // commonly used shorthands + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) container.NewRunCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) container.NewExecCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) container.NewPsCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) image.NewBuildCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) image.NewPullCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) image.NewPushCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) image.NewImagesCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) registry.NewLoginCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) registry.NewLogoutCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) registry.NewSearchCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) system.NewVersionCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) system.NewInfoCommand(dockerCli), // management commands + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) builder.NewBakeStubCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) builder.NewBuilderCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) checkpoint.NewCheckpointCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) container.NewContainerCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) context.NewContextCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) image.NewImageCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) manifest.NewManifestCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) network.NewNetworkCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) plugin.NewPluginCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) system.NewSystemCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) trust.NewTrustCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) volume.NewVolumeCommand(dockerCli), // orchestration (swarm) commands + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) config.NewConfigCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) node.NewNodeCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) secret.NewSecretCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) service.NewServiceCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) stack.NewStackCommand(dockerCli), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) swarm.NewSwarmCommand(dockerCli), // legacy commands may be hidden + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewAttachCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewCommitCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewCopyCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewCreateCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewDiffCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewExportCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewKillCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewLogsCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewPauseCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewPortCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewRenameCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewRestartCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewRmCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewStartCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewStatsCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewStopCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewTopCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewUnpauseCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewUpdateCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(container.NewWaitCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewHistoryCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewImportCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewLoadCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewRemoveCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewSaveCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(image.NewTagCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(system.NewEventsCommand(dockerCli)), + //nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283) hide(system.NewInspectCommand(dockerCli)), ) } diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index 41ebebf642c7..9a7d67b174ec 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -13,11 +13,6 @@ import ( "github.com/spf13/cobra" ) -// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion. -// -// Deprecated: use [cobra.CompletionFunc]. -type ValidArgsFn = cobra.CompletionFunc - // APIClientProvider provides a method to get an [client.APIClient], initializing // it if needed. // @@ -146,7 +141,9 @@ func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCom return nil, cobra.ShellCompDirectiveDefault } -// NoComplete is used for commands where there's no relevant completion +// NoComplete is used for commands where there's no relevant completion. +// +// Deprecated: use [cobra.NoFileCompletions]. This function will be removed in the next release. func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go index 12cd0ce425ee..2c2aa307fa1d 100644 --- a/cli/command/completion/functions_test.go +++ b/cli/command/completion/functions_test.go @@ -288,12 +288,6 @@ func TestCompleteNetworkNames(t *testing.T) { } } -func TestCompleteNoComplete(t *testing.T) { - values, directives := NoComplete(nil, nil, "") - assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp)) - assert.Check(t, is.Len(values, 0)) -} - func TestCompletePlatforms(t *testing.T) { values, directives := Platforms(nil, nil, "") assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") diff --git a/cli/command/config/cmd.go b/cli/command/config/cmd.go index d83e33b2057b..ccfe6ae921a9 100644 --- a/cli/command/config/cmd.go +++ b/cli/command/config/cmd.go @@ -9,22 +9,28 @@ import ( ) // NewConfigCommand returns a cobra command for `config` subcommands -func NewConfigCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewConfigCommand(dockerCLI command.Cli) *cobra.Command { + return newConfigCommand(dockerCLI) +} + +func newConfigCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Manage Swarm configs", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.30", "swarm": "manager", }, } cmd.AddCommand( - newConfigListCommand(dockerCli), - newConfigCreateCommand(dockerCli), - newConfigInspectCommand(dockerCli), - newConfigRemoveCommand(dockerCli), + newConfigListCommand(dockerCLI), + newConfigCreateCommand(dockerCLI), + newConfigInspectCommand(dockerCLI), + newConfigRemoveCommand(dockerCLI), ) return cmd } diff --git a/cli/command/config/create.go b/cli/command/config/create.go index 8fefd19d5926..e6e2c39b073b 100644 --- a/cli/command/config/create.go +++ b/cli/command/config/create.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/swarm" "github.com/moby/sys/sequential" @@ -16,6 +15,8 @@ import ( ) // CreateOptions specifies some options that are used when creating a config. +// +// Deprecated: this type was for internal use and will be removed in the next release. type CreateOptions struct { Name string TemplateDriver string @@ -23,9 +24,17 @@ type CreateOptions struct { Labels opts.ListOpts } -func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command { - createOpts := CreateOptions{ - Labels: opts.NewListOpts(opts.ValidateLabel), +// createOptions specifies some options that are used when creating a config. +type createOptions struct { + name string + templateDriver string + file string + labels opts.ListOpts +} + +func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command { + createOpts := createOptions{ + labels: opts.NewListOpts(opts.ValidateLabel), } cmd := &cobra.Command{ @@ -33,39 +42,51 @@ func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command { Short: "Create a config from a file or STDIN", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - createOpts.Name = args[0] - createOpts.File = args[1] - return RunConfigCreate(cmd.Context(), dockerCli, createOpts) + createOpts.name = args[0] + createOpts.file = args[1] + return runCreate(cmd.Context(), dockerCLI, createOpts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() - flags.VarP(&createOpts.Labels, "label", "l", "Config labels") - flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver") - flags.SetAnnotation("template-driver", "version", []string{"1.37"}) + flags.VarP(&createOpts.labels, "label", "l", "Config labels") + flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver") + _ = flags.SetAnnotation("template-driver", "version", []string{"1.37"}) return cmd } // RunConfigCreate creates a config with the given options. +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error { + return runCreate(ctx, dockerCLI, createOptions{ + name: options.Name, + templateDriver: options.TemplateDriver, + file: options.File, + labels: options.Labels, + }) +} + +// runCreate creates a config with the given options. +func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { apiClient := dockerCLI.Client() - configData, err := readConfigData(dockerCLI.In(), options.File) + configData, err := readConfigData(dockerCLI.In(), options.file) if err != nil { - return errors.Errorf("Error reading content from %q: %v", options.File, err) + return errors.Errorf("Error reading content from %q: %v", options.file, err) } spec := swarm.ConfigSpec{ Annotations: swarm.Annotations{ - Name: options.Name, - Labels: opts.ConvertKVStringsToMap(options.Labels.GetSlice()), + Name: options.name, + Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), }, Data: configData, } - if options.TemplateDriver != "" { + if options.templateDriver != "" { spec.Templating = &swarm.Driver{ - Name: options.TemplateDriver, + Name: options.templateDriver, } } r, err := apiClient.ConfigCreate(ctx, spec) diff --git a/cli/command/config/formatter.go b/cli/command/config/formatter.go index f2defa721b80..32521c749229 100644 --- a/cli/command/config/formatter.go +++ b/cli/command/config/formatter.go @@ -30,7 +30,14 @@ Data: ) // NewFormat returns a Format for rendering using a config Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string, quiet bool) formatter.Format { + return newFormat(source, quiet) +} + +// newFormat returns a Format for rendering using a configContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return configInspectPrettyTemplate @@ -44,7 +51,14 @@ func NewFormat(source string, quiet bool) formatter.Format { } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, configs []swarm.Config) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, configs []swarm.Config) error { + return formatWrite(fmtCtx, configs) +} + +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, configs []swarm.Config) error { render := func(format func(subContext formatter.SubContext) error) error { for _, config := range configs { configCtx := &configContext{c: config} @@ -54,7 +68,7 @@ func FormatWrite(ctx formatter.Context, configs []swarm.Config) error { } return nil } - return ctx.Write(newConfigContext(), render) + return fmtCtx.Write(newConfigContext(), render) } func newConfigContext() *configContext { @@ -115,9 +129,16 @@ func (c *configContext) Label(name string) string { } // InspectFormatWrite renders the context for a list of configs -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != configInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// +// Deprecated: this function was only used internally and will be removed in the next release. +func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + return inspectFormatWrite(fmtCtx, refs, getRef) +} + +// inspectFormatWrite renders the context for a list of configs +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != configInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } render := func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { @@ -135,7 +156,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } return nil } - return ctx.Write(&configInspectContext{}, render) + return fmtCtx.Write(&configInspectContext{}, render) } type configInspectContext struct { diff --git a/cli/command/config/formatter_test.go b/cli/command/config/formatter_test.go index 8736a876a48b..7ddead7d56e8 100644 --- a/cli/command/config/formatter_test.go +++ b/cli/command/config/formatter_test.go @@ -27,21 +27,21 @@ func TestConfigContextFormatWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `ID NAME CREATED UPDATED 1 passwords Less than a second ago Less than a second ago 2 id_rsa Less than a second ago Less than a second ago `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME passwords id_rsa `, }, { - formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)}, `1-passwords 2-id_rsa `, @@ -64,7 +64,7 @@ id_rsa t.Run(string(tc.context.Format), func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, configs); err != nil { + if err := formatWrite(tc.context, configs); err != nil { assert.ErrorContains(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) diff --git a/cli/command/config/inspect.go b/cli/command/config/inspect.go index 1983857eb578..1b6ab16bc071 100644 --- a/cli/command/config/inspect.go +++ b/cli/command/config/inspect.go @@ -16,57 +16,76 @@ import ( ) // InspectOptions contains options for the docker config inspect command. +// +// Deprecated: this type was for internal use and will be removed in the next release. type InspectOptions struct { Names []string Format string Pretty bool } -func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command { - opts := InspectOptions{} +// inspectOptions contains options for the docker config inspect command. +type inspectOptions struct { + names []string + format string + pretty bool +} + +func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command { + opts := inspectOptions{} cmd := &cobra.Command{ Use: "inspect [OPTIONS] CONFIG [CONFIG...]", Short: "Display detailed information on one or more configs", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Names = args - return RunConfigInspect(cmd.Context(), dockerCli, opts) + opts.names = args + return runInspect(cmd.Context(), dockerCLI, opts) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return completeNames(dockerCLI)(cmd, args, toComplete) }, } - cmd.Flags().StringVarP(&opts.Format, "format", "f", "", flagsHelper.InspectFormatHelp) - cmd.Flags().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format") + cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) + cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } // RunConfigInspect inspects the given Swarm config. +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error { + return runInspect(ctx, dockerCLI, inspectOptions{ + names: opts.Names, + format: opts.Format, + pretty: opts.Pretty, + }) +} + +// runInspect inspects the given Swarm config. +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { apiClient := dockerCLI.Client() - if opts.Pretty { - opts.Format = "pretty" + if opts.pretty { + opts.format = "pretty" } getRef := func(id string) (any, []byte, error) { return apiClient.ConfigInspectWithRaw(ctx, id) } - f := opts.Format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } configCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewFormat(f, false), + Format: newFormat(opts.format, false), } - if err := InspectFormatWrite(configCtx, opts.Names, getRef); err != nil { + if err := inspectFormatWrite(configCtx, opts.names, getRef); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/config/ls.go b/cli/command/config/ls.go index f6e68cfaa2ef..d7cb10f1c990 100644 --- a/cli/command/config/ls.go +++ b/cli/command/config/ls.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -16,14 +15,23 @@ import ( ) // ListOptions contains options for the docker config ls command. +// +// Deprecated: this type was for internal use and will be removed in the next release. type ListOptions struct { Quiet bool Format string Filter opts.FilterOpt } -func newConfigListCommand(dockerCli command.Cli) *cobra.Command { - listOpts := ListOptions{Filter: opts.NewFilterOpt()} +// listOptions contains options for the docker config ls command. +type listOptions struct { + quiet bool + format string + filter opts.FilterOpt +} + +func newConfigListCommand(dockerCLI command.Cli) *cobra.Command { + listOpts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -31,31 +39,42 @@ func newConfigListCommand(dockerCli command.Cli) *cobra.Command { Short: "List configs", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return RunConfigList(cmd.Context(), dockerCli, listOpts) + return runList(cmd.Context(), dockerCLI, listOpts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() - flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs") - flags.StringVar(&listOpts.Format, "format", "", flagsHelper.FormatHelp) - flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp) + flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } // RunConfigList lists Swarm configs. +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error { + return runList(ctx, dockerCLI, listOptions{ + quiet: options.Quiet, + format: options.Format, + filter: options.Filter, + }) +} + +// runList lists Swarm configs. +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { apiClient := dockerCLI.Client() - configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.Filter.Value()}) + configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.filter.Value()}) if err != nil { return err } - format := options.Format + format := options.format if len(format) == 0 { - if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.Quiet { + if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.quiet { format = dockerCLI.ConfigFile().ConfigFormat } else { format = formatter.TableFormatKey @@ -68,7 +87,7 @@ func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptio configCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewFormat(format, options.Quiet), + Format: newFormat(format, options.quiet), } - return FormatWrite(configCtx, configs) + return formatWrite(configCtx, configs) } diff --git a/cli/command/config/remove.go b/cli/command/config/remove.go index 01cbe331c135..e74a2722475f 100644 --- a/cli/command/config/remove.go +++ b/cli/command/config/remove.go @@ -11,34 +11,40 @@ import ( ) // RemoveOptions contains options for the docker config rm command. +// +// Deprecated: this type was for internal use and will be removed in the next release. type RemoveOptions struct { Names []string } -func newConfigRemoveCommand(dockerCli command.Cli) *cobra.Command { +func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command { return &cobra.Command{ Use: "rm CONFIG [CONFIG...]", Aliases: []string{"remove"}, Short: "Remove one or more configs", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts := RemoveOptions{ - Names: args, - } - return RunConfigRemove(cmd.Context(), dockerCli, opts) + return runRemove(cmd.Context(), dockerCLI, args) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return completeNames(dockerCLI)(cmd, args, toComplete) }, } } // RunConfigRemove removes the given Swarm configs. +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error { + return runRemove(ctx, dockerCLI, opts.Names) +} + +// runRemove removes the given Swarm configs. +func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error { apiClient := dockerCLI.Client() var errs []error - for _, name := range opts.Names { + for _, name := range names { if err := apiClient.ConfigRemove(ctx, name); err != nil { errs = append(errs, err) continue diff --git a/cli/command/container/attach.go b/cli/command/container/attach.go index f5ee5cf31115..c23bb0c049e5 100644 --- a/cli/command/container/attach.go +++ b/cli/command/container/attach.go @@ -41,7 +41,13 @@ func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClie } // NewAttachCommand creates a new cobra.Command for `docker attach` +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewAttachCommand(dockerCLI command.Cli) *cobra.Command { + return newAttachCommand(dockerCLI) +} + +func newAttachCommand(dockerCLI command.Cli) *cobra.Command { var opts AttachOptions cmd := &cobra.Command{ diff --git a/cli/command/container/attach_test.go b/cli/command/container/attach_test.go index b4ae6e9f45a0..89ea9e9be93a 100644 --- a/cli/command/container/attach_test.go +++ b/cli/command/container/attach_test.go @@ -74,7 +74,7 @@ func TestNewAttachCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) + cmd := newAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/cmd.go b/cli/command/container/cmd.go index 4ff00e74b591..7d0105373515 100644 --- a/cli/command/container/cmd.go +++ b/cli/command/container/cmd.go @@ -7,39 +7,45 @@ import ( ) // NewContainerCommand returns a cobra command for `container` subcommands -func NewContainerCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewContainerCommand(dockerCLI command.Cli) *cobra.Command { + return newContainerCommand(dockerCLI) +} + +func newContainerCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "container", Short: "Manage containers", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), } cmd.AddCommand( - NewAttachCommand(dockerCli), - NewCommitCommand(dockerCli), - NewCopyCommand(dockerCli), - NewCreateCommand(dockerCli), - NewDiffCommand(dockerCli), - NewExecCommand(dockerCli), - NewExportCommand(dockerCli), - NewKillCommand(dockerCli), - NewLogsCommand(dockerCli), - NewPauseCommand(dockerCli), - NewPortCommand(dockerCli), - NewRenameCommand(dockerCli), - NewRestartCommand(dockerCli), - newRemoveCommand(dockerCli), - NewRunCommand(dockerCli), - NewStartCommand(dockerCli), - NewStatsCommand(dockerCli), - NewStopCommand(dockerCli), - NewTopCommand(dockerCli), - NewUnpauseCommand(dockerCli), - NewUpdateCommand(dockerCli), - NewWaitCommand(dockerCli), - newListCommand(dockerCli), - newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), + newAttachCommand(dockerCLI), + newCommitCommand(dockerCLI), + newCopyCommand(dockerCLI), + newCreateCommand(dockerCLI), + newDiffCommand(dockerCLI), + newExecCommand(dockerCLI), + newExportCommand(dockerCLI), + newKillCommand(dockerCLI), + newLogsCommand(dockerCLI), + newPauseCommand(dockerCLI), + newPortCommand(dockerCLI), + newRenameCommand(dockerCLI), + newRestartCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newRunCommand(dockerCLI), + newStartCommand(dockerCLI), + newStatsCommand(dockerCLI), + newStopCommand(dockerCLI), + newTopCommand(dockerCLI), + newUnpauseCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newWaitCommand(dockerCLI), + newListCommand(dockerCLI), + newInspectCommand(dockerCLI), + newPruneCommand(dockerCLI), ) return cmd } diff --git a/cli/command/container/commit.go b/cli/command/container/commit.go index 8c8c798f68bc..9dde0a183f02 100644 --- a/cli/command/container/commit.go +++ b/cli/command/container/commit.go @@ -23,7 +23,13 @@ type commitOptions struct { } // NewCommitCommand creates a new cobra.Command for `docker commit` -func NewCommitCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewCommitCommand(dockerCLI command.Cli) *cobra.Command { + return newCommitCommand(dockerCLI) +} + +func newCommitCommand(dockerCLI command.Cli) *cobra.Command { var options commitOptions cmd := &cobra.Command{ @@ -35,12 +41,12 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { options.reference = args[1] } - return runCommit(cmd.Context(), dockerCli, &options) + return runCommit(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker container commit, docker commit", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } flags := cmd.Flags() diff --git a/cli/command/container/commit_test.go b/cli/command/container/commit_test.go index f1a62571fc7b..a2a08930f939 100644 --- a/cli/command/container/commit_test.go +++ b/cli/command/container/commit_test.go @@ -29,7 +29,7 @@ func TestRunCommit(t *testing.T) { }, }) - cmd := NewCommitCommand(cli) + cmd := newCommitCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs( []string{ @@ -60,7 +60,7 @@ func TestRunCommitClientError(t *testing.T) { }, }) - cmd := NewCommitCommand(cli) + cmd := newCommitCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"container-id"}) diff --git a/cli/command/container/completion_test.go b/cli/command/container/completion_test.go index 0977fdc5192b..3721423d67b2 100644 --- a/cli/command/container/completion_test.go +++ b/cli/command/container/completion_test.go @@ -59,7 +59,7 @@ func TestCompletePid(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ containerListFunc: tc.containerListFunc, }) - completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete) + completions, directive := completePid(cli)(newRunCommand(cli), nil, tc.toComplete) assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions)) assert.Check(t, is.Equal(directive, tc.expectedDirective)) }) diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index 40b038458703..fb37897089a1 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -122,7 +122,13 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6 } // NewCopyCommand creates a new `docker cp` command -func NewCopyCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewCopyCommand(dockerCLI command.Cli) *cobra.Command { + return newCopyCommand(dockerCLI) +} + +func newCopyCommand(dockerCLI command.Cli) *cobra.Command { var opts copyOptions cmd := &cobra.Command{ @@ -147,9 +153,9 @@ container source to stdout.`, opts.destination = args[1] if !cmd.Flag("quiet").Changed { // User did not specify "quiet" flag; suppress output if no terminal is attached - opts.quiet = !dockerCli.Out().IsTerminal() + opts.quiet = !dockerCLI.Out().IsTerminal() } - return runCopy(cmd.Context(), dockerCli, opts) + return runCopy(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker container cp, docker cp", diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 88d274dfbf3b..05368ef98723 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -11,7 +11,7 @@ import ( "path" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/cli/cli" @@ -52,7 +52,13 @@ type createOptions struct { } // NewCreateCommand creates a new cobra.Command for `docker create` -func NewCreateCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewCreateCommand(dockerCLI command.Cli) *cobra.Command { + return newCreateCommand(dockerCLI) +} + +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { var options createOptions var copts *containerOptions @@ -65,12 +71,12 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { copts.Args = args[1:] } - return runCreate(cmd.Context(), dockerCli, cmd.Flags(), &options, copts) + return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), &options, copts) }, Annotations: map[string]string{ "aliases": "docker container create, docker create", }, - ValidArgsFunction: completion.ImageNames(dockerCli, -1), + ValidArgsFunction: completion.ImageNames(dockerCLI, -1), } flags := cmd.Flags() @@ -86,17 +92,20 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { // with hostname flags.Bool("help", false, "Print usage") - command.AddPlatformFlag(flags, &options.platform) - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + // TODO(thaJeztah): consider adding platform as "image create option" on containerOptions + addPlatformFlag(flags, &options.platform) + _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) + + flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification") copts = addFlags(flags) - addCompletions(cmd, dockerCli) + addCompletions(cmd, dockerCLI) flags.VisitAll(func(flag *pflag.Flag) { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd @@ -341,7 +350,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name) if err != nil { // Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior. - if cerrdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing { + if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing { if !options.quiet { // we don't want to write to stdout anything apart from container.ID _, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) diff --git a/cli/command/container/create_test.go b/cli/command/container/create_test.go index fd94b624c822..3e1379b161af 100644 --- a/cli/command/container/create_test.go +++ b/cli/command/container/create_test.go @@ -116,7 +116,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) { t.Run(tc.PullPolicy, func(t *testing.T) { pullCounter := 0 - client := &fakeClient{ + apiClient := &fakeClient{ createContainerFunc: func( config *container.Config, hostConfig *container.HostConfig, @@ -140,7 +140,7 @@ func TestCreateContainerImagePullPolicy(t *testing.T) { return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil }, } - fakeCLI := test.NewFakeCli(client) + fakeCLI := test.NewFakeCli(apiClient) id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{ name: "name", platform: runtime.GOOS, @@ -206,7 +206,7 @@ func TestCreateContainerValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewCreateCommand(test.NewFakeCli(&fakeClient{})) + cmd := newCreateCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -249,6 +249,7 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST", "true") fakeCLI := test.NewFakeCli(&fakeClient{ createContainerFunc: func(config *container.Config, hostConfig *container.HostConfig, @@ -258,9 +259,9 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) { ) (container.CreateResponse, error) { return container.CreateResponse{}, errors.New("shouldn't try to pull image") }, - }, test.EnableContentTrust) + }) fakeCLI.SetNotaryClient(tc.notaryFunc) - cmd := NewCreateCommand(fakeCLI) + cmd := newCreateCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -314,7 +315,7 @@ func TestNewCreateCommandWithWarnings(t *testing.T) { return container.CreateResponse{Warnings: tc.warnings}, nil }, }) - cmd := NewCreateCommand(fakeCLI) + cmd := newCreateCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() @@ -366,7 +367,7 @@ func TestCreateContainerWithProxyConfig(t *testing.T) { }, }, }) - cmd := NewCreateCommand(fakeCLI) + cmd := newCreateCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"image:tag"}) err := cmd.Execute() diff --git a/cli/command/container/diff.go b/cli/command/container/diff.go index 93791fbd094e..74293639a9e2 100644 --- a/cli/command/container/diff.go +++ b/cli/command/container/diff.go @@ -11,18 +11,24 @@ import ( ) // NewDiffCommand creates a new cobra.Command for `docker diff` -func NewDiffCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewDiffCommand(dockerCLI command.Cli) *cobra.Command { + return newDiffCommand(dockerCLI) +} + +func newDiffCommand(dockerCLI command.Cli) *cobra.Command { 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 { - return runDiff(cmd.Context(), dockerCli, args[0]) + return runDiff(cmd.Context(), dockerCLI, args[0]) }, Annotations: map[string]string{ "aliases": "docker container diff, docker diff", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } } @@ -33,7 +39,7 @@ func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) err } diffCtx := formatter.Context{ Output: dockerCLI.Out(), - Format: NewDiffFormat("{{.Type}} {{.Path}}"), + Format: newDiffFormat("{{.Type}} {{.Path}}"), } - return DiffFormatWrite(diffCtx, changes) + return diffFormatWrite(diffCtx, changes) } diff --git a/cli/command/container/diff_test.go b/cli/command/container/diff_test.go index 685a95d125ba..4de209d5d186 100644 --- a/cli/command/container/diff_test.go +++ b/cli/command/container/diff_test.go @@ -36,7 +36,7 @@ func TestRunDiff(t *testing.T) { }, }) - cmd := NewDiffCommand(cli) + cmd := newDiffCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"container-id"}) @@ -68,7 +68,7 @@ func TestRunDiffClientError(t *testing.T) { }, }) - cmd := NewDiffCommand(cli) + cmd := newDiffCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/errors.go b/cli/command/container/errors.go index 957aa25fa6c5..d0d6f7055329 100644 --- a/cli/command/container/errors.go +++ b/cli/command/container/errors.go @@ -1,9 +1,9 @@ package container -import cerrdefs "github.com/containerd/errdefs" +import "github.com/containerd/errdefs" func invalidParameter(err error) error { - if err == nil || cerrdefs.IsInvalidArgument(err) { + if err == nil || errdefs.IsInvalidArgument(err) { return err } return invalidParameterErr{err} @@ -17,7 +17,7 @@ func (e invalidParameterErr) Unwrap() error { } func notFound(err error) error { - if err == nil || cerrdefs.IsNotFound(err) { + if err == nil || errdefs.IsNotFound(err) { return err } return notFoundErr{err} diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index b491e24a1b8e..b4fdca397192 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -40,7 +40,13 @@ func NewExecOptions() ExecOptions { } // NewExecCommand creates a new cobra.Command for `docker exec` -func NewExecCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewExecCommand(dockerCLI command.Cli) *cobra.Command { + return newExecCommand(dockerCLI) +} + +func newExecCommand(dockerCLI command.Cli) *cobra.Command { options := NewExecOptions() cmd := &cobra.Command{ @@ -50,9 +56,9 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { containerIDorName := args[0] options.Command = args[1:] - return RunExec(cmd.Context(), dockerCli, containerIDorName, options) + return RunExec(cmd.Context(), dockerCLI, containerIDorName, options) }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State != container.StatePaused }), Annotations: map[string]string{ diff --git a/cli/command/container/exec_test.go b/cli/command/container/exec_test.go index 9690d091c4d3..c03f333efbe8 100644 --- a/cli/command/container/exec_test.go +++ b/cli/command/container/exec_test.go @@ -234,13 +234,13 @@ func TestGetExecExitStatus(t *testing.T) { } for _, testcase := range testcases { - client := &fakeClient{ + apiClient := &fakeClient{ execInspectFunc: func(id string) (container.ExecInspect, error) { assert.Check(t, is.Equal(execID, id)) return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError }, } - err := getExecExitStatus(context.Background(), client, execID) + err := getExecExitStatus(context.Background(), apiClient, execID) assert.Check(t, is.Equal(testcase.expectedError, err)) } } @@ -263,7 +263,7 @@ func TestNewExecCommandErrors(t *testing.T) { } for _, tc := range testCases { fakeCLI := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}) - cmd := NewExecCommand(fakeCLI) + cmd := newExecCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) assert.ErrorContains(t, cmd.Execute(), tc.expectedError) diff --git a/cli/command/container/export.go b/cli/command/container/export.go index 990c2e66f8c8..ccb0ca29bbd5 100644 --- a/cli/command/container/export.go +++ b/cli/command/container/export.go @@ -18,7 +18,13 @@ type exportOptions struct { } // NewExportCommand creates a new `docker export` command -func NewExportCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewExportCommand(dockerCLI command.Cli) *cobra.Command { + return newExportCommand(dockerCLI) +} + +func newExportCommand(dockerCLI command.Cli) *cobra.Command { var opts exportOptions cmd := &cobra.Command{ @@ -27,12 +33,12 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] - return runExport(cmd.Context(), dockerCli, opts) + return runExport(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker container export, docker export", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), } flags := cmd.Flags() diff --git a/cli/command/container/export_test.go b/cli/command/container/export_test.go index 182713ab993a..9451eefcb7d7 100644 --- a/cli/command/container/export_test.go +++ b/cli/command/container/export_test.go @@ -19,7 +19,7 @@ func TestContainerExportOutputToFile(t *testing.T) { return io.NopCloser(strings.NewReader("bar")), nil }, }) - cmd := NewExportCommand(cli) + cmd := newExportCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"-o", dir.Join("foo"), "container"}) assert.NilError(t, cmd.Execute()) @@ -37,7 +37,7 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) { return io.NopCloser(strings.NewReader("foo")), nil }, }) - cmd := NewExportCommand(cli) + cmd := newExportCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"-o", "/dev/random", "container"}) diff --git a/cli/command/container/formatter_diff.go b/cli/command/container/formatter_diff.go index 822e1eef51b0..32af3ed64504 100644 --- a/cli/command/container/formatter_diff.go +++ b/cli/command/container/formatter_diff.go @@ -13,7 +13,14 @@ const ( ) // NewDiffFormat returns a format for use with a diff Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewDiffFormat(source string) formatter.Format { + return newDiffFormat(source) +} + +// newDiffFormat returns a format for use with a diff [formatter.Context]. +func newDiffFormat(source string) formatter.Format { if source == formatter.TableFormatKey { return defaultDiffTableFormat } @@ -21,16 +28,22 @@ func NewDiffFormat(source string) formatter.Format { } // DiffFormatWrite writes formatted diff using the Context -func DiffFormatWrite(ctx formatter.Context, changes []container.FilesystemChange) error { - render := func(format func(subContext formatter.SubContext) error) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error { + return diffFormatWrite(fmtCtx, changes) +} + +// diffFormatWrite writes formatted diff using the [formatter.Context]. +func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error { + return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error { for _, change := range changes { if err := format(&diffContext{c: change}); err != nil { return err } } return nil - } - return ctx.Write(newDiffContext(), render) + }) } type diffContext struct { @@ -39,12 +52,14 @@ type diffContext struct { } func newDiffContext() *diffContext { - diffCtx := diffContext{} - diffCtx.Header = formatter.SubHeaderContext{ - "Type": changeTypeHeader, - "Path": pathHeader, + return &diffContext{ + HeaderContext: formatter.HeaderContext{ + Header: formatter.SubHeaderContext{ + "Type": changeTypeHeader, + "Path": pathHeader, + }, + }, } - return &diffCtx } func (d *diffContext) MarshalJSON() ([]byte, error) { diff --git a/cli/command/container/formatter_diff_test.go b/cli/command/container/formatter_diff_test.go index e06117e84a2e..51ff9df6669b 100644 --- a/cli/command/container/formatter_diff_test.go +++ b/cli/command/container/formatter_diff_test.go @@ -16,7 +16,7 @@ func TestDiffContextFormatWrite(t *testing.T) { expected string }{ { - formatter.Context{Format: NewDiffFormat("table")}, + formatter.Context{Format: newDiffFormat("table")}, `CHANGE TYPE PATH C /var/log/app.log A /usr/app/app.js @@ -24,7 +24,7 @@ D /usr/app/old_app.js `, }, { - formatter.Context{Format: NewDiffFormat("table {{.Path}}")}, + formatter.Context{Format: newDiffFormat("table {{.Path}}")}, `PATH /var/log/app.log /usr/app/app.js @@ -32,7 +32,7 @@ D /usr/app/old_app.js `, }, { - formatter.Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, + formatter.Context{Format: newDiffFormat("{{.Type}}: {{.Path}}")}, `C: /var/log/app.log A: /usr/app/app.js D: /usr/app/old_app.js @@ -50,7 +50,7 @@ D: /usr/app/old_app.js t.Run(string(tc.context.Format), func(t *testing.T) { out := bytes.NewBufferString("") tc.context.Output = out - err := DiffFormatWrite(tc.context, diffs) + err := diffFormatWrite(tc.context, diffs) if err != nil { assert.Error(t, err, tc.expected) } else { diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index 48371eedc890..626c63142d91 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -167,6 +167,7 @@ func (c *statsContext) Container() string { } func (c *statsContext) Name() string { + // TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char. if len(c.s.Name) > 1 { return c.s.Name[1:] } diff --git a/cli/command/container/formatter_stats_test.go b/cli/command/container/formatter_stats_test.go index 91c69a5d7c26..59a7d70429a1 100644 --- a/cli/command/container/formatter_stats_test.go +++ b/cli/command/container/formatter_stats_test.go @@ -5,45 +5,181 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/cli/internal/test" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestContainerStatsContext(t *testing.T) { - containerID := test.RandomID() + const actorID = "c74518277ddc15a6afeaaeb06ee5f7433dcb27188224777c1efa7df1e8766d65" var ctx statsContext - tt := []struct { + tests := []struct { + name string stats StatsEntry osType string expValue string expHeader string call func() string }{ - {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, - {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, - {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage}, - {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, + { + name: "Container id", + stats: StatsEntry{ID: actorID, Container: actorID}, + expValue: actorID, + expHeader: containerHeader, + call: ctx.Container, + }, + { + name: "Container name", + stats: StatsEntry{ID: actorID, Container: "a-long-container-name"}, + expValue: "a-long-container-name", + expHeader: containerHeader, + call: ctx.Container, + }, + { + name: "ID", + stats: StatsEntry{ID: actorID}, + expValue: actorID, + expHeader: formatter.ContainerIDHeader, + call: ctx.ID, + }, + { + name: "Name", + stats: StatsEntry{Name: "/container-name"}, + expValue: "container-name", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "Name empty", + stats: StatsEntry{Name: ""}, + expValue: "--", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "Name prefix only", + stats: StatsEntry{Name: "/"}, + expValue: "--", + expHeader: formatter.ContainerIDHeader, + call: ctx.Name, + }, + { + name: "CPUPerc", + stats: StatsEntry{CPUPercentage: 5.5}, + expValue: "5.50%", + expHeader: cpuPercHeader, + call: ctx.CPUPerc, + }, + { + name: "CPUPerc invalid", + stats: StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, + expValue: "--", + expHeader: cpuPercHeader, + call: ctx.CPUPerc, + }, + { + name: "NetIO", + stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, + expValue: "0.31B / 12.3B", + expHeader: netIOHeader, + call: ctx.NetIO, + }, + { + name: "NetIO invalid", + stats: StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, + expValue: "--", + expHeader: netIOHeader, + call: ctx.NetIO, + }, + { + name: "BlockIO", + stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, + expValue: "0.1B / 2.3B", + expHeader: blockIOHeader, + call: ctx.BlockIO, + }, + { + name: "BlockIO invalid", + stats: StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, + expValue: "--", + expHeader: blockIOHeader, + call: ctx.BlockIO, + }, + { + name: "MemPerc", + stats: StatsEntry{MemoryPercentage: 10.2}, + expValue: "10.20%", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemPerc invalid", + stats: StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, + expValue: "--", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemPerc windows", + stats: StatsEntry{MemoryPercentage: 10.2}, + osType: "windows", + expValue: "--", + expHeader: memPercHeader, + call: ctx.MemPerc, + }, + { + name: "MemUsage", + stats: StatsEntry{Memory: 24, MemoryLimit: 30}, + expValue: "24B / 30B", + expHeader: memUseHeader, + call: ctx.MemUsage, + }, + { + name: "MemUsage invalid", + stats: StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, + expValue: "-- / --", + expHeader: memUseHeader, + call: ctx.MemUsage, + }, + { + name: "MemUsage windows", + stats: StatsEntry{Memory: 24, MemoryLimit: 30}, + osType: "windows", + expValue: "24B", + expHeader: winMemUseHeader, + call: ctx.MemUsage, + }, + { + name: "PIDs", + stats: StatsEntry{PidsCurrent: 10}, + expValue: "10", + expHeader: pidsHeader, + call: ctx.PIDs, + }, + { + name: "PIDs invalid", + stats: StatsEntry{PidsCurrent: 10, IsInvalid: true}, + expValue: "--", + expHeader: pidsHeader, + call: ctx.PIDs, + }, + { + name: "PIDs windows", + stats: StatsEntry{PidsCurrent: 10}, + osType: "windows", + expValue: "--", + expHeader: pidsHeader, + call: ctx.PIDs, + }, } - for _, te := range tt { - ctx = statsContext{s: te.stats, os: te.osType} - if v := te.call(); v != te.expValue { - t.Fatalf("Expected %q, got %q", te.expValue, v) - } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx = statsContext{s: tc.stats, os: tc.osType} + if v := tc.call(); v != tc.expValue { + t.Fatalf("Expected %q, got %q", tc.expValue, v) + } + }) } } diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go index 3d5c59941831..1d8521563050 100644 --- a/cli/command/container/kill.go +++ b/cli/command/container/kill.go @@ -18,7 +18,13 @@ type killOptions struct { } // NewKillCommand creates a new cobra.Command for `docker kill` -func NewKillCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewKillCommand(dockerCLI command.Cli) *cobra.Command { + return newKillCommand(dockerCLI) +} + +func newKillCommand(dockerCLI command.Cli) *cobra.Command { var opts killOptions cmd := &cobra.Command{ @@ -27,12 +33,12 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runKill(cmd.Context(), dockerCli, &opts) + return runKill(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container kill, docker kill", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } flags := cmd.Flags() diff --git a/cli/command/container/kill_test.go b/cli/command/container/kill_test.go index 7bd1f10b9272..c5268dd4bc66 100644 --- a/cli/command/container/kill_test.go +++ b/cli/command/container/kill_test.go @@ -24,7 +24,7 @@ func TestRunKill(t *testing.T) { }, }) - cmd := NewKillCommand(cli) + cmd := newKillCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{ @@ -56,7 +56,7 @@ func TestRunKillClientError(t *testing.T) { }, }) - cmd := NewKillCommand(cli) + cmd := newKillCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/list.go b/cli/command/container/list.go index 4523fe51d216..06e3a73d4ec7 100644 --- a/cli/command/container/list.go +++ b/cli/command/container/list.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -29,7 +28,13 @@ type psOptions struct { } // NewPsCommand creates a new cobra.Command for `docker ps` +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewPsCommand(dockerCLI command.Cli) *cobra.Command { + return newPsCommand(dockerCLI) +} + +func newPsCommand(dockerCLI command.Cli) *cobra.Command { options := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -44,7 +49,7 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command { "category-top": "3", "aliases": "docker container ls, docker container list, docker container ps, docker ps", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -62,7 +67,7 @@ func NewPsCommand(dockerCLI command.Cli) *cobra.Command { } func newListCommand(dockerCLI command.Cli) *cobra.Command { - cmd := *NewPsCommand(dockerCLI) + cmd := *newPsCommand(dockerCLI) cmd.Aliases = []string{"ps", "list"} cmd.Use = "ls [OPTIONS]" return &cmd @@ -82,7 +87,7 @@ func buildContainerListOptions(options *psOptions) (*container.ListOptions, erro // always validate template when `--format` is used, for consistency if len(options.format) > 0 { - tmpl, err := templates.NewParse("", options.format) + tmpl, err := templates.Parse(options.format) if err != nil { return nil, errors.Wrap(err, "failed to parse template") } diff --git a/cli/command/container/logs.go b/cli/command/container/logs.go index 3d536f721b73..62b6f7489785 100644 --- a/cli/command/container/logs.go +++ b/cli/command/container/logs.go @@ -24,7 +24,13 @@ type logsOptions struct { } // NewLogsCommand creates a new cobra.Command for `docker logs` -func NewLogsCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewLogsCommand(dockerCLI command.Cli) *cobra.Command { + return newLogsCommand(dockerCLI) +} + +func newLogsCommand(dockerCLI command.Cli) *cobra.Command { var opts logsOptions cmd := &cobra.Command{ @@ -33,12 +39,12 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] - return runLogs(cmd.Context(), dockerCli, &opts) + return runLogs(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container logs, docker logs", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), } flags := cmd.Flags() diff --git a/cli/command/container/opts.go b/cli/command/container/opts.go index 647dc5d51008..b520d6b13a51 100644 --- a/cli/command/container/opts.go +++ b/cli/command/container/opts.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "net" "os" "path" "path/filepath" @@ -12,8 +13,8 @@ import ( "strings" "time" - "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/internal/lazyregexp" + "github.com/docker/cli/internal/volumespec" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" @@ -141,6 +142,16 @@ type containerOptions struct { Args []string } +// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and +// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default. +// +// It should not be used for new uses, which may have a different API version +// requirement. +func addPlatformFlag(flags *pflag.FlagSet, target *string) { + flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) +} + // addFlags adds all command line flags that will be used by parse to the FlagSet func addFlags(flags *pflag.FlagSet) *containerOptions { copts := &containerOptions{ @@ -340,7 +351,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con // Validate the input mac address if copts.macAddress != "" { - if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { + if _, err := net.ParseMAC(strings.TrimSpace(copts.macAddress)); err != nil { return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) } } @@ -364,7 +375,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con volumes := copts.volumes.GetMap() // add any bind targets to the list of container volumes for bind := range copts.volumes.GetMap() { - parsed, err := loader.ParseVolume(bind) + parsed, err := volumespec.Parse(bind) if err != nil { return nil, err } @@ -873,7 +884,7 @@ func parseNetworkAttachmentOpt(ep opts.NetworkAttachmentOpts) (*networktypes.End } } if ep.MacAddress != "" { - if _, err := opts.ValidateMACAddress(ep.MacAddress); err != nil { + if _, err := net.ParseMAC(strings.TrimSpace(ep.MacAddress)); err != nil { return nil, errors.Errorf("%s is not a valid mac address", ep.MacAddress) } epConfig.MacAddress = ep.MacAddress diff --git a/cli/command/container/pause.go b/cli/command/container/pause.go index 78dc6fe37d8d..d5c75c861143 100644 --- a/cli/command/container/pause.go +++ b/cli/command/container/pause.go @@ -17,7 +17,13 @@ type pauseOptions struct { } // NewPauseCommand creates a new cobra.Command for `docker pause` -func NewPauseCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPauseCommand(dockerCLI command.Cli) *cobra.Command { + return newPauseCommand(dockerCLI) +} + +func newPauseCommand(dockerCLI command.Cli) *cobra.Command { var opts pauseOptions return &cobra.Command{ @@ -26,12 +32,12 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runPause(cmd.Context(), dockerCli, &opts) + return runPause(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container pause, docker pause", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool { return ctr.State != container.StatePaused }), } diff --git a/cli/command/container/pause_test.go b/cli/command/container/pause_test.go index a359797a94cf..04e4f40672b4 100644 --- a/cli/command/container/pause_test.go +++ b/cli/command/container/pause_test.go @@ -21,7 +21,7 @@ func TestRunPause(t *testing.T) { }, ) - cmd := NewPauseCommand(cli) + cmd := newPauseCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs([]string{"container-id-1", "container-id-2"}) @@ -47,7 +47,7 @@ func TestRunPauseClientError(t *testing.T) { }, ) - cmd := NewPauseCommand(cli) + cmd := newPauseCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"container-id-1", "container-id-2"}) diff --git a/cli/command/container/port.go b/cli/command/container/port.go index 3ed950f37582..a67e939c8920 100644 --- a/cli/command/container/port.go +++ b/cli/command/container/port.go @@ -24,7 +24,13 @@ type portOptions struct { } // NewPortCommand creates a new cobra.Command for `docker port` +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewPortCommand(dockerCli command.Cli) *cobra.Command { + return newPortCommand(dockerCli) +} + +func newPortCommand(dockerCli command.Cli) *cobra.Command { var opts portOptions cmd := &cobra.Command{ diff --git a/cli/command/container/port_test.go b/cli/command/container/port_test.go index 5ed907dcb2d7..a904135ad03e 100644 --- a/cli/command/container/port_test.go +++ b/cli/command/container/port_test.go @@ -66,7 +66,7 @@ func TestNewPortCommandOutput(t *testing.T) { return ci, nil }, }) - cmd := NewPortCommand(cli) + cmd := newPortCommand(cli) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"some_container", tc.port}) err := cmd.Execute() diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index d75338718e36..2e0439ae070f 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -20,7 +19,13 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for containers -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPruneCommand(dockerCLI command.Cli) *cobra.Command { + return newPruneCommand(dockerCLI) +} + +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -28,18 +33,18 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove all stopped containers", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options) + spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - fmt.Fprintln(dockerCli.Out(), output) + fmt.Fprintln(dockerCLI.Out(), output) } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/container/prune_test.go b/cli/command/container/prune_test.go index 6700235c3ddb..bfc321d6117d 100644 --- a/cli/command/container/prune_test.go +++ b/cli/command/container/prune_test.go @@ -20,7 +20,7 @@ func TestContainerPrunePromptTermination(t *testing.T) { return container.PruneReport{}, errors.New("fakeClient containerPruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/rename.go b/cli/command/container/rename.go index a871e38d1ef9..2f028e872e1f 100644 --- a/cli/command/container/rename.go +++ b/cli/command/container/rename.go @@ -18,7 +18,13 @@ type renameOptions struct { } // NewRenameCommand creates a new cobra.Command for `docker rename` -func NewRenameCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewRenameCommand(dockerCLI command.Cli) *cobra.Command { + return newRenameCommand(dockerCLI) +} + +func newRenameCommand(dockerCLI command.Cli) *cobra.Command { var opts renameOptions cmd := &cobra.Command{ @@ -28,12 +34,12 @@ func NewRenameCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.oldName = args[0] opts.newName = args[1] - return runRename(cmd.Context(), dockerCli, &opts) + return runRename(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container rename, docker rename", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), } return cmd } diff --git a/cli/command/container/rename_test.go b/cli/command/container/rename_test.go index 9a2cae4b3cac..2e14e771e534 100644 --- a/cli/command/container/rename_test.go +++ b/cli/command/container/rename_test.go @@ -43,7 +43,7 @@ func TestRunRename(t *testing.T) { }, }) - cmd := NewRenameCommand(cli) + cmd := newRenameCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{tc.oldName, tc.newName}) @@ -66,7 +66,7 @@ func TestRunRenameClientError(t *testing.T) { }, }) - cmd := NewRenameCommand(cli) + cmd := newRenameCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"oldName", "newName"}) diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 379b6a12eba5..1e7954e261de 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -21,7 +21,13 @@ type restartOptions struct { } // NewRestartCommand creates a new cobra.Command for `docker restart` -func NewRestartCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewRestartCommand(dockerCLI command.Cli) *cobra.Command { + return newRestartCommand(dockerCLI) +} + +func newRestartCommand(dockerCLI command.Cli) *cobra.Command { var opts restartOptions cmd := &cobra.Command{ @@ -34,12 +40,12 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command { } opts.containers = args opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time") - return runRestart(cmd.Context(), dockerCli, &opts) + return runRestart(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container restart, docker restart", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), } flags := cmd.Flags() diff --git a/cli/command/container/restart_test.go b/cli/command/container/restart_test.go index f7986a87514f..c50134bf1cfb 100644 --- a/cli/command/container/restart_test.go +++ b/cli/command/container/restart_test.go @@ -76,7 +76,7 @@ func TestRestart(t *testing.T) { }, Version: "1.36", }) - cmd := NewRestartCommand(cli) + cmd := newRestartCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go index 3206bb59924c..cf3d6a66a881 100644 --- a/cli/command/container/rm.go +++ b/cli/command/container/rm.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" @@ -23,7 +23,13 @@ type rmOptions struct { } // NewRmCommand creates a new cobra.Command for `docker rm` -func NewRmCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewRmCommand(dockerCLI command.Cli) *cobra.Command { + return newRmCommand(dockerCLI) +} + +func newRmCommand(dockerCLI command.Cli) *cobra.Command { var opts rmOptions cmd := &cobra.Command{ @@ -32,12 +38,12 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runRm(cmd.Context(), dockerCli, &opts) + return runRm(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container rm, docker container remove, docker rm", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true, func(ctr container.Summary) bool { + ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool { return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated }), } @@ -53,7 +59,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command { // top-level "docker rm", it also adds a "remove" alias to support // "docker container remove" in addition to "docker container rm". func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRmCommand(dockerCli) + cmd := *newRmCommand(dockerCli) cmd.Aliases = []string{"rm", "remove"} return &cmd } @@ -75,7 +81,7 @@ func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error { var errs []error for _, name := range opts.containers { if err := <-errChan; err != nil { - if opts.force && cerrdefs.IsNotFound(err) { + if opts.force && errdefs.IsNotFound(err) { _, _ = fmt.Fprintln(dockerCLI.Err(), err) continue } diff --git a/cli/command/container/rm_test.go b/cli/command/container/rm_test.go index e9850a9b93b8..1bbaaff213c0 100644 --- a/cli/command/container/rm_test.go +++ b/cli/command/container/rm_test.go @@ -41,7 +41,7 @@ func TestRemoveForce(t *testing.T) { }, Version: "1.36", }) - cmd := NewRmCommand(cli) + cmd := newRmCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/run.go b/cli/command/container/run.go index b86ad9d5b275..1b55400daf06 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -10,6 +10,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/trust" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/container" "github.com/moby/sys/signal" @@ -28,7 +29,13 @@ type runOptions struct { } // NewRunCommand create a new `docker run` command -func NewRunCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewRunCommand(dockerCLI command.Cli) *cobra.Command { + return newRunCommand(dockerCLI) +} + +func newRunCommand(dockerCLI command.Cli) *cobra.Command { var options runOptions var copts *containerOptions @@ -41,9 +48,9 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { copts.Args = args[1:] } - return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts) + return runRun(cmd.Context(), dockerCLI, cmd.Flags(), &options, copts) }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), Annotations: map[string]string{ "category-top": "1", "aliases": "docker container run, docker run", @@ -66,18 +73,19 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { // with hostname flags.Bool("help", false, "Print usage") - command.AddPlatformFlag(flags, &options.platform) - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + // TODO(thaJeztah): consider adding platform as "image create option" on containerOptions + addPlatformFlag(flags, &options.platform) + flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification") copts = addFlags(flags) _ = cmd.RegisterFlagCompletionFunc("detach-keys", completeDetachKeys) - addCompletions(cmd, dockerCli) + addCompletions(cmd, dockerCLI) flags.VisitAll(func(flag *pflag.Flag) { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd diff --git a/cli/command/container/run_test.go b/cli/command/container/run_test.go index f60ebde360a1..332c6a493f7e 100644 --- a/cli/command/container/run_test.go +++ b/cli/command/container/run_test.go @@ -2,9 +2,7 @@ package container import ( "context" - "encoding/json" "errors" - "fmt" "io" "net" "syscall" @@ -20,7 +18,8 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/pflag" "gotest.tools/v3/assert" @@ -40,7 +39,7 @@ func TestRunValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewRunCommand(test.NewFakeCli(&fakeClient{})) + cmd := newRunCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -64,7 +63,7 @@ func TestRunLabel(t *testing.T) { }, Version: "1.36", }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"--detach=true", "--label", "foo", "busybox"}) assert.NilError(t, cmd.Execute()) } @@ -111,7 +110,7 @@ func TestRunAttach(t *testing.T) { fc.SetIn(streams.NewIn(tty)) }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"-it", "busybox"}) cmd.SilenceUsage = true cmdErrC := make(chan error, 1) @@ -188,7 +187,7 @@ func TestRunAttachTermination(t *testing.T) { fc.SetIn(streams.NewIn(tty)) }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs([]string{"-it", "busybox"}) cmd.SilenceUsage = true cmdErrC := make(chan error, 1) @@ -242,23 +241,19 @@ func TestRunPullTermination(t *testing.T) { _ = server.Close() }) go func() { - enc := json.NewEncoder(server) + id := test.RandomID()[:12] // short-ID + progressOutput := streamformatter.NewJSONProgressOutput(server, true) for i := 0; i < 100; i++ { select { case <-ctx.Done(): assert.NilError(t, server.Close(), "failed to close imageCreateFunc server") return default: - assert.NilError(t, enc.Encode(jsonmessage.JSONMessage{ - Status: "Downloading", - ID: fmt.Sprintf("id-%d", i), - TimeNano: time.Now().UnixNano(), - Time: time.Now().Unix(), - Progress: &jsonmessage.JSONProgress{ - Current: int64(i), - Total: 100, - Start: 0, - }, + assert.NilError(t, progressOutput.WriteProgress(progress.Progress{ + ID: id, + Message: "Downloading", + Current: int64(i), + Total: 100, })) time.Sleep(100 * time.Millisecond) } @@ -270,7 +265,7 @@ func TestRunPullTermination(t *testing.T) { Version: "1.30", }) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"--pull", "always", "foobar:latest"}) @@ -328,6 +323,7 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST", "true") fakeCLI := test.NewFakeCli(&fakeClient{ createContainerFunc: func(config *container.Config, hostConfig *container.HostConfig, @@ -337,9 +333,9 @@ func TestRunCommandWithContentTrustErrors(t *testing.T) { ) (container.CreateResponse, error) { return container.CreateResponse{}, errors.New("shouldn't try to pull image") }, - }, test.EnableContentTrust) + }) fakeCLI.SetNotaryClient(tc.notaryFunc) - cmd := NewRunCommand(fakeCLI) + cmd := newRunCommand(fakeCLI) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/container/start.go b/cli/command/container/start.go index a71bdc8fde65..3daf586e0a8b 100644 --- a/cli/command/container/start.go +++ b/cli/command/container/start.go @@ -28,7 +28,13 @@ type StartOptions struct { } // NewStartCommand creates a new cobra.Command for `docker start` +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewStartCommand(dockerCli command.Cli) *cobra.Command { + return newStartCommand(dockerCli) +} + +func newStartCommand(dockerCli command.Cli) *cobra.Command { var opts StartOptions cmd := &cobra.Command{ diff --git a/cli/command/container/stats.go b/cli/command/container/stats.go index 5538e61dce20..f8ff880ca7ed 100644 --- a/cli/command/container/stats.go +++ b/cli/command/container/stats.go @@ -64,7 +64,13 @@ type StatsOptions struct { } // NewStatsCommand creates a new [cobra.Command] for "docker stats". +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewStatsCommand(dockerCLI command.Cli) *cobra.Command { + return newStatsCommand(dockerCLI) +} + +func newStatsCommand(dockerCLI command.Cli) *cobra.Command { options := StatsOptions{} cmd := &cobra.Command{ @@ -133,7 +139,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) eh := newEventHandler() if options.All { eh.setHandler(events.ActionCreate, func(e events.Message) { - s := NewStats(e.Actor.ID[:12]) + s := NewStats(e.Actor.ID) if cStats.add(s) { waitFirst.Add(1) go collect(ctx, s, apiClient, !options.NoStream, waitFirst) @@ -142,7 +148,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) } eh.setHandler(events.ActionStart, func(e events.Message) { - s := NewStats(e.Actor.ID[:12]) + s := NewStats(e.Actor.ID) if cStats.add(s) { waitFirst.Add(1) go collect(ctx, s, apiClient, !options.NoStream, waitFirst) @@ -151,7 +157,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) if !options.All { eh.setHandler(events.ActionDie, func(e events.Message) { - cStats.remove(e.Actor.ID[:12]) + cStats.remove(e.Actor.ID) }) } @@ -204,7 +210,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions) return err } for _, ctr := range cs { - s := NewStats(ctr.ID[:12]) + s := NewStats(ctr.ID) if cStats.add(s) { waitFirst.Add(1) go collect(ctx, s, apiClient, !options.NoStream, waitFirst) @@ -357,7 +363,12 @@ func (eh *eventHandler) watch(c <-chan events.Message) { if !exists { continue } - logrus.Debugf("event handler: received event: %v", e) + if e.Actor.ID == "" { + logrus.WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action) + continue + } + + logrus.WithField("event", e).Debugf("event handler: received %s event for: %s", e.Action, e.Actor.ID) go h(e) } } diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go index c6b331e964fd..e84b9e959c3c 100644 --- a/cli/command/container/stop.go +++ b/cli/command/container/stop.go @@ -21,7 +21,13 @@ type stopOptions struct { } // NewStopCommand creates a new cobra.Command for `docker stop` -func NewStopCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewStopCommand(dockerCLI command.Cli) *cobra.Command { + return newStopCommand(dockerCLI) +} + +func newStopCommand(dockerCLI command.Cli) *cobra.Command { var opts stopOptions cmd := &cobra.Command{ @@ -34,12 +40,12 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command { } opts.containers = args opts.timeoutChanged = cmd.Flags().Changed("timeout") || cmd.Flags().Changed("time") - return runStop(cmd.Context(), dockerCli, &opts) + return runStop(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container stop, docker stop", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } flags := cmd.Flags() diff --git a/cli/command/container/stop_test.go b/cli/command/container/stop_test.go index fc2b88c7acc4..424227f9b25a 100644 --- a/cli/command/container/stop_test.go +++ b/cli/command/container/stop_test.go @@ -77,7 +77,7 @@ func TestStop(t *testing.T) { }, Version: "1.36", }) - cmd := NewStopCommand(cli) + cmd := newStopCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/container/top.go b/cli/command/container/top.go index 411fcbbf79cd..d6a5970e358f 100644 --- a/cli/command/container/top.go +++ b/cli/command/container/top.go @@ -19,7 +19,13 @@ type topOptions struct { } // NewTopCommand creates a new cobra.Command for `docker top` -func NewTopCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewTopCommand(dockerCLI command.Cli) *cobra.Command { + return newTopCommand(dockerCLI) +} + +func newTopCommand(dockerCLI command.Cli) *cobra.Command { var opts topOptions cmd := &cobra.Command{ @@ -29,12 +35,12 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] opts.args = args[1:] - return runTop(cmd.Context(), dockerCli, &opts) + return runTop(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container top, docker top", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } flags := cmd.Flags() diff --git a/cli/command/container/unpause.go b/cli/command/container/unpause.go index fd5a516cace4..f5a002f3e279 100644 --- a/cli/command/container/unpause.go +++ b/cli/command/container/unpause.go @@ -17,7 +17,13 @@ type unpauseOptions struct { } // NewUnpauseCommand creates a new cobra.Command for `docker unpause` +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewUnpauseCommand(dockerCli command.Cli) *cobra.Command { + return newUnpauseCommand(dockerCli) +} + +func newUnpauseCommand(dockerCli command.Cli) *cobra.Command { var opts unpauseOptions cmd := &cobra.Command{ diff --git a/cli/command/container/update.go b/cli/command/container/update.go index 275ec5f64634..0f960477cb42 100644 --- a/cli/command/container/update.go +++ b/cli/command/container/update.go @@ -37,7 +37,13 @@ type updateOptions struct { } // NewUpdateCommand creates a new cobra.Command for `docker update` -func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewUpdateCommand(dockerCLI command.Cli) *cobra.Command { + return newUpdateCommand(dockerCLI) +} + +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { var options updateOptions cmd := &cobra.Command{ @@ -47,12 +53,12 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { options.containers = args options.nFlag = cmd.Flags().NFlag() - return runUpdate(cmd.Context(), dockerCli, &options) + return runUpdate(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker container update, docker update", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, true), + ValidArgsFunction: completion.ContainerNames(dockerCLI, true), } flags := cmd.Flags() diff --git a/cli/command/container/wait.go b/cli/command/container/wait.go index 89f2ba17acb6..95c389667730 100644 --- a/cli/command/container/wait.go +++ b/cli/command/container/wait.go @@ -16,7 +16,13 @@ type waitOptions struct { } // NewWaitCommand creates a new cobra.Command for `docker wait` -func NewWaitCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewWaitCommand(dockerCLI command.Cli) *cobra.Command { + return newWaitCommand(dockerCLI) +} + +func newWaitCommand(dockerCLI command.Cli) *cobra.Command { var opts waitOptions cmd := &cobra.Command{ @@ -25,12 +31,12 @@ func NewWaitCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args - return runWait(cmd.Context(), dockerCli, &opts) + return runWait(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "aliases": "docker container wait, docker wait", }, - ValidArgsFunction: completion.ContainerNames(dockerCli, false), + ValidArgsFunction: completion.ContainerNames(dockerCLI, false), } return cmd diff --git a/cli/command/context/cmd.go b/cli/command/context/cmd.go index f8b9e80d9399..f50e0f248f2c 100644 --- a/cli/command/context/cmd.go +++ b/cli/command/context/cmd.go @@ -7,23 +7,30 @@ import ( ) // NewContextCommand returns the context cli subcommand -func NewContextCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewContextCommand(dockerCLI command.Cli) *cobra.Command { + return newContextCommand(dockerCLI) +} + +// newContextCommand returns the context cli subcommand +func newContextCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "context", Short: "Manage contexts", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), } cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newUseCommand(dockerCli), - newExportCommand(dockerCli), - newImportCommand(dockerCli), - newRemoveCommand(dockerCli), - newUpdateCommand(dockerCli), - newInspectCommand(dockerCli), - newShowCommand(dockerCli), + newCreateCommand(dockerCLI), + newListCommand(dockerCLI), + newUseCommand(dockerCLI), + newExportCommand(dockerCLI), + newImportCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newShowCommand(dockerCLI), ) return cmd } diff --git a/cli/command/context/create.go b/cli/command/context/create.go index 313aca142e4a..32dfb00d27c2 100644 --- a/cli/command/context/create.go +++ b/cli/command/context/create.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "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/tabwriter" "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" @@ -19,6 +18,8 @@ import ( ) // CreateOptions are the options used for creating a context +// +// Deprecated: this type was for internal use and will be removed in the next release. type CreateOptions struct { Name string Description string @@ -30,6 +31,18 @@ type CreateOptions struct { metaData map[string]any } +// createOptions are the options used for creating a context +type createOptions struct { + name string + description string + endpoint map[string]string + from string + + // Additional Metadata to store in the context. This option is not + // currently exposed to the user. + metaData map[string]any +} + func longCreateDescription() string { buf := bytes.NewBuffer(nil) buf.WriteString("Create a context\n\nDocker endpoint config:\n\n") @@ -44,52 +57,68 @@ func longCreateDescription() string { } func newCreateCommand(dockerCLI command.Cli) *cobra.Command { - opts := &CreateOptions{} + opts := createOptions{} cmd := &cobra.Command{ Use: "create [OPTIONS] CONTEXT", Short: "Create a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Name = args[0] - return RunCreate(dockerCLI, opts) + opts.name = args[0] + return runCreate(dockerCLI, &opts) }, Long: longCreateDescription(), - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() - flags.StringVar(&opts.Description, "description", "", "Description of the context") - flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") - flags.StringVar(&opts.From, "from", "", "create context from a named context") + flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringToStringVar(&opts.endpoint, "docker", nil, "set the docker endpoint") + flags.StringVar(&opts.from, "from", "", "create context from a named context") return cmd } // RunCreate creates a Docker context + +// Deprecated: this function was for internal use and will be removed in the next release. func RunCreate(dockerCLI command.Cli, o *CreateOptions) error { + if o == nil { + o = &CreateOptions{} + } + + return runCreate(dockerCLI, &createOptions{ + name: o.Name, + description: o.Description, + endpoint: o.Docker, + metaData: o.metaData, + }) +} + +// runCreate creates a Docker context +func runCreate(dockerCLI command.Cli, opts *createOptions) error { s := dockerCLI.ContextStore() - err := checkContextNameForCreation(s, o.Name) + err := checkContextNameForCreation(s, opts.name) if err != nil { return err } switch { - case o.From == "" && o.Docker == nil: - err = createFromExistingContext(s, dockerCLI.CurrentContext(), o) - case o.From != "": - err = createFromExistingContext(s, o.From, o) + case opts.from == "" && opts.endpoint == nil: + err = createFromExistingContext(s, dockerCLI.CurrentContext(), opts) + case opts.from != "": + err = createFromExistingContext(s, opts.from, opts) default: - err = createNewContext(s, o) + err = createNewContext(s, opts) } if err == nil { - _, _ = fmt.Fprintln(dockerCLI.Out(), o.Name) - _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully created context %q\n", o.Name) + _, _ = fmt.Fprintln(dockerCLI.Out(), opts.name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully created context %q\n", opts.name) } return err } -func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { - if o.Docker == nil { +func createNewContext(contextStore store.ReaderWriter, opts *createOptions) error { + if opts.endpoint == nil { return errors.New("docker endpoint configuration is required") } - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(contextStore, o.Docker) + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(contextStore, opts.endpoint) if err != nil { return fmt.Errorf("unable to create docker endpoint config: %w", err) } @@ -98,10 +127,10 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { docker.DockerEndpoint: dockerEP, }, Metadata: command.DockerContext{ - Description: o.Description, - AdditionalFields: o.metaData, + Description: opts.description, + AdditionalFields: opts.metaData, }, - Name: o.Name, + Name: opts.name, } contextTLSData := store.ContextTLSData{} if dockerTLS != nil { @@ -115,14 +144,14 @@ func createNewContext(contextStore store.ReaderWriter, o *CreateOptions) error { if err := contextStore.CreateOrUpdate(contextMetadata); err != nil { return err } - return contextStore.ResetTLSMaterial(o.Name, &contextTLSData) + return contextStore.ResetTLSMaterial(opts.name, &contextTLSData) } func checkContextNameForCreation(s store.Reader, name string) error { if err := store.ValidateContextName(name); err != nil { return err } - if _, err := s.GetMetadata(name); !cerrdefs.IsNotFound(err) { + if _, err := s.GetMetadata(name); !errdefs.IsNotFound(err) { if err != nil { return fmt.Errorf("error while getting existing contexts: %w", err) } @@ -131,16 +160,16 @@ func checkContextNameForCreation(s store.Reader, name string) error { return nil } -func createFromExistingContext(s store.ReaderWriter, fromContextName string, o *CreateOptions) error { - if len(o.Docker) != 0 { +func createFromExistingContext(s store.ReaderWriter, fromContextName string, opts *createOptions) error { + if len(opts.endpoint) != 0 { return errors.New("cannot use --docker flag when --from is set") } reader := store.Export(fromContextName, &descriptionDecorator{ Reader: s, - description: o.Description, + description: opts.description, }) defer reader.Close() - return store.Import(o.Name, s, reader) + return store.Import(opts.name, s, reader) } type descriptionDecorator struct { diff --git a/cli/command/context/create_test.go b/cli/command/context/create_test.go index 794f95de04d5..b3344dc9e94b 100644 --- a/cli/command/context/create_test.go +++ b/cli/command/context/create_test.go @@ -60,7 +60,7 @@ func TestCreate(t *testing.T) { assert.NilError(t, cli.ContextStore().CreateOrUpdate(store.Metadata{Name: "existing-context"})) tests := []struct { doc string - options CreateOptions + options createOptions expecterErr string }{ { @@ -69,30 +69,30 @@ func TestCreate(t *testing.T) { }, { doc: "reserved name", - options: CreateOptions{ - Name: "default", + options: createOptions{ + name: "default", }, expecterErr: `"default" is a reserved context name`, }, { doc: "whitespace-only name", - options: CreateOptions{ - Name: " ", + options: createOptions{ + name: " ", }, expecterErr: `context name " " is invalid`, }, { doc: "existing context", - options: CreateOptions{ - Name: "existing-context", + options: createOptions{ + name: "existing-context", }, expecterErr: `context "existing-context" already exists`, }, { doc: "invalid docker host", - options: CreateOptions{ - Name: "invalid-docker-host", - Docker: map[string]string{ + options: createOptions{ + name: "invalid-docker-host", + endpoint: map[string]string{ "host": "some///invalid/host", }, }, @@ -100,27 +100,27 @@ func TestCreate(t *testing.T) { }, { doc: "ssh host with skip-tls-verify=false", - options: CreateOptions{ - Name: "skip-tls-verify-false", - Docker: map[string]string{ + options: createOptions{ + name: "skip-tls-verify-false", + endpoint: map[string]string{ "host": "ssh://example.com,skip-tls-verify=false", }, }, }, { doc: "ssh host with skip-tls-verify=true", - options: CreateOptions{ - Name: "skip-tls-verify-true", - Docker: map[string]string{ + options: createOptions{ + name: "skip-tls-verify-true", + endpoint: map[string]string{ "host": "ssh://example.com,skip-tls-verify=true", }, }, }, { doc: "ssh host with skip-tls-verify=INVALID", - options: CreateOptions{ - Name: "skip-tls-verify-invalid", - Docker: map[string]string{ + options: createOptions{ + name: "skip-tls-verify-invalid", + endpoint: map[string]string{ "host": "ssh://example.com", "skip-tls-verify": "INVALID", }, @@ -129,9 +129,9 @@ func TestCreate(t *testing.T) { }, { doc: "unknown option", - options: CreateOptions{ - Name: "unknown-option", - Docker: map[string]string{ + options: createOptions{ + name: "unknown-option", + endpoint: map[string]string{ "UNKNOWN": "value", }, }, @@ -140,7 +140,7 @@ func TestCreate(t *testing.T) { } for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { - err := RunCreate(cli, &tc.options) + err := runCreate(cli, &tc.options) if tc.expecterErr == "" { assert.NilError(t, err) } else { @@ -159,9 +159,9 @@ func assertContextCreateLogging(t *testing.T, cli *test.FakeCli, n string) { func TestCreateOrchestratorEmpty(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{}, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, "test") @@ -187,20 +187,20 @@ func TestCreateFromContext(t *testing.T) { cli := makeFakeCli(t) cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "original", - Description: "original description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, &createOptions{ + name: "original", + description: "original description", + endpoint: map[string]string{ keyHost: "tcp://42.42.42.42:2375", }, })) assertContextCreateLogging(t, cli, "original") cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "dummy", - Description: "dummy description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, &createOptions{ + name: "dummy", + description: "dummy description", + endpoint: map[string]string{ keyHost: "tcp://24.24.24.24:2375", }, })) @@ -211,11 +211,11 @@ func TestCreateFromContext(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cli.ResetOutputBuffers() - err := RunCreate(cli, &CreateOptions{ - From: "original", - Name: tc.name, - Description: tc.description, - Docker: tc.docker, + err := runCreate(cli, &createOptions{ + from: "original", + name: tc.name, + description: tc.description, + endpoint: tc.docker, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, tc.name) @@ -251,10 +251,10 @@ func TestCreateFromCurrent(t *testing.T) { cli := makeFakeCli(t) cli.ResetOutputBuffers() - assert.NilError(t, RunCreate(cli, &CreateOptions{ - Name: "original", - Description: "original description", - Docker: map[string]string{ + assert.NilError(t, runCreate(cli, &createOptions{ + name: "original", + description: "original description", + endpoint: map[string]string{ keyHost: "tcp://42.42.42.42:2375", }, })) @@ -265,9 +265,9 @@ func TestCreateFromCurrent(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cli.ResetOutputBuffers() - err := RunCreate(cli, &CreateOptions{ - Name: tc.name, - Description: tc.description, + err := runCreate(cli, &createOptions{ + name: tc.name, + description: tc.description, }) assert.NilError(t, err) assertContextCreateLogging(t, cli, tc.name) diff --git a/cli/command/context/export-import_test.go b/cli/command/context/export-import_test.go index 9aabd6ab4490..cfbac778ad6f 100644 --- a/cli/command/context/export-import_test.go +++ b/cli/command/context/export-import_test.go @@ -21,14 +21,11 @@ func TestExportImportWithFile(t *testing.T) { "MyCustomMetadata": t.Name(), }) cli.ErrBuffer().Reset() - assert.NilError(t, RunExport(cli, &ExportOptions{ - ContextName: "test", - Dest: contextFile, - })) + assert.NilError(t, runExport(cli, "test", contextFile)) assert.Equal(t, cli.ErrBuffer().String(), fmt.Sprintf("Written file %q\n", contextFile)) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunImport(cli, "test2", contextFile)) + assert.NilError(t, runImport(cli, "test2", contextFile)) context1, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetMetadata("test2") @@ -55,15 +52,12 @@ func TestExportImportPipe(t *testing.T) { }) cli.ErrBuffer().Reset() cli.OutBuffer().Reset() - assert.NilError(t, RunExport(cli, &ExportOptions{ - ContextName: "test", - Dest: "-", - })) + assert.NilError(t, runExport(cli, "test", "-")) assert.Equal(t, cli.ErrBuffer().String(), "") cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunImport(cli, "test2", "-")) + assert.NilError(t, runImport(cli, "test2", "-")) context1, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) context2, err := cli.ContextStore().GetMetadata("test2") @@ -88,6 +82,6 @@ func TestExportExistingFile(t *testing.T) { cli := makeFakeCli(t) cli.ErrBuffer().Reset() assert.NilError(t, os.WriteFile(contextFile, []byte{}, 0o644)) - err := RunExport(cli, &ExportOptions{ContextName: "test", Dest: contextFile}) + err := runExport(cli, "test", contextFile) assert.Assert(t, os.IsExist(err)) } diff --git a/cli/command/context/export.go b/cli/command/context/export.go index 96e35b67505f..90d5b723203d 100644 --- a/cli/command/context/export.go +++ b/cli/command/context/export.go @@ -13,6 +13,8 @@ import ( ) // ExportOptions are the options used for exporting a context +// +// Deprecated: this type was for internal use and will be removed in the next release. type ExportOptions struct { ContextName string Dest string @@ -24,15 +26,14 @@ func newExportCommand(dockerCLI command.Cli) *cobra.Command { Short: "Export a context to a tar archive FILE or a tar stream on STDOUT.", Args: cli.RequiresRangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { - opts := &ExportOptions{ - ContextName: args[0], - } + contextName := args[0] + var dest string if len(args) == 2 { - opts.Dest = args[1] + dest = args[1] } else { - opts.Dest = opts.ContextName + ".dockercontext" + dest = contextName + ".dockercontext" } - return RunExport(dockerCLI, opts) + return runExport(dockerCLI, contextName, dest) }, ValidArgsFunction: completeContextNames(dockerCLI, 1, true), } @@ -65,11 +66,21 @@ func writeTo(dockerCli command.Cli, reader io.Reader, dest string) error { } // RunExport exports a Docker context +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunExport(dockerCli command.Cli, opts *ExportOptions) error { - if err := store.ValidateContextName(opts.ContextName); err != nil && opts.ContextName != command.DefaultContextName { + if opts == nil { + opts = &ExportOptions{} + } + return runExport(dockerCli, opts.ContextName, opts.Dest) +} + +// runExport exports a Docker context. +func runExport(dockerCLI command.Cli, contextName string, dest string) error { + if err := store.ValidateContextName(contextName); err != nil && contextName != command.DefaultContextName { return err } - reader := store.Export(opts.ContextName, dockerCli.ContextStore()) + reader := store.Export(contextName, dockerCLI.ContextStore()) defer reader.Close() - return writeTo(dockerCli, reader, opts.Dest) + return writeTo(dockerCLI, reader, dest) } diff --git a/cli/command/context/import.go b/cli/command/context/import.go index 182defcf53dc..aa7a728e1c78 100644 --- a/cli/command/context/import.go +++ b/cli/command/context/import.go @@ -18,7 +18,7 @@ func newImportCommand(dockerCli command.Cli) *cobra.Command { Short: "Import a context from a tar or zip file", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return RunImport(dockerCli, args[0], args[1]) + return runImport(dockerCli, args[0], args[1]) }, // TODO(thaJeztah): this should also include "-" ValidArgsFunction: completion.FileNames, @@ -27,14 +27,21 @@ func newImportCommand(dockerCli command.Cli) *cobra.Command { } // RunImport imports a Docker context -func RunImport(dockerCli command.Cli, name string, source string) error { - if err := checkContextNameForCreation(dockerCli.ContextStore(), name); err != nil { +// +// Deprecated: this function was for internal use and will be removed in the next release. +func RunImport(dockerCLI command.Cli, name string, source string) error { + return runImport(dockerCLI, name, source) +} + +// runImport imports a Docker context. +func runImport(dockerCLI command.Cli, name string, source string) error { + if err := checkContextNameForCreation(dockerCLI.ContextStore(), name); err != nil { return err } var reader io.Reader if source == "-" { - reader = dockerCli.In() + reader = dockerCLI.In() } else { f, err := os.Open(source) if err != nil { @@ -44,11 +51,11 @@ func RunImport(dockerCli command.Cli, name string, source string) error { reader = f } - if err := store.Import(name, dockerCli.ContextStore(), reader); err != nil { + if err := store.Import(name, dockerCLI.ContextStore(), reader); err != nil { return err } - _, _ = fmt.Fprintln(dockerCli.Out(), name) - _, _ = fmt.Fprintf(dockerCli.Err(), "Successfully imported context %q\n", name) + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully imported context %q\n", name) return nil } diff --git a/cli/command/context/list.go b/cli/command/context/list.go index b4258b91a302..88e080cacd27 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -10,7 +10,6 @@ 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/context/docker" flagsHelper "github.com/docker/cli/cli/flags" @@ -34,7 +33,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(dockerCli, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/context/list_test.go b/cli/command/context/list_test.go index ea7dd1e7c9e0..c98699b74f56 100644 --- a/cli/command/context/list_test.go +++ b/cli/command/context/list_test.go @@ -19,10 +19,10 @@ func createTestContexts(t *testing.T, cli command.Cli, name ...string) { func createTestContext(t *testing.T, cli command.Cli, name string, metaData map[string]any) { t.Helper() - err := RunCreate(cli, &CreateOptions{ - Name: name, - Description: "description of " + name, - Docker: map[string]string{keyHost: "https://someswarmserver.example.com"}, + err := runCreate(cli, &createOptions{ + name: name, + description: "description of " + name, + endpoint: map[string]string{keyHost: "https://someswarmserver.example.com"}, metaData: metaData, }) diff --git a/cli/command/context/remove.go b/cli/command/context/remove.go index 0f73cb1fd749..c3397e8b9c76 100644 --- a/cli/command/context/remove.go +++ b/cli/command/context/remove.go @@ -11,34 +11,48 @@ import ( ) // RemoveOptions are the options used to remove contexts +// +// Deprecated: this type was for internal use and will be removed in the next release. type RemoveOptions struct { Force bool } +// removeOptions are the options used to remove contexts. +type removeOptions struct { + force bool +} + func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { - var opts RemoveOptions + var opts removeOptions cmd := &cobra.Command{ Use: "rm CONTEXT [CONTEXT...]", Aliases: []string{"remove"}, Short: "Remove one or more contexts", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return RunRemove(dockerCLI, opts, args) + return runRemove(dockerCLI, opts, args) }, ValidArgsFunction: completeContextNames(dockerCLI, -1, false), } - cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force the removal of a context in use") + cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Force the removal of a context in use") return cmd } // RunRemove removes one or more contexts -func RunRemove(dockerCLI command.Cli, opts RemoveOptions, names []string) error { +// +// Deprecated: this function was for internal use and will be removed in the next release. +func RunRemove(dockerCLI command.Cli, opts removeOptions, names []string) error { + return runRemove(dockerCLI, opts, names) +} + +// runRemove removes one or more contexts. +func runRemove(dockerCLI command.Cli, opts removeOptions, names []string) error { var errs []error currentCtx := dockerCLI.CurrentContext() for _, name := range names { if name == "default" { errs = append(errs, errors.New(`context "default" cannot be removed`)) - } else if err := doRemove(dockerCLI, name, name == currentCtx, opts.Force); err != nil { + } else if err := doRemove(dockerCLI, name, name == currentCtx, opts.force); err != nil { errs = append(errs, err) } else { _, _ = fmt.Fprintln(dockerCLI.Out(), name) diff --git a/cli/command/context/remove_test.go b/cli/command/context/remove_test.go index bbcddec6d16b..d121f39d6df8 100644 --- a/cli/command/context/remove_test.go +++ b/cli/command/context/remove_test.go @@ -4,7 +4,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "gotest.tools/v3/assert" @@ -14,20 +14,20 @@ import ( func TestRemove(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") - assert.NilError(t, RunRemove(cli, RemoveOptions{}, []string{"other"})) + assert.NilError(t, runRemove(cli, removeOptions{}, []string{"other"})) _, err := cli.ContextStore().GetMetadata("current") assert.NilError(t, err) _, err = cli.ContextStore().GetMetadata("other") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } func TestRemoveNotAContext(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") - err := RunRemove(cli, RemoveOptions{}, []string{"not-a-context"}) + err := runRemove(cli, removeOptions{}, []string{"not-a-context"}) assert.ErrorContains(t, err, `context "not-a-context" does not exist`) - err = RunRemove(cli, RemoveOptions{Force: true}, []string{"not-a-context"}) + err = runRemove(cli, removeOptions{force: true}, []string{"not-a-context"}) assert.NilError(t, err) } @@ -35,7 +35,7 @@ func TestRemoveCurrent(t *testing.T) { cli := makeFakeCli(t) createTestContexts(t, cli, "current", "other") cli.SetCurrentContext("current") - err := RunRemove(cli, RemoveOptions{}, []string{"current"}) + err := runRemove(cli, removeOptions{}, []string{"current"}) assert.ErrorContains(t, err, `context "current" is in use, set -f flag to force remove`) } @@ -49,7 +49,7 @@ func TestRemoveCurrentForce(t *testing.T) { cli := makeFakeCli(t, withCliConfig(testCfg)) createTestContexts(t, cli, "current", "other") cli.SetCurrentContext("current") - assert.NilError(t, RunRemove(cli, RemoveOptions{Force: true}, []string{"current"})) + assert.NilError(t, runRemove(cli, removeOptions{force: true}, []string{"current"})) reloadedConfig, err := config.Load(configDir) assert.NilError(t, err) assert.Equal(t, "", reloadedConfig.CurrentContext) @@ -59,6 +59,6 @@ func TestRemoveDefault(t *testing.T) { cli := makeFakeCli(t) createTestContext(t, cli, "other", nil) cli.SetCurrentContext("current") - err := RunRemove(cli, RemoveOptions{}, []string{"default"}) + err := runRemove(cli, removeOptions{}, []string{"default"}) assert.ErrorContains(t, err, `context "default" cannot be removed`) } diff --git a/cli/command/context/show.go b/cli/command/context/show.go index dd33f9e02a84..d5e4a828e35a 100644 --- a/cli/command/context/show.go +++ b/cli/command/context/show.go @@ -5,7 +5,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/spf13/cobra" ) @@ -19,7 +18,7 @@ func newShowCommand(dockerCli command.Cli) *cobra.Command { runShow(dockerCli) return nil }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } return cmd } diff --git a/cli/command/context/update.go b/cli/command/context/update.go index 0995c52ef553..2c6a54eda03a 100644 --- a/cli/command/context/update.go +++ b/cli/command/context/update.go @@ -13,12 +13,21 @@ import ( ) // UpdateOptions are the options used to update a context +// +// Deprecated: this type was for internal use and will be removed in the next release. type UpdateOptions struct { Name string Description string Docker map[string]string } +// updateOptions are the options used to update a context. +type updateOptions struct { + name string + description string + endpoint map[string]string +} + func longUpdateDescription() string { buf := bytes.NewBuffer(nil) buf.WriteString("Update a context\n\nDocker endpoint config:\n\n") @@ -33,31 +42,45 @@ func longUpdateDescription() string { } func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { - opts := &UpdateOptions{} + opts := updateOptions{} cmd := &cobra.Command{ Use: "update [OPTIONS] CONTEXT", Short: "Update a context", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.Name = args[0] - return RunUpdate(dockerCLI, opts) + opts.name = args[0] + return runUpdate(dockerCLI, &opts) }, Long: longUpdateDescription(), ValidArgsFunction: completeContextNames(dockerCLI, 1, false), } flags := cmd.Flags() - flags.StringVar(&opts.Description, "description", "", "Description of the context") - flags.StringToStringVar(&opts.Docker, "docker", nil, "set the docker endpoint") + flags.StringVar(&opts.description, "description", "", "Description of the context") + flags.StringToStringVar(&opts.endpoint, "docker", nil, "set the docker endpoint") return cmd } // RunUpdate updates a Docker context +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { - if err := store.ValidateContextName(o.Name); err != nil { + if o == nil { + o = &UpdateOptions{} + } + return runUpdate(dockerCLI, &updateOptions{ + name: o.Name, + description: o.Description, + endpoint: o.Docker, + }) +} + +// runUpdate updates a Docker context. +func runUpdate(dockerCLI command.Cli, opts *updateOptions) error { + if err := store.ValidateContextName(opts.name); err != nil { return err } s := dockerCLI.ContextStore() - c, err := s.GetMetadata(o.Name) + c, err := s.GetMetadata(opts.name) if err != nil { return err } @@ -65,16 +88,16 @@ func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { if err != nil { return err } - if o.Description != "" { - dockerContext.Description = o.Description + if opts.description != "" { + dockerContext.Description = opts.description } c.Metadata = dockerContext tlsDataToReset := make(map[string]*store.EndpointTLSData) - if o.Docker != nil { - dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(s, o.Docker) + if opts.endpoint != nil { + dockerEP, dockerTLS, err := getDockerEndpointMetadataAndTLS(s, opts.endpoint) if err != nil { return fmt.Errorf("unable to create docker endpoint config: %w", err) } @@ -88,13 +111,13 @@ func RunUpdate(dockerCLI command.Cli, o *UpdateOptions) error { return err } for ep, tlsData := range tlsDataToReset { - if err := s.ResetEndpointTLSMaterial(o.Name, ep, tlsData); err != nil { + if err := s.ResetEndpointTLSMaterial(opts.name, ep, tlsData); err != nil { return err } } - _, _ = fmt.Fprintln(dockerCLI.Out(), o.Name) - _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully updated context %q\n", o.Name) + _, _ = fmt.Fprintln(dockerCLI.Out(), opts.name) + _, _ = fmt.Fprintf(dockerCLI.Err(), "Successfully updated context %q\n", opts.name) return nil } diff --git a/cli/command/context/update_test.go b/cli/command/context/update_test.go index 0e4a63914559..3d6493003139 100644 --- a/cli/command/context/update_test.go +++ b/cli/command/context/update_test.go @@ -11,16 +11,16 @@ import ( func TestUpdateDescriptionOnly(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{}, }) assert.NilError(t, err) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() - assert.NilError(t, RunUpdate(cli, &UpdateOptions{ - Name: "test", - Description: "description", + assert.NilError(t, runUpdate(cli, &updateOptions{ + name: "test", + description: "description", })) c, err := cli.ContextStore().GetMetadata("test") assert.NilError(t, err) @@ -35,9 +35,9 @@ func TestUpdateDescriptionOnly(t *testing.T) { func TestUpdateDockerOnly(t *testing.T) { cli := makeFakeCli(t) createTestContext(t, cli, "test", nil) - assert.NilError(t, RunUpdate(cli, &UpdateOptions{ - Name: "test", - Docker: map[string]string{ + assert.NilError(t, runUpdate(cli, &updateOptions{ + name: "test", + endpoint: map[string]string{ keyHost: "tcp://some-host", }, })) @@ -52,14 +52,14 @@ func TestUpdateDockerOnly(t *testing.T) { func TestUpdateInvalidDockerHost(t *testing.T) { cli := makeFakeCli(t) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{}, }) assert.NilError(t, err) - err = RunUpdate(cli, &UpdateOptions{ - Name: "test", - Docker: map[string]string{ + err = runUpdate(cli, &updateOptions{ + name: "test", + endpoint: map[string]string{ keyHost: "some///invalid/host", }, }) diff --git a/cli/command/context/use.go b/cli/command/context/use.go index 6b0d927a80af..2fad07eafd4f 100644 --- a/cli/command/context/use.go +++ b/cli/command/context/use.go @@ -17,7 +17,7 @@ func newUseCommand(dockerCLI command.Cli) *cobra.Command { Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - return RunUse(dockerCLI, name) + return runUse(dockerCLI, name) }, ValidArgsFunction: completeContextNames(dockerCLI, 1, false), } @@ -25,7 +25,14 @@ func newUseCommand(dockerCLI command.Cli) *cobra.Command { } // RunUse set the current Docker context +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunUse(dockerCLI command.Cli, name string) error { + return runUse(dockerCLI, name) +} + +// runUse set the current Docker context +func runUse(dockerCLI command.Cli, name string) error { // configValue uses an empty string for "default" var configValue string if name != command.DefaultContextName { diff --git a/cli/command/context/use_test.go b/cli/command/context/use_test.go index 8c7265dac829..0e584b2e6e6c 100644 --- a/cli/command/context/use_test.go +++ b/cli/command/context/use_test.go @@ -9,7 +9,7 @@ import ( "runtime" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" @@ -23,9 +23,9 @@ func TestUse(t *testing.T) { configFilePath := filepath.Join(configDir, "config.json") testCfg := configfile.New(configFilePath) cli := makeFakeCli(t, withCliConfig(testCfg)) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{}, }) assert.NilError(t, err) assert.NilError(t, newUseCommand(cli).RunE(nil, []string{"test"})) @@ -47,7 +47,7 @@ func TestUse(t *testing.T) { func TestUseNoExist(t *testing.T) { cli := makeFakeCli(t) err := newUseCommand(cli).RunE(nil, []string{"test"}) - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } // TestUseDefaultWithoutConfigFile verifies that the CLI does not create @@ -89,9 +89,9 @@ func TestUseHostOverride(t *testing.T) { configFilePath := filepath.Join(configDir, "config.json") testCfg := configfile.New(configFilePath) cli := makeFakeCli(t, withCliConfig(testCfg)) - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{}, }) assert.NilError(t, err) @@ -136,9 +136,9 @@ func TestUseHostOverrideEmpty(t *testing.T) { assert.NilError(t, cli.Initialize(flags.NewClientOptions())) } loadCli() - err := RunCreate(cli, &CreateOptions{ - Name: "test", - Docker: map[string]string{"host": socketPath}, + err := runCreate(cli, &createOptions{ + name: "test", + endpoint: map[string]string{"host": socketPath}, }) assert.NilError(t, err) diff --git a/cli/command/defaultcontextstore.go b/cli/command/defaultcontextstore.go index 9b49b3af2a68..6a01a3c68fc8 100644 --- a/cli/command/defaultcontextstore.go +++ b/cli/command/defaultcontextstore.go @@ -52,7 +52,14 @@ type EndpointDefaultResolver interface { } // ResolveDefaultContext creates a Metadata for the current CLI invocation parameters +// +// Deprecated: this function is exported for testing and meant for internal use. It will be removed in the next release. func ResolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) { + return resolveDefaultContext(opts, config) +} + +// resolveDefaultContext creates a Metadata for the current CLI invocation parameters +func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) { contextTLSData := store.ContextTLSData{ Endpoints: make(map[string]store.EndpointTLSData), } diff --git a/cli/command/defaultcontextstore_test.go b/cli/command/defaultcontextstore_test.go index d0c8d09b758a..927298cb6669 100644 --- a/cli/command/defaultcontextstore_test.go +++ b/cli/command/defaultcontextstore_test.go @@ -7,7 +7,7 @@ import ( "crypto/rand" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" @@ -59,7 +59,7 @@ func TestDefaultContextInitializer(t *testing.T) { assert.NilError(t, err) t.Setenv("DOCKER_HOST", "ssh://someswarmserver") cli.configFile = &configfile.ConfigFile{} - ctx, err := ResolveDefaultContext(&cliflags.ClientOptions{ + ctx, err := resolveDefaultContext(&cliflags.ClientOptions{ TLS: true, TLSOptions: &tlsconfig.Options{ CAFile: "./testdata/ca.pem", @@ -158,7 +158,7 @@ func TestErrCreateDefault(t *testing.T) { Metadata: testContext{Bar: "baz"}, Name: "default", }) - assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) + assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument)) assert.Error(t, err, "default context cannot be created nor updated") } @@ -166,7 +166,7 @@ func TestErrRemoveDefault(t *testing.T) { meta := testDefaultMetadata() s := testStore(t, meta, store.ContextTLSData{}) err := s.Remove("default") - assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument)) + assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument)) assert.Error(t, err, "default context cannot be removed") } @@ -174,5 +174,5 @@ func TestErrTLSDataError(t *testing.T) { meta := testDefaultMetadata() s := testStore(t, meta, store.ContextTLSData{}) _, err := s.GetTLSData("default", "noop", "noop") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } diff --git a/cli/command/formatter/tabwriter/tabwriter.go b/cli/command/formatter/tabwriter/tabwriter.go index e7473cd9bbdb..a03d94ab609c 100644 --- a/cli/command/formatter/tabwriter/tabwriter.go +++ b/cli/command/formatter/tabwriter/tabwriter.go @@ -12,7 +12,7 @@ // based on https://github.com/golang/go/blob/master/src/text/tabwriter/tabwriter.go Last modified 690ac40 on 31 Jan -//nolint:gocyclo,nakedret,unused // ignore linting errors, so that we can stick close to upstream +//nolint:gocyclo,gofumpt,nakedret,unused // ignore linting errors, so that we can stick close to upstream package tabwriter import ( diff --git a/cli/command/idresolver/idresolver_test.go b/cli/command/idresolver/idresolver_test.go index dbf2f3875f84..4be7d71eb51a 100644 --- a/cli/command/idresolver/idresolver_test.go +++ b/cli/command/idresolver/idresolver_test.go @@ -12,13 +12,13 @@ import ( ) func TestResolveError(t *testing.T) { - cli := &fakeClient{ + apiClient := &fakeClient{ nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { return swarm.Node{}, []byte{}, errors.New("error inspecting node") }, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) _, err := idResolver.Resolve(context.Background(), struct{}{}, "nodeID") assert.Error(t, err, "unsupported type") @@ -26,7 +26,7 @@ func TestResolveError(t *testing.T) { func TestResolveWithNoResolveOption(t *testing.T) { resolved := false - cli := &fakeClient{ + apiClient := &fakeClient{ nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { resolved = true return swarm.Node{}, []byte{}, nil @@ -37,7 +37,7 @@ func TestResolveWithNoResolveOption(t *testing.T) { }, } - idResolver := New(cli, true) + idResolver := New(apiClient, true) id, err := idResolver.Resolve(context.Background(), swarm.Node{}, "nodeID") assert.NilError(t, err) @@ -47,14 +47,14 @@ func TestResolveWithNoResolveOption(t *testing.T) { func TestResolveWithCache(t *testing.T) { inspectCounter := 0 - cli := &fakeClient{ + apiClient := &fakeClient{ nodeInspectFunc: func(nodeID string) (swarm.Node, []byte, error) { inspectCounter++ return *builders.Node(builders.NodeName("node-foo")), []byte{}, nil }, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) ctx := context.Background() for i := 0; i < 2; i++ { @@ -97,10 +97,10 @@ func TestResolveNode(t *testing.T) { ctx := context.Background() for _, tc := range testCases { - cli := &fakeClient{ + apiClient := &fakeClient{ nodeInspectFunc: tc.nodeInspectFunc, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) id, err := idResolver.Resolve(ctx, swarm.Node{}, tc.nodeID) assert.NilError(t, err) @@ -132,10 +132,10 @@ func TestResolveService(t *testing.T) { ctx := context.Background() for _, tc := range testCases { - cli := &fakeClient{ + apiClient := &fakeClient{ serviceInspectFunc: tc.serviceInspectFunc, } - idResolver := New(cli, false) + idResolver := New(apiClient, false) id, err := idResolver.Resolve(ctx, swarm.Service{}, tc.serviceID) assert.NilError(t, err) diff --git a/cli/command/image/build.go b/cli/command/image/build.go index 66beeee2bba2..aebb421ee40a 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -10,7 +10,6 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" "github.com/distribution/reference" @@ -28,7 +27,6 @@ import ( buildtypes "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/container" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/builder/remotecontext/urlutil" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/moby/go-archive" @@ -76,12 +74,6 @@ func (o buildOptions) dockerfileFromStdin() bool { return o.dockerfileName == "-" } -// contextFromStdin returns true when the user specified that the build context -// should be read from stdin -func (o buildOptions) contextFromStdin() bool { - return o.context == "-" -} - func newBuildOptions() buildOptions { ulimits := make(map[string]*container.Ulimit) return buildOptions{ @@ -94,7 +86,14 @@ func newBuildOptions() buildOptions { } // NewBuildCommand creates a new `docker build` command -func NewBuildCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewBuildCommand(dockerCLI command.Cli) *cobra.Command { + return newBuildCommand(dockerCLI) +} + +// newBuildCommand creates a new `docker build` command +func newBuildCommand(dockerCli command.Cli) *cobra.Command { options := newBuildOptions() cmd := &cobra.Command{ @@ -152,7 +151,7 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"}) flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + flags.BoolVar(&options.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification") flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") flags.SetAnnotation("platform", "version", []string{"1.38"}) @@ -185,25 +184,27 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { //nolint:gocyclo func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) error { var ( - err error buildCtx io.ReadCloser dockerfileCtx io.ReadCloser contextDir string - tempDir string relDockerfile string progBuff io.Writer buildBuff io.Writer remote string ) + contextType, err := build.DetectContextType(options.context) + if err != nil { + return err + } + if options.dockerfileFromStdin() { - if options.contextFromStdin() { + if contextType == build.ContextTypeStdin { return errors.New("invalid argument: can't use stdin for both build context and dockerfile") } dockerfileCtx = dockerCli.In() } - specifiedContext := options.context progBuff = dockerCli.Out() buildBuff = dockerCli.Out() if options.quiet { @@ -217,13 +218,19 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } } - switch { - case options.contextFromStdin(): + switch contextType { + case build.ContextTypeStdin: // buildCtx is tar archive. if stdin was dockerfile then it is wrapped buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) - case isLocalDir(specifiedContext): - contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) - if err == nil && strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + if err != nil { + return fmt.Errorf("unable to prepare context from STDIN: %w", err) + } + case build.ContextTypeLocal: + contextDir, relDockerfile, err = build.GetContextFromLocalDir(options.context, options.dockerfileName) + if err != nil { + return errors.Errorf("unable to prepare context: %s", err) + } + if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { // Dockerfile is outside of build-context; read the Dockerfile and pass it as dockerfileCtx dockerfileCtx, err = os.Open(options.dockerfileName) if err != nil { @@ -231,24 +238,23 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } defer dockerfileCtx.Close() } - case urlutil.IsGitURL(specifiedContext): - tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) - case urlutil.IsURL(specifiedContext): - buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) - default: - return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) - } - - if err != nil { - if options.quiet && urlutil.IsURL(specifiedContext) { - _, _ = fmt.Fprintln(dockerCli.Err(), progBuff) + case build.ContextTypeGit: + var tempDir string + tempDir, relDockerfile, err = build.GetContextFromGitURL(options.context, options.dockerfileName) + if err != nil { + return errors.Errorf("unable to prepare context: %s", err) } - return errors.Errorf("unable to prepare context: %s", err) - } - - if tempDir != "" { - defer os.RemoveAll(tempDir) + defer func() { + _ = os.RemoveAll(tempDir) + }() contextDir = tempDir + case build.ContextTypeRemote: + buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, options.context, options.dockerfileName) + if err != nil && options.quiet { + _, _ = fmt.Fprintln(dockerCli.Err(), progBuff) + } + default: + return errors.Errorf("unable to prepare context: path %q not found", options.context) } // read from a directory into tar archive @@ -259,7 +265,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) } if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { - return errors.Wrap(err, "error checking context") + return errors.Wrap(err, "checking context") } // And canonicalize dockerfile name to a platform-independent one @@ -333,11 +339,19 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) configFile := dockerCli.ConfigFile() creds, _ := configFile.GetAllCredentials() authConfigs := make(map[string]registrytypes.AuthConfig, len(creds)) - for k, auth := range creds { - authConfigs[k] = registrytypes.AuthConfig(auth) + for k, authConfig := range creds { + authConfigs[k] = registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + } } buildOpts := imageBuildOptions(dockerCli, options) - buildOpts.Version = buildtypes.BuilderV1 buildOpts.Dockerfile = relDockerfile buildOpts.AuthConfigs = authConfigs buildOpts.RemoteContext = remote @@ -347,7 +361,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) if options.quiet { _, _ = fmt.Fprintf(dockerCli.Err(), "%s", progBuff) } - cancel() return err } defer response.Body.Close() @@ -364,7 +377,8 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) err = jsonstream.Display(ctx, response.Body, streams.NewOut(buildBuff), jsonstream.WithAuxCallback(aux)) if err != nil { - if jerr, ok := err.(*jsonstream.JSONError); ok { + var jerr *jsonstream.JSONError + if errors.As(err, &jerr) { // If no error code is set, default to 1 if jerr.Code == 0 { jerr.Code = 1 @@ -377,16 +391,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) return err } - // Windows: show error message about modified file permissions if the - // daemon isn't running Windows. - if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - _, _ = fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ - "image from Windows against a non-Windows Docker host. All files and "+ - "directories added to build context will have '-rwxr-xr-x' permissions. "+ - "It is recommended to double check and reset permissions for sensitive "+ - "files and directories.") - } - // Everything worked so if -q was provided the output from the daemon // should be just the image ID and we'll print that to stdout. if options.quiet { @@ -415,11 +419,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) return nil } -func isLocalDir(c string) bool { - _, err := os.Stat(c) - return err == nil -} - type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) // validateTag checks if the given image name can be resolved. @@ -543,6 +542,7 @@ func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.Rea func imageBuildOptions(dockerCli command.Cli, options buildOptions) buildtypes.ImageBuildOptions { configFile := dockerCli.ConfigFile() return buildtypes.ImageBuildOptions{ + Version: buildtypes.BuilderV1, Memory: options.memory.Value(), MemorySwap: options.memorySwap.Value(), Tags: options.tags.GetSlice(), diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index ca70d5484c77..3c7065b9b6b8 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -16,7 +16,7 @@ import ( "strings" "time" - "github.com/docker/docker/builder/remotecontext/git" + "github.com/docker/cli/cli/command/image/build/internal/git" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/moby/go-archive" @@ -25,9 +25,14 @@ import ( "github.com/pkg/errors" ) +// DefaultDockerfileName is the Default filename with Docker commands, read by docker build +// +// Deprecated: this const is no longer used and will be removed in the next release. +const DefaultDockerfileName string = "Dockerfile" + const ( - // DefaultDockerfileName is the Default filename with Docker commands, read by docker build - DefaultDockerfileName string = "Dockerfile" + // defaultDockerfileName is the Default filename with Docker commands, read by docker build + defaultDockerfileName string = "Dockerfile" // archiveHeaderSize is the number of bytes in an archive header archiveHeaderSize = 512 ) @@ -80,7 +85,7 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { if err != nil && os.IsPermission(err) { return errors.Errorf("no permission to read from '%s'", filePath) } - currentFile.Close() + _ = currentFile.Close() } return nil }) @@ -97,10 +102,21 @@ func filepathMatches(matcher *patternmatcher.PatternMatcher, file string) (bool, // DetectArchiveReader detects whether the input stream is an archive or a // Dockerfile and returns a buffered version of input, safe to consume in lieu -// of input. If an archive is detected, isArchive is set to true, and to false +// of input. If an archive is detected, ok is set to true, and to false +// otherwise, in which case it is safe to assume input represents the contents +// of a Dockerfile. +// +// Deprecated: this utility was only used internally, and will be removed in the next release. +func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err error) { + return detectArchiveReader(input) +} + +// detectArchiveReader detects whether the input stream is an archive or a +// Dockerfile and returns a buffered version of input, safe to consume in lieu +// of input. If an archive is detected, ok is set to true, and to false // otherwise, in which case it is safe to assume input represents the contents // of a Dockerfile. -func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, err error) { +func detectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err error) { buf := bufio.NewReader(input) magic, err := buf.Peek(archiveHeaderSize * 2) @@ -108,13 +124,22 @@ 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 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 -// name specified by DefaultDockerfileName and returns the path to the +// name specified by defaultDockerfileName and returns the path to the // temporary directory containing the Dockerfile. +// +// Deprecated: this utility was only used internally, and will be removed in the next release. func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { + return writeTempDockerfile(rc) +} + +// writeTempDockerfile writes a Dockerfile stream to a temporary file with a +// name specified by defaultDockerfileName and returns the path to the +// temporary directory containing the Dockerfile. +func writeTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { // err is a named return value, due to the defer call below. dockerfileDir, err = os.MkdirTemp("", "docker-build-tempdockerfile-") if err != nil { @@ -126,7 +151,7 @@ func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { } }() - f, err := os.Create(filepath.Join(dockerfileDir, DefaultDockerfileName)) + f, err := os.Create(filepath.Join(dockerfileDir, defaultDockerfileName)) if err != nil { return "", err } @@ -141,12 +166,12 @@ func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) { // Dockerfile or tar archive. Returns a tar archive used as a context and a // path to the Dockerfile inside the tar. func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { - rc, isArchive, err := DetectArchiveReader(rc) + rc, ok, err := detectArchiveReader(rc) if err != nil { return nil, "", err } - if isArchive { + if ok { return rc, dockerfileName, nil } @@ -159,7 +184,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC return nil, "", errors.New("ambiguous Dockerfile source: both stdin and flag correspond to Dockerfiles") } - dockerfileDir, err := WriteTempDockerfile(rc) + dockerfileDir, err := writeTempDockerfile(rc) if err != nil { return nil, "", err } @@ -171,14 +196,22 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC return newReadCloserWrapper(tarArchive, func() error { err := tarArchive.Close() - os.RemoveAll(dockerfileDir) + _ = os.RemoveAll(dockerfileDir) return err - }), DefaultDockerfileName, nil + }), defaultDockerfileName, nil } // IsArchive checks for the magic bytes of a tar or any supported compression // algorithm. +// +// Deprecated: this utility was used internally and will be removed in the next release. func IsArchive(header []byte) bool { + return isArchive(header) +} + +// isArchive checks for the magic bytes of a tar or any supported compression +// algorithm. +func isArchive(header []byte) bool { if compression.Detect(header) != compression.None { return true } @@ -201,7 +234,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory") } - absContextDir, err = ResolveAndValidateContextPath(absContextDir) + absContextDir, err = resolveAndValidateContextPath(absContextDir) if err != nil { return "", "", err } @@ -242,7 +275,7 @@ func getWithStatusError(url string) (resp *http.Response, err error) { } msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status) body, err := io.ReadAll(resp.Body) - resp.Body.Close() + _ = resp.Body.Close() if err != nil { return nil, errors.Wrapf(err, "%s: error reading body", msg) } @@ -254,7 +287,7 @@ func getWithStatusError(url string) (resp *http.Response, err error) { // the relative path of the dockerfile in that context directory, and a non-nil // error on success. func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, error) { - localDir, err := ResolveAndValidateContextPath(localDir) + localDir, err := resolveAndValidateContextPath(localDir) if err != nil { return "", "", err } @@ -274,7 +307,18 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, er // ResolveAndValidateContextPath uses the given context directory for a `docker build` // and returns the absolute path to the context directory. +// +// Deprecated: this utility was used internally and will be removed in the next +// release. Use [DetectContextType] to detect the context-type, and use +// [GetContextFromLocalDir], [GetContextFromLocalDir], [GetContextFromGitURL], +// or [GetContextFromURL] instead. func ResolveAndValidateContextPath(givenContextDir string) (string, error) { + return resolveAndValidateContextPath(givenContextDir) +} + +// resolveAndValidateContextPath uses the given context directory for a `docker build` +// and returns the absolute path to the context directory. +func resolveAndValidateContextPath(givenContextDir string) (string, error) { absContextDir, err := filepath.Abs(givenContextDir) if err != nil { return "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) @@ -318,12 +362,12 @@ func getDockerfileRelPath(absContextDir, givenDockerfile string) (string, error) if absDockerfile == "" { // No -f/--file was specified so use the default relative to the // context directory. - absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) + absDockerfile = filepath.Join(absContextDir, defaultDockerfileName) // Just to be nice ;-) look for 'dockerfile' too but only // use it if we found it, otherwise ignore this check if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { - altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) + altPath := filepath.Join(absContextDir, strings.ToLower(defaultDockerfileName)) if _, err = os.Lstat(altPath); err == nil { absDockerfile = altPath } @@ -374,7 +418,7 @@ func isUNC(path string) bool { // the relative path to the dockerfile in the context. func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { file, err := io.ReadAll(dockerfileCtx) - dockerfileCtx.Close() + _ = dockerfileCtx.Close() if err != nil { return nil, "", err } @@ -438,17 +482,19 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { go func() { compressWriter, err := compression.CompressStream(pipeWriter, archive.Gzip) if err != nil { - pipeWriter.CloseWithError(err) + _ = pipeWriter.CloseWithError(err) } - defer buildCtx.Close() + defer func() { + _ = buildCtx.Close() + }() if _, err := io.Copy(compressWriter, buildCtx); err != nil { - pipeWriter.CloseWithError(errors.Wrap(err, "failed to compress context")) - compressWriter.Close() + _ = pipeWriter.CloseWithError(errors.Wrap(err, "failed to compress context")) + _ = compressWriter.Close() return } - compressWriter.Close() - pipeWriter.Close() + _ = compressWriter.Close() + _ = pipeWriter.Close() }() return pipeReader, nil diff --git a/cli/command/image/build/context_detect.go b/cli/command/image/build/context_detect.go new file mode 100644 index 000000000000..c5edc4ecf161 --- /dev/null +++ b/cli/command/image/build/context_detect.go @@ -0,0 +1,39 @@ +package build + +import ( + "fmt" + "os" + + "github.com/docker/cli/cli/command/image/build/internal/urlutil" +) + +// ContextType describes the type (source) of build-context specified. +type ContextType string + +const ( + ContextTypeStdin ContextType = "stdin" // ContextTypeStdin indicates that the build-context is a TAR archive passed through STDIN. + ContextTypeLocal ContextType = "local" // ContextTypeLocal indicates that the build-context is a local directory. + ContextTypeRemote ContextType = "remote" // ContextTypeRemote indicates that the build-context is a remote URL. + ContextTypeGit ContextType = "git" // ContextTypeGit indicates that the build-context is a GIT URL. +) + +// DetectContextType detects the type (source) of the build-context. +func DetectContextType(specifiedContext string) (ContextType, error) { + switch { + case specifiedContext == "-": + return ContextTypeStdin, nil + case isLocalDir(specifiedContext): + return ContextTypeLocal, nil + case urlutil.IsGitURL(specifiedContext): + return ContextTypeGit, nil + case urlutil.IsURL(specifiedContext): + return ContextTypeRemote, nil + default: + return "", fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) + } +} + +func isLocalDir(c string) bool { + _, err := os.Stat(c) + return err == nil +} diff --git a/cli/command/image/build/context_test.go b/cli/command/image/build/context_test.go index 45533d6ebb4c..b0c83a46b1d2 100644 --- a/cli/command/image/build/context_test.go +++ b/cli/command/image/build/context_test.go @@ -31,7 +31,7 @@ func prepareNoFiles(t *testing.T) string { func prepareOneFile(t *testing.T) string { t.Helper() contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) return contextDir } @@ -66,7 +66,7 @@ func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) chdir(t, contextDir) @@ -74,23 +74,23 @@ func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromLocalDirLocalFile(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) testFilename := createTestTempFile(t, contextDir, "tmpTest", "test") absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") @@ -112,13 +112,13 @@ func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { contextDir := createTestTempDir(t) chdir(t, contextDir) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) - absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, defaultDockerfileName) assert.NilError(t, err) assert.Check(t, is.Equal(contextDir, absContextDir)) - assert.Check(t, is.Equal(DefaultDockerfileName, relDockerfile)) + assert.Check(t, is.Equal(defaultDockerfileName, relDockerfile)) } func TestGetContextFromReaderString(t *testing.T) { @@ -135,7 +135,7 @@ func TestGetContextFromReaderString(t *testing.T) { } buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) + _, _ = buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() @@ -150,8 +150,8 @@ func TestGetContextFromReaderString(t *testing.T) { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + if relDockerfile != defaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", defaultDockerfileName, relDockerfile) } } @@ -164,12 +164,12 @@ func TestGetContextFromReaderStringConflict(t *testing.T) { func TestGetContextFromReaderTar(t *testing.T) { contextDir := createTestTempDir(t) - createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents) + createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents) tarStream, err := archive.Tar(contextDir, compression.None) assert.NilError(t, err) - tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) + tarArchive, relDockerfile, err := GetContextFromReader(tarStream, defaultDockerfileName) assert.NilError(t, err) tarReader := tar.NewReader(tarArchive) @@ -177,12 +177,12 @@ func TestGetContextFromReaderTar(t *testing.T) { header, err := tarReader.Next() assert.NilError(t, err) - if header.Name != DefaultDockerfileName { - t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) + if header.Name != defaultDockerfileName { + t.Fatalf("Dockerfile name should be: %s, got: %s", defaultDockerfileName, header.Name) } buff := new(bytes.Buffer) - buff.ReadFrom(tarReader) + _, _ = buff.ReadFrom(tarReader) contents := buff.String() _, err = tarReader.Next() @@ -197,8 +197,8 @@ func TestGetContextFromReaderTar(t *testing.T) { t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) } - if relDockerfile != DefaultDockerfileName { - t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + if relDockerfile != defaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", defaultDockerfileName, relDockerfile) } } @@ -223,7 +223,7 @@ func TestValidateContextDirectoryWithOneFile(t *testing.T) { } func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { - testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) + testValidateContextDirectory(t, prepareOneFile, []string{defaultDockerfileName}) } // createTestTempDir creates a temporary directory for testing. It returns the @@ -263,7 +263,7 @@ func chdir(t *testing.T, dir string) { } func TestIsArchive(t *testing.T) { - testcases := []struct { + tests := []struct { doc string header []byte expected bool @@ -289,13 +289,15 @@ func TestIsArchive(t *testing.T) { expected: false, }, } - for _, testcase := range testcases { - assert.Check(t, is.Equal(testcase.expected, IsArchive(testcase.header)), testcase.doc) + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + assert.Check(t, is.Equal(tc.expected, isArchive(tc.header)), tc.doc) + }) } } func TestDetectArchiveReader(t *testing.T) { - testcases := []struct { + tests := []struct { file string desc string expected bool @@ -316,14 +318,18 @@ func TestDetectArchiveReader(t *testing.T) { expected: false, }, } - for _, testcase := range testcases { - content, err := os.Open(testcase.file) - assert.NilError(t, err) - defer content.Close() - - _, isArchive, err := DetectArchiveReader(content) - assert.NilError(t, err) - assert.Check(t, is.Equal(testcase.expected, isArchive), testcase.file) + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + content, err := os.Open(tc.file) + assert.NilError(t, err) + defer func() { + _ = content.Close() + }() + + _, isArchive, err := detectArchiveReader(content) + assert.NilError(t, err) + assert.Check(t, is.Equal(tc.expected, isArchive), tc.file) + }) } } diff --git a/cli/command/image/build/dockerignore.go b/cli/command/image/build/dockerignore.go index 357210437c03..8d11aa5e10d6 100644 --- a/cli/command/image/build/dockerignore.go +++ b/cli/command/image/build/dockerignore.go @@ -21,7 +21,9 @@ func ReadDockerignore(contextDir string) ([]string, error) { case err != nil: return nil, err } - defer f.Close() + defer func() { + _ = f.Close() + }() patterns, err := ignorefile.ReadAll(f) if err != nil { diff --git a/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go b/cli/command/image/build/internal/git/gitutils.go similarity index 99% rename from vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go rename to cli/command/image/build/internal/git/gitutils.go index b02993511fbb..1f3063b50433 100644 --- a/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go +++ b/cli/command/image/build/internal/git/gitutils.go @@ -148,6 +148,9 @@ func supportsShallowClone(remoteURL string) bool { // Try a HEAD request and fallback to a Get request on error res, err := http.Head(serviceURL) // #nosec G107 + if err == nil { + _ = res.Body.Close() + } if err != nil || res.StatusCode != http.StatusOK { res, err = http.Get(serviceURL) // #nosec G107 if err == nil { diff --git a/cli/command/image/build/internal/git/gitutils_test.go b/cli/command/image/build/internal/git/gitutils_test.go new file mode 100644 index 000000000000..61dfb56d62f8 --- /dev/null +++ b/cli/command/image/build/internal/git/gitutils_test.go @@ -0,0 +1,382 @@ +package git + +import ( + "bytes" + "fmt" + "net/http" + "net/http/cgi" + "net/http/httptest" + "net/url" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestParseRemoteURL(t *testing.T) { + tests := []struct { + doc string + url string + expected gitRepo + }{ + { + doc: "git scheme uppercase, no url-fragment", + url: "GIT://github.com/user/repo.git", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "git scheme, no url-fragment", + url: "git://github.com/user/repo.git", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "git scheme, with url-fragment", + url: "git://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "git://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "https scheme, no url-fragment", + url: "https://github.com/user/repo.git", + expected: gitRepo{ + remote: "https://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "https scheme, with url-fragment", + url: "https://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "https://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "git@, no url-fragment", + url: "git@github.com:user/repo.git", + expected: gitRepo{ + remote: "git@github.com:user/repo.git", + ref: "master", + }, + }, + { + doc: "git@, with url-fragment", + url: "git@github.com:user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "git@github.com:user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "ssh, no url-fragment", + url: "ssh://github.com/user/repo.git", + expected: gitRepo{ + remote: "ssh://github.com/user/repo.git", + ref: "master", + }, + }, + { + doc: "ssh, with url-fragment", + url: "ssh://github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "ssh://github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + { + doc: "ssh, with url-fragment and user", + url: "ssh://foo%40barcorp.com@github.com/user/repo.git#mybranch:mydir/mysubdir/", + expected: gitRepo{ + remote: "ssh://foo%40barcorp.com@github.com/user/repo.git", + ref: "mybranch", + subdir: "mydir/mysubdir/", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + repo, err := parseRemoteURL(tc.url) + assert.NilError(t, err) + assert.Check(t, is.DeepEqual(tc.expected, repo, cmp.AllowUnexported(gitRepo{}))) + }) + } +} + +func TestCloneArgsSmartHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, err := url.Parse(server.URL) + assert.NilError(t, err) + + serverURL.Path = "/repo.git" + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query().Get("service") + w.Header().Set("Content-Type", fmt.Sprintf("application/x-%s-advertisement", q)) + }) + + args := fetchArgs(serverURL.String(), "master") + exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func TestCloneArgsDumbHttp(t *testing.T) { + mux := http.NewServeMux() + server := httptest.NewServer(mux) + serverURL, _ := url.Parse(server.URL) + + serverURL.Path = "/repo.git" + + mux.HandleFunc("/repo.git/info/refs", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + }) + + args := fetchArgs(serverURL.String(), "master") + exp := []string{"fetch", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func TestCloneArgsGit(t *testing.T) { + args := fetchArgs("git://github.com/docker/docker", "master") + exp := []string{"fetch", "--depth", "1", "origin", "--", "master"} + assert.Check(t, is.DeepEqual(exp, args)) +} + +func gitGetConfig(name string) string { + b, err := gitRepo{}.gitWithinDir("", "config", "--get", name) + if err != nil { + // since we are interested in empty or non empty string, + // we can safely ignore the err here. + return "" + } + return strings.TrimSpace(string(b)) +} + +func TestCheckoutGit(t *testing.T) { + root := t.TempDir() + + gitpath, err := exec.LookPath("git") + assert.NilError(t, err) + gitversion, _ := exec.Command(gitpath, "version").CombinedOutput() + t.Logf("%s", gitversion) // E.g. "git version 2.30.2" + + // Serve all repositories under root using the Smart HTTP protocol so + // they can be cloned. The Dumb HTTP protocol is incompatible with + // shallow cloning but we unconditionally shallow-clone submodules, and + // we explicitly disable the file protocol. + // (Another option would be to use `git daemon` and the Git protocol, + // but that listens on a fixed port number which is a recipe for + // disaster in CI. Funnily enough, `git daemon --port=0` works but there + // is no easy way to discover which port got picked!) + + // Associate git-http-backend logs with the current (sub)test. + // Incompatible with parallel subtests. + currentSubtest := t + githttp := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var logs bytes.Buffer + (&cgi.Handler{ + Path: gitpath, + Args: []string{"http-backend"}, + Dir: root, + Env: []string{ + "GIT_PROJECT_ROOT=" + root, + "GIT_HTTP_EXPORT_ALL=1", + }, + Stderr: &logs, + }).ServeHTTP(w, r) + if logs.Len() == 0 { + return + } + for { + line, err := logs.ReadString('\n') + currentSubtest.Log("git-http-backend: " + line) + if err != nil { + break + } + } + }) + server := httptest.NewServer(&githttp) + defer server.Close() + + eol := "\n" + autocrlf := gitGetConfig("core.autocrlf") + switch autocrlf { + case "true": + eol = "\r\n" + case "false", "input", "": + // accepted values + default: + t.Logf(`unknown core.autocrlf value: "%s"`, autocrlf) + } + + must := func(out []byte, err error) { + t.Helper() + if len(out) > 0 { + t.Logf("%s", out) + } + assert.NilError(t, err) + } + + gitDir := filepath.Join(root, "repo") + must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", gitDir)) + must(gitRepo{}.gitWithinDir(gitDir, "config", "user.email", "test@docker.com")) + must(gitRepo{}.gitWithinDir(gitDir, "config", "user.name", "Docker test")) + assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch"), 0o644)) + + subDir := filepath.Join(gitDir, "subdir") + assert.NilError(t, os.Mkdir(subDir, 0o755)) + assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 5000"), 0o644)) + + if runtime.GOOS != "windows" { + assert.NilError(t, os.Symlink("../subdir", filepath.Join(gitDir, "parentlink"))) + assert.NilError(t, os.Symlink("/subdir", filepath.Join(gitDir, "absolutelink"))) + } + + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "First commit")) + must(gitRepo{}.gitWithinDir(gitDir, "checkout", "-b", "test")) + + assert.NilError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM scratch\nEXPOSE 3000"), 0o644)) + assert.NilError(t, os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM busybox\nEXPOSE 5000"), 0o644)) + + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "Branch commit")) + must(gitRepo{}.gitWithinDir(gitDir, "checkout", "master")) + + // set up submodule + subrepoDir := filepath.Join(root, "subrepo") + must(gitRepo{}.gitWithinDir(root, "-c", "init.defaultBranch=master", "init", subrepoDir)) + must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.email", "test@docker.com")) + must(gitRepo{}.gitWithinDir(subrepoDir, "config", "user.name", "Docker test")) + + assert.NilError(t, os.WriteFile(filepath.Join(subrepoDir, "subfile"), []byte("subcontents"), 0o644)) + + must(gitRepo{}.gitWithinDir(subrepoDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(subrepoDir, "commit", "-am", "Subrepo initial")) + + must(gitRepo{}.gitWithinDir(gitDir, "submodule", "add", server.URL+"/subrepo", "sub")) + must(gitRepo{}.gitWithinDir(gitDir, "add", "-A")) + must(gitRepo{}.gitWithinDir(gitDir, "commit", "-am", "With submodule")) + + type singleCase struct { + frag string + exp string + fail bool + submodule bool + } + + cases := []singleCase{ + {"", "FROM scratch", false, true}, + {"master", "FROM scratch", false, true}, + {":subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, + {":nosubdir", "", true, false}, // missing directory error + {":Dockerfile", "", true, false}, // not a directory error + {"master:nosubdir", "", true, false}, + {"master:subdir", "FROM scratch" + eol + "EXPOSE 5000", false, false}, + {"master:../subdir", "", true, false}, + {"test", "FROM scratch" + eol + "EXPOSE 3000", false, false}, + {"test:", "FROM scratch" + eol + "EXPOSE 3000", false, false}, + {"test:subdir", "FROM busybox" + eol + "EXPOSE 5000", false, false}, + } + + if runtime.GOOS != "windows" { + // Windows GIT (2.7.1 x64) does not support parentlink/absolutelink. Sample output below + // git --work-tree .\repo --git-dir .\repo\.git add -A + // error: readlink("absolutelink"): Function not implemented + // error: unable to index file absolutelink + // fatal: adding files failed + cases = append(cases, singleCase{frag: "master:absolutelink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + cases = append(cases, singleCase{frag: "master:parentlink", exp: "FROM scratch" + eol + "EXPOSE 5000", fail: false}) + } + + for _, c := range cases { + t.Run(c.frag, func(t *testing.T) { + currentSubtest = t + ref, subdir := getRefAndSubdir(c.frag) + r, err := gitRepo{remote: server.URL + "/repo", ref: ref, subdir: subdir}.clone() + + if c.fail { + assert.Check(t, is.ErrorContains(err, "")) + return + } + assert.NilError(t, err) + defer os.RemoveAll(r) + if c.submodule { + b, err := os.ReadFile(filepath.Join(r, "sub/subfile")) + assert.NilError(t, err) + assert.Check(t, is.Equal("subcontents", string(b))) + } else { + _, err := os.Stat(filepath.Join(r, "sub/subfile")) + assert.ErrorContains(t, err, "") + assert.Assert(t, os.IsNotExist(err)) + } + + b, err := os.ReadFile(filepath.Join(r, "Dockerfile")) + assert.NilError(t, err) + assert.Check(t, is.Equal(c.exp, string(b))) + }) + } +} + +func TestValidGitTransport(t *testing.T) { + gitUrls := []string{ + "git://github.com/docker/docker", + "git@github.com:docker/docker.git", + "git@bitbucket.org:atlassianlabs/atlassian-docker.git", + "https://github.com/docker/docker.git", + "http://github.com/docker/docker.git", + "http://github.com/docker/docker.git#branch", + "http://github.com/docker/docker.git#:dir", + } + incompleteGitUrls := []string{ + "github.com/docker/docker", + } + + for _, u := range gitUrls { + if !isGitTransport(u) { + t.Fatalf("%q should be detected as valid Git prefix", u) + } + } + + for _, u := range incompleteGitUrls { + if isGitTransport(u) { + t.Fatalf("%q should not be detected as valid Git prefix", u) + } + } +} + +func TestGitInvalidRef(t *testing.T) { + gitUrls := []string{ + "git://github.com/moby/moby#--foo bar", + "git@github.com/moby/moby#--upload-pack=sleep;:", + "git@g.com:a/b.git#-B", + "git@g.com:a/b.git#with space", + } + + for _, u := range gitUrls { + _, err := Clone(u) + assert.Assert(t, err != nil) + // On Windows, git has different case for the "invalid refspec" error, + // so we can't use ErrorContains. + assert.Check(t, is.Contains(strings.ToLower(err.Error()), "invalid refspec")) + } +} diff --git a/vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go b/cli/command/image/build/internal/urlutil/urlutil.go similarity index 98% rename from vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go rename to cli/command/image/build/internal/urlutil/urlutil.go index a2225b5960e1..b1cf47ce316b 100644 --- a/vendor/github.com/docker/docker/builder/remotecontext/urlutil/urlutil.go +++ b/cli/command/image/build/internal/urlutil/urlutil.go @@ -8,7 +8,7 @@ package urlutil import ( "strings" - "github.com/docker/docker/internal/lazyregexp" + "github.com/docker/cli/internal/lazyregexp" ) // urlPathWithFragmentSuffix matches fragments to use as Git reference and build diff --git a/cli/command/image/build/internal/urlutil/urlutil_test.go b/cli/command/image/build/internal/urlutil/urlutil_test.go new file mode 100644 index 000000000000..f6d0a35de3ec --- /dev/null +++ b/cli/command/image/build/internal/urlutil/urlutil_test.go @@ -0,0 +1,42 @@ +package urlutil + +import "testing" + +var ( + gitUrls = []string{ + "git://github.com/docker/docker", + "git@github.com:docker/docker.git", + "git@bitbucket.org:atlassianlabs/atlassian-docker.git", + "https://github.com/docker/docker.git", + "http://github.com/docker/docker.git", + "http://github.com/docker/docker.git#branch", + "http://github.com/docker/docker.git#:dir", + } + incompleteGitUrls = []string{ + "github.com/docker/docker", + } + invalidGitUrls = []string{ + "http://github.com/docker/docker.git:#branch", + "https://github.com/docker/dgit", + } +) + +func TestIsGIT(t *testing.T) { + for _, url := range gitUrls { + if !IsGitURL(url) { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range incompleteGitUrls { + if !IsGitURL(url) { + t.Fatalf("%q should be detected as valid Git url", url) + } + } + + for _, url := range invalidGitUrls { + if IsGitURL(url) { + t.Fatalf("%q should not be detected as valid Git prefix", url) + } + } +} diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 22105e47c024..f1bfacf24bdc 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -123,7 +123,7 @@ COPY data /data // to support testing (ex: docker/cli#294) func TestRunBuildFromGitHubSpecialCase(t *testing.T) { t.Setenv("DOCKER_BUILDKIT", "0") - cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{})) + cmd := newBuildCommand(test.NewFakeCli(&fakeClient{})) // Clone a small repo that exists so git doesn't prompt for credentials cmd.SetArgs([]string{"github.com/docker/for-win"}) cmd.SetOut(io.Discard) @@ -146,7 +146,7 @@ func TestRunBuildFromLocalGitHubDir(t *testing.T) { assert.NilError(t, err) client := test.NewFakeCli(&fakeClient{}) - cmd := NewBuildCommand(client) + cmd := newBuildCommand(client) cmd.SetArgs([]string{buildDir}) cmd.SetOut(io.Discard) err = cmd.Execute() diff --git a/cli/command/image/cmd.go b/cli/command/image/cmd.go index c035da17f05c..1532a7e21bc3 100644 --- a/cli/command/image/cmd.go +++ b/cli/command/image/cmd.go @@ -7,7 +7,14 @@ import ( ) // NewImageCommand returns a cobra command for `image` subcommands -func NewImageCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewImageCommand(dockerCLI command.Cli) *cobra.Command { + return newImageCommand(dockerCLI) +} + +// newImageCommand returns a cobra command for `image` subcommands +func newImageCommand(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Manage images", @@ -15,18 +22,18 @@ func NewImageCommand(dockerCli command.Cli) *cobra.Command { RunE: command.ShowHelp(dockerCli.Err()), } cmd.AddCommand( - NewBuildCommand(dockerCli), - NewHistoryCommand(dockerCli), - NewImportCommand(dockerCli), - NewLoadCommand(dockerCli), - NewPullCommand(dockerCli), - NewPushCommand(dockerCli), - NewSaveCommand(dockerCli), - NewTagCommand(dockerCli), + newBuildCommand(dockerCli), + newHistoryCommand(dockerCli), + newImportCommand(dockerCli), + newLoadCommand(dockerCli), + newPullCommand(dockerCli), + newPushCommand(dockerCli), + newSaveCommand(dockerCli), + newTagCommand(dockerCli), newListCommand(dockerCli), - newRemoveCommand(dockerCli), + newImageRemoveCommand(dockerCli), newInspectCommand(dockerCli), - NewPruneCommand(dockerCli), + newPruneCommand(dockerCli), ) return cmd } diff --git a/cli/command/image/formatter_history.go b/cli/command/image/formatter_history.go index e2fcd155ce23..2a71fb94a1d2 100644 --- a/cli/command/image/formatter_history.go +++ b/cli/command/image/formatter_history.go @@ -20,7 +20,14 @@ const ( ) // NewHistoryFormat returns a format for rendering an HistoryContext +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewHistoryFormat(source string, quiet bool, human bool) formatter.Format { + return newHistoryFormat(source, quiet, human) +} + +// newHistoryFormat returns a format for rendering a historyContext. +func newHistoryFormat(source string, quiet bool, human bool) formatter.Format { if source == formatter.TableFormatKey { switch { case quiet: @@ -36,10 +43,17 @@ func NewHistoryFormat(source string, quiet bool, human bool) formatter.Format { } // HistoryWrite writes the context -func HistoryWrite(ctx formatter.Context, human bool, histories []image.HistoryResponseItem) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func HistoryWrite(fmtCtx formatter.Context, human bool, histories []image.HistoryResponseItem) error { + return historyWrite(fmtCtx, human, histories) +} + +// historyWrite writes the context +func historyWrite(fmtCtx formatter.Context, human bool, histories []image.HistoryResponseItem) error { render := func(format func(subContext formatter.SubContext) error) error { for _, history := range histories { - historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} + historyCtx := &historyContext{trunc: fmtCtx.Trunc, h: history, human: human} if err := format(historyCtx); err != nil { return err } @@ -55,7 +69,7 @@ func HistoryWrite(ctx formatter.Context, human bool, histories []image.HistoryRe "Size": formatter.SizeHeader, "Comment": commentHeader, } - return ctx.Write(historyCtx, render) + return fmtCtx.Write(historyCtx, render) } type historyContext struct { diff --git a/cli/command/image/formatter_history_test.go b/cli/command/image/formatter_history_test.go index 611f7d50674f..b5e71f6be09a 100644 --- a/cli/command/image/formatter_history_test.go +++ b/cli/command/image/formatter_history_test.go @@ -237,7 +237,7 @@ imageID6 17 years ago /bin/bash echo 183MB }{ { formatter.Context{ - Format: NewHistoryFormat("table", false, true), + Format: newHistoryFormat("table", false, true), Trunc: true, Output: out, }, @@ -245,7 +245,7 @@ imageID6 17 years ago /bin/bash echo 183MB }, { formatter.Context{ - Format: NewHistoryFormat("table", false, true), + Format: newHistoryFormat("table", false, true), Trunc: false, Output: out, }, @@ -255,7 +255,7 @@ imageID6 17 years ago /bin/bash echo 183MB for _, tc := range cases { t.Run(string(tc.context.Format), func(t *testing.T) { - err := HistoryWrite(tc.context, true, histories) + err := historyWrite(tc.context, true, histories) assert.NilError(t, err) assert.Equal(t, out.String(), tc.expected) // Clean buffer diff --git a/cli/command/image/history.go b/cli/command/image/history.go index 075b3d84db31..24817c1ef496 100644 --- a/cli/command/image/history.go +++ b/cli/command/image/history.go @@ -25,7 +25,14 @@ type historyOptions struct { } // NewHistoryCommand creates a new `docker history` command -func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewHistoryCommand(dockerCLI command.Cli) *cobra.Command { + return newHistoryCommand(dockerCLI) +} + +// newHistoryCommand creates a new `docker history` command +func newHistoryCommand(dockerCLI command.Cli) *cobra.Command { var opts historyOptions cmd := &cobra.Command{ @@ -34,9 +41,9 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.image = args[0] - return runHistory(cmd.Context(), dockerCli, opts) + return runHistory(cmd.Context(), dockerCLI, opts) }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), Annotations: map[string]string{ "aliases": "docker image history, docker history", }, @@ -77,8 +84,8 @@ func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) historyCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewHistoryFormat(format, opts.quiet, opts.human), + Format: newHistoryFormat(format, opts.quiet, opts.human), Trunc: !opts.noTrunc, } - return HistoryWrite(historyCtx, opts.human, history) + return historyWrite(historyCtx, opts.human, history) } diff --git a/cli/command/image/history_test.go b/cli/command/image/history_test.go index 7ecc054d0a1b..a5dc0cb86ccf 100644 --- a/cli/command/image/history_test.go +++ b/cli/command/image/history_test.go @@ -42,7 +42,7 @@ func TestNewHistoryCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) + cmd := newHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -114,7 +114,7 @@ func TestNewHistoryCommandSuccess(t *testing.T) { // printed in the current timezone t.Setenv("TZ", "UTC") cli := test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}) - cmd := NewHistoryCommand(cli) + cmd := newHistoryCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/cli/command/image/import.go b/cli/command/image/import.go index a55ea4ab5bca..d94e9cf72a03 100644 --- a/cli/command/image/import.go +++ b/cli/command/image/import.go @@ -23,7 +23,14 @@ type importOptions struct { } // NewImportCommand creates a new `docker import` command -func NewImportCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewImportCommand(dockerCLI command.Cli) *cobra.Command { + return newImportCommand(dockerCLI) +} + +// newImportCommand creates a new `docker import` command +func newImportCommand(dockerCLI command.Cli) *cobra.Command { var options importOptions cmd := &cobra.Command{ @@ -35,7 +42,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { options.reference = args[1] } - return runImport(cmd.Context(), dockerCli, options) + return runImport(cmd.Context(), dockerCLI, options) }, Annotations: map[string]string{ "aliases": "docker image import, docker import", @@ -47,7 +54,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command { options.changes = dockeropts.NewListOpts(nil) flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image") flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image") - command.AddPlatformFlag(flags, &options.platform) + addPlatformFlag(flags, &options.platform) _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) return cmd diff --git a/cli/command/image/import_test.go b/cli/command/image/import_test.go index d6d69948df1f..05675a977a53 100644 --- a/cli/command/image/import_test.go +++ b/cli/command/image/import_test.go @@ -34,7 +34,7 @@ func TestNewImportCommandErrors(t *testing.T) { }, } for _, tc := range testCases { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -43,7 +43,7 @@ func TestNewImportCommandErrors(t *testing.T) { } func TestNewImportCommandInvalidFile(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"}) @@ -99,7 +99,7 @@ func TestNewImportCommandSuccess(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) + cmd := newImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/list.go b/cli/command/image/list.go index ce2238dc8298..b0ea568e050f 100644 --- a/cli/command/image/list.go +++ b/cli/command/image/list.go @@ -29,7 +29,14 @@ type imagesOptions struct { } // NewImagesCommand creates a new `docker images` command +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { + return newImagesCommand(dockerCLI) +} + +// newImagesCommand creates a new `docker images` command +func newImagesCommand(dockerCLI command.Cli) *cobra.Command { options := imagesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -69,7 +76,7 @@ func NewImagesCommand(dockerCLI command.Cli) *cobra.Command { } func newListCommand(dockerCLI command.Cli) *cobra.Command { - cmd := *NewImagesCommand(dockerCLI) + cmd := *newImagesCommand(dockerCLI) cmd.Aliases = []string{"list"} cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" return &cmd diff --git a/cli/command/image/list_test.go b/cli/command/image/list_test.go index 8b3ff715becf..06363857efe4 100644 --- a/cli/command/image/list_test.go +++ b/cli/command/image/list_test.go @@ -36,7 +36,7 @@ func TestNewImagesCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) + cmd := newImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -85,7 +85,7 @@ func TestNewImagesCommandSuccess(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}) cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat}) - cmd := NewImagesCommand(cli) + cmd := newImagesCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -104,7 +104,7 @@ func TestNewListCommandAlias(t *testing.T) { func TestNewListCommandAmbiguous(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) - cmd := NewImagesCommand(cli) + cmd := newImagesCommand(cli) cmd.SetOut(io.Discard) // Set the Use field to mimic that the command was called as "docker images", diff --git a/cli/command/image/load.go b/cli/command/image/load.go index 8d3d3906d0e0..442178e54a4e 100644 --- a/cli/command/image/load.go +++ b/cli/command/image/load.go @@ -22,7 +22,14 @@ type loadOptions struct { } // NewLoadCommand creates a new `docker load` command -func NewLoadCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewLoadCommand(dockerCLI command.Cli) *cobra.Command { + return newLoadCommand(dockerCLI) +} + +// newLoadCommand creates a new `docker load` command +func newLoadCommand(dockerCLI command.Cli) *cobra.Command { var opts loadOptions cmd := &cobra.Command{ @@ -30,12 +37,12 @@ func NewLoadCommand(dockerCli command.Cli) *cobra.Command { Short: "Load an image from a tar archive or STDIN", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runLoad(cmd.Context(), dockerCli, opts) + return runLoad(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker image load, docker load", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/image/load_test.go b/cli/command/image/load_test.go index d3a7dfd7dd98..143d4d59c9ba 100644 --- a/cli/command/image/load_test.go +++ b/cli/command/image/load_test.go @@ -54,7 +54,7 @@ func TestNewLoadCommandErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) cli.In().SetIsTerminal(tc.isTerminalIn) - cmd := NewLoadCommand(cli) + cmd := newLoadCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -65,7 +65,7 @@ func TestNewLoadCommandErrors(t *testing.T) { func TestNewLoadCommandInvalidInput(t *testing.T) { expectedError := "open *" - cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{})) + cmd := newLoadCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"--input", "*"}) @@ -117,7 +117,7 @@ func TestNewLoadCommandSuccess(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}) - cmd := NewLoadCommand(cli) + cmd := newLoadCommand(cli) cmd.SetOut(io.Discard) cmd.SetArgs(tc.args) err := cmd.Execute() diff --git a/cli/command/image/opts.go b/cli/command/image/opts.go new file mode 100644 index 000000000000..37378b6ece65 --- /dev/null +++ b/cli/command/image/opts.go @@ -0,0 +1,17 @@ +package image + +import ( + "os" + + "github.com/spf13/pflag" +) + +// addPlatformFlag adds "--platform" to a set of flags for API version 1.32 and +// later, using the value of "DOCKER_DEFAULT_PLATFORM" (if set) as a default. +// +// It should not be used for new uses, which may have a different API version +// requirement. +func addPlatformFlag(flags *pflag.FlagSet, target *string) { + flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") + _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) +} diff --git a/cli/command/image/prune.go b/cli/command/image/prune.go index eec9b1c0ba92..adc9773c30bd 100644 --- a/cli/command/image/prune.go +++ b/cli/command/image/prune.go @@ -8,7 +8,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/go-units" @@ -23,7 +22,14 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for images -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPruneCommand(dockerCLI command.Cli) *cobra.Command { + return newPruneCommand(dockerCLI) +} + +// newPruneCommand returns a new cobra prune command for images +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -31,18 +37,18 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused images", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options) + spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - fmt.Fprintln(dockerCli.Out(), output) + fmt.Fprintln(dockerCLI.Out(), output) } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index 235e3a7a1754..a45e0b7435f5 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -2,21 +2,17 @@ package image import ( "context" + "errors" "fmt" - "strings" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/trust" - "github.com/pkg/errors" "github.com/spf13/cobra" ) -// PullOptions defines what and how to pull -type PullOptions = pullOptions - // pullOptions defines what and how to pull. type pullOptions struct { remote string @@ -27,7 +23,14 @@ type pullOptions struct { } // NewPullCommand creates a new `docker pull` command -func NewPullCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPullCommand(dockerCLI command.Cli) *cobra.Command { + return newPullCommand(dockerCLI) +} + +// newPullCommand creates a new `docker pull` command +func newPullCommand(dockerCLI command.Cli) *cobra.Command { var opts pullOptions cmd := &cobra.Command{ @@ -36,13 +39,15 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runPull(cmd.Context(), dockerCli, opts) + return runPull(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "category-top": "5", "aliases": "docker image pull, docker pull", }, - ValidArgsFunction: completion.NoComplete, + // Complete with local images to help pulling the latest version + // of images that are in the image cache. + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), } flags := cmd.Flags() @@ -50,19 +55,14 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") - command.AddPlatformFlag(flags, &opts.platform) - command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + addPlatformFlag(flags, &opts.platform) + flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification") _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) return cmd } -// RunPull performs a pull against the engine based on the specified options -func RunPull(ctx context.Context, dockerCLI command.Cli, opts PullOptions) error { - return runPull(ctx, dockerCLI, opts) -} - // runPull performs a pull against the engine based on the specified options func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error { distributionRef, err := reference.ParseNormalizedNamed(opts.remote) @@ -78,7 +78,7 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error } } - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(dockerCLI), distributionRef.String()) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), distributionRef.String()) if err != nil { return err } @@ -86,15 +86,13 @@ func runPull(ctx context.Context, dockerCLI command.Cli, opts pullOptions) error // Check if reference has a digest _, isCanonical := distributionRef.(reference.Canonical) if !opts.untrusted && !isCanonical { - err = trustedPull(ctx, dockerCLI, imgRefAndAuth, opts) + if err := trustedPull(ctx, dockerCLI, imgRefAndAuth, opts); err != nil { + return err + } } else { - err = imagePullPrivileged(ctx, dockerCLI, imgRefAndAuth, opts) - } - if err != nil { - if strings.Contains(err.Error(), "when fetching 'plugin'") { - return errors.New(err.Error() + " - Use `docker plugin install`") + if err := imagePullPrivileged(ctx, dockerCLI, imgRefAndAuth, opts); err != nil { + return err } - return err } _, _ = fmt.Fprintln(dockerCLI.Out(), imgRefAndAuth.Reference().String()) return nil diff --git a/cli/command/image/pull_test.go b/cli/command/image/pull_test.go index a853949f32ca..baf375fdfeed 100644 --- a/cli/command/image/pull_test.go +++ b/cli/command/image/pull_test.go @@ -40,7 +40,7 @@ func TestNewPullCommandErrors(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{}) - cmd := NewPullCommand(cli) + cmd := newPullCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -79,7 +79,7 @@ func TestNewPullCommandSuccess(t *testing.T) { return io.NopCloser(strings.NewReader("")), nil }, }) - cmd := NewPullCommand(cli) + cmd := newPullCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -118,13 +118,14 @@ func TestNewPullCommandWithContentTrustErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST", "true") cli := test.NewFakeCli(&fakeClient{ imagePullFunc: func(ref string, options image.PullOptions) (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("")), errors.New("shouldn't try to pull image") }, - }, test.EnableContentTrust) + }) cli.SetNotaryClient(tc.notaryFunc) - cmd := NewPullCommand(cli) + cmd := newPullCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/push.go b/cli/command/image/push.go index a875ac0d9f8d..db19e8fb4c2b 100644 --- a/cli/command/image/push.go +++ b/cli/command/image/push.go @@ -15,12 +15,11 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/streams" + "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/tui" "github.com/docker/docker/api/types/auxprogress" "github.com/docker/docker/api/types/image" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/morikuni/aec" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -36,7 +35,14 @@ type pushOptions struct { } // NewPushCommand creates a new `docker push` command -func NewPushCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPushCommand(dockerCLI command.Cli) *cobra.Command { + return newPushCommand(dockerCLI) +} + +// newPushCommand creates a new `docker push` command +func newPushCommand(dockerCLI command.Cli) *cobra.Command { var opts pushOptions cmd := &cobra.Command{ @@ -45,19 +51,19 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runPush(cmd.Context(), dockerCli, opts) + return runPush(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "category-top": "6", "aliases": "docker image push, docker push", }, - ValidArgsFunction: completion.ImageNames(dockerCli, 1), + ValidArgsFunction: completion.ImageNames(dockerCLI, 1), } flags := cmd.Flags() flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output") - command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image signing") // Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to // pushing the image as-is. This also avoids forcing the platform selection @@ -74,8 +80,6 @@ Image index won't be pushed, meaning that other manifests, including attestation } // runPush performs a push against the engine based on the specified options. -// -//nolint:gocyclo // ignore cyclomatic complexity 17 of func `runPush` is high (> 16) for now. func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error { var platform *ocispec.Platform out := tui.NewOutput(dockerCli.Out()) @@ -94,9 +98,11 @@ To push the complete multi-platform image, remove the --platform flag. } ref, err := reference.ParseNormalizedNamed(opts.remote) - switch { - case err != nil: + if err != nil { return err + } + + switch { case opts.all && !reference.IsNameOnly(ref): return errors.New("tag can't be used with --all-tags/-a") case !opts.all && reference.IsNameOnly(ref): @@ -106,47 +112,37 @@ To push the complete multi-platform image, remove the --platform flag. } } - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) - // Resolve the Auth config relevant for this server - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), ref.String()) if err != nil { return err } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") - } - options := image.PushOptions{ + + responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), image.PushOptions{ All: opts.all, RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, Platform: platform, - } - - responseBody, err := dockerCli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) + }) if err != nil { return err } defer func() { + _ = responseBody.Close() for _, note := range notes { out.PrintNote(note) } }() - defer responseBody.Close() if !opts.untrusted { - // TODO pushTrustedReference currently doesn't respect `--quiet` - return pushTrustedReference(ctx, dockerCli, repoInfo, ref, authConfig, responseBody) + return pushTrustedReference(ctx, dockerCli, ref, responseBody) } if opts.quiet { err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux())) if err == nil { - fmt.Fprintln(dockerCli.Out(), ref.String()) + _, _ = fmt.Fprintln(dockerCli.Out(), ref.String()) } return err } diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go index 88415ea8e0df..5c43b94fcb38 100644 --- a/cli/command/image/push_test.go +++ b/cli/command/image/push_test.go @@ -40,7 +40,7 @@ func TestNewPushCommandErrors(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}) - cmd := NewPushCommand(cli) + cmd := newPushCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -73,7 +73,7 @@ func TestNewPushCommandSuccess(t *testing.T) { return io.NopCloser(strings.NewReader("")), nil }, }) - cmd := NewPushCommand(cli) + cmd := newPushCommand(cli) cmd.SetOut(cli.OutBuffer()) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go index 7c8b49ee0a99..2974897fe421 100644 --- a/cli/command/image/remove.go +++ b/cli/command/image/remove.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/containerd/platforms" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -21,7 +21,14 @@ type removeOptions struct { } // NewRemoveCommand creates a new `docker remove` command +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewRemoveCommand(dockerCLI command.Cli) *cobra.Command { + return newRemoveCommand(dockerCLI) +} + +// newRemoveCommand creates a new `docker remove` command +func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { var options removeOptions cmd := &cobra.Command{ @@ -50,8 +57,9 @@ func NewRemoveCommand(dockerCLI command.Cli) *cobra.Command { return cmd } -func newRemoveCommand(dockerCli command.Cli) *cobra.Command { - cmd := *NewRemoveCommand(dockerCli) +// newImageRemoveCommand is a sub-command under `image` (`docker image rm`) +func newImageRemoveCommand(dockerCli command.Cli) *cobra.Command { + cmd := *newRemoveCommand(dockerCli) cmd.Aliases = []string{"rmi", "remove"} cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" return &cmd @@ -79,7 +87,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, opts removeOptions, i for _, img := range images { dels, err := apiClient.ImageRemove(ctx, img, options) if err != nil { - if !cerrdefs.IsNotFound(err) { + if !errdefs.IsNotFound(err) { fatalErr = true } errs = append(errs, err) diff --git a/cli/command/image/remove_test.go b/cli/command/image/remove_test.go index 16c0a3c4a1c6..4ffc7273be4e 100644 --- a/cli/command/image/remove_test.go +++ b/cli/command/image/remove_test.go @@ -24,7 +24,7 @@ func (n notFound) Error() string { func (notFound) NotFound() {} func TestNewRemoveCommandAlias(t *testing.T) { - cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{})) + cmd := newImageRemoveCommand(test.NewFakeCli(&fakeClient{})) assert.Check(t, cmd.HasAlias("rmi")) assert.Check(t, cmd.HasAlias("remove")) assert.Check(t, !cmd.HasAlias("other")) @@ -63,7 +63,7 @@ func TestNewRemoveCommandErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{ + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{ imageRemoveFunc: tc.imageRemoveFunc, })) cmd.SetOut(io.Discard) @@ -122,7 +122,7 @@ func TestNewRemoveCommandSuccess(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc}) - cmd := NewRemoveCommand(cli) + cmd := newRemoveCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/image/save.go b/cli/command/image/save.go index 64bd72a74d8d..9ffad043dc4d 100644 --- a/cli/command/image/save.go +++ b/cli/command/image/save.go @@ -21,7 +21,14 @@ type saveOptions struct { } // NewSaveCommand creates a new `docker save` command -func NewSaveCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewSaveCommand(dockerCLI command.Cli) *cobra.Command { + return newSaveCommand(dockerCLI) +} + +// newSaveCommand creates a new `docker save` command +func newSaveCommand(dockerCLI command.Cli) *cobra.Command { var opts saveOptions cmd := &cobra.Command{ @@ -30,12 +37,12 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.images = args - return runSave(cmd.Context(), dockerCli, opts) + return runSave(cmd.Context(), dockerCLI, opts) }, Annotations: map[string]string{ "aliases": "docker image save, docker save", }, - ValidArgsFunction: completion.ImageNames(dockerCli, -1), + ValidArgsFunction: completion.ImageNames(dockerCLI, -1), } flags := cmd.Flags() diff --git a/cli/command/image/save_test.go b/cli/command/image/save_test.go index 7a3e93eb3547..f3104cc3f75c 100644 --- a/cli/command/image/save_test.go +++ b/cli/command/image/save_test.go @@ -61,7 +61,7 @@ func TestNewSaveCommandErrors(t *testing.T) { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}) cli.Out().SetIsTerminal(tc.isTerminal) - cmd := NewSaveCommand(cli) + cmd := newSaveCommand(cli) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) @@ -114,7 +114,7 @@ func TestNewSaveCommandSuccess(t *testing.T) { } for _, tc := range testCases { t.Run(strings.Join(tc.args, " "), func(t *testing.T) { - cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{ + cmd := newSaveCommand(test.NewFakeCli(&fakeClient{ imageSaveFunc: tc.imageSaveFunc, })) cmd.SetOut(io.Discard) diff --git a/cli/command/image/tag.go b/cli/command/image/tag.go index 7a495afca044..0eb345e2dea8 100644 --- a/cli/command/image/tag.go +++ b/cli/command/image/tag.go @@ -15,7 +15,14 @@ type tagOptions struct { } // NewTagCommand creates a new `docker tag` command -func NewTagCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewTagCommand(dockerCLI command.Cli) *cobra.Command { + return newTagCommand(dockerCLI) +} + +// newTagCommand creates a new `docker tag` command +func newTagCommand(dockerCli command.Cli) *cobra.Command { var opts tagOptions cmd := &cobra.Command{ diff --git a/cli/command/image/tag_test.go b/cli/command/image/tag_test.go index 65ceb60323ed..9f67c8d69e24 100644 --- a/cli/command/image/tag_test.go +++ b/cli/command/image/tag_test.go @@ -17,7 +17,7 @@ func TestCliNewTagCommandErrors(t *testing.T) { } expectedError := "'tag' requires 2 arguments" for _, args := range testCases { - cmd := NewTagCommand(test.NewFakeCli(&fakeClient{})) + cmd := newTagCommand(test.NewFakeCli(&fakeClient{})) cmd.SetArgs(args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -26,7 +26,7 @@ func TestCliNewTagCommandErrors(t *testing.T) { } func TestCliNewTagCommand(t *testing.T) { - cmd := NewTagCommand( + cmd := newTagCommand( test.NewFakeCli(&fakeClient{ imageTagFunc: func(image string, ref string) error { assert.Check(t, is.Equal("image1", image)) diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index e2980e847713..cb17505592a4 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -8,12 +8,13 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" + "github.com/docker/cli/internal/registry" "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -42,8 +43,18 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) ( } // pushTrustedReference pushes a canonical reference to the trust server. -func pushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error { - return trust.PushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in, command.UserAgent()) +func pushTrustedReference(ctx context.Context, dockerCLI command.Cli, ref reference.Named, responseBody io.Reader) error { + // Resolve the Repository name from fqn to RepositoryInfo, and create an + // IndexInfo. Docker Content Trust uses the IndexInfo.Official field to + // select the right domain for Docker Hub's Notary server; + // https://github.com/docker/cli/blob/v28.4.0/cli/trust/trust.go#L65-L79 + indexInfo := registry.NewIndexInfo(ref) + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + } + authConfig := command.ResolveAuthConfig(dockerCLI.ConfigFile(), indexInfo) + return trust.PushTrustedReference(ctx, dockerCLI, repoInfo, ref, authConfig, responseBody, command.UserAgent()) } // trustedPull handles content trust pulling of an image @@ -65,7 +76,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image if err != nil { return err } - updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), trustedRef.String()) + updatedImgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), trustedRef.String()) if err != nil { return err } @@ -149,13 +160,9 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru if err != nil { return err } - var requestPrivilege registrytypes.RequestAuthConfig - if cli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(cli, imgRefAndAuth.RepoInfo().Index, "pull") - } responseBody, err := cli.Client().ImagePull(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), image.PullOptions{ RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, All: opts.all, Platform: opts.platform, }) @@ -173,7 +180,7 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru // TrustedReference returns the canonical trusted reference for an image reference func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, AuthResolver(cli), ref.String()) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), ref.String()) if err != nil { return nil, err } @@ -211,9 +218,16 @@ func convertTarget(t client.Target) (target, error) { }, nil } -// AuthResolver returns an auth resolver function from a command.Cli -func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { +// AuthResolver returns an auth resolver function from a [config.Provider]. +// +// Deprecated: this function was only used internally and will be removed in the next release. +func AuthResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + return authResolver(dockerCLI) +} + +// authResolver returns an auth resolver function from a [config.Provider]. +func authResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { - return command.ResolveAuthConfig(cli.ConfigFile(), index) + return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index) } } diff --git a/cli/command/manifest/annotate.go b/cli/command/manifest/annotate.go index 0f2a10aade1e..6fc898203253 100644 --- a/cli/command/manifest/annotate.go +++ b/cli/command/manifest/annotate.go @@ -5,11 +5,12 @@ import ( "fmt" "path/filepath" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/manifest/store" - registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/internal/registryclient" "github.com/docker/docker/api/types/registry" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -54,6 +55,7 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig { return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index) } + // FIXME(thaJeztah): this should use the userAgent as configured on the dockerCLI. return registryclient.NewRegistryClient(resolver, command.UserAgent(), allowInsecure) } @@ -96,7 +98,7 @@ func runManifestAnnotate(dockerCLI command.Cli, opts annotateOptions) error { manifestStore := newManifestStore(dockerCLI) imageManifest, err := manifestStore.Get(targetRef, imgRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target) case err != nil: return err diff --git a/cli/command/manifest/client_test.go b/cli/command/manifest/client_test.go index 497251c70e16..16b7912ab6f5 100644 --- a/cli/command/manifest/client_test.go +++ b/cli/command/manifest/client_test.go @@ -5,7 +5,7 @@ import ( "github.com/distribution/reference" manifesttypes "github.com/docker/cli/cli/manifest/types" - "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/internal/registryclient" "github.com/docker/distribution" "github.com/opencontainers/go-digest" ) @@ -45,4 +45,4 @@ func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Name return digest.Digest(""), nil } -var _ client.RegistryClient = &fakeRegistryClient{} +var _ registryclient.RegistryClient = &fakeRegistryClient{} diff --git a/cli/command/manifest/cmd.go b/cli/command/manifest/cmd.go index 939f02b7bc5c..46d1b0d668c7 100644 --- a/cli/command/manifest/cmd.go +++ b/cli/command/manifest/cmd.go @@ -10,7 +10,14 @@ import ( ) // NewManifestCommand returns a cobra command for `manifest` subcommands -func NewManifestCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewManifestCommand(dockerCLI command.Cli) *cobra.Command { + return newManifestCommand(dockerCLI) +} + +// newManifestCommand returns a cobra command for `manifest` subcommands +func newManifestCommand(dockerCLI command.Cli) *cobra.Command { // use dockerCli as command.Cli cmd := &cobra.Command{ Use: "manifest COMMAND", @@ -18,16 +25,16 @@ func NewManifestCommand(dockerCli command.Cli) *cobra.Command { Long: manifestDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - _, _ = fmt.Fprint(dockerCli.Err(), "\n"+cmd.UsageString()) + _, _ = fmt.Fprint(dockerCLI.Err(), "\n"+cmd.UsageString()) }, Annotations: map[string]string{"experimentalCLI": ""}, } cmd.AddCommand( - newCreateListCommand(dockerCli), - newInspectCommand(dockerCli), - newAnnotateCommand(dockerCli), - newPushListCommand(dockerCli), - newRmManifestListCommand(dockerCli), + newCreateListCommand(dockerCLI), + newInspectCommand(dockerCLI), + newAnnotateCommand(dockerCLI), + newPushListCommand(dockerCLI), + newRmManifestListCommand(dockerCLI), ) return cmd } diff --git a/cli/command/manifest/create_list.go b/cli/command/manifest/create_list.go index 58c18124dfce..71483c0fa80d 100644 --- a/cli/command/manifest/create_list.go +++ b/cli/command/manifest/create_list.go @@ -4,9 +4,9 @@ import ( "context" "fmt" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/manifest/store" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -44,7 +44,7 @@ func createManifestList(ctx context.Context, dockerCLI command.Cli, args []strin manifestStore := newManifestStore(dockerCLI) _, err = manifestStore.GetList(targetRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): // New manifest list case err != nil: return err diff --git a/cli/command/manifest/push.go b/cli/command/manifest/push.go index ccfa84cd6c5b..c772cf2c3396 100644 --- a/cli/command/manifest/push.go +++ b/cli/command/manifest/push.go @@ -10,7 +10,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/manifest/types" - registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/internal/registryclient" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/ocischema" diff --git a/cli/command/manifest/util.go b/cli/command/manifest/util.go index be9fe9ea1ba4..c10a98b0d8f4 100644 --- a/cli/command/manifest/util.go +++ b/cli/command/manifest/util.go @@ -3,9 +3,9 @@ package manifest import ( "context" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/manifest/store" "github.com/docker/cli/cli/manifest/types" ) @@ -72,7 +72,7 @@ func normalizeReference(ref string) (reference.Named, error) { func getManifest(ctx context.Context, dockerCLI command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) { data, err := newManifestStore(dockerCLI).Get(listRef, namedRef) switch { - case store.IsNotFound(err): + case errdefs.IsNotFound(err): return newRegistryClient(dockerCLI, insecure).GetManifest(ctx, namedRef) case err != nil: return types.ImageManifest{}, err diff --git a/cli/command/network/cmd.go b/cli/command/network/cmd.go index 3db3088ebe95..140702667358 100644 --- a/cli/command/network/cmd.go +++ b/cli/command/network/cmd.go @@ -7,22 +7,29 @@ import ( ) // NewNetworkCommand returns a cobra command for `network` subcommands -func NewNetworkCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewNetworkCommand(dockerCLI command.Cli) *cobra.Command { + return newNetworkCommand(dockerCLI) +} + +// newNetworkCommand returns a cobra command for `network` subcommands +func newNetworkCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "network", Short: "Manage networks", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.21"}, } cmd.AddCommand( - newConnectCommand(dockerCli), - newCreateCommand(dockerCli), - newDisconnectCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), + newConnectCommand(dockerCLI), + newCreateCommand(dockerCLI), + newDisconnectCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPruneCommand(dockerCLI), ) return cmd } diff --git a/cli/command/network/create.go b/cli/command/network/create.go index c8fca4129429..c19444f90def 100644 --- a/cli/command/network/create.go +++ b/cli/command/network/create.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" @@ -69,7 +68,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { return runCreate(cmd.Context(), dockerCLI.Client(), dockerCLI.Out(), options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/network/formatter.go b/cli/command/network/formatter.go index 6a15a6be1070..f002cc62d3b9 100644 --- a/cli/command/network/formatter.go +++ b/cli/command/network/formatter.go @@ -17,8 +17,15 @@ const ( internalHeader = "INTERNAL" ) -// NewFormat returns a Format for rendering using a network Context +// NewFormat returns a Format for rendering using a network Context. +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string, quiet bool) formatter.Format { + return newFormat(source, quiet) +} + +// newFormat returns a [formatter.Format] for rendering a networkContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -35,10 +42,17 @@ func NewFormat(source string, quiet bool) formatter.Format { } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, networks []network.Summary) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, networks []network.Summary) error { + return formatWrite(fmtCtx, networks) +} + +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, networks []network.Summary) error { render := func(format func(subContext formatter.SubContext) error) error { for _, nw := range networks { - networkCtx := &networkContext{trunc: ctx.Trunc, n: nw} + networkCtx := &networkContext{trunc: fmtCtx.Trunc, n: nw} if err := format(networkCtx); err != nil { return err } @@ -57,7 +71,7 @@ func FormatWrite(ctx formatter.Context, networks []network.Summary) error { "Labels": formatter.LabelsHeader, "CreatedAt": formatter.CreatedAtHeader, } - return ctx.Write(&networkCtx, render) + return fmtCtx.Write(&networkCtx, render) } type networkContext struct { diff --git a/cli/command/network/formatter_test.go b/cli/command/network/formatter_test.go index ddeeb895fdb6..f0dae0377877 100644 --- a/cli/command/network/formatter_test.go +++ b/cli/command/network/formatter_test.go @@ -91,27 +91,27 @@ func TestNetworkContextWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `NETWORK ID NAME DRIVER SCOPE networkID1 foobar_baz foo local networkID2 foobar_bar bar local `, }, { - formatter.Context{Format: NewFormat("table", true)}, + formatter.Context{Format: newFormat("table", true)}, `networkID1 networkID2 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", false)}, + formatter.Context{Format: newFormat("table {{.Name}}", false)}, `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar @@ -119,7 +119,7 @@ foobar_bar }, // Raw Format { - formatter.Context{Format: NewFormat("raw", false)}, + formatter.Context{Format: newFormat("raw", false)}, `network_id: networkID1 name: foobar_baz driver: foo @@ -133,21 +133,21 @@ scope: local `, }, { - formatter.Context{Format: NewFormat("raw", true)}, + formatter.Context{Format: newFormat("raw", true)}, `network_id: networkID1 network_id: networkID2 `, }, // Custom Format { - formatter.Context{Format: NewFormat("{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.Name}}", false)}, `foobar_baz foobar_bar `, }, // Custom Format with CreatedAt { - formatter.Context{Format: NewFormat("{{.Name}} {{.CreatedAt}}", false)}, + formatter.Context{Format: newFormat("{{.Name}} {{.CreatedAt}}", false)}, `foobar_baz 2016-01-01 00:00:00 +0000 UTC foobar_bar 2017-01-01 00:00:00 +0000 UTC `, @@ -166,7 +166,7 @@ foobar_bar 2017-01-01 00:00:00 +0000 UTC t.Run(string(tc.context.Format), func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, networks) + err := formatWrite(tc.context, networks) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -187,7 +187,7 @@ func TestNetworkContextWriteJSON(t *testing.T) { } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, networks) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, networks) if err != nil { t.Fatal(err) } @@ -206,7 +206,7 @@ func TestNetworkContextWriteJSONField(t *testing.T) { {ID: "networkID2", Name: "foobar_bar"}, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, networks) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, networks) if err != nil { t.Fatal(err) } diff --git a/cli/command/network/list.go b/cli/command/network/list.go index 0fb3934103a5..9ac2367b6180 100644 --- a/cli/command/network/list.go +++ b/cli/command/network/list.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -33,7 +32,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -45,17 +44,17 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() - networkResources, err := client.NetworkList(ctx, network.ListOptions{Filters: options.filter.Value()}) +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() + networkResources, err := apiClient.NetworkList(ctx, network.ListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 { - if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().NetworksFormat + if len(dockerCLI.ConfigFile().NetworksFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().NetworksFormat } else { format = formatter.TableFormatKey } @@ -66,9 +65,9 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er }) networksCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), Trunc: !options.noTrunc, } - return FormatWrite(networksCtx, networkResources) + return formatWrite(networksCtx, networkResources) } diff --git a/cli/command/network/prune.go b/cli/command/network/prune.go index fce7adf3ed9a..0752c39b6e73 100644 --- a/cli/command/network/prune.go +++ b/cli/command/network/prune.go @@ -18,7 +18,14 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for networks -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPruneCommand(dockerCLI command.Cli) *cobra.Command { + return newPruneCommand(dockerCLI) +} + +// newPruneCommand returns a new cobra prune command for networks +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -26,12 +33,12 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove all unused networks", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - output, err := runPrune(cmd.Context(), dockerCli, options) + output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - _, _ = fmt.Fprintln(dockerCli.Out(), output) + _, _ = fmt.Fprintln(dockerCLI.Out(), output) } return nil }, diff --git a/cli/command/network/prune_test.go b/cli/command/network/prune_test.go index 67a57e8c8af1..ee54108f772e 100644 --- a/cli/command/network/prune_test.go +++ b/cli/command/network/prune_test.go @@ -20,7 +20,7 @@ func TestNetworkPrunePromptTermination(t *testing.T) { return network.PruneReport{}, errors.New("fakeClient networkPruneFunc should not be called") }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/network/remove.go b/cli/command/network/remove.go index 69578a48c0ab..7446260318e4 100644 --- a/cli/command/network/remove.go +++ b/cli/command/network/remove.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" @@ -59,7 +59,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, networks []string, op } } if err := apiClient.NetworkRemove(ctx, name); err != nil { - if opts.force && cerrdefs.IsNotFound(err) { + if opts.force && errdefs.IsNotFound(err) { continue } _, _ = fmt.Fprintln(dockerCLI.Err(), err) diff --git a/cli/command/node/cmd.go b/cli/command/node/cmd.go index 655f0170b756..6737e05864fc 100644 --- a/cli/command/node/cmd.go +++ b/cli/command/node/cmd.go @@ -12,25 +12,32 @@ import ( ) // NewNodeCommand returns a cobra command for `node` subcommands -func NewNodeCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewNodeCommand(dockerCLI command.Cli) *cobra.Command { + return newNodeCommand(dockerCLI) +} + +// newNodeCommand returns a cobra command for `node` subcommands +func newNodeCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "node", Short: "Manage Swarm nodes", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, } cmd.AddCommand( - newDemoteCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newPromoteCommand(dockerCli), - newRemoveCommand(dockerCli), - newPsCommand(dockerCli), - newUpdateCommand(dockerCli), + newDemoteCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newPromoteCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPsCommand(dockerCLI), + newUpdateCommand(dockerCLI), ) return cmd } @@ -48,6 +55,8 @@ func Reference(ctx context.Context, apiClient client.APIClient, ref string) (str // If there's no node ID in /info, the node probably // isn't a manager. Call a swarm-specific endpoint to // get a more specific error message. + // + // FIXME(thaJeztah): this should not require calling a Swarm endpoint, and we could just suffice with info / ping (which has swarm status). _, err = apiClient.NodeList(ctx, swarm.NodeListOptions{}) if err != nil { return "", err diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go index 5ede173cd1cc..981b45cae5f9 100644 --- a/cli/command/node/demote.go +++ b/cli/command/node/demote.go @@ -22,17 +22,16 @@ func newDemoteCommand(dockerCli command.Cli) *cobra.Command { } } -func runDemote(ctx context.Context, dockerCli command.Cli, nodes []string) error { +func runDemote(ctx context.Context, dockerCLI command.Cli, nodes []string) error { demote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleWorker { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s is already a worker.\n", node.ID) return errNoRoleChange } node.Spec.Role = swarm.NodeRoleWorker return nil } - success := func(nodeID string) { - _, _ = fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID) - } - return updateNodes(ctx, dockerCli, nodes, demote, success) + return updateNodes(ctx, dockerCLI.Client(), nodes, demote, func(nodeID string) { + _, _ = fmt.Fprintf(dockerCLI.Out(), "Manager %s demoted in the swarm.\n", nodeID) + }) } diff --git a/cli/command/node/formatter.go b/cli/command/node/formatter.go index c49ad30abe5b..36ad47d6563b 100644 --- a/cli/command/node/formatter.go +++ b/cli/command/node/formatter.go @@ -80,7 +80,14 @@ TLS Info: ) // NewFormat returns a Format for rendering using a node Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string, quiet bool) formatter.Format { + return newFormat(source, quiet) +} + +// newFormat returns a Format for rendering using a nodeContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return nodeInspectPrettyTemplate @@ -99,7 +106,14 @@ func NewFormat(source string, quiet bool) formatter.Format { } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, nodes []swarm.Node, info system.Info) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, nodes []swarm.Node, info system.Info) error { + return formatWrite(fmtCtx, nodes, info) +} + +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, nodes []swarm.Node, info system.Info) error { render := func(format func(subContext formatter.SubContext) error) error { for _, node := range nodes { nodeCtx := &nodeContext{n: node, info: info} @@ -120,7 +134,7 @@ func FormatWrite(ctx formatter.Context, nodes []swarm.Node, info system.Info) er "EngineVersion": engineVersionHeader, "TLSStatus": tlsStatusHeader, } - return ctx.Write(&nodeCtx, render) + return fmtCtx.Write(&nodeCtx, render) } type nodeContext struct { @@ -180,9 +194,16 @@ func (c *nodeContext) EngineVersion() string { } // InspectFormatWrite renders the context for a list of nodes -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != nodeInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// +// Deprecated: this function was only used internally and will be removed in the next release. +func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + return inspectFormatWrite(fmtCtx, refs, getRef) +} + +// inspectFormatWrite renders the context for a list of nodes. +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != nodeInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } render := func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { @@ -200,7 +221,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } return nil } - return ctx.Write(&nodeInspectContext{}, render) + return fmtCtx.Write(&nodeInspectContext{}, render) } type nodeInspectContext struct { diff --git a/cli/command/node/formatter_test.go b/cli/command/node/formatter_test.go index 8983f337a6cb..8dcdbdced9d0 100644 --- a/cli/command/node/formatter_test.go +++ b/cli/command/node/formatter_test.go @@ -74,7 +74,7 @@ func TestNodeContextWrite(t *testing.T) { }, // Table format { - context: formatter.Context{Format: NewFormat("table", false)}, + context: formatter.Context{Format: newFormat("table", false)}, expected: `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION nodeID1 foobar_baz Foo Drain Leader 18.03.0-ce nodeID2 foobar_bar Bar Active Reachable 1.2.3 @@ -82,7 +82,7 @@ nodeID3 foobar_boo Boo Active ` + "\n", // clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table", true)}, + context: formatter.Context{Format: newFormat("table", true)}, expected: `nodeID1 nodeID2 nodeID3 @@ -90,7 +90,7 @@ nodeID3 clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.Hostname}}", false)}, + context: formatter.Context{Format: newFormat("table {{.Hostname}}", false)}, expected: `HOSTNAME foobar_baz foobar_bar @@ -99,7 +99,7 @@ foobar_boo clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.Hostname}}", true)}, + context: formatter.Context{Format: newFormat("table {{.Hostname}}", true)}, expected: `HOSTNAME foobar_baz foobar_bar @@ -108,7 +108,7 @@ foobar_boo clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, expected: `ID HOSTNAME TLS STATUS nodeID1 foobar_baz Needs Rotation nodeID2 foobar_bar Ready @@ -117,7 +117,7 @@ nodeID3 foobar_boo Unknown clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { // no cluster TLS status info, TLS status for all nodes is unknown - context: formatter.Context{Format: NewFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("table {{.ID}}\t{{.Hostname}}\t{{.TLSStatus}}", false)}, expected: `ID HOSTNAME TLS STATUS nodeID1 foobar_baz Unknown nodeID2 foobar_bar Unknown @@ -127,7 +127,7 @@ nodeID3 foobar_boo Unknown }, // Raw Format { - context: formatter.Context{Format: NewFormat("raw", false)}, + context: formatter.Context{Format: newFormat("raw", false)}, expected: `node_id: nodeID1 hostname: foobar_baz status: Foo @@ -148,7 +148,7 @@ manager_status: ` + "\n\n", // to preserve whitespace clusterInfo: swarm.ClusterInfo{TLSInfo: swarm.TLSInfo{TrustRoot: "hi"}}, }, { - context: formatter.Context{Format: NewFormat("raw", true)}, + context: formatter.Context{Format: newFormat("raw", true)}, expected: `node_id: nodeID1 node_id: nodeID2 node_id: nodeID3 @@ -157,7 +157,7 @@ node_id: nodeID3 }, // Custom Format { - context: formatter.Context{Format: NewFormat("{{.Hostname}} {{.TLSStatus}}", false)}, + context: formatter.Context{Format: newFormat("{{.Hostname}} {{.TLSStatus}}", false)}, expected: `foobar_baz Needs Rotation foobar_bar Ready foobar_boo Unknown @@ -205,7 +205,7 @@ foobar_boo Unknown var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, nodes, system.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}}) + err := formatWrite(tc.context, nodes, system.Info{Swarm: swarm.Info{Cluster: &tc.clusterInfo}}) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -252,7 +252,7 @@ func TestNodeContextWriteJSON(t *testing.T) { {ID: "nodeID3", Description: swarm.NodeDescription{Hostname: "foobar_boo", Engine: swarm.EngineDescription{EngineVersion: "18.03.0-ce"}}}, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, nodes, testcase.info) if err != nil { t.Fatal(err) } @@ -272,7 +272,7 @@ func TestNodeContextWriteJSONField(t *testing.T) { {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, system.Info{}) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, nodes, system.Info{}) if err != nil { t.Fatal(err) } @@ -317,10 +317,10 @@ func TestNodeInspectWriteContext(t *testing.T) { } out := bytes.NewBufferString("") context := formatter.Context{ - Format: NewFormat("pretty", false), + Format: newFormat("pretty", false), Output: out, } - err := InspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) { + err := inspectFormatWrite(context, []string{"nodeID1"}, func(string) (any, []byte, error) { return node, nil, nil }) if err != nil { diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go index f293861d478b..1ef4777f4d3f 100644 --- a/cli/command/node/inspect.go +++ b/cli/command/node/inspect.go @@ -41,35 +41,34 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" } getRef := func(ref string) (any, []byte, error) { - nodeRef, err := Reference(ctx, client, ref) + nodeRef, err := Reference(ctx, apiClient, ref) if err != nil { return nil, nil, err } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + node, _, err := apiClient.NodeInspectWithRaw(ctx, nodeRef) return node, nil, err } - f := opts.format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } nodeCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f, false), + Output: dockerCLI.Out(), + Format: newFormat(opts.format, false), } - if err := InspectFormatWrite(nodeCtx, opts.nodeIds, getRef); err != nil { + if err := inspectFormatWrite(nodeCtx, opts.nodeIds, getRef); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/node/list.go b/cli/command/node/list.go index 3d319deeb946..3364b2daa217 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -34,7 +33,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display IDs") @@ -45,25 +44,25 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() - nodes, err := client.NodeList( - ctx, - swarm.NodeListOptions{Filters: options.filter.Value()}) + nodes, err := apiClient.NodeList(ctx, swarm.NodeListOptions{ + Filters: options.filter.Value(), + }) if err != nil { return err } - info := system.Info{} + var info system.Info if len(nodes) > 0 && !options.quiet { // only non-empty nodes and not quiet, should we call /info api - info, err = client.Info(ctx) + info, err = apiClient.Info(ctx) if err != nil { return err } @@ -72,17 +71,17 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er format := options.format if len(format) == 0 { format = formatter.TableFormatKey - if len(dockerCli.ConfigFile().NodesFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().NodesFormat + if len(dockerCLI.ConfigFile().NodesFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().NodesFormat } } nodesCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), } sort.Slice(nodes, func(i, j int) bool { return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname) }) - return FormatWrite(nodesCtx, nodes, info) + return formatWrite(nodesCtx, nodes, info) } diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go index 983229526447..0ecdeb7f1e19 100644 --- a/cli/command/node/promote.go +++ b/cli/command/node/promote.go @@ -22,17 +22,16 @@ func newPromoteCommand(dockerCli command.Cli) *cobra.Command { } } -func runPromote(ctx context.Context, dockerCli command.Cli, nodes []string) error { +func runPromote(ctx context.Context, dockerCLI command.Cli, nodes []string) error { promote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleManager { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s is already a manager.\n", node.ID) return errNoRoleChange } node.Spec.Role = swarm.NodeRoleManager return nil } - success := func(nodeID string) { - _, _ = fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) - } - return updateNodes(ctx, dockerCli, nodes, promote, success) + return updateNodes(ctx, dockerCLI.Client(), nodes, promote, func(nodeID string) { + _, _ = fmt.Fprintf(dockerCLI.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) + }) } diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go index c34ea0e676e3..62b5ee141348 100644 --- a/cli/command/node/ps.go +++ b/cli/command/node/ps.go @@ -6,7 +6,6 @@ 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/idresolver" "github.com/docker/cli/cli/command/task" "github.com/docker/cli/opts" @@ -54,13 +53,13 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } -func runPs(ctx context.Context, dockerCli command.Cli, options psOptions) error { - client := dockerCli.Client() +func runPs(ctx context.Context, dockerCLI command.Cli, options psOptions) error { + apiClient := dockerCLI.Client() var ( errs []string @@ -68,13 +67,13 @@ func runPs(ctx context.Context, dockerCli command.Cli, options psOptions) error ) for _, nodeID := range options.nodeIDs { - nodeRef, err := Reference(ctx, client, nodeID) + nodeRef, err := Reference(ctx, apiClient, nodeID) if err != nil { errs = append(errs, err.Error()) continue } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + node, _, err := apiClient.NodeInspectWithRaw(ctx, nodeRef) if err != nil { errs = append(errs, err.Error()) continue @@ -83,7 +82,7 @@ func runPs(ctx context.Context, dockerCli command.Cli, options psOptions) error filter := options.filter.Value() filter.Add("node", node.ID) - nodeTasks, err := client.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) + nodeTasks, err := apiClient.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) if err != nil { errs = append(errs, err.Error()) continue @@ -94,11 +93,11 @@ func runPs(ctx context.Context, dockerCli command.Cli, options psOptions) error format := options.format if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet) + format = task.DefaultFormat(dockerCLI.ConfigFile(), options.quiet) } if len(errs) == 0 || len(tasks) != 0 { - if err := task.Print(ctx, dockerCli, tasks, idresolver.New(client, options.noResolve), !options.noTrunc, options.quiet, format); err != nil { + if err := task.Print(ctx, dockerCLI, tasks, idresolver.New(apiClient, options.noResolve), !options.noTrunc, options.quiet, format); err != nil { errs = append(errs, err.Error()) } } diff --git a/cli/command/node/update.go b/cli/command/node/update.go index 0c0d3bdc287a..9ece2c9599ec 100644 --- a/cli/command/node/update.go +++ b/cli/command/node/update.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -42,23 +43,20 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } -func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { - success := func(_ string) { - fmt.Fprintln(dockerCli.Out(), nodeID) - } - return updateNodes(ctx, dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) +func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, nodeID string) error { + return updateNodes(ctx, dockerCLI.Client(), []string{nodeID}, mergeNodeUpdate(flags), func(_ string) { + _, _ = fmt.Fprintln(dockerCLI.Out(), nodeID) + }) } -func updateNodes(ctx context.Context, dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { - client := dockerCli.Client() - +func updateNodes(ctx context.Context, apiClient client.NodeAPIClient, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { for _, nodeID := range nodes { - node, _, err := client.NodeInspectWithRaw(ctx, nodeID) + node, _, err := apiClient.NodeInspectWithRaw(ctx, nodeID) if err != nil { return err } @@ -70,7 +68,7 @@ func updateNodes(ctx context.Context, dockerCli command.Cli, nodes []string, mer } return err } - err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec) + err = apiClient.NodeUpdate(ctx, node.ID, node.Version, node.Spec) if err != nil { return err } diff --git a/cli/command/plugin/cmd.go b/cli/command/plugin/cmd.go index 2e79ab1d49b5..a72890cebcaa 100644 --- a/cli/command/plugin/cmd.go +++ b/cli/command/plugin/cmd.go @@ -7,26 +7,33 @@ import ( ) // NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPluginCommand(dockerCLI command.Cli) *cobra.Command { + return newPluginCommand(dockerCLI) +} + +// newPluginCommand returns a cobra command for `plugin` subcommands +func newPluginCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "plugin", Short: "Manage plugins", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.25"}, } cmd.AddCommand( - newDisableCommand(dockerCli), - newEnableCommand(dockerCli), - newInspectCommand(dockerCli), - newInstallCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newSetCommand(dockerCli), - newPushCommand(dockerCli), - newCreateCommand(dockerCli), - newUpgradeCommand(dockerCli), + newDisableCommand(dockerCLI), + newEnableCommand(dockerCLI), + newInspectCommand(dockerCLI), + newInstallCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newSetCommand(dockerCLI), + newPushCommand(dockerCLI), + newCreateCommand(dockerCLI), + newUpgradeCommand(dockerCLI), ) return cmd } diff --git a/cli/command/plugin/create.go b/cli/command/plugin/create.go index 8f408d76f3fa..2b744c469582 100644 --- a/cli/command/plugin/create.go +++ b/cli/command/plugin/create.go @@ -10,7 +10,6 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" @@ -40,7 +39,7 @@ func validateConfig(path string) error { return err } -// validateContextDir validates the given dir and returns abs path on success. +// validateContextDir validates the given dir and returns its absolute path on success. func validateContextDir(contextDir string) (string, error) { absContextDir, err := filepath.Abs(contextDir) if err != nil { @@ -76,7 +75,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { options.context = args[1] return runCreate(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/plugin/disable.go b/cli/command/plugin/disable.go index 084b50365d1e..b58990456b90 100644 --- a/cli/command/plugin/disable.go +++ b/cli/command/plugin/disable.go @@ -1,7 +1,6 @@ package plugin import ( - "context" "fmt" "github.com/docker/cli/cli" @@ -10,27 +9,24 @@ import ( "github.com/spf13/cobra" ) -func newDisableCommand(dockerCli command.Cli) *cobra.Command { - var force bool +func newDisableCommand(dockerCLI command.Cli) *cobra.Command { + var opts types.PluginDisableOptions cmd := &cobra.Command{ Use: "disable [OPTIONS] PLUGIN", Short: "Disable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDisable(cmd.Context(), dockerCli, args[0], force) + name := args[0] + if err := dockerCLI.Client().PluginDisable(cmd.Context(), name, opts); err != nil { + return err + } + _, _ = fmt.Fprintln(dockerCLI.Out(), name) + return nil }, } flags := cmd.Flags() - flags.BoolVarP(&force, "force", "f", false, "Force the disable of an active plugin") + flags.BoolVarP(&opts.Force, "force", "f", false, "Force the disable of an active plugin") return cmd } - -func runDisable(ctx context.Context, dockerCli command.Cli, name string, force bool) error { - if err := dockerCli.Client().PluginDisable(ctx, name, types.PluginDisableOptions{Force: force}); err != nil { - return err - } - fmt.Fprintln(dockerCli.Out(), name) - return nil -} diff --git a/cli/command/plugin/enable.go b/cli/command/plugin/enable.go index e1724fd50cf8..befd553cc8b4 100644 --- a/cli/command/plugin/enable.go +++ b/cli/command/plugin/enable.go @@ -11,38 +11,31 @@ import ( "github.com/spf13/cobra" ) -type enableOpts struct { - timeout int - name string -} - func newEnableCommand(dockerCli command.Cli) *cobra.Command { - var opts enableOpts + var opts types.PluginEnableOptions cmd := &cobra.Command{ Use: "enable [OPTIONS] PLUGIN", Short: "Enable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] - return runEnable(cmd.Context(), dockerCli, &opts) + name := args[0] + if err := runEnable(cmd.Context(), dockerCli, name, opts); err != nil { + return err + } + _, _ = fmt.Fprintln(dockerCli.Out(), name) + return nil }, } flags := cmd.Flags() - flags.IntVar(&opts.timeout, "timeout", 30, "HTTP client timeout (in seconds)") + flags.IntVar(&opts.Timeout, "timeout", 30, "HTTP client timeout (in seconds)") return cmd } -func runEnable(ctx context.Context, dockerCli command.Cli, opts *enableOpts) error { - name := opts.name - if opts.timeout < 0 { - return errors.Errorf("negative timeout %d is invalid", opts.timeout) - } - - if err := dockerCli.Client().PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { - return err +func runEnable(ctx context.Context, dockerCli command.Cli, name string, opts types.PluginEnableOptions) error { + if opts.Timeout < 0 { + return errors.Errorf("negative timeout %d is invalid", opts.Timeout) } - fmt.Fprintln(dockerCli.Out(), name) - return nil + return dockerCli.Client().PluginEnable(ctx, name, opts) } diff --git a/cli/command/plugin/enable_test.go b/cli/command/plugin/enable_test.go index bb0861e6ce20..65a096d8c663 100644 --- a/cli/command/plugin/enable_test.go +++ b/cli/command/plugin/enable_test.go @@ -48,7 +48,7 @@ func TestPluginEnableErrors(t *testing.T) { })) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/plugin/formatter.go b/cli/command/plugin/formatter.go index 27c24b52cdeb..07ecf7d40956 100644 --- a/cli/command/plugin/formatter.go +++ b/cli/command/plugin/formatter.go @@ -12,10 +12,23 @@ const ( enabledHeader = "ENABLED" pluginIDHeader = "ID" + + rawFormat = `plugin_id: {{.ID}} +name: {{.Name}} +description: {{.Description}} +enabled: {{.Enabled}} +` ) // NewFormat returns a Format for rendering using a plugin Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string, quiet bool) formatter.Format { + return newFormat(source, quiet) +} + +// newFormat returns a Format for rendering using a pluginContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -26,16 +39,23 @@ func NewFormat(source string, quiet bool) formatter.Format { if quiet { return `plugin_id: {{.ID}}` } - return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` + return rawFormat } return formatter.Format(source) } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, plugins []*types.Plugin) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, plugins []*types.Plugin) error { + return formatWrite(fmtCtx, plugins) +} + +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, plugins []*types.Plugin) error { render := func(format func(subContext formatter.SubContext) error) error { - for _, plugin := range plugins { - pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} + for _, p := range plugins { + pluginCtx := &pluginContext{trunc: fmtCtx.Trunc, p: *p} if err := format(pluginCtx); err != nil { return err } @@ -50,7 +70,7 @@ func FormatWrite(ctx formatter.Context, plugins []*types.Plugin) error { "Enabled": enabledHeader, "PluginReference": formatter.ImageHeader, } - return ctx.Write(&pluginCtx, render) + return fmtCtx.Write(&pluginCtx, render) } type pluginContext struct { diff --git a/cli/command/plugin/formatter_test.go b/cli/command/plugin/formatter_test.go index 63f1ce29e983..ddaa68c859ec 100644 --- a/cli/command/plugin/formatter_test.go +++ b/cli/command/plugin/formatter_test.go @@ -19,85 +19,106 @@ import ( func TestPluginContext(t *testing.T) { pluginID := test.RandomID() - var ctx pluginContext - cases := []struct { + var pCtx pluginContext + tests := []struct { pluginCtx pluginContext expValue string call func() string }{ - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: false, - }, pluginID, ctx.ID}, - {pluginContext{ - p: types.Plugin{ID: pluginID}, - trunc: true, - }, formatter.TruncateID(pluginID), ctx.ID}, - {pluginContext{ - p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", ctx.Name}, - {pluginContext{ - p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", ctx.Description}, + { + pluginCtx: pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: false, + }, + expValue: pluginID, + call: pCtx.ID, + }, + { + pluginCtx: pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: true, + }, + expValue: formatter.TruncateID(pluginID), + call: pCtx.ID, + }, + { + pluginCtx: pluginContext{ + p: types.Plugin{Name: "plugin_name"}, + }, + expValue: "plugin_name", + call: pCtx.Name, + }, + { + pluginCtx: pluginContext{ + p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, + }, + expValue: "plugin_description", + call: pCtx.Description, + }, } - for _, c := range cases { - ctx = c.pluginCtx - v := c.call() + for _, tc := range tests { + pCtx = tc.pluginCtx + v := tc.call() if strings.Contains(v, ",") { - test.CompareMultipleValues(t, v, c.expValue) - } else if v != c.expValue { - t.Fatalf("Expected %s, was %s\n", c.expValue, v) + test.CompareMultipleValues(t, v, tc.expValue) + } else if v != tc.expValue { + t.Fatalf("Expected %s, was %s\n", tc.expValue, v) } } } func TestPluginContextWrite(t *testing.T) { - cases := []struct { + tests := []struct { + doc string context formatter.Context expected string }{ - // Errors { - formatter.Context{Format: "{{InvalidFunction}}"}, - `template parsing error: template: :1: function "InvalidFunction" not defined`, + doc: "invalid function", + context: formatter.Context{Format: "{{InvalidFunction}}"}, + expected: `template parsing error: template: :1: function "InvalidFunction" not defined`, }, { - formatter.Context{Format: "{{nil}}"}, - `template parsing error: template: :1:2: executing "" at : nil is not a command`, + doc: "nil template", + context: formatter.Context{Format: "{{nil}}"}, + expected: `template parsing error: template: :1:2: executing "" at : nil is not a command`, }, - // Table format { - formatter.Context{Format: NewFormat("table", false)}, - `ID NAME DESCRIPTION ENABLED + doc: "table format", + context: formatter.Context{Format: newFormat("table", false)}, + expected: `ID NAME DESCRIPTION ENABLED pluginID1 foobar_baz description 1 true pluginID2 foobar_bar description 2 false `, }, { - formatter.Context{Format: NewFormat("table", true)}, - `pluginID1 + doc: "table format, quiet", + context: formatter.Context{Format: newFormat("table", true)}, + expected: `pluginID1 pluginID2 `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", false)}, - `NAME + doc: "table format name col", + context: formatter.Context{Format: newFormat("table {{.Name}}", false)}, + expected: `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, - `NAME + doc: "table format name col, quiet", + context: formatter.Context{Format: newFormat("table {{.Name}}", true)}, + expected: `NAME foobar_baz foobar_bar `, }, - // Raw Format { - formatter.Context{Format: NewFormat("raw", false)}, - `plugin_id: pluginID1 + doc: "raw format", + context: formatter.Context{Format: newFormat("raw", false)}, + expected: `plugin_id: pluginID1 name: foobar_baz description: description 1 enabled: true @@ -110,15 +131,16 @@ enabled: false `, }, { - formatter.Context{Format: NewFormat("raw", true)}, - `plugin_id: pluginID1 + doc: "raw format, quiet", + context: formatter.Context{Format: newFormat("raw", true)}, + expected: `plugin_id: pluginID1 plugin_id: pluginID2 `, }, - // Custom Format { - formatter.Context{Format: NewFormat("{{.Name}}", false)}, - `foobar_baz + doc: "custom format", + context: formatter.Context{Format: newFormat("{{.Name}}", false)}, + expected: `foobar_baz foobar_bar `, }, @@ -129,12 +151,12 @@ foobar_bar {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, } - for _, tc := range cases { - t.Run(string(tc.context.Format), func(t *testing.T) { + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { var out bytes.Buffer tc.context.Output = &out - err := FormatWrite(tc.context, plugins) + err := formatWrite(tc.context, plugins) if err != nil { assert.Error(t, err, tc.expected) } else { @@ -155,7 +177,7 @@ func TestPluginContextWriteJSON(t *testing.T) { } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, plugins) + err := formatWrite(formatter.Context{Format: "{{json .}}", Output: out}, plugins) if err != nil { t.Fatal(err) } @@ -174,7 +196,7 @@ func TestPluginContextWriteJSONField(t *testing.T) { {ID: "pluginID2", Name: "foobar_bar"}, } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, plugins) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, plugins) if err != nil { t.Fatal(err) } diff --git a/cli/command/plugin/inspect.go b/cli/command/plugin/inspect.go index 9b6a453c9336..289fe94f5e24 100644 --- a/cli/command/plugin/inspect.go +++ b/cli/command/plugin/inspect.go @@ -36,11 +36,9 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - getRef := func(ref string) (any, []byte, error) { - return client.PluginInspectWithRaw(ctx, ref) - } - - return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() + return inspect.Inspect(dockerCLI.Out(), opts.pluginNames, opts.format, func(ref string) (any, []byte, error) { + return apiClient.PluginInspectWithRaw(ctx, ref) + }) } diff --git a/cli/command/plugin/inspect_test.go b/cli/command/plugin/inspect_test.go index 414ff479a0c2..296408eed5bb 100644 --- a/cli/command/plugin/inspect_test.go +++ b/cli/command/plugin/inspect_test.go @@ -21,14 +21,14 @@ var pluginFoo = &types.Plugin{ Documentation: "plugin foo documentation", Entrypoint: []string{"/foo"}, Interface: types.PluginConfigInterface{ - Socket: "pluginfoo.sock", + Socket: "plugin-foo.sock", }, Linux: types.PluginConfigLinux{ Capabilities: []string{"CAP_SYS_ADMIN"}, }, WorkDir: "workdir-foo", Rootfs: &types.PluginConfigRootfs{ - DiffIds: []string{"sha256:8603eedd4ea52cebb2f22b45405a3dc8f78ba3e31bf18f27b4547a9ff930e0bd"}, + DiffIds: []string{"sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}, Type: "layers", }, }, @@ -71,7 +71,7 @@ func TestInspectErrors(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -142,7 +142,7 @@ func TestInspect(t *testing.T) { cmd := newInspectCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), tc.golden) diff --git a/cli/command/plugin/install.go b/cli/command/plugin/install.go index 027478c0fc33..21bbf63aebd6 100644 --- a/cli/command/plugin/install.go +++ b/cli/command/plugin/install.go @@ -3,17 +3,17 @@ package plugin import ( "context" "fmt" - "strings" "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" "github.com/docker/cli/internal/prompt" + "github.com/docker/cli/internal/registry" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -29,9 +29,9 @@ type pluginOptions struct { untrusted bool } -func loadPullFlags(dockerCli command.Cli, opts *pluginOptions, flags *pflag.FlagSet) { +func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) { flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") - command.AddTrustVerificationFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image verification") } func newInstallCommand(dockerCli command.Cli) *cobra.Command { @@ -50,13 +50,13 @@ func newInstallCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() - loadPullFlags(dockerCli, &options, flags) + loadPullFlags(&options, flags) flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") flags.StringVar(&options.localName, "alias", "", "Local name for plugin") return cmd } -func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) { +func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOptions) (types.PluginInstallOptions, error) { // Names with both tag and digest will be treated by the daemon // as a pull by digest with a local name for the tag // (if no local name is provided). @@ -65,8 +65,7 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti return types.PluginInstallOptions{}, err } - repoInfo, _ := registry.ParseRepositoryInfo(ref) - + indexInfo := registry.NewIndexInfo(ref) remote := ref.String() _, isCanonical := ref.(reference.Canonical) @@ -84,24 +83,19 @@ func buildPullConfig(ctx context.Context, dockerCli command.Cli, opts pluginOpti remote = reference.FamiliarString(trusted) } - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) + authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) if err != nil { return types.PluginInstallOptions{}, err } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName) - } - options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, RemoteRef: remote, Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote), - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, Args: opts.args, } return options, nil @@ -120,18 +114,17 @@ func runInstall(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) localName = reference.FamiliarString(reference.TagNameOnly(aref)) } - options, err := buildPullConfig(ctx, dockerCLI, opts, "plugin install") + options, err := buildPullConfig(ctx, dockerCLI, opts) if err != nil { return err } responseBody, err := dockerCLI.Client().PluginInstall(ctx, localName, options) if err != nil { - if strings.Contains(err.Error(), "(image) when fetching") { - return errors.New(err.Error() + " - Use \"docker image pull\"") - } return err } - defer responseBody.Close() + defer func() { + _ = responseBody.Close() + }() if err := jsonstream.Display(ctx, responseBody, dockerCLI.Out()); err != nil { return err } diff --git a/cli/command/plugin/install_test.go b/cli/command/plugin/install_test.go index b59ed98fcbee..f534f0063a11 100644 --- a/cli/command/plugin/install_test.go +++ b/cli/command/plugin/install_test.go @@ -32,7 +32,7 @@ func TestInstallErrors(t *testing.T) { }, { description: "invalid plugin name", - args: []string{"UPPERCASE_REPONAME"}, + args: []string{"UPPERCASE_REPO_NAME"}, expectedError: "invalid", }, { @@ -43,14 +43,6 @@ func TestInstallErrors(t *testing.T) { return nil, errors.New("error installing plugin") }, }, - { - description: "installation error due to missing image", - args: []string{"foo"}, - expectedError: "docker image pull", - installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { - return nil, errors.New("(image) when fetching") - }, - }, } for _, tc := range testCases { @@ -94,11 +86,12 @@ func TestInstallContentTrustErrors(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { + t.Setenv("DOCKER_CONTENT_TRUST", "true") cli := test.NewFakeCli(&fakeClient{ pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) { return nil, errors.New("should not try to install plugin") }, - }, test.EnableContentTrust) + }) cli.SetNotaryClient(tc.notaryFunc) cmd := newInstallCommand(cli) cmd.SetArgs(tc.args) diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go index 0c7234a2c3b0..85521671465e 100644 --- a/cli/command/plugin/list.go +++ b/cli/command/plugin/list.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -32,7 +31,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -66,8 +65,8 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er pluginsCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Format: newFormat(format, options.quiet), Trunc: !options.noTrunc, } - return FormatWrite(pluginsCtx, plugins) + return formatWrite(pluginsCtx, plugins) } diff --git a/cli/command/plugin/list_test.go b/cli/command/plugin/list_test.go index 91665da654f1..798db7fb9fb6 100644 --- a/cli/command/plugin/list_test.go +++ b/cli/command/plugin/list_test.go @@ -51,7 +51,7 @@ func TestListErrors(t *testing.T) { cmd := newListCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -170,7 +170,7 @@ func TestList(t *testing.T) { cmd := newListCommand(cli) cmd.SetArgs(tc.args) for key, value := range tc.flags { - cmd.Flags().Set(key, value) + assert.NilError(t, cmd.Flags().Set(key, value)) } assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), tc.golden) diff --git a/cli/command/plugin/push.go b/cli/command/plugin/push.go index 402068034797..b4db95bb0f03 100644 --- a/cli/command/plugin/push.go +++ b/cli/command/plugin/push.go @@ -8,8 +8,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" + "github.com/docker/cli/internal/registry" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -33,7 +33,7 @@ func newPushCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() - command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled()) + flags.BoolVar(&opts.untrusted, "disable-content-trust", !trust.Enabled(), "Skip image signing") return cmd } @@ -49,8 +49,8 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error named = reference.TagNameOnly(named) - repoInfo, _ := registry.ParseRepositoryInfo(named) - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), repoInfo.Index) + indexInfo := registry.NewIndexInfo(named) + authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) if err != nil { return err @@ -60,9 +60,15 @@ func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error if err != nil { return err } - defer responseBody.Close() + defer func() { + _ = responseBody.Close() + }() if !opts.untrusted { + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(named), + Index: indexInfo, + } return trust.PushTrustedReference(ctx, dockerCli, repoInfo, named, authConfig, responseBody, command.UserAgent()) } diff --git a/cli/command/plugin/remove_test.go b/cli/command/plugin/remove_test.go index de40a28ade52..f38e22b40a4c 100644 --- a/cli/command/plugin/remove_test.go +++ b/cli/command/plugin/remove_test.go @@ -64,7 +64,7 @@ func TestRemoveWithForceOption(t *testing.T) { }) cmd := newRemoveCommand(cli) cmd.SetArgs([]string{"plugin-foo"}) - cmd.Flags().Set("force", "true") + assert.NilError(t, cmd.Flags().Set("force", "true")) assert.NilError(t, cmd.Execute()) assert.Check(t, force) assert.Check(t, is.Equal("plugin-foo\n", cli.OutBuffer().String())) diff --git a/cli/command/plugin/set.go b/cli/command/plugin/set.go index 64442097b3aa..2d1eecc0ad0b 100644 --- a/cli/command/plugin/set.go +++ b/cli/command/plugin/set.go @@ -7,7 +7,7 @@ import ( ) func newSetCommand(dockerCli command.Cli) *cobra.Command { - cmd := &cobra.Command{ + return &cobra.Command{ Use: "set PLUGIN KEY=VALUE [KEY=VALUE...]", Short: "Change settings for a plugin", Args: cli.RequiresMinArgs(2), @@ -15,6 +15,4 @@ func newSetCommand(dockerCli command.Cli) *cobra.Command { return dockerCli.Client().PluginSet(cmd.Context(), args[0], args[1:]) }, } - - return cmd } diff --git a/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden b/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden index 65c8d39ce995..be510f40349b 100644 --- a/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden +++ b/cli/command/plugin/testdata/plugin-inspect-single-without-format.golden @@ -15,7 +15,7 @@ ], "Env": null, "Interface": { - "Socket": "pluginfoo.sock", + "Socket": "plugin-foo.sock", "Types": null }, "IpcHost": false, @@ -36,7 +36,7 @@ "WorkDir": "workdir-foo", "rootfs": { "diff_ids": [ - "sha256:8603eedd4ea52cebb2f22b45405a3dc8f78ba3e31bf18f27b4547a9ff930e0bd" + "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" ], "type": "layers" } diff --git a/cli/command/plugin/upgrade.go b/cli/command/plugin/upgrade.go index da9ea4076d84..4ebf003e39fd 100644 --- a/cli/command/plugin/upgrade.go +++ b/cli/command/plugin/upgrade.go @@ -3,7 +3,6 @@ package plugin import ( "context" "fmt" - "strings" "github.com/distribution/reference" "github.com/docker/cli/cli" @@ -31,7 +30,7 @@ func newUpgradeCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() - loadPullFlags(dockerCli, &options, flags) + loadPullFlags(&options, flags) flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") return cmd } @@ -73,19 +72,18 @@ func runUpgrade(ctx context.Context, dockerCLI command.Cli, opts pluginOptions) } } - options, err := buildPullConfig(ctx, dockerCLI, opts, "plugin upgrade") + options, err := buildPullConfig(ctx, dockerCLI, opts) if err != nil { return err } responseBody, err := dockerCLI.Client().PluginUpgrade(ctx, opts.localName, options) if err != nil { - if strings.Contains(err.Error(), "target is image") { - return errors.New(err.Error() + " - Use `docker image pull`") - } return err } - defer responseBody.Close() + defer func() { + _ = responseBody.Close() + }() if err := jsonstream.Display(ctx, responseBody, dockerCLI.Out()); err != nil { return err } diff --git a/cli/command/registry.go b/cli/command/registry.go index be49d85b6bb8..0deb74cd9170 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -31,11 +31,13 @@ const ( // authConfigKey is the key used to store credentials for Docker Hub. It is // a copy of [registry.IndexServer]. // -// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer const authConfigKey = "https://index.docker.io/v1/" // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // for the given command to prompt the user for username and password. +// +// Deprecated: this function is no longer used and will be removed in the next release. func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig { configKey := getAuthConfigKey(index.Name) isDefaultRegistry := configKey == authConfigKey || index.Official @@ -66,6 +68,8 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf // // It is similar to [registry.ResolveAuthConfig], but uses the credentials- // store, instead of looking up credentials from a map. +// +// [registry.ResolveAuthConfig]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#ResolveAuthConfig func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig { configKey := index.Name if index.Official { @@ -73,7 +77,16 @@ func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInf } a, _ := cfg.GetAuthConfig(configKey) - return registrytypes.AuthConfig(a) + return registrytypes.AuthConfig{ + Username: a.Username, + Password: a.Password, + ServerAddress: a.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: a.Auth, + IdentityToken: a.IdentityToken, + RegistryToken: a.RegistryToken, + } } // GetDefaultAuthConfig gets the default auth config given a serverAddress @@ -82,36 +95,27 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve if !isDefaultRegistry { serverAddress = credentials.ConvertToHostname(serverAddress) } - authconfig := configtypes.AuthConfig{} + authCfg := configtypes.AuthConfig{} var err error if checkCredStore { - authconfig, err = cfg.GetAuthConfig(serverAddress) + authCfg, err = cfg.GetAuthConfig(serverAddress) if err != nil { return registrytypes.AuthConfig{ ServerAddress: serverAddress, }, err } } - authconfig.ServerAddress = serverAddress - authconfig.IdentityToken = "" - return registrytypes.AuthConfig(authconfig), nil -} -// ConfigureAuth handles prompting of user's username and password if needed. -// -// Deprecated: use [PromptUserForCredentials] instead. -func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { - defaultUsername := authConfig.Username - serverAddress := authConfig.ServerAddress - - newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress) - if err != nil { - return err - } + return registrytypes.AuthConfig{ + Username: authCfg.Username, + Password: authCfg.Password, + ServerAddress: serverAddress, - authConfig.Username = newAuthConfig.Username - authConfig.Password = newAuthConfig.Password - return nil + // TODO(thaJeztah): Are these expected to be included? + Auth: authCfg.Auth, + IdentityToken: "", + RegistryToken: authCfg.RegistryToken, + }, nil } // PromptUserForCredentials handles the CLI prompt for the user to input @@ -209,37 +213,37 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword }, nil } -// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete -// image. The auth configuration is serialized as a base64url encoded RFC4648, -// section 5) JSON string for sending through the X-Registry-Auth header. +// RetrieveAuthTokenFromImage retrieves an encoded auth token given a +// complete image reference. The auth configuration is serialized as a +// base64url encoded ([RFC 4648, Section 5]) JSON string for sending through +// the "X-Registry-Auth" header. // -// For details on base64url encoding, see: -// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5 +// [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5 func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) { - // Retrieve encoded auth token from the image reference - authConfig, err := resolveAuthConfigFromImage(cfg, image) + registryRef, err := reference.ParseNormalizedNamed(image) if err != nil { return "", err } - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) + configKey := getAuthConfigKey(reference.Domain(registryRef)) + authConfig, err := cfg.GetAuthConfig(configKey) if err != nil { return "", err } - return encodedAuth, nil -} -// resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (registrytypes.AuthConfig, error) { - registryRef, err := reference.ParseNormalizedNamed(image) - if err != nil { - return registrytypes.AuthConfig{}, err - } - configKey := getAuthConfigKey(reference.Domain(registryRef)) - a, err := cfg.GetAuthConfig(configKey) + encodedAuth, err := registrytypes.EncodeAuthConfig(registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { - return registrytypes.AuthConfig{}, err + return "", err } - return registrytypes.AuthConfig(a), nil + return encodedAuth, nil } // getAuthConfigKey special-cases using the full index address of the official @@ -248,8 +252,8 @@ func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (regis // It is similar to [registry.GetAuthConfigKey], but does not require on // [registrytypes.IndexInfo] as intermediate. // -// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker/registry#GetAuthConfigKey -// [registrytypes.IndexInfo]:https://pkg.go.dev/github.com/docker/docker/api/types/registry#IndexInfo +// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey +// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo func getAuthConfigKey(domainName string) string { if domainName == "docker.io" || domainName == "index.docker.io" { return authConfigKey diff --git a/cli/command/registry/formatter_search.go b/cli/command/registry/formatter_search.go index 23f56ac69e63..2de91434a29f 100644 --- a/cli/command/registry/formatter_search.go +++ b/cli/command/registry/formatter_search.go @@ -16,8 +16,15 @@ const ( automatedHeader = "AUTOMATED" ) -// NewSearchFormat returns a Format for rendering using a network Context +// NewSearchFormat returns a Format for rendering using a search Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewSearchFormat(source string) formatter.Format { + return newFormat(source) +} + +// newFormat returns a Format for rendering using a searchContext. +func newFormat(source string) formatter.Format { switch source { case "", formatter.TableFormatKey: return defaultSearchTableFormat @@ -26,10 +33,17 @@ func NewSearchFormat(source string) formatter.Format { } // SearchWrite writes the context -func SearchWrite(ctx formatter.Context, results []registrytypes.SearchResult) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func SearchWrite(fmtCtx formatter.Context, results []registrytypes.SearchResult) error { + return formatWrite(fmtCtx, results) +} + +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, results []registrytypes.SearchResult) error { render := func(format func(subContext formatter.SubContext) error) error { for _, result := range results { - searchCtx := &searchContext{trunc: ctx.Trunc, s: result} + searchCtx := &searchContext{trunc: fmtCtx.Trunc, s: result} if err := format(searchCtx); err != nil { return err } @@ -43,7 +57,7 @@ func SearchWrite(ctx formatter.Context, results []registrytypes.SearchResult) er "StarCount": starsHeader, "IsOfficial": officialHeader, } - return ctx.Write(&searchCtx, render) + return fmtCtx.Write(&searchCtx, render) } type searchContext struct { diff --git a/cli/command/registry/formatter_search_test.go b/cli/command/registry/formatter_search_test.go index 082b0377e030..e8cb6456f32c 100644 --- a/cli/command/registry/formatter_search_test.go +++ b/cli/command/registry/formatter_search_test.go @@ -157,12 +157,12 @@ func TestSearchContextWrite(t *testing.T) { }, { doc: "Table format", - format: NewSearchFormat("table"), + format: newFormat("table"), expected: string(golden.Get(t, "search-context-write-table.golden")), }, { doc: "Table format, single column", - format: NewSearchFormat("table {{.Name}}"), + format: newFormat("table {{.Name}}"), expected: `NAME result1 result2 @@ -170,14 +170,14 @@ result2 }, { doc: "Custom format, single field", - format: NewSearchFormat("{{.Name}}"), + format: newFormat("{{.Name}}"), expected: `result1 result2 `, }, { doc: "Custom Format, two columns", - format: NewSearchFormat("{{.Name}} {{.StarCount}}"), + format: newFormat("{{.Name}} {{.StarCount}}"), expected: `result1 5000 result2 5 `, @@ -192,7 +192,7 @@ result2 5 for _, tc := range cases { t.Run(tc.doc, func(t *testing.T) { var out bytes.Buffer - err := SearchWrite(formatter.Context{Format: tc.format, Output: &out}, results) + err := formatWrite(formatter.Context{Format: tc.format, Output: &out}, results) if tc.expectedErr != "" { assert.Check(t, is.Error(err, tc.expectedErr)) } else { diff --git a/cli/command/registry/login.go b/cli/command/registry/login.go index 3679e51eddd5..d8b48210051a 100644 --- a/cli/command/registry/login.go +++ b/cli/command/registry/login.go @@ -8,17 +8,16 @@ import ( "strconv" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/config/configfile" configtypes "github.com/docker/cli/cli/config/types" "github.com/docker/cli/internal/oauth/manager" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/tui" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -32,7 +31,14 @@ type loginOptions struct { } // NewLoginCommand creates a new `docker login` command +// +// Deprecated: Do not import commands directly. They will be removed in a future release. func NewLoginCommand(dockerCLI command.Cli) *cobra.Command { + return newLoginCommand(dockerCLI) +} + +// newLoginCommand creates a new `docker login` command +func newLoginCommand(dockerCLI command.Cli) *cobra.Command { var opts loginOptions cmd := &cobra.Command{ @@ -52,7 +58,7 @@ func NewLoginCommand(dockerCLI command.Cli) *cobra.Command { Annotations: map[string]string{ "category-top": "8", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -159,7 +165,7 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth response, err := dockerCLI.Client().RegistryLogin(ctx, authConfig) if err != nil { - if cerrdefs.IsUnauthorized(err) { + if errdefs.IsUnauthorized(err) { _, _ = fmt.Fprintln(dockerCLI.Err(), "Stored credentials invalid or expired") } else { _, _ = fmt.Fprintln(dockerCLI.Err(), "Login did not succeed, error:", err) @@ -178,10 +184,15 @@ func loginWithStoredCredentials(ctx context.Context, dockerCLI command.Cli, auth return response.Status, err } +// OauthLoginEscapeHatchEnvVar disables the browser-based OAuth login workflow. +// +// Deprecated: this const was only used internally and will be removed in the next release. const OauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN" +const oauthLoginEscapeHatchEnvVar = "DOCKER_CLI_DISABLE_OAUTH_LOGIN" + func isOauthLoginDisabled() bool { - if v := os.Getenv(OauthLoginEscapeHatchEnvVar); v != "" { + if v := os.Getenv(oauthLoginEscapeHatchEnvVar); v != "" { enabled, err := strconv.ParseBool(v) if err != nil { return false @@ -248,12 +259,30 @@ func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg st return "", err } - response, err := loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig(*authConfig)) + response, err := loginWithRegistry(ctx, dockerCLI.Client(), registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) if err != nil { return "", err } - if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig(*authConfig)); err != nil { + if err = storeCredentials(dockerCLI.ConfigFile(), registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }); err != nil { return "", err } @@ -262,7 +291,16 @@ func loginWithDeviceCodeFlow(ctx context.Context, dockerCLI command.Cli) (msg st func storeCredentials(cfg *configfile.ConfigFile, authConfig registrytypes.AuthConfig) error { creds := cfg.GetCredentialsStore(authConfig.ServerAddress) - if err := creds.Store(configtypes.AuthConfig(authConfig)); err != nil { + if err := creds.Store(configtypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }); err != nil { return errors.Errorf("Error saving credentials: %v", err) } @@ -288,7 +326,7 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis return nil, err } - _, token, err := svc.Auth(ctx, &auth, command.UserAgent()) + token, err := svc.Auth(ctx, &auth, command.UserAgent()) if err != nil { return nil, err } diff --git a/cli/command/registry/login_test.go b/cli/command/registry/login_test.go index 58ca73c6a6cf..756da7d74f3c 100644 --- a/cli/command/registry/login_test.go +++ b/cli/command/registry/login_test.go @@ -13,11 +13,11 @@ import ( configtypes "github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/prompt" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/test" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" @@ -533,7 +533,7 @@ func TestIsOauthLoginDisabled(t *testing.T) { } for _, tc := range testCases { - t.Setenv(OauthLoginEscapeHatchEnvVar, tc.envVar) + t.Setenv(oauthLoginEscapeHatchEnvVar, tc.envVar) disabled := isOauthLoginDisabled() @@ -584,7 +584,7 @@ func TestLoginValidateFlags(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewLoginCommand(test.NewFakeCli(&fakeClient{})) + cmd := newLoginCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 2af2cdad3f3f..148152bbd0ec 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -8,12 +8,19 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/internal/oauth/manager" - "github.com/docker/docker/registry" + "github.com/docker/cli/internal/registry" "github.com/spf13/cobra" ) // NewLogoutCommand creates a new `docker logout` command -func NewLogoutCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewLogoutCommand(dockerCLI command.Cli) *cobra.Command { + return newLogoutCommand(dockerCLI) +} + +// newLogoutCommand creates a new `docker logout` command +func newLogoutCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "logout [SERVER]", Short: "Log out from a registry", @@ -24,7 +31,7 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 0 { serverAddress = args[0] } - return runLogout(cmd.Context(), dockerCli, serverAddress) + return runLogout(cmd.Context(), dockerCLI, serverAddress) }, Annotations: map[string]string{ "category-top": "9", diff --git a/cli/command/registry/search.go b/cli/command/registry/search.go index 5007322fc86b..f735cdaeedd1 100644 --- a/cli/command/registry/search.go +++ b/cli/command/registry/search.go @@ -3,13 +3,13 @@ package registry import ( "context" "fmt" + "strings" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/opts" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -22,7 +22,14 @@ type searchOptions struct { } // NewSearchCommand creates a new `docker search` command -func NewSearchCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewSearchCommand(dockerCLI command.Cli) *cobra.Command { + return newSearchCommand(dockerCLI) +} + +// newSearchCommand creates a new `docker search` command +func newSearchCommand(dockerCLI command.Cli) *cobra.Command { options := searchOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -31,7 +38,7 @@ func NewSearchCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.term = args[0] - return runSearch(cmd.Context(), dockerCli, options) + return runSearch(cmd.Context(), dockerCLI, options) }, Annotations: map[string]string{ "category-top": "10", @@ -52,24 +59,14 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions if options.filter.Value().Contains("is-automated") { _, _ = fmt.Fprintln(dockerCli.Err(), `WARNING: the "is-automated" filter is deprecated, and searching for "is-automated=true" will not yield any results in future.`) } - indexInfo, err := registry.ParseSearchIndexInfo(options.term) + encodedAuth, err := getAuth(dockerCli, options.term) if err != nil { return err } - authConfig := command.ResolveAuthConfig(dockerCli.ConfigFile(), indexInfo) - encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) - if err != nil { - return err - } - - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCli.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCli, indexInfo, "search") - } results, err := dockerCli.Client().ImageSearch(ctx, options.term, registrytypes.SearchOptions{ RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, Filters: options.filter.Value(), Limit: options.limit, }) @@ -79,8 +76,51 @@ func runSearch(ctx context.Context, dockerCli command.Cli, options searchOptions searchCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewSearchFormat(options.format), + Format: newFormat(options.format), Trunc: !options.noTrunc, } - return SearchWrite(searchCtx, results) + return formatWrite(searchCtx, results) +} + +// authConfigKey is the key used to store credentials for Docker Hub. It is +// a copy of [registry.IndexServer]. +// +// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer +const authConfigKey = "https://index.docker.io/v1/" + +// getAuth will use fetch auth based on the given search-term. If the search +// does not contain a hostname for the registry, it assumes Docker Hub is used, +// and resolves authentication for Docker Hub, otherwise it resolves authentication +// for the given registry. +func getAuth(dockerCLI command.Cli, reposName string) (encodedAuth string, err error) { + authCfgKey := splitReposSearchTerm(reposName) + if authCfgKey == "docker.io" || authCfgKey == "index.docker.io" { + authCfgKey = authConfigKey + } + + // Ignoring errors here, which was the existing behavior (likely + // "no credentials found"). We'll get an error when search failed, + // so fine to ignore in most situations. + authConfig, _ := dockerCLI.ConfigFile().GetAuthConfig(authCfgKey) + return registrytypes.EncodeAuthConfig(registrytypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + }) +} + +// splitReposSearchTerm breaks a search term into an index name and remote name +func splitReposSearchTerm(reposName string) string { + nameParts := strings.SplitN(reposName, "/", 2) + if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { + // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), + // use the default Docker Hub registry (docker.io) + return "docker.io" + } + return nameParts[0] } diff --git a/cli/command/registry_test.go b/cli/command/registry_test.go index 4c6bb9f89abb..853dcc875eae 100644 --- a/cli/command/registry_test.go +++ b/cli/command/registry_test.go @@ -58,8 +58,17 @@ func TestGetDefaultAuthConfig(t *testing.T) { }, } cfg := configfile.New("filename") - for _, authconfig := range testAuthConfigs { - assert.Check(t, cfg.GetCredentialsStore(authconfig.ServerAddress).Store(configtypes.AuthConfig(authconfig))) + for _, authConfig := range testAuthConfigs { + assert.Check(t, cfg.GetCredentialsStore(authConfig.ServerAddress).Store(configtypes.AuthConfig{ + Username: authConfig.Username, + Password: authConfig.Password, + ServerAddress: authConfig.ServerAddress, + + // TODO(thaJeztah): Are these expected to be included? + Auth: authConfig.Auth, + IdentityToken: authConfig.IdentityToken, + RegistryToken: authConfig.RegistryToken, + })) } for _, tc := range testCases { serverAddress := tc.inputServerAddress @@ -185,9 +194,9 @@ func TestRetrieveAuthTokenFromImage(t *testing.T) { imageRef := path.Join(tc.prefix, remoteRef) actual, err := command.RetrieveAuthTokenFromImage(&cfg, imageRef) assert.NilError(t, err) - ac, err := registry.DecodeAuthConfig(actual) + expectedAuthCfg, err := registry.EncodeAuthConfig(tc.expectedAuthCfg) assert.NilError(t, err) - assert.Check(t, is.DeepEqual(*ac, tc.expectedAuthCfg)) + assert.Equal(t, actual, expectedAuthCfg) } }) } diff --git a/cli/command/secret/cmd.go b/cli/command/secret/cmd.go index f31348a7e87f..2fb8aa60e18c 100644 --- a/cli/command/secret/cmd.go +++ b/cli/command/secret/cmd.go @@ -9,22 +9,28 @@ import ( ) // NewSecretCommand returns a cobra command for `secret` subcommands -func NewSecretCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewSecretCommand(dockerCLI command.Cli) *cobra.Command { + return newSecretCommand(dockerCLI) +} + +func newSecretCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "secret", Short: "Manage Swarm secrets", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.25", "swarm": "manager", }, } cmd.AddCommand( - newSecretListCommand(dockerCli), - newSecretCreateCommand(dockerCli), - newSecretInspectCommand(dockerCli), - newSecretRemoveCommand(dockerCli), + newSecretListCommand(dockerCLI), + newSecretCreateCommand(dockerCLI), + newSecretInspectCommand(dockerCLI), + newSecretRemoveCommand(dockerCLI), ) return cmd } diff --git a/cli/command/secret/create.go b/cli/command/secret/create.go index 07975247010d..76f66bace750 100644 --- a/cli/command/secret/create.go +++ b/cli/command/secret/create.go @@ -49,8 +49,8 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error { - client := dockerCli.Client() +func runSecretCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { + apiClient := dockerCLI.Client() var secretData []byte if options.driver != "" { @@ -59,7 +59,7 @@ func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createO } } else { var err error - secretData, err = readSecretData(dockerCli.In(), options.file) + secretData, err = readSecretData(dockerCLI.In(), options.file) if err != nil { return err } @@ -82,12 +82,12 @@ func runSecretCreate(ctx context.Context, dockerCli command.Cli, options createO Name: options.templateDriver, } } - r, err := client.SecretCreate(ctx, spec) + r, err := apiClient.SecretCreate(ctx, spec) if err != nil { return err } - _, _ = fmt.Fprintln(dockerCli.Out(), r.ID) + _, _ = fmt.Fprintln(dockerCLI.Out(), r.ID) return nil } diff --git a/cli/command/secret/formatter.go b/cli/command/secret/formatter.go index e30547628665..7a7df1384e9e 100644 --- a/cli/command/secret/formatter.go +++ b/cli/command/secret/formatter.go @@ -29,7 +29,14 @@ Updated at: {{.UpdatedAt}}` ) // NewFormat returns a Format for rendering using a secret Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string, quiet bool) formatter.Format { + return newFormat(source, quiet) +} + +// newFormat returns a Format for rendering using a secretContext. +func newFormat(source string, quiet bool) formatter.Format { switch source { case formatter.PrettyFormatKey: return secretInspectPrettyTemplate @@ -43,7 +50,14 @@ func NewFormat(source string, quiet bool) formatter.Format { } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, secrets []swarm.Secret) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, secrets []swarm.Secret) error { + return formatWrite(fmtCtx, secrets) +} + +// formatWrite writes the context +func formatWrite(fmtCtx formatter.Context, secrets []swarm.Secret) error { render := func(format func(subContext formatter.SubContext) error) error { for _, secret := range secrets { secretCtx := &secretContext{s: secret} @@ -53,7 +67,7 @@ func FormatWrite(ctx formatter.Context, secrets []swarm.Secret) error { } return nil } - return ctx.Write(newSecretContext(), render) + return fmtCtx.Write(newSecretContext(), render) } func newSecretContext() *secretContext { @@ -122,9 +136,16 @@ func (c *secretContext) Label(name string) string { } // InspectFormatWrite renders the context for a list of secrets -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { - if ctx.Format != secretInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// +// Deprecated: this function was only used internally and will be removed in the next release. +func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + return inspectFormatWrite(fmtCtx, refs, getRef) +} + +// inspectFormatWrite renders the context for a list of secrets. +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { + if fmtCtx.Format != secretInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } render := func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { @@ -142,7 +163,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.Get } return nil } - return ctx.Write(&secretInspectContext{}, render) + return fmtCtx.Write(&secretInspectContext{}, render) } type secretInspectContext struct { diff --git a/cli/command/secret/formatter_test.go b/cli/command/secret/formatter_test.go index 8ede765a57be..0f682454394c 100644 --- a/cli/command/secret/formatter_test.go +++ b/cli/command/secret/formatter_test.go @@ -27,21 +27,21 @@ func TestSecretContextFormatWrite(t *testing.T) { }, // Table format { - formatter.Context{Format: NewFormat("table", false)}, + formatter.Context{Format: newFormat("table", false)}, `ID NAME DRIVER CREATED UPDATED 1 passwords Less than a second ago Less than a second ago 2 id_rsa Less than a second ago Less than a second ago `, }, { - formatter.Context{Format: NewFormat("table {{.Name}}", true)}, + formatter.Context{Format: newFormat("table {{.Name}}", true)}, `NAME passwords id_rsa `, }, { - formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)}, + formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)}, `1-passwords 2-id_rsa `, @@ -65,7 +65,7 @@ id_rsa var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, secrets); err != nil { + if err := formatWrite(tc.context, secrets); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) diff --git a/cli/command/secret/inspect.go b/cli/command/secret/inspect.go index 22fed4b6e7a1..fea34f2e0a3b 100644 --- a/cli/command/secret/inspect.go +++ b/cli/command/secret/inspect.go @@ -41,30 +41,29 @@ func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runSecretInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" } getRef := func(id string) (any, []byte, error) { - return client.SecretInspectWithRaw(ctx, id) + return apiClient.SecretInspectWithRaw(ctx, id) } - f := opts.format // check if the user is trying to apply a template to the pretty format, which // is not supported - if strings.HasPrefix(f, "pretty") && f != "pretty" { + if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { return errors.New("cannot supply extra formatting options to the pretty template") } secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f, false), + Output: dockerCLI.Out(), + Format: newFormat(opts.format, false), } - if err := InspectFormatWrite(secretCtx, opts.names, getRef); err != nil { + if err := inspectFormatWrite(secretCtx, opts.names, getRef); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go index 65d8f23e0e8c..783ffc00c579 100644 --- a/cli/command/secret/ls.go +++ b/cli/command/secret/ls.go @@ -44,17 +44,17 @@ func newSecretListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runSecretList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() +func runSecretList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() - secrets, err := client.SecretList(ctx, swarm.SecretListOptions{Filters: options.filter.Value()}) + secrets, err := apiClient.SecretList(ctx, swarm.SecretListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 { - if len(dockerCli.ConfigFile().SecretFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().SecretFormat + if len(dockerCLI.ConfigFile().SecretFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().SecretFormat } else { format = formatter.TableFormatKey } @@ -65,8 +65,8 @@ func runSecretList(ctx context.Context, dockerCli command.Cli, options listOptio }) secretCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(format, options.quiet), + Output: dockerCLI.Out(), + Format: newFormat(format, options.quiet), } - return FormatWrite(secretCtx, secrets) + return formatWrite(secretCtx, secrets) } diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index 6b6626cc1443..52713726a074 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -7,27 +7,34 @@ import ( ) // NewServiceCommand returns a cobra command for `service` subcommands -func NewServiceCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewServiceCommand(dockerCLI command.Cli) *cobra.Command { + return newServiceCommand(dockerCLI) +} + +// newServiceCommand returns a cobra command for `service` subcommands +func newServiceCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "service", Short: "Manage Swarm services", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "manager", }, } cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newPsCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newScaleCommand(dockerCli), - newUpdateCommand(dockerCli), - newLogsCommand(dockerCli), - newRollbackCommand(dockerCli), + newCreateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newPsCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newScaleCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newLogsCommand(dockerCLI), + newRollbackCommand(dockerCLI), ) return cmd } diff --git a/cli/command/service/create.go b/cli/command/service/create.go index f63f65aefe24..621fc9f36feb 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -29,7 +29,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { } return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.StringVar(&opts.mode, flagMode, "replicated", `Service mode ("replicated", "global", "replicated-job", "global-job")`) @@ -94,7 +94,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index aee5453f10f9..ed22e48331c6 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -196,7 +196,14 @@ Ports: ` // NewFormat returns a Format for rendering using a Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewFormat(source string) formatter.Format { + return newFormat(source) +} + +// newFormat returns a Format for rendering using a Context. +func newFormat(source string) formatter.Format { switch source { case formatter.PrettyFormatKey: return serviceInspectPrettyTemplate @@ -218,9 +225,16 @@ func resolveNetworks(service swarm.Service, getNetwork inspect.GetRefFunc) map[s } // InspectFormatWrite renders the context for a list of services -func InspectFormatWrite(ctx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { - if ctx.Format != serviceInspectPrettyTemplate { - return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) +// +// Deprecated: this function was only used internally and will be removed in the next release. +func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { + return inspectFormatWrite(fmtCtx, refs, getRef, getNetwork) +} + +// inspectFormatWrite renders the context for a list of services +func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef, getNetwork inspect.GetRefFunc) error { + if fmtCtx.Format != serviceInspectPrettyTemplate { + return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) } render := func(format func(subContext formatter.SubContext) error) error { for _, ref := range refs { @@ -238,7 +252,7 @@ func InspectFormatWrite(ctx formatter.Context, refs []string, getRef, getNetwork } return nil } - return ctx.Write(&serviceInspectContext{}, render) + return fmtCtx.Write(&serviceInspectContext{}, render) } type serviceInspectContext struct { diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go index e3ae937125bc..aa8cbcbcf667 100644 --- a/cli/command/service/inspect.go +++ b/cli/command/service/inspect.go @@ -7,10 +7,9 @@ import ( "context" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/docker/api/types/network" @@ -52,13 +51,13 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() if opts.pretty { opts.format = "pretty" @@ -66,16 +65,16 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) getRef := func(ref string) (any, []byte, error) { // Service inspect shows defaults values in empty fields. - service, _, err := client.ServiceInspectWithRaw(ctx, ref, swarm.ServiceInspectOptions{InsertDefaults: true}) - if err == nil || !cerrdefs.IsNotFound(err) { + service, _, err := apiClient.ServiceInspectWithRaw(ctx, ref, swarm.ServiceInspectOptions{InsertDefaults: true}) + if err == nil || !errdefs.IsNotFound(err) { return service, nil, err } return nil, nil, errors.Errorf("Error: no such service: %s", ref) } getNetwork := func(ref string) (any, []byte, error) { - nw, _, err := client.NetworkInspectWithRaw(ctx, ref, network.InspectOptions{Scope: "swarm"}) - if err == nil || !cerrdefs.IsNotFound(err) { + nw, _, err := apiClient.NetworkInspectWithRaw(ctx, ref, network.InspectOptions{Scope: "swarm"}) + if err == nil || !errdefs.IsNotFound(err) { return nw, nil, err } return nil, nil, errors.Errorf("Error: no such network: %s", ref) @@ -84,8 +83,8 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) f := opts.format if len(f) == 0 { f = "raw" - if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { - f = dockerCli.ConfigFile().ServiceInspectFormat + if len(dockerCLI.ConfigFile().ServiceInspectFormat) > 0 { + f = dockerCLI.ConfigFile().ServiceInspectFormat } } @@ -96,11 +95,11 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) } serviceCtx := formatter.Context{ - Output: dockerCli.Out(), - Format: NewFormat(f), + Output: dockerCLI.Out(), + Format: newFormat(f), } - if err := InspectFormatWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { + if err := inspectFormatWrite(serviceCtx, opts.refs, getRef, getNetwork); err != nil { return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil diff --git a/cli/command/service/inspect_test.go b/cli/command/service/inspect_test.go index d7aa42e8f9bd..17e084bd4fba 100644 --- a/cli/command/service/inspect_test.go +++ b/cli/command/service/inspect_test.go @@ -131,7 +131,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Format: format, } - err := InspectFormatWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, + err := inspectFormatWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (any, []byte, error) { return s, nil, nil }, @@ -149,12 +149,12 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) } func TestPrettyPrint(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) golden.Assert(t, s, "service-inspect-pretty.golden") } func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) if strings.Contains(s, "UpdateStatus") { t.Fatal("Pretty print failed before parsing UpdateStatus") } @@ -167,8 +167,8 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) { now := time.Now() // s1: [{"ID":..}] // s2: {"ID":..} - s1 := formatServiceInspect(t, NewFormat(""), now) - s2 := formatServiceInspect(t, NewFormat("{{json .}}"), now) + s1 := formatServiceInspect(t, newFormat(""), now) + s2 := formatServiceInspect(t, newFormat("{{json .}}"), now) var m1Wrap []map[string]any if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { t.Fatal(err) @@ -185,7 +185,7 @@ func TestJSONFormatWithNoUpdateConfig(t *testing.T) { } func TestPrettyPrintWithConfigsAndSecrets(t *testing.T) { - s := formatServiceInspect(t, NewFormat("pretty"), time.Now()) + s := formatServiceInspect(t, newFormat("pretty"), time.Now()) assert.Check(t, is.Contains(s, "Log Driver:"), "Pretty print missing Log Driver") assert.Check(t, is.Contains(s, "Configs:"), "Pretty print missing configs") assert.Check(t, is.Contains(s, "Secrets:"), "Pretty print missing secrets") diff --git a/cli/command/service/list.go b/cli/command/service/list.go index d2768cc0e197..278a7dcc7239 100644 --- a/cli/command/service/list.go +++ b/cli/command/service/list.go @@ -5,7 +5,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -33,7 +32,7 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCLI, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -45,7 +44,7 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index 7c651d859bb2..b92f8dcf0fba 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -9,10 +9,9 @@ import ( "strconv" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "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" @@ -73,7 +72,7 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } @@ -93,12 +92,12 @@ func runLogs(ctx context.Context, dockerCli command.Cli, opts *logsOptions) erro service, _, err := apiClient.ServiceInspectWithRaw(ctx, opts.target, swarm.ServiceInspectOptions{}) if err != nil { // if it's any error other than service not found, it's Real - if !cerrdefs.IsNotFound(err) { + if !errdefs.IsNotFound(err) { return err } task, _, err := apiClient.TaskInspectWithRaw(ctx, opts.target) if err != nil { - if cerrdefs.IsNotFound(err) { + if errdefs.IsNotFound(err) { // if the task isn't found, rewrite the error to be clear // that we looked for services AND tasks and found none err = fmt.Errorf("no such task or service: %v", opts.target) diff --git a/cli/command/service/opts_test.go b/cli/command/service/opts_test.go index 799d32cebd2d..22ae28fa5bea 100644 --- a/cli/command/service/opts_test.go +++ b/cli/command/service/opts_test.go @@ -190,7 +190,7 @@ func TestToServiceNetwork(t *testing.T) { {Name: "zzz-network", ID: "id111"}, } - client := &fakeClient{ + apiClient := &fakeClient{ networkInspectFunc: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { for _, nw := range nws { if nw.ID == networkID || nw.Name == networkID { @@ -212,7 +212,7 @@ func TestToServiceNetwork(t *testing.T) { ctx := context.Background() flags := newCreateCommand(nil).Flags() - service, err := o.ToService(ctx, client, flags) + service, err := o.ToService(ctx, apiClient, flags) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id111"}, {Target: "id555"}, {Target: "id999"}}, service.TaskTemplate.Networks)) } diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go index 601014054d0d..b55db793d29b 100644 --- a/cli/command/service/ps.go +++ b/cli/command/service/ps.go @@ -6,7 +6,6 @@ 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/idresolver" "github.com/docker/cli/cli/command/node" "github.com/docker/cli/cli/command/task" @@ -52,7 +51,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } diff --git a/cli/command/service/ps_test.go b/cli/command/service/ps_test.go index 5301846a551a..46e0dafe0232 100644 --- a/cli/command/service/ps_test.go +++ b/cli/command/service/ps_test.go @@ -15,7 +15,7 @@ import ( ) func TestCreateFilter(t *testing.T) { - client := &fakeClient{ + apiClient := &fakeClient{ serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{ {ID: "idmatch"}, @@ -33,7 +33,7 @@ func TestCreateFilter(t *testing.T) { filter: filter, } - actual, notfound, err := createFilter(context.Background(), client, options) + actual, notfound, err := createFilter(context.Background(), apiClient, options) assert.NilError(t, err) assert.Check(t, is.DeepEqual(notfound, []string{"no such service: notfound"})) @@ -47,7 +47,7 @@ func TestCreateFilter(t *testing.T) { } func TestCreateFilterWithAmbiguousIDPrefixError(t *testing.T) { - client := &fakeClient{ + apiClient := &fakeClient{ serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{ {ID: "aaaone"}, @@ -59,22 +59,22 @@ func TestCreateFilterWithAmbiguousIDPrefixError(t *testing.T) { services: []string{"aaa"}, filter: opts.NewFilterOpt(), } - _, _, err := createFilter(context.Background(), client, options) + _, _, err := createFilter(context.Background(), apiClient, options) assert.Error(t, err, "multiple services found with provided prefix: aaa") } func TestCreateFilterNoneFound(t *testing.T) { - client := &fakeClient{} + apiClient := &fakeClient{} options := psOptions{ services: []string{"foo", "notfound"}, filter: opts.NewFilterOpt(), } - _, _, err := createFilter(context.Background(), client, options) + _, _, err := createFilter(context.Background(), apiClient, options) assert.Error(t, err, "no such service: foo\nno such service: notfound") } func TestRunPSWarnsOnNotFound(t *testing.T) { - client := &fakeClient{ + apiClient := &fakeClient{ serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{ {ID: "foo"}, @@ -82,7 +82,7 @@ func TestRunPSWarnsOnNotFound(t *testing.T) { }, } - cli := test.NewFakeCli(client) + cli := test.NewFakeCli(apiClient) options := psOptions{ services: []string{"foo", "bar"}, filter: opts.NewFilterOpt(), @@ -95,7 +95,7 @@ func TestRunPSWarnsOnNotFound(t *testing.T) { } func TestRunPSQuiet(t *testing.T) { - client := &fakeClient{ + apiClient := &fakeClient{ serviceListFunc: func(ctx context.Context, options swarm.ServiceListOptions) ([]swarm.Service, error) { return []swarm.Service{{ID: "foo"}}, nil }, @@ -104,7 +104,7 @@ func TestRunPSQuiet(t *testing.T) { }, } - cli := test.NewFakeCli(client) + cli := test.NewFakeCli(apiClient) ctx := context.Background() err := runPS(ctx, cli, psOptions{services: []string{"foo"}, quiet: true, filter: opts.NewFilterOpt()}) assert.NilError(t, err) @@ -119,13 +119,14 @@ func TestUpdateNodeFilter(t *testing.T) { filters.Arg("node", "self"), ) - client := &fakeClient{ + apiClient := &fakeClient{ infoFunc: func(_ context.Context) (system.Info, error) { return system.Info{Swarm: swarm.Info{NodeID: selfNodeID}}, nil }, } - updateNodeFilter(context.Background(), client, filter) + err := updateNodeFilter(context.Background(), apiClient, filter) + assert.NilError(t, err) expected := filters.NewArgs( filters.Arg("node", "one"), diff --git a/cli/command/service/rollback.go b/cli/command/service/rollback.go index 3b4a445f2e23..a100c9f0d9fa 100644 --- a/cli/command/service/rollback.go +++ b/cli/command/service/rollback.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/versions" "github.com/spf13/cobra" @@ -35,7 +34,7 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } diff --git a/cli/command/service/trust.go b/cli/command/service/trust.go index 6fb4d129a5ff..fe4b08c67b8a 100644 --- a/cli/command/service/trust.go +++ b/cli/command/service/trust.go @@ -6,8 +6,8 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/trust" + "github.com/docker/cli/internal/registry" "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -15,7 +15,7 @@ import ( ) func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm.ServiceSpec) error { - if !dockerCli.ContentTrustEnabled() { + if !trust.Enabled() { // When not using content trust, digest resolution happens later when // contacting the registry to retrieve image information. return nil @@ -51,9 +51,12 @@ func resolveServiceImageDigestContentTrust(dockerCli command.Cli, service *swarm } func trustedResolveDigest(cli command.Cli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := command.ResolveAuthConfig(cli.ConfigFile(), repoInfo.Index) - + indexInfo := registry.NewIndexInfo(ref) + authConfig := command.ResolveAuthConfig(cli.ConfigFile(), indexInfo) + repoInfo := &trust.RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + } notaryRepo, err := trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), repoInfo, &authConfig, "pull") if err != nil { return nil, errors.Wrap(err, "error establishing connection to trust repository") diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 2b75b6b59b18..7ebcb461b55c 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -112,9 +112,9 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { // Add needs parsing, Remove only needs the key flags.Var(newListOptsVar(), flagGenericResourcesRemove, "Remove a Generic resource") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + flags.SetAnnotation(flagGenericResourcesRemove, "version", []string{"1.32"}) flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource") - flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + flags.SetAnnotation(flagGenericResourcesAdd, "version", []string{"1.32"}) // TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package) // _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames) @@ -137,7 +137,7 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index d022b0c3d507..239ba8249197 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -851,7 +851,7 @@ func TestUpdateNetworks(t *testing.T) { {Name: "zzz-network", ID: "id111"}, } - client := &fakeClient{ + apiClient := &fakeClient{ networkInspectFunc: func(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) { for _, nw := range nws { if nw.ID == networkID || nw.Name == networkID { @@ -874,28 +874,28 @@ func TestUpdateNetworks(t *testing.T) { flags := newUpdateCommand(nil).Flags() err := flags.Set(flagNetworkAdd, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkAdd, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.Error(t, err, "service is already attached to network aaa-network") assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkAdd, "id555") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.Error(t, err, "service is already attached to network id555") assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}, {Target: "id999"}}, svc.TaskTemplate.Networks)) flags = newUpdateCommand(nil).Flags() err = flags.Set(flagNetworkRemove, "id999") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id555"}}, svc.TaskTemplate.Networks)) @@ -904,7 +904,7 @@ func TestUpdateNetworks(t *testing.T) { assert.NilError(t, err) err = flags.Set(flagNetworkRemove, "aaa-network") assert.NilError(t, err) - err = updateService(ctx, client, flags, &svc) + err = updateService(ctx, apiClient, flags, &svc) assert.NilError(t, err) assert.Check(t, is.DeepEqual([]swarm.NetworkAttachmentConfig{{Target: "id999"}}, svc.TaskTemplate.Networks)) } diff --git a/cli/command/stack/cmd.go b/cli/command/stack/cmd.go index e46f49ad9740..17efafb4c005 100644 --- a/cli/command/stack/cmd.go +++ b/cli/command/stack/cmd.go @@ -11,12 +11,19 @@ import ( ) // NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewStackCommand(dockerCLI command.Cli) *cobra.Command { + return newStackCommand(dockerCLI) +} + +// newStackCommand returns a cobra command for `stack` subcommands +func newStackCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "stack [OPTIONS]", Short: "Manage Swarm stacks", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.25", "swarm": "manager", @@ -25,18 +32,18 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command { defaultHelpFunc := cmd.HelpFunc() cmd.SetHelpFunc(func(c *cobra.Command, args []string) { if err := cmd.Root().PersistentPreRunE(c, args); err != nil { - fmt.Fprintln(dockerCli.Err(), err) + fmt.Fprintln(dockerCLI.Err(), err) return } defaultHelpFunc(c, args) }) cmd.AddCommand( - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newPsCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newConfigCommand(dockerCli), + newDeployCommand(dockerCLI), + newListCommand(dockerCLI), + newPsCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newServicesCommand(dockerCLI), + newConfigCommand(dockerCLI), ) flags := cmd.PersistentFlags() flags.String("orchestrator", "", "Orchestrator to use (swarm|all)") diff --git a/cli/command/stack/config.go b/cli/command/stack/config.go index 045db77b5fa6..9af8acf10338 100644 --- a/cli/command/stack/config.go +++ b/cli/command/stack/config.go @@ -6,7 +6,6 @@ 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/stack/loader" "github.com/docker/cli/cli/command/stack/options" composeLoader "github.com/docker/cli/cli/compose/loader" @@ -36,7 +35,7 @@ func newConfigCommand(dockerCli command.Cli) *cobra.Command { _, err = fmt.Fprintf(dockerCli.Out(), "%s", cfg) return err }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/stack/formatter/formatter.go b/cli/command/stack/formatter/formatter.go index 07b322f0616a..7cfe8a89af0f 100644 --- a/cli/command/stack/formatter/formatter.go +++ b/cli/command/stack/formatter/formatter.go @@ -8,21 +8,31 @@ import ( const ( // SwarmStackTableFormat is the default Swarm stack format + // + // Deprecated: this type was for internal use and will be removed in the next release. SwarmStackTableFormat formatter.Format = "table {{.Name}}\t{{.Services}}" stackServicesHeader = "SERVICES" // TableFormatKey is an alias for formatter.TableFormatKey + // + // Deprecated: this type was for internal use and will be removed in the next release. TableFormatKey = formatter.TableFormatKey ) // Context is an alias for formatter.Context +// +// Deprecated: this type was for internal use and will be removed in the next release. type Context = formatter.Context // Format is an alias for formatter.Format +// +// Deprecated: this type was for internal use and will be removed in the next release. type Format = formatter.Format // Stack contains deployed stack information. +// +// Deprecated: this type was for internal use and will be removed in the next release. type Stack struct { // Name is the name of the stack Name string @@ -31,6 +41,8 @@ type Stack struct { } // StackWrite writes formatted stacks using the Context +// +// Deprecated: this function was for internal use and will be removed in the next release. func StackWrite(ctx formatter.Context, stacks []*Stack) error { render := func(format func(subContext formatter.SubContext) error) error { for _, stack := range stacks { diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index 4ec1b30085d4..74a7fd98f8b0 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -7,7 +7,6 @@ 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/stack/formatter" "github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/swarm" @@ -16,8 +15,10 @@ import ( "github.com/spf13/cobra" ) +type listOptions = options.List + func newListCommand(dockerCli command.Cli) *cobra.Command { - opts := options.List{} + opts := listOptions{} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -25,9 +26,9 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { Short: "List stacks", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return RunList(cmd.Context(), dockerCli, opts) + return runList(cmd.Context(), dockerCli, opts) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -36,17 +37,24 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { } // RunList performs a stack list against the specified swarm cluster -func RunList(ctx context.Context, dockerCli command.Cli, opts options.List) error { - ss, err := swarm.GetStacks(ctx, dockerCli.Client()) +// +// Deprecated: this function was for internal use and will be removed in the next release. +func RunList(ctx context.Context, dockerCLI command.Cli, opts options.List) error { + return runList(ctx, dockerCLI, opts) +} + +// runList performs a stack list against the specified swarm cluster +func runList(ctx context.Context, dockerCLI command.Cli, opts listOptions) error { + ss, err := swarm.GetStacks(ctx, dockerCLI.Client()) if err != nil { return err } stacks := make([]*formatter.Stack, 0, len(ss)) stacks = append(stacks, ss...) - return format(dockerCli.Out(), opts, stacks) + return format(dockerCLI.Out(), opts, stacks) } -func format(out io.Writer, opts options.List, stacks []*formatter.Stack) error { +func format(out io.Writer, opts listOptions, stacks []*formatter.Stack) error { fmt := formatter.Format(opts.Format) if fmt == "" || fmt == formatter.TableFormatKey { fmt = formatter.SwarmStackTableFormat diff --git a/cli/command/stack/loader/loader.go b/cli/command/stack/loader/loader.go index 75d485b01464..8187efb5a669 100644 --- a/cli/command/stack/loader/loader.go +++ b/cli/command/stack/loader/loader.go @@ -22,6 +22,8 @@ import ( ) // LoadComposefile parse the composefile specified in the cli and returns its Config and version. +// +// Deprecated: this function was for internal use and will be removed in the next release. func LoadComposefile(dockerCli command.Cli, opts options.Deploy) (*composetypes.Config, error) { configDetails, err := GetConfigDetails(opts.Composefiles, dockerCli.In()) if err != nil { @@ -84,6 +86,8 @@ func propertyWarnings(properties map[string]string) string { } // GetConfigDetails parse the composefiles specified in the cli and returns their ConfigDetails +// +// Deprecated: this function was for internal use and will be removed in the next release. func GetConfigDetails(composefiles []string, stdin io.Reader) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails diff --git a/cli/command/stack/options/opts.go b/cli/command/stack/options/opts.go index 28d4c3262207..bd239e892ffd 100644 --- a/cli/command/stack/options/opts.go +++ b/cli/command/stack/options/opts.go @@ -3,6 +3,8 @@ package options import "github.com/docker/cli/opts" // Deploy holds docker stack deploy options +// +// Deprecated: this type was for internal use and will be removed in the next release. type Deploy struct { Composefiles []string Namespace string @@ -14,18 +16,24 @@ type Deploy struct { } // Config holds docker stack config options +// +// Deprecated: this type was for internal use and will be removed in the next release. type Config struct { Composefiles []string SkipInterpolation bool } // List holds docker stack ls options +// +// Deprecated: this type was for internal use and will be removed in the next release. type List struct { Format string AllNamespaces bool } // PS holds docker stack ps options +// +// Deprecated: this type was for internal use and will be removed in the next release. type PS struct { Filter opts.FilterOpt NoTrunc bool @@ -36,12 +44,16 @@ type PS struct { } // Remove holds docker stack remove options +// +// Deprecated: this type was for internal use and will be removed in the next release. type Remove struct { Namespaces []string Detach bool } // Services holds docker stack services options +// +// Deprecated: this type was for internal use and will be removed in the next release. type Services struct { Quiet bool Format string diff --git a/cli/command/stack/remove_test.go b/cli/command/stack/remove_test.go index 1a6923b747d7..6c843e6f6ecb 100644 --- a/cli/command/stack/remove_test.go +++ b/cli/command/stack/remove_test.go @@ -99,14 +99,14 @@ func TestRemoveStackSkipEmpty(t *testing.T) { allConfigs := []string{objectName("bar", "config1")} allConfigIDs := buildObjectIDs(allConfigs) - fakeClient := &fakeClient{ + apiClient := &fakeClient{ version: "1.30", services: allServices, networks: allNetworks, secrets: allSecrets, configs: allConfigs, } - fakeCli := test.NewFakeCli(fakeClient) + fakeCli := test.NewFakeCli(apiClient) cmd := newRemoveCommand(fakeCli) cmd.SetArgs([]string{"foo", "bar"}) @@ -120,10 +120,10 @@ func TestRemoveStackSkipEmpty(t *testing.T) { } assert.Check(t, is.Equal(strings.Join(expectedList, "\n"), fakeCli.OutBuffer().String())) assert.Check(t, is.Contains(fakeCli.ErrBuffer().String(), "Nothing found in stack: foo\n")) - assert.Check(t, is.DeepEqual(allServiceIDs, fakeClient.removedServices)) - assert.Check(t, is.DeepEqual(allNetworkIDs, fakeClient.removedNetworks)) - assert.Check(t, is.DeepEqual(allSecretIDs, fakeClient.removedSecrets)) - assert.Check(t, is.DeepEqual(allConfigIDs, fakeClient.removedConfigs)) + assert.Check(t, is.DeepEqual(allServiceIDs, apiClient.removedServices)) + assert.Check(t, is.DeepEqual(allNetworkIDs, apiClient.removedNetworks)) + assert.Check(t, is.DeepEqual(allSecretIDs, apiClient.removedSecrets)) + assert.Check(t, is.DeepEqual(allConfigIDs, apiClient.removedConfigs)) } func TestRemoveContinueAfterError(t *testing.T) { @@ -140,7 +140,7 @@ func TestRemoveContinueAfterError(t *testing.T) { allConfigIDs := buildObjectIDs(allConfigs) removedServices := []string{} - cli := &fakeClient{ + apiClient := &fakeClient{ version: "1.30", services: allServices, networks: allNetworks, @@ -156,14 +156,14 @@ func TestRemoveContinueAfterError(t *testing.T) { return nil }, } - cmd := newRemoveCommand(test.NewFakeCli(cli)) + cmd := newRemoveCommand(test.NewFakeCli(apiClient)) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs([]string{"foo", "bar"}) assert.Error(t, cmd.Execute(), "failed to remove some resources from stack: foo") assert.Check(t, is.DeepEqual(allServiceIDs, removedServices)) - assert.Check(t, is.DeepEqual(allNetworkIDs, cli.removedNetworks)) - assert.Check(t, is.DeepEqual(allSecretIDs, cli.removedSecrets)) - assert.Check(t, is.DeepEqual(allConfigIDs, cli.removedConfigs)) + assert.Check(t, is.DeepEqual(allNetworkIDs, apiClient.removedNetworks)) + assert.Check(t, is.DeepEqual(allSecretIDs, apiClient.removedSecrets)) + assert.Check(t, is.DeepEqual(allConfigIDs, apiClient.removedConfigs)) } diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go index f91ba71edfa5..cdeac2a2b9de 100644 --- a/cli/command/stack/services.go +++ b/cli/command/stack/services.go @@ -18,8 +18,11 @@ import ( "github.com/spf13/cobra" ) -func newServicesCommand(dockerCli command.Cli) *cobra.Command { - opts := options.Services{Filter: cliopts.NewFilterOpt()} +// servicesOptions holds docker stack services options +type servicesOptions = options.Services + +func newServicesCommand(dockerCLI command.Cli) *cobra.Command { + opts := servicesOptions{Filter: cliopts.NewFilterOpt()} cmd := &cobra.Command{ Use: "services [OPTIONS] STACK", @@ -30,10 +33,10 @@ func newServicesCommand(dockerCli command.Cli) *cobra.Command { if err := validateStackName(opts.Namespace); err != nil { return err } - return RunServices(cmd.Context(), dockerCli, opts) + return runServices(cmd.Context(), dockerCLI, opts) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return completeNames(dockerCli)(cmd, args, toComplete) + return completeNames(dockerCLI)(cmd, args, toComplete) }, } flags := cmd.Flags() @@ -44,15 +47,22 @@ func newServicesCommand(dockerCli command.Cli) *cobra.Command { } // RunServices performs a stack services against the specified swarm cluster -func RunServices(ctx context.Context, dockerCli command.Cli, opts options.Services) error { - services, err := swarm.GetServices(ctx, dockerCli, opts) +// +// Deprecated: this function was for internal use and will be removed in the next release. +func RunServices(ctx context.Context, dockerCLI command.Cli, opts options.Services) error { + return runServices(ctx, dockerCLI, opts) +} + +// runServices performs a stack services against the specified swarm cluster +func runServices(ctx context.Context, dockerCLI command.Cli, opts servicesOptions) error { + services, err := swarm.GetServices(ctx, dockerCLI, opts) if err != nil { return err } - return formatWrite(dockerCli, services, opts) + return formatWrite(dockerCLI, services, opts) } -func formatWrite(dockerCLI command.Cli, services []swarmtypes.Service, opts options.Services) error { +func formatWrite(dockerCLI command.Cli, services []swarmtypes.Service, opts servicesOptions) error { // if no services in the stack, print message and exit 0 if len(services) == 0 { _, _ = fmt.Fprintln(dockerCLI.Err(), "Nothing found in stack:", opts.Namespace) diff --git a/cli/command/stack/swarm/deploy.go b/cli/command/stack/swarm/deploy.go index bfd71f5f6566..cf3a52418ff3 100644 --- a/cli/command/stack/swarm/deploy.go +++ b/cli/command/stack/swarm/deploy.go @@ -23,6 +23,8 @@ const ( ) // RunDeploy is the swarm implementation of docker stack deploy +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunDeploy(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts *options.Deploy, cfg *composetypes.Config) error { if err := validateResolveImageFlag(opts); err != nil { return err diff --git a/cli/command/stack/swarm/deploy_composefile.go b/cli/command/stack/swarm/deploy_composefile.go index edde88962a9d..8c0bad15b660 100644 --- a/cli/command/stack/swarm/deploy_composefile.go +++ b/cli/command/stack/swarm/deploy_composefile.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli/command" servicecli "github.com/docker/cli/cli/command/service" "github.com/docker/cli/cli/command/stack/options" @@ -97,7 +97,7 @@ func validateExternalNetworks(ctx context.Context, apiClient client.NetworkAPICl } nw, err := apiClient.NetworkInspect(ctx, networkName, network.InspectOptions{}) switch { - case cerrdefs.IsNotFound(err): + case errdefs.IsNotFound(err): return fmt.Errorf("network %q is declared as external, but could not be found. You need to create a swarm-scoped network before the stack is deployed", networkName) case err != nil: return err @@ -119,7 +119,7 @@ func createSecrets(ctx context.Context, dockerCLI command.Cli, secrets []swarm.S if err := apiClient.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { return fmt.Errorf("failed to update secret %s: %w", secretSpec.Name, err) } - case cerrdefs.IsNotFound(err): + case errdefs.IsNotFound(err): // secret does not exist, then we create a new one. _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating secret", secretSpec.Name) if _, err := apiClient.SecretCreate(ctx, secretSpec); err != nil { @@ -143,7 +143,7 @@ func createConfigs(ctx context.Context, dockerCLI command.Cli, configs []swarm.C if err := apiClient.ConfigUpdate(ctx, config.ID, config.Meta.Version, configSpec); err != nil { return fmt.Errorf("failed to update config %s: %w", configSpec.Name, err) } - case cerrdefs.IsNotFound(err): + case errdefs.IsNotFound(err): // config does not exist, then we create a new one. _, _ = fmt.Fprintln(dockerCLI.Out(), "Creating config", configSpec.Name) if _, err := apiClient.ConfigCreate(ctx, configSpec); err != nil { diff --git a/cli/command/stack/swarm/deploy_test.go b/cli/command/stack/swarm/deploy_test.go index 1c62eb3e9832..e37d5812a5a9 100644 --- a/cli/command/stack/swarm/deploy_test.go +++ b/cli/command/stack/swarm/deploy_test.go @@ -18,11 +18,11 @@ func TestPruneServices(t *testing.T) { "new": {}, "keep": {}, } - client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} - dockerCli := test.NewFakeCli(client) + apiClient := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} + dockerCli := test.NewFakeCli(apiClient) pruneServices(ctx, dockerCli, namespace, services) - assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), client.removedServices)) + assert.Check(t, is.DeepEqual(buildObjectIDs([]string{objectName("foo", "remove")}), apiClient.removedServices)) } // TestServiceUpdateResolveImageChanged tests that the service's diff --git a/cli/command/stack/swarm/list.go b/cli/command/stack/swarm/list.go index ec951d71d395..22b175a1eda0 100644 --- a/cli/command/stack/swarm/list.go +++ b/cli/command/stack/swarm/list.go @@ -11,6 +11,8 @@ import ( ) // GetStacks lists the swarm stacks. +// +// Deprecated: this function was for internal use and will be removed in the next release. func GetStacks(ctx context.Context, apiClient client.ServiceAPIClient) ([]*formatter.Stack, error) { services, err := apiClient.ServiceList( ctx, diff --git a/cli/command/stack/swarm/ps.go b/cli/command/stack/swarm/ps.go index d213d525d6e7..fc16b7eb725d 100644 --- a/cli/command/stack/swarm/ps.go +++ b/cli/command/stack/swarm/ps.go @@ -12,11 +12,13 @@ import ( ) // RunPS is the swarm implementation of docker stack ps -func RunPS(ctx context.Context, dockerCli command.Cli, opts options.PS) error { +// +// Deprecated: this function was for internal use and will be removed in the next release. +func RunPS(ctx context.Context, dockerCLI command.Cli, opts options.PS) error { filter := getStackFilterFromOpt(opts.Namespace, opts.Filter) - client := dockerCli.Client() - tasks, err := client.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) + apiClient := dockerCLI.Client() + tasks, err := apiClient.TaskList(ctx, swarm.TaskListOptions{Filters: filter}) if err != nil { return err } @@ -27,8 +29,8 @@ func RunPS(ctx context.Context, dockerCli command.Cli, opts options.PS) error { format := opts.Format if len(format) == 0 { - format = task.DefaultFormat(dockerCli.ConfigFile(), opts.Quiet) + format = task.DefaultFormat(dockerCLI.ConfigFile(), opts.Quiet) } - return task.Print(ctx, dockerCli, tasks, idresolver.New(client, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format) + return task.Print(ctx, dockerCLI, tasks, idresolver.New(apiClient, opts.NoResolve), !opts.NoTrunc, opts.Quiet, format) } diff --git a/cli/command/stack/swarm/remove.go b/cli/command/stack/swarm/remove.go index cd426d5111d5..42812262ab95 100644 --- a/cli/command/stack/swarm/remove.go +++ b/cli/command/stack/swarm/remove.go @@ -15,6 +15,8 @@ import ( ) // RunRemove is the swarm implementation of docker stack remove +// +// Deprecated: this function was for internal use and will be removed in the next release. func RunRemove(ctx context.Context, dockerCli command.Cli, opts options.Remove) error { apiClient := dockerCli.Client() diff --git a/cli/command/stack/swarm/services.go b/cli/command/stack/swarm/services.go index e75d3ccf8c80..4a2eed34f5c1 100644 --- a/cli/command/stack/swarm/services.go +++ b/cli/command/stack/swarm/services.go @@ -10,10 +10,12 @@ import ( ) // GetServices is the swarm implementation of listing stack services -func GetServices(ctx context.Context, dockerCli command.Cli, opts options.Services) ([]swarm.Service, error) { +// +// Deprecated: this function was for internal use and will be removed in the next release. +func GetServices(ctx context.Context, dockerCLI command.Cli, opts options.Services) ([]swarm.Service, error) { var ( - err error - client = dockerCli.Client() + err error + apiClient = dockerCLI.Client() ) listOpts := swarm.ServiceListOptions{ @@ -25,7 +27,7 @@ func GetServices(ctx context.Context, dockerCli command.Cli, opts options.Servic Status: !opts.Quiet, } - services, err := client.ServiceList(ctx, listOpts) + services, err := apiClient.ServiceList(ctx, listOpts) if err != nil { return nil, err } @@ -43,7 +45,7 @@ func GetServices(ctx context.Context, dockerCli command.Cli, opts options.Servic // situations where the client uses the "default" version. To account for // these situations, we do a quick check for services that do not have // a ServiceStatus set, and perform a lookup for those. - services, err = service.AppendServiceStatus(ctx, client, services) + services, err = service.AppendServiceStatus(ctx, apiClient, services) if err != nil { return nil, err } diff --git a/cli/command/swarm/ca.go b/cli/command/swarm/ca.go index a648d3e83308..7155f7e11e1f 100644 --- a/cli/command/swarm/ca.go +++ b/cli/command/swarm/ca.go @@ -8,7 +8,6 @@ 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/swarm/progress" "github.com/docker/cli/internal/jsonstream" "github.com/docker/docker/api/types/swarm" @@ -40,7 +39,7 @@ func newCACommand(dockerCli command.Cli) *cobra.Command { "version": "1.30", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -54,10 +53,10 @@ func newCACommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts caOptions) error { - client := dockerCli.Client() +func runCA(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts caOptions) error { + apiClient := dockerCLI.Client() - swarmInspect, err := client.SwarmInspect(ctx) + swarmInspect, err := apiClient.SwarmInspect(ctx) if err != nil { return err } @@ -68,7 +67,7 @@ func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opt return fmt.Errorf("`--%s` flag requires the `--rotate` flag to update the CA", f) } } - return displayTrustRoot(dockerCli.Out(), swarmInspect) + return displayTrustRoot(dockerCLI.Out(), swarmInspect) } if flags.Changed(flagExternalCA) && len(opts.externalCA.Value()) > 0 && !flags.Changed(flagCACert) { @@ -83,14 +82,14 @@ func runCA(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opt } updateSwarmSpec(&swarmInspect.Spec, flags, opts) - if err := client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil { + if err := apiClient.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, swarm.UpdateFlags{}); err != nil { return err } if opts.detach { return nil } - return attach(ctx, dockerCli, opts) + return attach(ctx, dockerCLI, opts) } func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) { @@ -106,13 +105,13 @@ func updateSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet, opts caOptions) { } } -func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { - client := dockerCli.Client() +func attach(ctx context.Context, dockerCLI command.Cli, opts caOptions) error { + apiClient := dockerCLI.Client() errChan := make(chan error, 1) pipeReader, pipeWriter := io.Pipe() go func() { - errChan <- progress.RootRotationProgress(ctx, client, pipeWriter) + errChan <- progress.RootRotationProgress(ctx, apiClient, pipeWriter) }() if opts.quiet { @@ -120,7 +119,7 @@ func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { return <-errChan } - err := jsonstream.Display(ctx, pipeReader, dockerCli.Out()) + err := jsonstream.Display(ctx, pipeReader, dockerCLI.Out()) if err == nil { err = <-errChan } @@ -128,17 +127,17 @@ func attach(ctx context.Context, dockerCli command.Cli, opts caOptions) error { return err } - swarmInspect, err := client.SwarmInspect(ctx) + swarmInspect, err := apiClient.SwarmInspect(ctx) if err != nil { return err } - return displayTrustRoot(dockerCli.Out(), swarmInspect) + return displayTrustRoot(dockerCLI.Out(), swarmInspect) } func displayTrustRoot(out io.Writer, info swarm.Swarm) error { if info.ClusterInfo.TLSInfo.TrustRoot == "" { return errors.New("No CA information available") } - fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot)) + _, _ = fmt.Fprintln(out, strings.TrimSpace(info.ClusterInfo.TLSInfo.TrustRoot)) return nil } diff --git a/cli/command/swarm/cmd.go b/cli/command/swarm/cmd.go index e78e33d003ec..abce56982ff3 100644 --- a/cli/command/swarm/cmd.go +++ b/cli/command/swarm/cmd.go @@ -8,26 +8,33 @@ import ( ) // NewSwarmCommand returns a cobra command for `swarm` subcommands -func NewSwarmCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewSwarmCommand(dockerCLI command.Cli) *cobra.Command { + return newSwarmCommand(dockerCLI) +} + +// newSwarmCommand returns a cobra command for `swarm` subcommands +func newSwarmCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "swarm", Short: "Manage Swarm", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{ "version": "1.24", "swarm": "", // swarm command itself does not require swarm to be enabled (so swarm init and join is always available on API 1.24 and up) }, } cmd.AddCommand( - newInitCommand(dockerCli), - newJoinCommand(dockerCli), - newJoinTokenCommand(dockerCli), - newUnlockKeyCommand(dockerCli), - newUpdateCommand(dockerCli), - newLeaveCommand(dockerCli), - newUnlockCommand(dockerCli), - newCACommand(dockerCli), + newInitCommand(dockerCLI), + newJoinCommand(dockerCLI), + newJoinTokenCommand(dockerCLI), + newUnlockKeyCommand(dockerCLI), + newUpdateCommand(dockerCLI), + newLeaveCommand(dockerCLI), + newUnlockCommand(dockerCLI), + newCACommand(dockerCLI), ) return cmd } diff --git a/cli/command/swarm/init.go b/cli/command/swarm/init.go index d61aab755bd0..f779787a02d9 100644 --- a/cli/command/swarm/init.go +++ b/cli/command/swarm/init.go @@ -8,7 +8,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -44,7 +43,7 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "", // swarm init does not require swarm to be active, and is always available on API 1.24 and up }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/swarm/join.go b/cli/command/swarm/join.go index 3e497e7f1f60..4da2f8edbc3f 100644 --- a/cli/command/swarm/join.go +++ b/cli/command/swarm/join.go @@ -52,8 +52,8 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runJoin(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) error { - client := dockerCli.Client() +func runJoin(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts joinOptions) error { + apiClient := dockerCLI.Client() req := swarm.JoinRequest{ JoinToken: opts.token, @@ -72,20 +72,20 @@ func runJoin(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, o } } - err := client.SwarmJoin(ctx, req) + err := apiClient.SwarmJoin(ctx, req) if err != nil { return err } - info, err := client.Info(ctx) + info, err := apiClient.Info(ctx) if err != nil { return err } if info.Swarm.ControlAvailable { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "This node joined a swarm as a manager.") } else { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "This node joined a swarm as a worker.") } return nil } diff --git a/cli/command/swarm/leave.go b/cli/command/swarm/leave.go index 9d7385215825..335dbb0042e5 100644 --- a/cli/command/swarm/leave.go +++ b/cli/command/swarm/leave.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/spf13/cobra" ) @@ -28,7 +27,7 @@ func newLeaveCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "active", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -36,13 +35,13 @@ func newLeaveCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runLeave(ctx context.Context, dockerCli command.Cli, opts leaveOptions) error { - client := dockerCli.Client() +func runLeave(ctx context.Context, dockerCLI command.Cli, opts leaveOptions) error { + apiClient := dockerCLI.Client() - if err := client.SwarmLeave(ctx, opts.force); err != nil { + if err := apiClient.SwarmLeave(ctx, opts.force); err != nil { return err } - fmt.Fprintln(dockerCli.Out(), "Node left the swarm.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "Node left the swarm.") return nil } diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go index 420482ee1eda..8f5980b00932 100644 --- a/cli/command/swarm/unlock.go +++ b/cli/command/swarm/unlock.go @@ -9,7 +9,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" @@ -29,18 +28,18 @@ func newUnlockCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } return cmd } -func runUnlock(ctx context.Context, dockerCli command.Cli) error { - client := dockerCli.Client() +func runUnlock(ctx context.Context, dockerCLI command.Cli) error { + apiClient := dockerCLI.Client() // First see if the node is actually part of a swarm, and if it is actually locked first. // If it's in any other state than locked, don't ask for the key. - info, err := client.Info(ctx) + info, err := apiClient.Info(ctx) if err != nil { return err } @@ -54,12 +53,12 @@ func runUnlock(ctx context.Context, dockerCli command.Cli) error { return errors.New("Error: swarm is not locked") } - key, err := readKey(dockerCli.In(), "Enter unlock key: ") + key, err := readKey(dockerCLI.In(), "Enter unlock key: ") if err != nil { return err } - return client.SwarmUnlock(ctx, swarm.UnlockRequest{ + return apiClient.SwarmUnlock(ctx, swarm.UnlockRequest{ UnlockKey: key, }) } diff --git a/cli/command/swarm/unlock_key.go b/cli/command/swarm/unlock_key.go index af62e0b09a95..a8cd82c5938c 100644 --- a/cli/command/swarm/unlock_key.go +++ b/cli/command/swarm/unlock_key.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -32,7 +31,7 @@ func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/swarm/update.go b/cli/command/swarm/update.go index 2e853a9312a0..f90041d39cc5 100644 --- a/cli/command/swarm/update.go +++ b/cli/command/swarm/update.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -33,7 +32,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { "version": "1.24", "swarm": "manager", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") @@ -41,12 +40,12 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { - client := dockerCli.Client() +func runUpdate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { + apiClient := dockerCLI.Client() var updateFlags swarm.UpdateFlags - swarmInspect, err := client.SwarmInspect(ctx) + swarmInspect, err := apiClient.SwarmInspect(ctx) if err != nil { return err } @@ -57,19 +56,19 @@ func runUpdate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) + err = apiClient.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) if err != nil { return err } - fmt.Fprintln(dockerCli.Out(), "Swarm updated.") + _, _ = fmt.Fprintln(dockerCLI.Out(), "Swarm updated.") if curAutoLock && !prevAutoLock { - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + unlockKeyResp, err := apiClient.SwarmGetUnlockKey(ctx) if err != nil { return errors.Wrap(err, "could not fetch unlock key") } - printUnlockCommand(dockerCli.Out(), unlockKeyResp.UnlockKey) + printUnlockCommand(dockerCLI.Out(), unlockKeyResp.UnlockKey) } return nil diff --git a/cli/command/system/cmd.go b/cli/command/system/cmd.go index 6accb98f0c4e..2d0dcf441d11 100644 --- a/cli/command/system/cmd.go +++ b/cli/command/system/cmd.go @@ -7,19 +7,26 @@ import ( ) // NewSystemCommand returns a cobra command for `system` subcommands -func NewSystemCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewSystemCommand(dockerCLI command.Cli) *cobra.Command { + return newSystemCommand(dockerCLI) +} + +// newSystemCommand returns a cobra command for `system` subcommands +func newSystemCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "system", Short: "Manage Docker", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), } cmd.AddCommand( - NewEventsCommand(dockerCli), - NewInfoCommand(dockerCli), - newDiskUsageCommand(dockerCli), - newPruneCommand(dockerCli), - newDialStdioCommand(dockerCli), + newEventsCommand(dockerCLI), + newInfoCommand(dockerCLI), + newDiskUsageCommand(dockerCLI), + newPruneCommand(dockerCLI), + newDialStdioCommand(dockerCLI), ) return cmd diff --git a/cli/command/system/completion_test.go b/cli/command/system/completion_test.go index 3e703a6a49b2..7872a7a58256 100644 --- a/cli/command/system/completion_test.go +++ b/cli/command/system/completion_test.go @@ -156,7 +156,7 @@ func TestCompleteEventFilter(t *testing.T) { for _, tc := range tests { cli := test.NewFakeCli(tc.client) - completions, directive := completeEventFilters(cli)(NewEventsCommand(cli), nil, tc.toComplete) + completions, directive := completeEventFilters(cli)(newEventsCommand(cli), nil, tc.toComplete) assert.DeepEqual(t, completions, tc.expected) assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp, fmt.Sprintf("wrong directive in completion for '%s'", tc.toComplete)) diff --git a/cli/command/system/df.go b/cli/command/system/df.go index f6a98a036776..cb040049fa53 100644 --- a/cli/command/system/df.go +++ b/cli/command/system/df.go @@ -5,7 +5,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/docker/api/types" @@ -29,7 +28,7 @@ func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command { return runDiskUsage(cmd.Context(), dockerCli, opts) }, Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/system/dial_stdio.go b/cli/command/system/dial_stdio.go index c8ccd181c794..fc1cbee27a73 100644 --- a/cli/command/system/dial_stdio.go +++ b/cli/command/system/dial_stdio.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -23,7 +22,7 @@ func newDialStdioCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDialStdio(cmd.Context(), dockerCli) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } return cmd } diff --git a/cli/command/system/events.go b/cli/command/system/events.go index d83d36351e33..f8d2e69fe11a 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -11,7 +11,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -28,7 +27,14 @@ type eventsOptions struct { } // NewEventsCommand creates a new cobra.Command for `docker events` -func NewEventsCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewEventsCommand(dockerCLI command.Cli) *cobra.Command { + return newEventsCommand(dockerCLI) +} + +// newEventsCommand creates a new cobra.Command for `docker events` +func newEventsCommand(dockerCLI command.Cli) *cobra.Command { options := eventsOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -36,12 +42,12 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { Short: "Get real time events from the server", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runEvents(cmd.Context(), dockerCli, &options) + return runEvents(cmd.Context(), dockerCLI, &options) }, Annotations: map[string]string{ "aliases": "docker system events, docker events", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -50,12 +56,12 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command { flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now. - _ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCli)) + _ = cmd.RegisterFlagCompletionFunc("filter", completeEventFilters(dockerCLI)) return cmd } -func runEvents(ctx context.Context, dockerCli command.Cli, options *eventsOptions) error { +func runEvents(ctx context.Context, dockerCLI command.Cli, options *eventsOptions) error { tmpl, err := makeTemplate(options.format) if err != nil { return cli.StatusError{ @@ -64,14 +70,14 @@ func runEvents(ctx context.Context, dockerCli command.Cli, options *eventsOption } } ctx, cancel := context.WithCancel(ctx) - evts, errs := dockerCli.Client().Events(ctx, events.ListOptions{ + evts, errs := dockerCLI.Client().Events(ctx, events.ListOptions{ Since: options.since, Until: options.until, Filters: options.filter.Value(), }) defer cancel() - out := dockerCli.Out() + out := dockerCLI.Out() for { select { diff --git a/cli/command/system/events_test.go b/cli/command/system/events_test.go index 847605a44860..b31ed3098a4d 100644 --- a/cli/command/system/events_test.go +++ b/cli/command/system/events_test.go @@ -18,9 +18,6 @@ func TestEventsFormat(t *testing.T) { var evts []events.Message //nolint:prealloc for i, action := range []events.Action{events.ActionCreate, events.ActionStart, events.ActionAttach, events.ActionDie} { evts = append(evts, events.Message{ - Status: string(action), - ID: "abc123", - From: "ubuntu:latest", Type: events.ContainerEventType, Action: action, Actor: events.Actor{ @@ -70,7 +67,7 @@ func TestEventsFormat(t *testing.T) { }() return messages, errs }}) - cmd := NewEventsCommand(cli) + cmd := newEventsCommand(cli) cmd.SetArgs(tc.args) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 7f82e4e5d85f..780d9b4dd0b8 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -14,16 +14,15 @@ import ( "github.com/docker/cli/cli" pluginmanager "github.com/docker/cli/cli-plugins/manager" "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/debug" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/internal/lazyregexp" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/templates" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" - "github.com/docker/docker/registry" "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -60,7 +59,14 @@ func (i *dockerInfo) clientPlatform() string { } // NewInfoCommand creates a new cobra.Command for `docker info` -func NewInfoCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewInfoCommand(dockerCLI command.Cli) *cobra.Command { + return newInfoCommand(dockerCLI) +} + +// newInfoCommand creates a new cobra.Command for `docker info` +func newInfoCommand(dockerCLI command.Cli) *cobra.Command { var opts infoOptions cmd := &cobra.Command{ @@ -68,13 +74,13 @@ func NewInfoCommand(dockerCli command.Cli) *cobra.Command { Short: "Display system-wide information", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runInfo(cmd.Context(), cmd, dockerCli, &opts) + return runInfo(cmd.Context(), cmd, dockerCLI, &opts) }, Annotations: map[string]string{ "category-top": "12", "aliases": "docker system info, docker info", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -162,7 +168,7 @@ func needsServerInfo(template string, info dockerInfo) bool { } // A template is provided and has at least one field set. - tmpl, err := templates.NewParse("", template) + tmpl, err := templates.Parse(template) if err != nil { // ignore parsing errors here, and let regular code handle them return true diff --git a/cli/command/system/info_test.go b/cli/command/system/info_test.go index d996ea23f49a..4ad66839f04a 100644 --- a/cli/command/system/info_test.go +++ b/cli/command/system/info_test.go @@ -2,6 +2,7 @@ package system import ( "encoding/base64" + "errors" "net" "testing" "time" @@ -78,7 +79,6 @@ var sampleInfoNoSwarm = system.Info{ IndexConfigs: map[string]*registrytypes.IndexInfo{ "docker.io": { Name: "docker.io", - Mirrors: nil, Secure: true, Official: true, }, @@ -109,16 +109,13 @@ var sampleInfoNoSwarm = system.Info{ Isolation: "", InitBinary: "docker-init", ContainerdCommit: system.Commit{ - ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", - Expected: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", + ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170", }, RuncCommit: system.Commit{ - ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", - Expected: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", + ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2", }, InitCommit: system.Commit{ - ID: "949e6fa", - Expected: "949e6fa", + ID: "949e6fa", }, SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"}, DefaultAddressPools: []system.NetworkAddressPool{ @@ -221,7 +218,7 @@ var samplePluginsInfo = []pluginmanager.Plugin{ { Name: "badplugin", Path: "/path/to/docker-badplugin", - Err: pluginmanager.NewPluginError("something wrong"), + Err: errors.New("something wrong"), }, } diff --git a/cli/command/system/inspect.go b/cli/command/system/inspect.go index 0afe65e41108..ef79831cd9af 100644 --- a/cli/command/system/inspect.go +++ b/cli/command/system/inspect.go @@ -9,7 +9,7 @@ import ( "fmt" "strings" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" @@ -60,7 +60,14 @@ type inspectOptions struct { } // NewInspectCommand creates a new cobra.Command for `docker inspect` -func NewInspectCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewInspectCommand(dockerCLI command.Cli) *cobra.Command { + return newInspectCommand(dockerCLI) +} + +// newInspectCommand creates a new cobra.Command for `docker inspect` +func newInspectCommand(dockerCLI command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -72,10 +79,10 @@ func NewInspectCommand(dockerCli command.Cli) *cobra.Command { if cmd.Flags().Changed("type") && opts.objectType == "" { return fmt.Errorf(`type is empty: must be one of "%s"`, strings.Join(allTypes, `", "`)) } - return runInspect(cmd.Context(), dockerCli, opts) + return runInspect(cmd.Context(), dockerCLI, opts) }, // TODO(thaJeztah): should we consider adding completion for common object-types? (images, containers?) - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -88,7 +95,7 @@ func NewInspectCommand(dockerCli command.Cli) *cobra.Command { // Set a default completion function if none was set. We don't look // up if it does already have one set, because Cobra does this for // us, and returns an error (which we ignore for this reason). - _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + _ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions) }) return cmd } @@ -279,7 +286,7 @@ func inspectAll(ctx context.Context, dockerCLI command.Cli, getSize bool, typeCo } func isErrSkippable(err error) bool { - return cerrdefs.IsNotFound(err) || + return errdefs.IsNotFound(err) || strings.Contains(err.Error(), "not supported") || strings.Contains(err.Error(), "invalid reference format") } diff --git a/cli/command/system/inspect_test.go b/cli/command/system/inspect_test.go index 56ad5fd5b588..3fbadc7f6d0f 100644 --- a/cli/command/system/inspect_test.go +++ b/cli/command/system/inspect_test.go @@ -32,7 +32,7 @@ func TestInspectValidateFlagsAndArgs(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - cmd := NewInspectCommand(test.NewFakeCli(&fakeClient{})) + cmd := newInspectCommand(test.NewFakeCli(&fakeClient{})) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) cmd.SetArgs(tc.args) diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index a1bcb5f2241b..a5ddde4a4a0e 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/builder" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/container" "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/network" @@ -45,7 +44,7 @@ func newPruneCommand(dockerCli command.Cli) *cobra.Command { return runPrune(cmd.Context(), dockerCli, options) }, Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/system/testdata/docker-events-json-template.golden b/cli/command/system/testdata/docker-events-json-template.golden index ec5343fe682e..1a0427163cf0 100644 --- a/cli/command/system/testdata/docker-events-json-template.golden +++ b/cli/command/system/testdata/docker-events-json-template.golden @@ -1,4 +1,4 @@ -{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} -{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} -{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} -{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} +{"Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/cli/command/system/testdata/docker-events-json.golden b/cli/command/system/testdata/docker-events-json.golden index ec5343fe682e..1a0427163cf0 100644 --- a/cli/command/system/testdata/docker-events-json.golden +++ b/cli/command/system/testdata/docker-events-json.golden @@ -1,4 +1,4 @@ -{"status":"create","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} -{"status":"start","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} -{"status":"attach","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} -{"status":"die","id":"abc123","from":"ubuntu:latest","Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} +{"Type":"container","Action":"create","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":1000000000,"timeNano":1000000000} +{"Type":"container","Action":"start","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":2000000000,"timeNano":2000000000} +{"Type":"container","Action":"attach","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":3000000000,"timeNano":3000000000} +{"Type":"container","Action":"die","Actor":{"ID":"abc123","Attributes":{"image":"ubuntu:latest"}},"scope":"local","time":4000000000,"timeNano":4000000000} diff --git a/cli/command/system/testdata/docker-info-badsec.json.golden b/cli/command/system/testdata/docker-info-badsec.json.golden index c20a427f208d..f385cac102e7 100644 --- a/cli/command/system/testdata/docker-info-badsec.json.golden +++ b/cli/command/system/testdata/docker-info-badsec.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["foo="],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ServerErrors":["a server error occurred"],"ClientInfo":{"Debug":false,"Context":"","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden index cdc36eb5dfa8..407404fefe05 100644 --- a/cli/command/system/testdata/docker-info-daemon-warnings.json.golden +++ b/cli/command/system/testdata/docker-info-daemon-warnings.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":["WARNING: No memory limit support","WARNING: No swap limit support","WARNING: No oom kill disable support","WARNING: No cpu cfs quota support","WARNING: No cpu cfs period support","WARNING: No cpu shares support","WARNING: No cpuset support","WARNING: IPv4 forwarding is disabled"],"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-no-swarm.json.golden b/cli/command/system/testdata/docker-info-no-swarm.json.golden index 7fc58311e8c1..4ad0bad91ea1 100644 --- a/cli/command/system/testdata/docker-info-no-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-no-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":true,"Platform":{"Name":"Docker Engine - Community"},"Version":"24.0.0","Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-plugins.json.golden b/cli/command/system/testdata/docker-info-plugins.json.golden index d7e39ee90eba..acd05043f49e 100644 --- a/cli/command/system/testdata/docker-info-plugins.json.golden +++ b/cli/command/system/testdata/docker-info-plugins.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","Version":"0.1.0","ShortDescription":"unit test is good","Name":"goodplugin","Path":"/path/to/docker-goodplugin"},{"SchemaVersion":"0.1.0","Vendor":"ACME Corp","ShortDescription":"this plugin has no version","Name":"unversionedplugin","Path":"/path/to/docker-unversionedplugin"},{"Name":"badplugin","Path":"/path/to/docker-badplugin","Err":"something wrong"}],"Warnings":null}} diff --git a/cli/command/system/testdata/docker-info-with-devices.json.golden b/cli/command/system/testdata/docker-info-with-devices.json.golden index c2fa190b03b9..30ab878c2aa5 100644 --- a/cli/command/system/testdata/docker-info-with-devices.json.golden +++ b/cli/command/system/testdata/docker-info-with-devices.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"Warnings":null} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"","NodeAddr":"","LocalNodeState":"inactive","ControlAvailable":false,"Error":"","RemoteManagers":null},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"DiscoveredDevices":[{"Source":"cdi","ID":"com.example.device1"},{"Source":"cdi","ID":"nvidia.com/gpu=gpu0"}],"Warnings":null} diff --git a/cli/command/system/testdata/docker-info-with-swarm.json.golden b/cli/command/system/testdata/docker-info-with-swarm.json.golden index 3351600a6f16..3de5f1388dec 100644 --- a/cli/command/system/testdata/docker-info-with-swarm.json.golden +++ b/cli/command/system/testdata/docker-info-with-swarm.json.golden @@ -1 +1 @@ -{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170","Expected":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2","Expected":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa","Expected":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} +{"ID":"EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"overlay2","DriverStatus":[["Backing Filesystem","extfs"],["Supports d_type","true"],["Using metacopy","false"],["Native Overlay Diff","true"]],"Plugins":{"Volume":["local"],"Network":["bridge","host","macvlan","null","overlay"],"Authorization":null,"Log":["awslogs","fluentd","gcplogs","gelf","journald","json-file","splunk","syslog"]},"MemoryLimit":true,"SwapLimit":true,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"PidsLimit":false,"IPv4Forwarding":true,"Debug":true,"NFd":33,"OomKillDisable":true,"NGoroutines":135,"SystemTime":"2017-08-24T17:44:34.077811894Z","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"4.4.0-87-generic","OperatingSystem":"Ubuntu 16.04.3 LTS","OSVersion":"","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"IndexConfigs":{"docker.io":{"Mirrors":null,"Name":"docker.io","Official":true,"Secure":true}},"InsecureRegistryCIDRs":["127.0.0.0/8"],"Mirrors":null},"NCPU":2,"MemTotal":2097356800,"GenericResources":null,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"system-sample","Labels":["provider=digitalocean"],"ExperimentalBuild":false,"ServerVersion":"17.06.1-ce","Runtimes":{"runc":{"path":"docker-runc"}},"DefaultRuntime":"runc","Swarm":{"NodeID":"qo2dfdig9mmxqkawulggepdih","NodeAddr":"165.227.107.89","LocalNodeState":"active","ControlAvailable":true,"Error":"","RemoteManagers":[{"NodeID":"qo2dfdig9mmxqkawulggepdih","Addr":"165.227.107.89:2377"}],"Nodes":1,"Managers":1,"Cluster":{"ID":"9vs5ygs0gguyyec4iqf2314c0","Version":{"Index":11},"CreatedAt":"2017-08-24T17:34:19.278062352Z","UpdatedAt":"2017-08-24T17:34:42.398815481Z","Spec":{"Name":"default","Labels":null,"Orchestration":{"TaskHistoryRetentionLimit":5},"Raft":{"SnapshotInterval":10000,"KeepOldSnapshots":0,"LogEntriesForSlowFollowers":500,"ElectionTick":3,"HeartbeatTick":1},"Dispatcher":{"HeartbeatPeriod":5000000000},"CAConfig":{"NodeCertExpiry":7776000000000000},"TaskDefaults":{},"EncryptionConfig":{"AutoLockManagers":true}},"TLSInfo":{"TrustRoot":"\n-----BEGIN CERTIFICATE-----\nMIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw\nEzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy\nOTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH\nA0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW\nUfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB\nAf8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO\nPQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH\n1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8\n-----END CERTIFICATE-----\n","CertIssuerSubject":"MBMxETAPBgNVBAMTCHN3YXJtLWNh","CertIssuerPublicKey":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="},"RootRotationInProgress":false,"DefaultAddrPool":null,"SubnetSize":0,"DataPathPort":0}},"LiveRestoreEnabled":false,"Isolation":"","InitBinary":"docker-init","ContainerdCommit":{"ID":"6e23458c129b551d5c9871e5174f6b1b7f6d1170"},"RuncCommit":{"ID":"810190ceaa507aa2727d7ae6f4790c76ec150bd2"},"InitCommit":{"ID":"949e6fa"},"SecurityOptions":["name=apparmor","name=seccomp,profile=default"],"DefaultAddressPools":[{"Base":"10.123.0.0/16","Size":24}],"CDISpecDirs":["/etc/cdi","/var/run/cdi"],"Warnings":null,"ClientInfo":{"Debug":false,"Context":"default","Plugins":[],"Warnings":null}} diff --git a/cli/command/system/version.go b/cli/command/system/version.go index 3a0ad75c351e..b9d57618b3e4 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -10,7 +10,6 @@ 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/formatter/tabwriter" flagsHelper "github.com/docker/cli/cli/flags" @@ -109,7 +108,14 @@ func newClientVersion(contextName string, dockerCli command.Cli) clientVersion { } // NewVersionCommand creates a new cobra.Command for `docker version` -func NewVersionCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewVersionCommand(dockerCLI command.Cli) *cobra.Command { + return newVersionCommand(dockerCLI) +} + +// newVersionCommand creates a new cobra.Command for `docker version` +func newVersionCommand(dockerCLI command.Cli) *cobra.Command { var opts versionOptions cmd := &cobra.Command{ @@ -117,12 +123,12 @@ func NewVersionCommand(dockerCli command.Cli) *cobra.Command { Short: "Show the Docker version information", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - return runVersion(cmd.Context(), dockerCli, &opts) + return runVersion(cmd.Context(), dockerCLI, &opts) }, Annotations: map[string]string{ "category-top": "10", }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) @@ -209,8 +215,7 @@ func newVersionTemplate(templateFormat string) (*template.Template, error) { case formatter.JSONFormatKey: templateFormat = formatter.JSONFormat } - tmpl := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}) - tmpl, err := tmpl.Parse(templateFormat) + tmpl, err := templates.New("version").Funcs(template.FuncMap{"getDetailsOrder": getDetailsOrder}).Parse(templateFormat) if err != nil { return nil, errors.Wrap(err, "template parsing error") } diff --git a/cli/command/system/version_test.go b/cli/command/system/version_test.go index 6f8b243de4b4..7b060e843720 100644 --- a/cli/command/system/version_test.go +++ b/cli/command/system/version_test.go @@ -20,7 +20,7 @@ func TestVersionWithoutServer(t *testing.T) { return types.Version{}, errors.New("no server") }, }) - cmd := NewVersionCommand(cli) + cmd := newVersionCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(cli.Err()) cmd.SetErr(io.Discard) diff --git a/cli/command/task/formatter.go b/cli/command/task/formatter.go index b87ab3a4b715..aba09f9d1928 100644 --- a/cli/command/task/formatter.go +++ b/cli/command/task/formatter.go @@ -23,7 +23,14 @@ const ( ) // NewTaskFormat returns a Format for rendering using a task Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewTaskFormat(source string, quiet bool) formatter.Format { + return newTaskFormat(source, quiet) +} + +// newTaskFormat returns a Format for rendering using a taskContext. +func newTaskFormat(source string, quiet bool) formatter.Format { switch source { case formatter.TableFormatKey: if quiet { @@ -40,10 +47,17 @@ func NewTaskFormat(source string, quiet bool) formatter.Format { } // FormatWrite writes the context -func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func FormatWrite(fmtCtx formatter.Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { + return formatWrite(fmtCtx, tasks, names, nodes) +} + +// formatWrite writes the context. +func formatWrite(fmtCtx formatter.Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { render := func(format func(subContext formatter.SubContext) error) error { for _, task := range tasks { - taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} + taskCtx := &taskContext{trunc: fmtCtx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} if err := format(taskCtx); err != nil { return err } @@ -61,7 +75,7 @@ func FormatWrite(ctx formatter.Context, tasks []swarm.Task, names map[string]str "Error": formatter.ErrorHeader, "Ports": formatter.PortsHeader, } - return ctx.Write(&taskCtx, render) + return fmtCtx.Write(&taskCtx, render) } type taskContext struct { diff --git a/cli/command/task/formatter_test.go b/cli/command/task/formatter_test.go index c857c64bb4eb..4d7ec9ee99b7 100644 --- a/cli/command/task/formatter_test.go +++ b/cli/command/task/formatter_test.go @@ -27,30 +27,30 @@ func TestTaskContextWrite(t *testing.T) { `template parsing error: template: :1:2: executing "" at : nil is not a command`, }, { - formatter.Context{Format: NewTaskFormat("table", true)}, + formatter.Context{Format: newTaskFormat("table", true)}, `taskID1 taskID2 `, }, { - formatter.Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, + formatter.Context{Format: newTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, string(golden.Get(t, "task-context-write-table-custom.golden")), }, { - formatter.Context{Format: NewTaskFormat("table {{.Name}}", true)}, + formatter.Context{Format: newTaskFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar `, }, { - formatter.Context{Format: NewTaskFormat("raw", true)}, + formatter.Context{Format: newTaskFormat("raw", true)}, `id: taskID1 id: taskID2 `, }, { - formatter.Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, + formatter.Context{Format: newTaskFormat("{{.Name}} {{.Node}}", false)}, `foobar_baz foo1 foobar_bar foo2 `, @@ -75,7 +75,7 @@ foobar_bar foo2 var out bytes.Buffer tc.context.Output = &out - if err := FormatWrite(tc.context, tasks, names, nodes); err != nil { + if err := formatWrite(tc.context, tasks, names, nodes); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) @@ -94,7 +94,7 @@ func TestTaskContextWriteJSONField(t *testing.T) { "taskID2": "foobar_bar", } out := bytes.NewBufferString("") - err := FormatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) + err := formatWrite(formatter.Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) if err != nil { t.Fatal(err) } diff --git a/cli/command/task/print.go b/cli/command/task/print.go index 4c9b362a2fb3..90b6bb967cbe 100644 --- a/cli/command/task/print.go +++ b/cli/command/task/print.go @@ -50,7 +50,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol tasksCtx := formatter.Context{ Output: dockerCli.Out(), - Format: NewTaskFormat(format, quiet), + Format: newTaskFormat(format, quiet), Trunc: trunc, } @@ -75,7 +75,7 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks []swarm.Task, resol nodes[task.ID] = nodeValue } - return FormatWrite(tasksCtx, tasks, names, nodes) + return formatWrite(tasksCtx, tasks, names, nodes) } // generateTaskNames generates names for the given tasks, and returns a copy of diff --git a/cli/command/trust.go b/cli/command/trust.go deleted file mode 100644 index 65f2408585d8..000000000000 --- a/cli/command/trust.go +++ /dev/null @@ -1,15 +0,0 @@ -package command - -import ( - "github.com/spf13/pflag" -) - -// AddTrustVerificationFlags adds content trust flags to the provided flagset -func AddTrustVerificationFlags(fs *pflag.FlagSet, v *bool, trusted bool) { - fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image verification") -} - -// AddTrustSigningFlags adds "signing" flags to the provided flagset -func AddTrustSigningFlags(fs *pflag.FlagSet, v *bool, trusted bool) { - fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image signing") -} diff --git a/cli/command/trust/cmd.go b/cli/command/trust/cmd.go index bb6ceace042b..cc0cd6769ffb 100644 --- a/cli/command/trust/cmd.go +++ b/cli/command/trust/cmd.go @@ -7,19 +7,25 @@ import ( ) // NewTrustCommand returns a cobra command for `trust` subcommands -func NewTrustCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewTrustCommand(dockerCLI command.Cli) *cobra.Command { + return newTrustCommand(dockerCLI) +} + +func newTrustCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "trust", Short: "Manage trust on Docker images", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), } cmd.AddCommand( - newRevokeCommand(dockerCli), - newSignCommand(dockerCli), - newTrustKeyCommand(dockerCli), - newTrustSignerCommand(dockerCli), - newInspectCommand(dockerCli), + newRevokeCommand(dockerCLI), + newSignCommand(dockerCLI), + newTrustKeyCommand(dockerCLI), + newTrustSignerCommand(dockerCLI), + newInspectCommand(dockerCLI), ) return cmd } diff --git a/cli/command/trust/common.go b/cli/command/trust/common.go index 7d1d50e29688..ab8a39c06507 100644 --- a/cli/command/trust/common.go +++ b/cli/command/trust/common.go @@ -8,8 +8,9 @@ import ( "strings" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" + "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/trust" + registrytypes "github.com/docker/docker/api/types/registry" "github.com/fvbommel/sortorder" "github.com/sirupsen/logrus" "github.com/theupdateframework/notary" @@ -66,7 +67,7 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth, a // lookupTrustInfo returns processed signature and role information about a notary repository. // This information is to be pretty printed or serialized into a machine-readable format. func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), remote) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(cli), remote) if err != nil { return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err } @@ -167,3 +168,10 @@ func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagR }) return signatureRows } + +// authResolver returns an auth resolver function from a [config.Provider]. +func authResolver(dockerCLI config.Provider) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig { + return command.ResolveAuthConfig(dockerCLI.ConfigFile(), index) + } +} diff --git a/cli/command/trust/formatter.go b/cli/command/trust/formatter.go index 9597cfbd7697..e536995fd711 100644 --- a/cli/command/trust/formatter.go +++ b/cli/command/trust/formatter.go @@ -21,7 +21,15 @@ const ( // Name: name of the signed tag // Digest: hex encoded digest of the contents // Signers: list of entities who signed the tag -type SignedTagInfo struct { +// +// Deprecated: this type was only used internally and will be removed in the next release. +type SignedTagInfo = signedTagInfo + +// signedTagInfo represents all formatted information needed to describe a signed tag: +// Name: name of the signed tag +// Digest: hex encoded digest of the contents +// Signers: list of entities who signed the tag +type signedTagInfo struct { Name string Digest string Signers []string @@ -30,23 +38,41 @@ type SignedTagInfo struct { // SignerInfo represents all formatted information needed to describe a signer: // Name: name of the signer role // Keys: the keys associated with the signer -type SignerInfo struct { +// +// Deprecated: this type was only used internally and will be removed in the next release. +type SignerInfo = signerInfo + +// signerInfo represents all formatted information needed to describe a signer: +// Name: name of the signer role +// Keys: the keys associated with the signer +type signerInfo struct { Name string Keys []string } // NewTrustTagFormat returns a Format for rendering using a trusted tag Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewTrustTagFormat() formatter.Format { return defaultTrustTagTableFormat } // NewSignerInfoFormat returns a Format for rendering a signer role info Context +// +// Deprecated: this function was only used internally and will be removed in the next release. func NewSignerInfoFormat() formatter.Format { return defaultSignerInfoTableFormat } // TagWrite writes the context -func TagWrite(ctx formatter.Context, signedTagInfoList []SignedTagInfo) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func TagWrite(fmtCtx formatter.Context, signedTagInfoList []signedTagInfo) error { + return tagWrite(fmtCtx, signedTagInfoList) +} + +// tagWrite writes the context +func tagWrite(fmtCtx formatter.Context, signedTagInfoList []signedTagInfo) error { render := func(format func(subContext formatter.SubContext) error) error { for _, signedTag := range signedTagInfoList { if err := format(&trustTagContext{s: signedTag}); err != nil { @@ -61,12 +87,12 @@ func TagWrite(ctx formatter.Context, signedTagInfoList []SignedTagInfo) error { "Digest": trustedDigestHeader, "Signers": signersHeader, } - return ctx.Write(&trustTagCtx, render) + return fmtCtx.Write(&trustTagCtx, render) } type trustTagContext struct { formatter.HeaderContext - s SignedTagInfo + s signedTagInfo } // SignedTag returns the name of the signed tag @@ -86,11 +112,18 @@ func (c *trustTagContext) Signers() string { } // SignerInfoWrite writes the context -func SignerInfoWrite(ctx formatter.Context, signerInfoList []SignerInfo) error { +// +// Deprecated: this function was only used internally and will be removed in the next release. +func SignerInfoWrite(fmtCtx formatter.Context, signerInfoList []signerInfo) error { + return signerInfoWrite(fmtCtx, signerInfoList) +} + +// signerInfoWrite writes the context. +func signerInfoWrite(fmtCtx formatter.Context, signerInfoList []signerInfo) error { render := func(format func(subContext formatter.SubContext) error) error { for _, signerInfo := range signerInfoList { if err := format(&signerInfoContext{ - trunc: ctx.Trunc, + trunc: fmtCtx.Trunc, s: signerInfo, }); err != nil { return err @@ -103,13 +136,13 @@ func SignerInfoWrite(ctx formatter.Context, signerInfoList []SignerInfo) error { "Signer": signerNameHeader, "Keys": keysHeader, } - return ctx.Write(&signerInfoCtx, render) + return fmtCtx.Write(&signerInfoCtx, render) } type signerInfoContext struct { formatter.HeaderContext trunc bool - s SignerInfo + s signerInfo } // Keys returns the sorted list of keys associated with the signer diff --git a/cli/command/trust/formatter_test.go b/cli/command/trust/formatter_test.go index 4c0c194f4c2a..413a2605aba4 100644 --- a/cli/command/trust/formatter_test.go +++ b/cli/command/trust/formatter_test.go @@ -23,7 +23,7 @@ func TestTrustTag(t *testing.T) { }{ { trustTagContext{ - s: SignedTagInfo{ + s: signedTagInfo{ Name: trustedTag, Digest: digest, Signers: nil, @@ -34,7 +34,7 @@ func TestTrustTag(t *testing.T) { }, { trustTagContext{ - s: SignedTagInfo{ + s: signedTagInfo{ Name: trustedTag, Digest: digest, Signers: nil, @@ -46,7 +46,7 @@ func TestTrustTag(t *testing.T) { // Empty signers makes a row with empty string { trustTagContext{ - s: SignedTagInfo{ + s: signedTagInfo{ Name: trustedTag, Digest: digest, Signers: nil, @@ -57,7 +57,7 @@ func TestTrustTag(t *testing.T) { }, { trustTagContext{ - s: SignedTagInfo{ + s: signedTagInfo{ Name: trustedTag, Digest: digest, Signers: []string{"alice", "bob", "claire"}, @@ -69,7 +69,7 @@ func TestTrustTag(t *testing.T) { // alphabetic signing on Signers { trustTagContext{ - s: SignedTagInfo{ + s: signedTagInfo{ Name: trustedTag, Digest: digest, Signers: []string{"claire", "bob", "alice"}, @@ -110,7 +110,7 @@ func TestTrustTagContextWrite(t *testing.T) { // Table Format { formatter.Context{ - Format: NewTrustTagFormat(), + Format: defaultTrustTagTableFormat, }, `SIGNED TAG DIGEST SIGNERS tag1 deadbeef alice @@ -120,7 +120,7 @@ tag3 bbbbbbbb }, } - signedTags := []SignedTagInfo{ + signedTags := []signedTagInfo{ {Name: "tag1", Digest: "deadbeef", Signers: []string{"alice"}}, {Name: "tag2", Digest: "aaaaaaaa", Signers: []string{"alice", "bob"}}, {Name: "tag3", Digest: "bbbbbbbb", Signers: []string{}}, @@ -131,7 +131,7 @@ tag3 bbbbbbbb var out bytes.Buffer tc.context.Output = &out - if err := TagWrite(tc.context, signedTags); err != nil { + if err := tagWrite(tc.context, signedTags); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) @@ -140,7 +140,7 @@ tag3 bbbbbbbb } } -// With no trust data, the TagWrite will print an empty table: +// With no trust data, the formatWrite will print an empty table: // it's up to the caller to decide whether or not to print this versus an error func TestTrustTagContextEmptyWrite(t *testing.T) { emptyCase := struct { @@ -148,16 +148,16 @@ func TestTrustTagContextEmptyWrite(t *testing.T) { expected string }{ formatter.Context{ - Format: NewTrustTagFormat(), + Format: defaultTrustTagTableFormat, }, `SIGNED TAG DIGEST SIGNERS `, } - emptySignedTags := []SignedTagInfo{} + emptySignedTags := []signedTagInfo{} out := bytes.NewBufferString("") emptyCase.context.Output = out - err := TagWrite(emptyCase.context, emptySignedTags) + err := tagWrite(emptyCase.context, emptySignedTags) assert.NilError(t, err) assert.Check(t, is.Equal(emptyCase.expected, out.String())) } @@ -168,15 +168,15 @@ func TestSignerInfoContextEmptyWrite(t *testing.T) { expected string }{ formatter.Context{ - Format: NewSignerInfoFormat(), + Format: defaultSignerInfoTableFormat, }, `SIGNER KEYS `, } - emptySignerInfo := []SignerInfo{} + emptySignerInfo := []signerInfo{} out := bytes.NewBufferString("") emptyCase.context.Output = out - err := SignerInfoWrite(emptyCase.context, emptySignerInfo) + err := signerInfoWrite(emptyCase.context, emptySignerInfo) assert.NilError(t, err) assert.Check(t, is.Equal(emptyCase.expected, out.String())) } @@ -202,7 +202,7 @@ func TestSignerInfoContextWrite(t *testing.T) { // Table Format { formatter.Context{ - Format: NewSignerInfoFormat(), + Format: defaultSignerInfoTableFormat, Trunc: true, }, `SIGNER KEYS @@ -214,7 +214,7 @@ eve foobarbazqux, key31, key32 // No truncation { formatter.Context{ - Format: NewSignerInfoFormat(), + Format: defaultSignerInfoTableFormat, }, `SIGNER KEYS alice key11, key12 @@ -224,7 +224,7 @@ eve foobarbazquxquux, key31, key32 }, } - signerInfo := []SignerInfo{ + signerInfo := []signerInfo{ {Name: "alice", Keys: []string{"key11", "key12"}}, {Name: "bob", Keys: []string{"key21"}}, {Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}}, @@ -234,7 +234,7 @@ eve foobarbazquxquux, key31, key32 var out bytes.Buffer tc.context.Output = &out - if err := SignerInfoWrite(tc.context, signerInfo); err != nil { + if err := signerInfoWrite(tc.context, signerInfo); err != nil { assert.Error(t, err, tc.expected) } else { assert.Equal(t, out.String(), tc.expected) diff --git a/cli/command/trust/inspect_pretty.go b/cli/command/trust/inspect_pretty.go index b0b5e8c51c84..3eac2cfbe827 100644 --- a/cli/command/trust/inspect_pretty.go +++ b/cli/command/trust/inspect_pretty.go @@ -56,33 +56,33 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) func printSignatures(out io.Writer, signatureRows []trustTagRow) error { trustTagCtx := formatter.Context{ Output: out, - Format: NewTrustTagFormat(), + Format: defaultTrustTagTableFormat, } // convert the formatted type before printing - formattedTags := []SignedTagInfo{} + formattedTags := []signedTagInfo{} for _, sigRow := range signatureRows { formattedSigners := sigRow.Signers if len(formattedSigners) == 0 { formattedSigners = append(formattedSigners, fmt.Sprintf("(%s)", releasedRoleName)) } - formattedTags = append(formattedTags, SignedTagInfo{ + formattedTags = append(formattedTags, signedTagInfo{ Name: sigRow.SignedTag, Digest: sigRow.Digest, Signers: formattedSigners, }) } - return TagWrite(trustTagCtx, formattedTags) + return tagWrite(trustTagCtx, formattedTags) } func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { signerInfoCtx := formatter.Context{ Output: out, - Format: NewSignerInfoFormat(), + Format: defaultSignerInfoTableFormat, Trunc: true, } - formattedSignerInfo := []SignerInfo{} + formattedSignerInfo := []signerInfo{} for name, keyIDs := range roleToKeyIDs { - formattedSignerInfo = append(formattedSignerInfo, SignerInfo{ + formattedSignerInfo = append(formattedSignerInfo, signerInfo{ Name: name, Keys: keyIDs, }) @@ -90,5 +90,5 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { sort.Slice(formattedSignerInfo, func(i, j int) bool { return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name) }) - return SignerInfoWrite(signerInfoCtx, formattedSignerInfo) + return signerInfoWrite(signerInfoCtx, formattedSignerInfo) } diff --git a/cli/command/trust/revoke.go b/cli/command/trust/revoke.go index 303c7b8c2d68..63096ada0865 100644 --- a/cli/command/trust/revoke.go +++ b/cli/command/trust/revoke.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/prompt" "github.com/pkg/errors" @@ -35,7 +34,7 @@ func newRevokeCommand(dockerCLI command.Cli) *cobra.Command { } func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, options revokeOptions) error { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), remote) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), remote) if err != nil { return err } diff --git a/cli/command/trust/sign.go b/cli/command/trust/sign.go index 077620155aee..c2ecd0fbbeb3 100644 --- a/cli/command/trust/sign.go +++ b/cli/command/trust/sign.go @@ -11,7 +11,6 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/trust" imagetypes "github.com/docker/docker/api/types/image" registrytypes "github.com/docker/docker/api/types/registry" @@ -45,7 +44,7 @@ func newSignCommand(dockerCLI command.Cli) *cobra.Command { func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOptions) error { imageName := options.imageName - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), imageName) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), imageName) if err != nil { return err } @@ -82,10 +81,6 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption return trust.NotaryError(imgRefAndAuth.RepoInfo().Name.Name(), err) } } - var requestPrivilege registrytypes.RequestAuthConfig - if dockerCLI.In().IsTerminal() { - requestPrivilege = command.RegistryAuthenticationPrivilegedFunc(dockerCLI, imgRefAndAuth.RepoInfo().Index, "push") - } target, err := createTarget(notaryRepo, imgRefAndAuth.Tag()) if err != nil || options.local { switch err := err.(type) { @@ -104,7 +99,7 @@ func runSignImage(ctx context.Context, dockerCLI command.Cli, options signOption } responseBody, err := dockerCLI.Client().ImagePush(ctx, reference.FamiliarString(imgRefAndAuth.Reference()), imagetypes.PushOptions{ RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, + PrivilegeFunc: nil, }) if err != nil { return err diff --git a/cli/command/trust/signer_add.go b/cli/command/trust/signer_add.go index 155347074190..c641d61927ee 100644 --- a/cli/command/trust/signer_add.go +++ b/cli/command/trust/signer_add.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/lazyregexp" "github.com/docker/cli/opts" @@ -80,7 +79,7 @@ func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOpti } func addSignerToRepo(ctx context.Context, dockerCLI command.Cli, signerName string, repoName string, signerPubKeys []data.PublicKey) error { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), repoName) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), repoName) if err != nil { return err } diff --git a/cli/command/trust/signer_remove.go b/cli/command/trust/signer_remove.go index 10d2c2933bad..ea9c1fe31355 100644 --- a/cli/command/trust/signer_remove.go +++ b/cli/command/trust/signer_remove.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/prompt" "github.com/pkg/errors" @@ -91,7 +90,7 @@ func maybePromptForSignerRemoval(ctx context.Context, dockerCLI command.Cli, rep // removeSingleSigner attempts to remove a single signer and returns whether signer removal happened. // The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation. func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, forceYes bool) (bool, error) { - imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), repoName) + imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), repoName) if err != nil { return false, err } diff --git a/cli/command/utils.go b/cli/command/utils.go index ab64ef8fc120..fdedc2a14fb9 100644 --- a/cli/command/utils.go +++ b/cli/command/utils.go @@ -14,30 +14,20 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/prompt" "github.com/docker/docker/api/types/filters" - "github.com/moby/sys/atomicwriter" "github.com/pkg/errors" - "github.com/spf13/pflag" ) -// CopyToFile writes the content of the reader to the specified file +// ErrPromptTerminated is returned if the user terminated the prompt. // -// Deprecated: use [atomicwriter.New]. -func CopyToFile(outfile string, r io.Reader) error { - writer, err := atomicwriter.New(outfile, 0o600) - if err != nil { - return err - } - defer writer.Close() - _, err = io.Copy(writer, r) - return err -} - +// Deprecated: this error is for internal use and will be removed in the next release. const ErrPromptTerminated = prompt.ErrTerminated // DisableInputEcho disables input echo on the provided streams.In. // This is useful when the user provides sensitive information like passwords. // The function returns a restore function that should be called to restore the // terminal state. +// +// Deprecated: this function is for internal use and will be removed in the next release. func DisableInputEcho(ins *streams.In) (restore func() error, err error) { return prompt.DisableInputEcho(ins) } @@ -49,6 +39,8 @@ func DisableInputEcho(ins *streams.In) (restore func() error, err error) { // When the prompt returns an error, the caller should propagate the error up // the stack and close the io.Reader used for the prompt which will prevent the // background goroutine from blocking indefinitely. +// +// Deprecated: this function is for internal use and will be removed in the next release. func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) { return prompt.ReadInput(ctx, in, out, message) } @@ -63,6 +55,8 @@ func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message st // When the prompt returns an error, the caller should propagate the error up // the stack and close the io.Reader used for the prompt which will prevent the // background goroutine from blocking indefinitely. +// +// Deprecated: this function is for internal use and will be removed in the next release. func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) { return prompt.Confirm(ctx, ins, outs, message) } @@ -108,12 +102,6 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters. return pruneFilters } -// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later. -func AddPlatformFlag(flags *pflag.FlagSet, target *string) { - flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") - _ = flags.SetAnnotation("platform", "version", []string{"1.32"}) -} - // ValidateOutputPath validates the output paths of the "docker cp" command. func ValidateOutputPath(path string) error { dir := filepath.Dir(filepath.Clean(path)) diff --git a/cli/command/volume/cmd.go b/cli/command/volume/cmd.go index 386352791e2f..f1065d49a507 100644 --- a/cli/command/volume/cmd.go +++ b/cli/command/volume/cmd.go @@ -7,21 +7,28 @@ import ( ) // NewVolumeCommand returns a cobra command for `volume` subcommands -func NewVolumeCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewVolumeCommand(dockerCLI command.Cli) *cobra.Command { + return newVolumeCommand(dockerCLI) +} + +// newVolumeCommand returns a cobra command for `volume` subcommands +func newVolumeCommand(dockerCLI command.Cli) *cobra.Command { cmd := &cobra.Command{ Use: "volume COMMAND", Short: "Manage volumes", Args: cli.NoArgs, - RunE: command.ShowHelp(dockerCli.Err()), + RunE: command.ShowHelp(dockerCLI.Err()), Annotations: map[string]string{"version": "1.21"}, } cmd.AddCommand( - newCreateCommand(dockerCli), - newInspectCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - NewPruneCommand(dockerCli), - newUpdateCommand(dockerCli), + newCreateCommand(dockerCLI), + newInspectCommand(dockerCLI), + newListCommand(dockerCLI), + newRemoveCommand(dockerCLI), + newPruneCommand(dockerCLI), + newUpdateCommand(dockerCLI), ) return cmd } diff --git a/cli/command/volume/create.go b/cli/command/volume/create.go index 8c13f65a74eb..06679f75c566 100644 --- a/cli/command/volume/create.go +++ b/cli/command/volume/create.go @@ -9,7 +9,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/volume" "github.com/spf13/cobra" @@ -59,7 +58,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { options.cluster = hasClusterVolumeOptionSet(cmd.Flags()) return runCreate(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() flags.StringVarP(&options.driver, "driver", "d", "local", "Specify volume driver name") diff --git a/cli/command/volume/inspect.go b/cli/command/volume/inspect.go index 1105a052068b..cc3abb72e8c3 100644 --- a/cli/command/volume/inspect.go +++ b/cli/command/volume/inspect.go @@ -38,13 +38,10 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { - client := dockerCli.Client() - - getVolFunc := func(name string) (any, []byte, error) { - i, err := client.VolumeInspect(ctx, name) +func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { + apiClient := dockerCLI.Client() + return inspect.Inspect(dockerCLI.Out(), opts.names, opts.format, func(name string) (any, []byte, error) { + i, err := apiClient.VolumeInspect(ctx, name) return i, nil, err - } - - return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) + }) } diff --git a/cli/command/volume/list.go b/cli/command/volume/list.go index e51bfe5e1c6e..4286da5b7561 100644 --- a/cli/command/volume/list.go +++ b/cli/command/volume/list.go @@ -6,7 +6,6 @@ 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" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" @@ -37,7 +36,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runList(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() @@ -51,17 +50,17 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { return cmd } -func runList(ctx context.Context, dockerCli command.Cli, options listOptions) error { - client := dockerCli.Client() - volumes, err := client.VolumeList(ctx, volume.ListOptions{Filters: options.filter.Value()}) +func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { + apiClient := dockerCLI.Client() + volumes, err := apiClient.VolumeList(ctx, volume.ListOptions{Filters: options.filter.Value()}) if err != nil { return err } format := options.format if len(format) == 0 && !options.cluster { - if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !options.quiet { - format = dockerCli.ConfigFile().VolumesFormat + if len(dockerCLI.ConfigFile().VolumesFormat) > 0 && !options.quiet { + format = dockerCLI.ConfigFile().VolumesFormat } else { format = formatter.TableFormatKey } @@ -90,7 +89,7 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er }) volumeCtx := formatter.Context{ - Output: dockerCli.Out(), + Output: dockerCLI.Out(), Format: formatter.NewVolumeFormat(format, options.quiet), } return formatter.VolumeWrite(volumeCtx, volumes.Volumes) diff --git a/cli/command/volume/prune.go b/cli/command/volume/prune.go index 0765e0228948..d8e1aa7508bc 100644 --- a/cli/command/volume/prune.go +++ b/cli/command/volume/prune.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/versions" @@ -22,7 +21,14 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli command.Cli) *cobra.Command { +// +// Deprecated: Do not import commands directly. They will be removed in a future release. +func NewPruneCommand(dockerCLI command.Cli) *cobra.Command { + return newPruneCommand(dockerCLI) +} + +// newPruneCommand returns a new cobra prune command for volumes +func newPruneCommand(dockerCLI command.Cli) *cobra.Command { options := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -30,18 +36,18 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { Short: "Remove unused local volumes", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options) + spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options) if err != nil { return err } if output != "" { - fmt.Fprintln(dockerCli.Out(), output) + fmt.Fprintln(dockerCLI.Out(), output) } - fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, Annotations: map[string]string{"version": "1.25"}, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: cobra.NoFileCompletions, } flags := cmd.Flags() diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 11ee8adef879..5c31c9cc001c 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -53,7 +53,7 @@ func TestVolumePruneErrors(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - cmd := NewPruneCommand( + cmd := newPruneCommand( test.NewFakeCli(&fakeClient{ volumePruneFunc: tc.volumePruneFunc, }), @@ -105,7 +105,7 @@ func TestVolumePruneSuccess(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { cli := test.NewFakeCli(&fakeClient{volumePruneFunc: tc.volumePruneFunc}) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) if tc.input != "" { cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(tc.input)))) } @@ -135,7 +135,7 @@ func TestVolumePruneForce(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ volumePruneFunc: tc.volumePruneFunc, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.Flags().Set("force", "true") assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("volume-prune.%s.golden", tc.name)) @@ -152,7 +152,7 @@ func TestVolumePrunePromptYes(t *testing.T) { }) cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden") @@ -170,7 +170,7 @@ func TestVolumePrunePromptNo(t *testing.T) { }) cli.SetIn(streams.NewIn(io.NopCloser(strings.NewReader(input)))) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) @@ -199,7 +199,7 @@ func TestVolumePrunePromptTerminate(t *testing.T) { }, }) - cmd := NewPruneCommand(cli) + cmd := newPruneCommand(cli) cmd.SetArgs([]string{}) cmd.SetOut(io.Discard) cmd.SetErr(io.Discard) diff --git a/opts/envfile.go b/cli/compose/loader/envfile.go similarity index 54% rename from opts/envfile.go rename to cli/compose/loader/envfile.go index 3a16e6c189bc..6430f4b5f5f8 100644 --- a/opts/envfile.go +++ b/cli/compose/loader/envfile.go @@ -1,4 +1,4 @@ -package opts +package loader import ( "os" @@ -6,19 +6,21 @@ import ( "github.com/docker/cli/pkg/kvfile" ) -// ParseEnvFile reads a file with environment variables enumerated by lines +// parseEnvFile reads a file with environment variables enumerated by lines // // “Environment variable names used by the utilities in the Shell and -// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase +// Utilities volume of [IEEE Std 1003.1-2001] consist solely of uppercase // letters, digits, and the '_' (underscore) from the characters defined in // Portable Character Set and do not begin with a digit. *But*, other // characters may be permitted by an implementation; applications shall // tolerate the presence of such names.” -// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html // -// As of #16585, it's up to application inside docker to validate or not +// As of [moby-16585], it's up to application inside docker to validate or not // environment variables, that's why we just strip leading whitespace and // nothing more. -func ParseEnvFile(filename string) ([]string, error) { +// +// [IEEE Std 1003.1-2001]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html +// [moby-16585]: https://github.com/moby/moby/issues/16585 +func parseEnvFile(filename string) ([]string, error) { return kvfile.Parse(filename, os.LookupEnv) } diff --git a/opts/envfile_test.go b/cli/compose/loader/envfile_test.go similarity index 79% rename from opts/envfile_test.go rename to cli/compose/loader/envfile_test.go index 2c01de4fd520..723b1e2269e7 100644 --- a/opts/envfile_test.go +++ b/cli/compose/loader/envfile_test.go @@ -1,4 +1,4 @@ -package opts +package loader import ( "os" @@ -17,13 +17,13 @@ func tmpFileWithContent(t *testing.T, content string) string { return fileName } -// Test ParseEnvFile for a non existent file +// Test parseEnvFile for a non existent file func TestParseEnvFileNonExistentFile(t *testing.T) { - _, err := ParseEnvFile("no_such_file") + _, err := parseEnvFile("no_such_file") assert.Check(t, is.ErrorType(err, os.IsNotExist)) } -// ParseEnvFile with environment variable import definitions +// parseEnvFile with environment variable import definitions func TestParseEnvVariableDefinitionsFile(t *testing.T) { content := `# comment= UNDEFINED_VAR @@ -32,7 +32,7 @@ DEFINED_VAR tmpFile := tmpFileWithContent(t, content) t.Setenv("DEFINED_VAR", "defined-value") - variables, err := ParseEnvFile(tmpFile) + variables, err := parseEnvFile(tmpFile) assert.NilError(t, err) expectedLines := []string{"DEFINED_VAR=defined-value"} diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index b2673394b5bb..3b788ca048f3 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -17,6 +17,7 @@ import ( "github.com/docker/cli/cli/compose/schema" "github.com/docker/cli/cli/compose/template" "github.com/docker/cli/cli/compose/types" + "github.com/docker/cli/internal/volumespec" "github.com/docker/cli/opts" "github.com/docker/cli/opts/swarmopts" "github.com/docker/docker/api/types/versions" @@ -41,6 +42,13 @@ type Options struct { discardEnvFiles bool } +// ParseVolume parses a volume spec without any knowledge of the target platform. +// +// This function is unused, but kept for backward-compatibility for external users. +func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { + return volumespec.Parse(spec) +} + // WithDiscardEnvFiles sets the Options to discard the `env_file` section after resolving to // the `environment` section func WithDiscardEnvFiles(options *Options) { @@ -460,7 +468,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l for _, file := range serviceConfig.EnvFile { filePath := absPath(workingDir, file) - fileVars, err := opts.ParseEnvFile(filePath) + fileVars, err := parseEnvFile(filePath) if err != nil { return err } @@ -756,7 +764,7 @@ var transformBuildConfig TransformerFunc = func(data any) (any, error) { var transformServiceVolumeConfig TransformerFunc = func(data any) (any, error) { switch value := data.(type) { case string: - return ParseVolume(value) + return volumespec.Parse(value) case map[string]any: return data, nil default: diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index 0804388a57ce..9d0c11b409bb 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -8,6 +8,8 @@ import ( "fmt" "strconv" "time" + + "github.com/docker/cli/internal/volumespec" ) // UnsupportedProperties not yet supported by this implementation of the compose file @@ -390,43 +392,23 @@ type ServicePortConfig struct { } // ServiceVolumeConfig are references to a volume used by a service -type ServiceVolumeConfig struct { - Type string `yaml:",omitempty" json:"type,omitempty"` - Source string `yaml:",omitempty" json:"source,omitempty"` - Target string `yaml:",omitempty" json:"target,omitempty"` - ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` - Consistency string `yaml:",omitempty" json:"consistency,omitempty"` - Bind *ServiceVolumeBind `yaml:",omitempty" json:"bind,omitempty"` - Volume *ServiceVolumeVolume `yaml:",omitempty" json:"volume,omitempty"` - Image *ServiceVolumeImage `yaml:",omitempty" json:"image,omitempty"` - Tmpfs *ServiceVolumeTmpfs `yaml:",omitempty" json:"tmpfs,omitempty"` - Cluster *ServiceVolumeCluster `yaml:",omitempty" json:"cluster,omitempty"` -} +type ServiceVolumeConfig = volumespec.VolumeConfig // ServiceVolumeBind are options for a service volume of type bind -type ServiceVolumeBind struct { - Propagation string `yaml:",omitempty" json:"propagation,omitempty"` -} +type ServiceVolumeBind = volumespec.BindOpts // ServiceVolumeVolume are options for a service volume of type volume -type ServiceVolumeVolume struct { - NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` - Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` -} +type ServiceVolumeVolume = volumespec.VolumeOpts // ServiceVolumeImage are options for a service volume of type image -type ServiceVolumeImage struct { - Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` -} +type ServiceVolumeImage = volumespec.ImageOpts // ServiceVolumeTmpfs are options for a service volume of type tmpfs -type ServiceVolumeTmpfs struct { - Size int64 `yaml:",omitempty" json:"size,omitempty"` -} +type ServiceVolumeTmpfs = volumespec.TmpFsOpts // ServiceVolumeCluster are options for a service volume of type cluster. // Deliberately left blank for future options, but unused now. -type ServiceVolumeCluster struct{} +type ServiceVolumeCluster = volumespec.ClusterOpts // FileReferenceConfig for a reference to a swarm file object type FileReferenceConfig struct { diff --git a/cli/config/credentials/file_store_test.go b/cli/config/credentials/file_store_test.go index e4a43e11fb14..d4c8375ea25e 100644 --- a/cli/config/credentials/file_store_test.go +++ b/cli/config/credentials/file_store_test.go @@ -43,13 +43,13 @@ func TestFileStoreIdempotent(t *testing.T) { }, }) authOne := types.AuthConfig{ + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", } authTwo := types.AuthConfig{ + Username: "bar@example.com", Auth: "also_super_secret_token", - Email: "bar@example.com", ServerAddress: "https://other.example.com", } @@ -106,8 +106,8 @@ func TestFileStoreAddCredentials(t *testing.T) { s := NewFileStore(f) auth := types.AuthConfig{ + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", } err := s.Store(auth) @@ -122,8 +122,8 @@ func TestFileStoreAddCredentials(t *testing.T) { func TestFileStoreGet(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, }} @@ -136,8 +136,8 @@ func TestFileStoreGet(t *testing.T) { if a.Auth != "super_secret_token" { t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) } - if a.Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + if a.Username != "foo@example.com" { + t.Fatalf("expected username `foo@example.com`, got %s", a.Username) } } @@ -146,13 +146,13 @@ func TestFileStoreGetAll(t *testing.T) { s2 := "https://example2.example.com" f := &fakeStore{configs: map[string]types.AuthConfig{ s1: { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, s2: { + Username: "foo@example2.com", Auth: "super_secret_token2", - Email: "foo@example2.com", ServerAddress: "https://example2.example.com", }, }} @@ -168,22 +168,22 @@ func TestFileStoreGetAll(t *testing.T) { if as[s1].Auth != "super_secret_token" { t.Fatalf("expected auth `super_secret_token`, got %s", as[s1].Auth) } - if as[s1].Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com`, got %s", as[s1].Email) + if as[s1].Username != "foo@example.com" { + t.Fatalf("expected username `foo@example.com`, got %s", as[s1].Username) } if as[s2].Auth != "super_secret_token2" { t.Fatalf("expected auth `super_secret_token2`, got %s", as[s2].Auth) } - if as[s2].Email != "foo@example2.com" { - t.Fatalf("expected email `foo@example2.com`, got %s", as[s2].Email) + if as[s2].Username != "foo@example2.com" { + t.Fatalf("expected username `foo@example2.com`, got %s", as[s2].Username) } } func TestFileStoreErase(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ "https://example.com": { + Username: "foo@example.com", Auth: "super_secret_token", - Email: "foo@example.com", ServerAddress: "https://example.com", }, }} @@ -203,9 +203,6 @@ func TestFileStoreErase(t *testing.T) { if a.Auth != "" { t.Fatalf("expected empty auth token, got %s", a.Auth) } - if a.Email != "" { - t.Fatalf("expected empty email, got %s", a.Email) - } } func TestConvertToHostname(t *testing.T) { diff --git a/cli/config/credentials/native_store_test.go b/cli/config/credentials/native_store_test.go index c2f843bfbb95..cb31071e602a 100644 --- a/cli/config/credentials/native_store_test.go +++ b/cli/config/credentials/native_store_test.go @@ -99,7 +99,6 @@ func TestNativeStoreAddCredentials(t *testing.T) { auth := types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: validServerAddress, } err := s.Store(auth) @@ -109,7 +108,6 @@ func TestNativeStoreAddCredentials(t *testing.T) { actual, ok := f.GetAuthConfigs()[validServerAddress] assert.Check(t, ok) expected := types.AuthConfig{ - Email: auth.Email, ServerAddress: auth.ServerAddress, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -124,7 +122,6 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { err := s.Store(types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: invalidServerAddress, }) assert.ErrorContains(t, err, "program failed") @@ -134,7 +131,7 @@ func TestNativeStoreAddInvalidCredentials(t *testing.T) { func TestNativeStoreGet(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ validServerAddress: { - Email: "foo@example.com", + Username: "foo@example.com", }, }} s := &nativeStore{ @@ -147,7 +144,6 @@ func TestNativeStoreGet(t *testing.T) { expected := types.AuthConfig{ Username: "foo", Password: "bar", - Email: "foo@example.com", ServerAddress: validServerAddress, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -155,9 +151,7 @@ func TestNativeStoreGet(t *testing.T) { func TestNativeStoreGetIdentityToken(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress2: { - Email: "foo@example2.com", - }, + validServerAddress2: {}, }} s := &nativeStore{ @@ -169,7 +163,6 @@ func TestNativeStoreGetIdentityToken(t *testing.T) { expected := types.AuthConfig{ IdentityToken: "abcd1234", - Email: "foo@example2.com", ServerAddress: validServerAddress2, } assert.Check(t, is.DeepEqual(expected, actual)) @@ -177,9 +170,7 @@ func TestNativeStoreGetIdentityToken(t *testing.T) { func TestNativeStoreGetAll(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -189,38 +180,20 @@ func TestNativeStoreGetAll(t *testing.T) { as, err := s.GetAll() assert.NilError(t, err) assert.Check(t, is.Len(as, 2)) - - if as[validServerAddress].Username != "foo" { - t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) - } - if as[validServerAddress].Password != "bar" { - t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) - } - if as[validServerAddress].IdentityToken != "" { - t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) - } - if as[validServerAddress].Email != "foo@example.com" { - t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) - } - if as[validServerAddress2].Username != "" { - t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) - } - if as[validServerAddress2].Password != "" { - t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) - } - if as[validServerAddress2].IdentityToken != "abcd1234" { - t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) - } - if as[validServerAddress2].Email != "" { - t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email) + expected := types.AuthConfig{ + Username: "foo", + Password: "bar", + ServerAddress: "https://index.docker.io/v1", + IdentityToken: "", } + actual, ok := as[validServerAddress] + assert.Check(t, ok) + assert.Check(t, is.DeepEqual(expected, actual)) } func TestNativeStoreGetMissingCredentials(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -233,9 +206,7 @@ func TestNativeStoreGetMissingCredentials(t *testing.T) { func TestNativeStoreGetInvalidAddress(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -248,9 +219,7 @@ func TestNativeStoreGetInvalidAddress(t *testing.T) { func TestNativeStoreErase(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ @@ -264,9 +233,7 @@ func TestNativeStoreErase(t *testing.T) { func TestNativeStoreEraseInvalidAddress(t *testing.T) { f := &fakeStore{configs: map[string]types.AuthConfig{ - validServerAddress: { - Email: "foo@example.com", - }, + validServerAddress: {}, }} s := &nativeStore{ diff --git a/cli/config/memorystore/store.go b/cli/config/memorystore/store.go index 199083464ed8..267e1e3437bd 100644 --- a/cli/config/memorystore/store.go +++ b/cli/config/memorystore/store.go @@ -3,7 +3,6 @@ package memorystore import ( - "errors" "fmt" "maps" "os" @@ -13,12 +12,17 @@ import ( "github.com/docker/cli/cli/config/types" ) -var errValueNotFound = errors.New("value not found") +// notFoundErr is the error returned when a plugin could not be found. +type notFoundErr string -func IsErrValueNotFound(err error) bool { - return errors.Is(err, errValueNotFound) +func (notFoundErr) NotFound() {} + +func (e notFoundErr) Error() string { + return string(e) } +var errValueNotFound notFoundErr = "value not found" + type Config struct { lock sync.RWMutex memoryCredentials map[string]types.AuthConfig diff --git a/cli/config/types/authconfig.go b/cli/config/types/authconfig.go index 056af6b84259..95eb27c868bf 100644 --- a/cli/config/types/authconfig.go +++ b/cli/config/types/authconfig.go @@ -7,8 +7,8 @@ type AuthConfig struct { Auth string `json:"auth,omitempty"` // Email is an optional value associated with the username. - // This field is deprecated and will be removed in a later - // version of docker. + // + // Deprecated: This field is deprecated since docker 1.11 (API v1.23) and will be removed in the next release. Email string `json:"email,omitempty"` ServerAddress string `json:"serveraddress,omitempty"` diff --git a/cli/connhelper/commandconn/commandconn_unix_test.go b/cli/connhelper/commandconn/commandconn_unix_test.go index 6c1219f4386e..b68ebe7bf4bb 100644 --- a/cli/connhelper/commandconn/commandconn_unix_test.go +++ b/cli/connhelper/commandconn/commandconn_unix_test.go @@ -4,12 +4,17 @@ package commandconn import ( "context" + "errors" "io" "io/fs" + "os" + "path/filepath" + "runtime" + "strconv" + "syscall" "testing" "time" - "github.com/docker/docker/pkg/process" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -51,16 +56,16 @@ func TestCloseRunningCommand(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) n, err := c.Write([]byte("hello")) assert.Check(t, is.Equal(len("hello"), n)) assert.NilError(t, err) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) done <- struct{}{} }() @@ -79,7 +84,7 @@ func TestCloseTwice(t *testing.T) { c, err := New(ctx, "sh", "-c", "echo hello; sleep 1; exit 0") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) b := make([]byte, 32) n, err := c.Read(b) @@ -88,11 +93,11 @@ func TestCloseTwice(t *testing.T) { err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) done <- struct{}{} }() @@ -111,7 +116,7 @@ func TestEOFTimeout(t *testing.T) { c, err := New(ctx, "sh", "-c", "sleep 20") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) cmdConn.stdout = mockStdoutEOF{} @@ -148,7 +153,7 @@ func TestCloseWhileWriting(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) writeErrC := make(chan error) go func() { @@ -164,7 +169,7 @@ func TestCloseWhileWriting(t *testing.T) { err = c.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) writeErr := <-writeErrC assert.ErrorContains(t, writeErr, "file already closed") @@ -176,7 +181,7 @@ func TestCloseWhileReading(t *testing.T) { c, err := New(ctx, "sh", "-c", "while true; do sleep 1; done") assert.NilError(t, err) cmdConn := c.(*commandConn) - assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, processAlive(cmdConn.cmd.Process.Pid)) readErrC := make(chan error) go func() { @@ -193,8 +198,37 @@ func TestCloseWhileReading(t *testing.T) { err = cmdConn.Close() assert.NilError(t, err) - assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid)) + assert.Check(t, !processAlive(cmdConn.cmd.Process.Pid)) readErr := <-readErrC assert.Check(t, is.ErrorIs(readErr, fs.ErrClosed)) } + +// processAlive returns true if a process with a given pid is running. It only considers +// positive PIDs; 0 (all processes in the current process group), -1 (all processes +// with a PID larger than 1), and negative (-n, all processes in process group +// "n") values for pid are never considered to be alive. +// +// It was forked from https://github.com/moby/moby/blob/v28.3.3/pkg/process/process_unix.go#L17-L42 +func processAlive(pid int) bool { + if pid < 1 { + return false + } + switch runtime.GOOS { + case "darwin": + // OS X does not have a proc filesystem. Use kill -0 pid to judge if the + // process exists. From KILL(2): https://www.freebsd.org/cgi/man.cgi?query=kill&sektion=2&manpath=OpenDarwin+7.2.1 + // + // Sig may be one of the signals specified in sigaction(2) or it may + // be 0, in which case error checking is performed but no signal is + // actually sent. This can be used to check the validity of pid. + err := syscall.Kill(pid, 0) + + // Either the PID was found (no error), or we get an EPERM, which means + // the PID exists, but we don't have permissions to signal it. + return err == nil || errors.Is(err, syscall.EPERM) + default: + _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))) + return err == nil + } +} diff --git a/cli/context/docker/load.go b/cli/context/docker/load.go index 89d43e2e3265..e37ee4646db6 100644 --- a/cli/context/docker/load.go +++ b/cli/context/docker/load.go @@ -101,7 +101,22 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) { if err != nil { return nil, err } - result = append(result, withHTTPClient(tlsConfig)) + + // If there's no tlsConfig available, we use the default HTTPClient. + if tlsConfig != nil { + result = append(result, + client.WithHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + DialContext: (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext, + }, + CheckRedirect: client.CheckRedirect, + }), + ) + } } result = append(result, client.WithHost(ep.Host)) } else { @@ -133,25 +148,6 @@ func isSocket(addr string) bool { } } -func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error { - return func(c *client.Client) error { - if tlsConfig == nil { - // Use the default HTTPClient - return nil - } - return client.WithHTTPClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConfig, - DialContext: (&net.Dialer{ - KeepAlive: 30 * time.Second, - Timeout: 30 * time.Second, - }).DialContext, - }, - CheckRedirect: client.CheckRedirect, - })(c) - } -} - // EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) { ep, ok := metadata.Endpoints[DockerEndpoint] diff --git a/cli/context/store/errors.go b/cli/context/store/errors.go index e85ce325a9a7..cabc3f7a342b 100644 --- a/cli/context/store/errors.go +++ b/cli/context/store/errors.go @@ -1,9 +1,9 @@ package store -import cerrdefs "github.com/containerd/errdefs" +import "github.com/containerd/errdefs" func invalidParameter(err error) error { - if err == nil || cerrdefs.IsInvalidArgument(err) { + if err == nil || errdefs.IsInvalidArgument(err) { return err } return invalidParameterErr{err} @@ -14,7 +14,7 @@ type invalidParameterErr struct{ error } func (invalidParameterErr) InvalidParameter() {} func notFound(err error) error { - if err == nil || cerrdefs.IsNotFound(err) { + if err == nil || errdefs.IsNotFound(err) { return err } return notFoundErr{err} diff --git a/cli/context/store/metadata_test.go b/cli/context/store/metadata_test.go index beaf133d379f..e7a6318e9c13 100644 --- a/cli/context/store/metadata_test.go +++ b/cli/context/store/metadata_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -26,7 +26,7 @@ func testMetadata(name string) Metadata { func TestMetadataGetNotExisting(t *testing.T) { testee := metadataStore{root: t.TempDir(), config: testCfg} _, err := testee.get("noexist") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestMetadataCreateGetRemove(t *testing.T) { @@ -60,7 +60,7 @@ func TestMetadataCreateGetRemove(t *testing.T) { assert.NilError(t, testee.remove("test-context")) assert.NilError(t, testee.remove("test-context")) // support duplicate remove _, err = testee.get("test-context") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestMetadataRespectJsonAnnotation(t *testing.T) { diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index 2d3074783a01..a5354538d083 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -17,7 +17,7 @@ import ( "path/filepath" "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -107,7 +107,7 @@ func TestRemove(t *testing.T) { })) assert.NilError(t, s.Remove("source")) _, err = s.GetMetadata("source") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) f, err := s.ListTLSFiles("source") assert.NilError(t, err) assert.Equal(t, 0, len(f)) @@ -122,7 +122,7 @@ func TestListEmptyStore(t *testing.T) { func TestErrHasCorrectContext(t *testing.T) { _, err := New(t.TempDir(), testCfg).GetMetadata("no-exists") assert.ErrorContains(t, err, "no-exists") - assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound)) + assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) } func TestDetectImportContentType(t *testing.T) { diff --git a/cli/context/store/tlsstore_test.go b/cli/context/store/tlsstore_test.go index a6aae68ae0c3..799362d54e14 100644 --- a/cli/context/store/tlsstore_test.go +++ b/cli/context/store/tlsstore_test.go @@ -3,7 +3,7 @@ package store import ( "testing" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "gotest.tools/v3/assert" ) @@ -13,7 +13,7 @@ func TestTlsCreateUpdateGetRemove(t *testing.T) { const contextName = "test-ctx" _, err := testee.getData(contextName, "test-ep", "test-data") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) err = testee.createOrUpdate(contextName, "test-ep", "test-data", []byte("data")) assert.NilError(t, err) @@ -29,7 +29,7 @@ func TestTlsCreateUpdateGetRemove(t *testing.T) { err = testee.removeEndpoint(contextName, "test-ep") assert.NilError(t, err) _, err = testee.getData(contextName, "test-ep", "test-data") - assert.ErrorType(t, err, cerrdefs.IsNotFound) + assert.ErrorType(t, err, errdefs.IsNotFound) } func TestTlsListAndBatchRemove(t *testing.T) { diff --git a/cli/flags/options.go b/cli/flags/options.go index fc168984b44b..309cb4616fa3 100644 --- a/cli/flags/options.go +++ b/cli/flags/options.go @@ -1,12 +1,12 @@ package flags import ( + "errors" "fmt" "os" "path/filepath" "github.com/docker/cli/cli/config" - "github.com/docker/cli/opts" "github.com/docker/docker/client" "github.com/docker/go-connections/tlsconfig" "github.com/sirupsen/logrus" @@ -54,6 +54,39 @@ var ( dockerTLS = os.Getenv(EnvEnableTLS) != "" ) +// hostVar is used for the '--host' / '-H' flag to set [ClientOptions.Hosts]. +// The [ClientOptions.Hosts] field is a slice because it was originally shared +// with the daemon config. However, the CLI only allows for a single host to +// be specified. +// +// hostVar presents itself as a "string", but stores the value in a string +// slice. It produces an error when trying to set multiple values, matching +// the check in [getServerHost]. +// +// [getServerHost]: https://github.com/docker/cli/blob/7eab668982645def1cd46fe1b60894cba6fd17a4/cli/command/cli.go#L542-L551 +type hostVar struct { + dst *[]string + set bool +} + +func (h *hostVar) String() string { + if h.dst == nil || len(*h.dst) == 0 { + return "" + } + return (*h.dst)[0] +} + +func (h *hostVar) Set(s string) error { + if h.set { + return errors.New("specify only one -H") + } + *h.dst = []string{s} + h.set = true + return nil +} + +func (*hostVar) Type() string { return "string" } + // ClientOptions are the options used to configure the client cli. type ClientOptions struct { Debug bool @@ -90,13 +123,13 @@ func (o *ClientOptions) InstallFlags(flags *pflag.FlagSet) { KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), } tlsOptions := o.TLSOptions - flags.Var(opts.NewQuotedString(&tlsOptions.CAFile), "tlscacert", "Trust certs signed only by this CA") - flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file") - flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file") + flags.Var("edString{&tlsOptions.CAFile}, "tlscacert", "Trust certs signed only by this CA") + flags.Var("edString{&tlsOptions.CertFile}, "tlscert", "Path to TLS certificate file") + flags.Var("edString{&tlsOptions.KeyFile}, "tlskey", "Path to TLS key file") - // opts.ValidateHost is not used here, so as to allow connection helpers - hostOpt := opts.NewNamedListOptsRef("hosts", &o.Hosts, nil) - flags.VarP(hostOpt, "host", "H", "Daemon socket to connect to") + // TODO(thaJeztah): show the default host. + // TODO(thaJeztah): this should be a string, not an "array" as we only allow a single host. + flags.VarP(&hostVar{dst: &o.Hosts}, "host", "H", "Daemon socket to connect to") flags.StringVarP(&o.Context, "context", "c", "", `Name of the context to use to connect to the daemon (overrides `+client.EnvOverrideHost+` env var and default context set with "docker context use")`) } @@ -146,3 +179,33 @@ func SetLogLevel(logLevel string) { logrus.SetLevel(logrus.InfoLevel) } } + +type quotedString struct { + value *string +} + +func (s *quotedString) Set(val string) error { + *s.value = trimQuotes(val) + return nil +} + +func (*quotedString) Type() string { + return "string" +} + +func (s *quotedString) String() string { + return *s.value +} + +func trimQuotes(value string) string { + if len(value) < 2 { + return value + } + lastIndex := len(value) - 1 + for _, char := range []byte{'\'', '"'} { + if value[0] == char && value[lastIndex] == char { + return value[1:lastIndex] + } + } + return value +} diff --git a/cli/manifest/store/store.go b/cli/manifest/store/store.go index e97e8628f125..7518b5d4a1dc 100644 --- a/cli/manifest/store/store.go +++ b/cli/manifest/store/store.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" "github.com/docker/distribution/manifest/manifestlist" @@ -152,27 +153,13 @@ func makeFilesafeName(ref string) string { return strings.ReplaceAll(fileName, "/", "_") } -type notFoundError struct { - object string +func newNotFoundError(ref string) error { + return errdefs.ErrNotFound.WithMessage("No such manifest: " + ref) } -func newNotFoundError(ref string) *notFoundError { - return ¬FoundError{object: ref} -} - -func (n *notFoundError) Error() string { - return "No such manifest: " + n.object -} - -// NotFound interface -func (*notFoundError) NotFound() {} - // IsNotFound returns true if the error is a not found error +// +// Deprecated: use [errdefs.IsNotFound]. This function will be removed in the next release. func IsNotFound(err error) bool { - _, ok := err.(notFound) - return ok -} - -type notFound interface { - NotFound() + return errdefs.IsNotFound(err) } diff --git a/cli/manifest/store/store_test.go b/cli/manifest/store/store_test.go index 2b3a57819f8c..d5c3ef67818e 100644 --- a/cli/manifest/store/store_test.go +++ b/cli/manifest/store/store_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/containerd/errdefs" "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" "github.com/google/go-cmp/cmp" @@ -86,7 +87,7 @@ func TestStoreSaveAndGet(t *testing.T) { actual, err := store.Get(tc.listRef, tc.manifestRef) if tc.expectedErr != "" { assert.Error(t, err, tc.expectedErr) - assert.Check(t, IsNotFound(err)) + assert.Check(t, errdefs.IsNotFound(err)) return } assert.NilError(t, err) @@ -117,5 +118,5 @@ func TestStoreGetListDoesNotExist(t *testing.T) { listRef := ref("list") _, err := store.GetList(listRef) assert.Error(t, err, "No such manifest: list") - assert.Check(t, IsNotFound(err)) + assert.Check(t, errdefs.IsNotFound(err)) } diff --git a/cli/registry/client/client_deprecated.go b/cli/registry/client/client_deprecated.go new file mode 100644 index 000000000000..b3ffde57496d --- /dev/null +++ b/cli/registry/client/client_deprecated.go @@ -0,0 +1,21 @@ +package client // Deprecated: this package was only used internally and will be removed in the next release. + +import "github.com/docker/cli/internal/registryclient" + +// RegistryClient is a client used to communicate with a Docker distribution +// registry. +// +// Deprecated: this interface was only used internally and will be removed in the next release. +type RegistryClient = registryclient.RegistryClient + +// NewRegistryClient returns a new RegistryClient with a resolver +// +// Deprecated: this function was only used internally and will be removed in the next release. +func NewRegistryClient(resolver registryclient.AuthConfigResolver, userAgent string, insecure bool) registryclient.RegistryClient { + return registryclient.NewRegistryClient(resolver, userAgent, insecure) +} + +// AuthConfigResolver returns Auth Configuration for an index +// +// Deprecated: this type was only used internally and will be removed in the next release. +type AuthConfigResolver = registryclient.AuthConfigResolver diff --git a/cli/trust/trust.go b/cli/trust/trust.go index 27453ae22ee4..a24ccaf829d9 100644 --- a/cli/trust/trust.go +++ b/cli/trust/trust.go @@ -3,6 +3,7 @@ package trust import ( "context" "encoding/json" + "fmt" "io" "net" "net/http" @@ -10,15 +11,16 @@ import ( "os" "path" "path/filepath" + "strconv" "time" "github.com/distribution/reference" "github.com/docker/cli/cli/config" + "github.com/docker/cli/internal/registry" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/docker/go-connections/tlsconfig" "github.com/opencontainers/go-digest" "github.com/pkg/errors" @@ -42,6 +44,20 @@ var ( ActionsPushAndPull = []string{"pull", "push"} ) +// Enabled returns whether content-trust is enabled through the DOCKER_CONTENT_TRUST env-var. +// +// IMPORTANT: this function is for internal use, and may be removed at any moment. +func Enabled() bool { + var enabled bool + if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { + if t, err := strconv.ParseBool(e); t || err != nil { + // treat any other value as true + enabled = true + } + } + return enabled +} + // NotaryServer is the endpoint serving the Notary trust server const NotaryServer = "https://notary.docker.io" @@ -92,14 +108,22 @@ func (scs simpleCredentialStore) RefreshToken(*url.URL, string) string { func (simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {} +const dctDeprecation = `WARNING: Docker is retiring DCT for Docker Official Images (DOI). + For details, refer to https://docs.docker.com/go/dct-deprecation/ + +` + // GetNotaryRepository returns a NotaryRepository which stores all the // information needed to operate on a notary repository. // It creates an HTTP transport providing authentication support. -func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { +func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) { server, err := Server(repoInfo.Index) if err != nil { return nil, err } + if server == NotaryServer { + _, _ = fmt.Fprint(os.Stderr, dctDeprecation) + } cfg := tlsconfig.ClientDefault() cfg.InsecureSkipVerify = !repoInfo.Index.Secure @@ -304,11 +328,18 @@ type ImageRefAndAuth struct { original string authConfig *registrytypes.AuthConfig reference reference.Named - repoInfo *registry.RepositoryInfo + repoInfo *RepositoryInfo tag string digest digest.Digest } +// RepositoryInfo describes a repository +type RepositoryInfo struct { + Name reference.Named + // Index points to registry information + Index *registrytypes.IndexInfo +} + // GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name // as an ImageRefAndAuth struct func GetImageReferencesAndAuth(ctx context.Context, @@ -320,16 +351,22 @@ func GetImageReferencesAndAuth(ctx context.Context, return ImageRefAndAuth{}, err } - // Resolve the Repository name from fqn to RepositoryInfo - repoInfo, _ := registry.ParseRepositoryInfo(ref) - authConfig := authResolver(ctx, repoInfo.Index) + // Resolve the Repository name from fqn to RepositoryInfo, and create an + // IndexInfo. Docker Content Trust uses the IndexInfo.Official field to + // select the right domain for Docker Hub's Notary server; + // https://github.com/docker/cli/blob/v28.4.0/cli/trust/trust.go#L65-L79 + indexInfo := registry.NewIndexInfo(ref) + authConfig := authResolver(ctx, indexInfo) return ImageRefAndAuth{ original: imgName, authConfig: &authConfig, reference: ref, - repoInfo: repoInfo, - tag: getTag(ref), - digest: getDigest(ref), + repoInfo: &RepositoryInfo{ + Name: reference.TrimNamed(ref), + Index: indexInfo, + }, + tag: getTag(ref), + digest: getDigest(ref), }, nil } @@ -366,7 +403,7 @@ func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named { } // RepoInfo returns the repository information for a given ImageRefAndAuth -func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo { +func (imgRefAuth *ImageRefAndAuth) RepoInfo() *RepositoryInfo { return imgRefAuth.repoInfo } diff --git a/cli/trust/trust_push.go b/cli/trust/trust_push.go index 1a8c5e4b7281..87a33e7ca2b1 100644 --- a/cli/trust/trust_push.go +++ b/cli/trust/trust_push.go @@ -13,7 +13,6 @@ import ( "github.com/docker/cli/internal/jsonstream" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" "github.com/pkg/errors" "github.com/theupdateframework/notary/client" @@ -32,7 +31,7 @@ type Streams interface { // PushTrustedReference pushes a canonical reference to the trust server. // //nolint:gocyclo -func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { +func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader, userAgent string) error { // If it is a trusted push we would like to find the target entry which match the // tag provided in the function and then do an AddTarget later. notaryTarget := &client.Target{} diff --git a/cmd/docker/builder.go b/cmd/docker/builder.go index cccae304fe5e..00fc1b40f1ab 100644 --- a/cmd/docker/builder.go +++ b/cmd/docker/builder.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/containerd/errdefs" pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command" @@ -36,7 +37,7 @@ const ( ) func newBuilderError(errorMsg string, pluginLoadErr error) error { - if pluginmanager.IsNotFound(pluginLoadErr) { + if errdefs.IsNotFound(pluginLoadErr) { return errors.New(errorMsg) } if pluginLoadErr != nil { diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index ccd61845a1d2..ed1de447812b 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -10,7 +10,7 @@ import ( "strings" "syscall" - cerrdefs "github.com/containerd/errdefs" + "github.com/containerd/errdefs" "github.com/docker/cli/cli" pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/socket" @@ -41,7 +41,7 @@ func main() { os.Exit(getExitCode(err)) } - if err != nil && !cerrdefs.IsCanceled(err) { + if err != nil && !errdefs.IsCanceled(err) { if err.Error() != "" { _, _ = fmt.Fprintln(os.Stderr, err) } @@ -163,8 +163,11 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { cmd.SetOut(dockerCli.Out()) commands.AddCommands(cmd, dockerCli) - cli.DisableFlagsInUseLine(cmd) - setValidateArgs(dockerCli, cmd) + visitAll(cmd, + setValidateArgs(dockerCli), + // prevent adding "[flags]" to the end of the usage line. + func(c *cobra.Command) { c.DisableFlagsInUseLine = true }, + ) // flags must be the top-level command flags, not cmd.Flags() return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) @@ -201,7 +204,7 @@ func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) { if err == nil { return helpcmd.Run() } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) } } @@ -240,7 +243,7 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) { if err == nil { return } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { ccmd.Println(err) return } @@ -265,14 +268,29 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) { }) } -func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) { - // The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. - // As a result, here we replace the existing Args validation func to a wrapper, - // where the wrapper will check to see if the feature is supported or not. - // The Args validation error will only be returned if the feature is supported. - cli.VisitAll(cmd, func(ccmd *cobra.Command) { +// visitAll traverses all commands from the root. +func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) { + for _, cmd := range root.Commands() { + visitAll(cmd, fns...) + } + for _, fn := range fns { + fn(root) + } +} + +// The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. +// As a result, here we replace the existing Args validation func to a wrapper, +// where the wrapper will check to see if the feature is supported or not. +// The Args validation error will only be returned if the feature is supported. +func setValidateArgs(dockerCLI versionDetails) func(*cobra.Command) { + return func(ccmd *cobra.Command) { // if there is no tags for a command or any of its parent, // there is no need to wrap the Args validation. + // + // FIXME(thaJeztah): can we memoize properties of the parent? + // visitAll traverses root -> all childcommands, and hasTags + // goes the reverse (cmd -> visit all parents), so we may + // end traversing two directions. if !hasTags(ccmd) { return } @@ -283,12 +301,12 @@ func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) { cmdArgs := ccmd.Args ccmd.Args = func(cmd *cobra.Command, args []string) error { - if err := isSupported(cmd, dockerCli); err != nil { + if err := isSupported(cmd, dockerCLI); err != nil { return err } return cmdArgs(cmd, args) } - }) + } } func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command, subcommand string, envs []string) error { @@ -321,19 +339,19 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command // signals to the subprocess because the shared // pgid makes the TTY a controlling terminal. // - // The plugin should have it's own copy of this + // The plugin should have its own copy of this // termination logic, and exit after 3 retries - // on it's own. + // on its own. if dockerCli.Out().IsTerminal() { return } - // Terminate the plugin server, which will - // close all connections with plugin - // subprocesses, and signal them to exit. + // Terminate the plugin server, which closes + // all connections with plugin subprocesses, + // and signal them to exit. // - // Repeated invocations will result in EINVAL, - // or EBADF; but that is fine for our purposes. + // Repeated invocations result in EINVAL or EBADF, + // but that is fine for our purposes. if srv != nil { _ = srv.Close() } @@ -351,15 +369,15 @@ func tryPluginRun(ctx context.Context, dockerCli command.Cli, cmd *cobra.Command go func() { retries := 0 - force := false // catch the first signal through context cancellation <-ctx.Done() - tryTerminatePlugin(force) + tryTerminatePlugin(false) // register subsequent signals signals := make(chan os.Signal, exitLimit) signal.Notify(signals, platformsignals.TerminationSignals...) + force := false for range signals { retries++ // If we're still running after 3 interruptions @@ -440,7 +458,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { } }() } else { - fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed") + _, _ = fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed") } dockerCli.InstrumentCobraCommands(ctx, cmd) @@ -451,12 +469,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { return err } - if cli.HasCompletionArg(args) { + if hasCompletionArg(args) { // We add plugin command stubs early only for completion. We don't // want to add them for normal command execution as it would cause // a significant performance hit. - err = pluginmanager.AddPluginCommandStubs(dockerCli, cmd) - if err != nil { + if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd); err != nil { return err } } @@ -473,7 +490,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { } return nil } - if !pluginmanager.IsNotFound(err) { + if !errdefs.IsNotFound(err) { // For plugin not found we fall through to // cmd.Execute() which deals with reporting // "command not found" in a consistent way. @@ -504,6 +521,16 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error { return err } +// hasCompletionArg returns true if a cobra completion arg request is found. +func hasCompletionArg(args []string) bool { + for _, arg := range args { + if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd { + return true + } + } + return false +} + type versionDetails interface { CurrentVersion() string ServerInfo() command.ServerInfo diff --git a/cmd/docker/docker_test.go b/cmd/docker/docker_test.go index 48d37131c4a6..99e9583cfb0c 100644 --- a/cmd/docker/docker_test.go +++ b/cmd/docker/docker_test.go @@ -14,6 +14,7 @@ import ( "github.com/docker/cli/cli/debug" platformsignals "github.com/docker/cli/cmd/docker/internal/signals" "github.com/sirupsen/logrus" + "github.com/spf13/cobra" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) @@ -108,3 +109,21 @@ func TestUserTerminatedError(t *testing.T) { assert.Equal(t, getExitCode(context.Cause(notifyCtx)), 143) } + +func TestVisitAll(t *testing.T) { + root := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub1sub1 := &cobra.Command{Use: "sub1sub1"} + sub1sub2 := &cobra.Command{Use: "sub1sub2"} + sub2 := &cobra.Command{Use: "sub2"} + + root.AddCommand(sub1, sub2) + sub1.AddCommand(sub1sub1, sub1sub2) + + var visited []string + visitAll(root, func(ccmd *cobra.Command) { + visited = append(visited, ccmd.Name()) + }) + expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"} + assert.DeepEqual(t, expected, visited) +} diff --git a/docker-bake.hcl b/docker-bake.hcl index 670e31c1f5dc..109dc213ff92 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -1,5 +1,5 @@ variable "GO_VERSION" { - default = "1.24.5" + default = "1.24.9" } variable "VERSION" { default = "" diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 0b81e090c8b9..d1c81a27cdf3 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.24.9 # 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 @@ -28,7 +28,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM golang AS gotestsum # 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 +ARG GOTESTSUM_VERSION=v1.13.0 RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=tmpfs,target=/go/src/ \ @@ -50,6 +50,7 @@ RUN apk add --no-cache \ coreutils \ curl \ git \ + git-daemon \ jq \ nano diff --git a/dockerfiles/Dockerfile.lint b/dockerfiles/Dockerfile.lint index 345943c2e5c0..9a89f3311a66 100644 --- a/dockerfiles/Dockerfile.lint +++ b/dockerfiles/Dockerfile.lint @@ -1,12 +1,13 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.24.9 # 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 +# GOLANGCI_LINT_VERSION sets the version of the golangci/golangci-lint image to use. +ARG GOLANGCI_LINT_VERSION=v2.5.0 FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint diff --git a/dockerfiles/Dockerfile.vendor b/dockerfiles/Dockerfile.vendor index e70a5921781d..d9949d70bfeb 100644 --- a/dockerfiles/Dockerfile.vendor +++ b/dockerfiles/Dockerfile.vendor @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.24.9 # 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 diff --git a/docs/deprecated.md b/docs/deprecated.md index 1a3c353da69f..80eebd5d8a0b 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -53,6 +53,8 @@ The following table provides an overview of the current status of deprecated fea | Status | Feature | Deprecated | Remove | |------------|------------------------------------------------------------------------------------------------------------------------------------|------------|--------| +| Deprecated | [Legacy links environment variables](#legacy-links-environment-variables) | v28.4 | v30.0 | +| Deprecated | [Special handling for quoted values for TLS flags](#special-handling-for-quoted-values-for-tls-flags) | v28.4 | v29.0 | | Deprecated | [Empty/nil fields in image Config from inspect API](#emptynil-fields-in-image-config-from-inspect-api) | v28.3 | v29.0 | | Deprecated | [Configuration for pushing non-distributable artifacts](#configuration-for-pushing-non-distributable-artifacts) | v28.0 | v29.0 | | Deprecated | [`--time` option on `docker stop` and `docker restart`](#--time-option-on-docker-stop-and-docker-restart) | v28.0 | - | @@ -63,6 +65,7 @@ The following table provides an overview of the current status of deprecated fea | Removed | [`Container` and `ContainerConfig` fields in Image inspect](#container-and-containerconfig-fields-in-image-inspect) | v25.0 | v26.0 | | Removed | [Deprecate legacy API versions](#deprecate-legacy-api-versions) | v25.0 | v26.0 | | Removed | [Container short ID in network Aliases field](#container-short-id-in-network-aliases-field) | v25.0 | v26.0 | +| Deprecated | [Mount `bind-nonrecursive` option](#mount-bind-nonrecursive-option) | v25.0 | v29.0 | | Removed | [IsAutomated field, and `is-automated` filter on `docker search`](#isautomated-field-and-is-automated-filter-on-docker-search) | v25.0 | v28.2 | | Removed | [logentries logging driver](#logentries-logging-driver) | v24.0 | v25.0 | | Removed | [OOM-score adjust for the daemon](#oom-score-adjust-for-the-daemon) | v24.0 | v25.0 | @@ -121,10 +124,66 @@ The following table provides an overview of the current status of deprecated fea | Removed | [`--run` flag on `docker commit`](#--run-flag-on-docker-commit) | v0.10 | v1.13 | | Removed | [Three arguments form in `docker import`](#three-arguments-form-in-docker-import) | v0.6.7 | v1.12 | +### Legacy links environment variables + +**Deprecated in release: v28.4** + +**Disabled by default in release: v29.0** + +**Target for removal in release: v30.0** + +Containers attached to the default bridge network can specify "legacy links" (e.g. +using `--links` on the CLI) to get access to other containers attached to that +network. The linking container (i.e., the container created with `--links`) automatically +gets environment variables that specify the IP address and port mappings of the linked +container. However, these environment variables are prefixed with the linked +container's names, making them impractical. + +Starting with Docker v29.0, these environment variables are no longer set by +default. Users who still depend on them can start Docker Engine with the +environment variable `DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS=1` set. + +Support for legacy links environment variables, as well as the `DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS` +will be removed in Docker Engine v30.0. + +### Special handling for quoted values for TLS flags + +**Deprecated in release: v28.4** + +**Target for removal in release: v29.0** + +The `--tlscacert`, `--tlscert`, and `--tlskey` command-line flags had +non-standard behavior for handling values contained in quotes (`"` or `'`). +Normally, quotes are handled by the shell, for example, in the following +example, the shell takes care of handling quotes before passing the values +to the `docker` CLI: + +```console +docker --some-option "some-value-in-quotes" ... +``` + +However, when passing values using an equal sign (`=`), this may not happen +and values may be handled including quotes; + +```console +docker --some-option="some-value-in-quotes" ... +``` + +This caused issues with "Docker Machine", which used this format as part +of its `docker-machine config` output, and the CLI carried special, non-standard +handling for these flags. + +Docker Machine reached EOL, and this special handling made the processing +of flag values inconsistent with other flags used, so this behavior is +deprecated. Users depending on this behavior are recommended to specify +the quoted values using a space between the flag and its value, as illustrated +above. + ### Empty/nil fields in image Config from inspect API -**Deprecated in Release: v28.3** -**Target For Removal In Release: v29.0** +**Deprecated in release: v28.3** + +**Target for removal in release: v29.0** The `Config` field returned by `docker image inspect` (and the `GET /images/{name}/json` API endpoint) currently includes certain fields even when they are empty or nil. @@ -150,8 +209,9 @@ API version for backward compatibility. ### Configuration for pushing non-distributable artifacts -**Deprecated in Release: v28.0** -**Target For Removal In Release: v29.0** +**Deprecated in release: v28.0** + +**Target for removal in release: v29.0** Non-distributable artifacts (also called foreign layers) were introduced in docker v1.12 to accommodate Windows images for which the EULA did not allow @@ -189,7 +249,7 @@ entirely. ### `--time` option on `docker stop` and `docker restart` -**Deprecated in Release: v28.0** +**Deprecated in release: v28.0** The `--time` option for the `docker stop`, `docker container stop`, `docker restart`, and `docker container restart` commands has been renamed to `--timeout` for @@ -199,8 +259,9 @@ Users are encouraged to migrate to using the `--timeout` option instead. ### Non-standard fields in image inspect -**Deprecated in Release: v27.0** -**Removed In Release: v28.2** +**Deprecated in release: v27.0** + +**Removed in release: v28.2** The `Config` field returned shown in `docker image inspect` (and as returned by the `GET /images/{name}/json` API endpoint) returns additional fields that are @@ -234,9 +295,11 @@ They continue to be included when using clients that use an older API version: ### Graphdriver plugins (experimental) -**Deprecated in Release: v27.0** -**Disabled by default in Release: v27.0** -**Target For Removal In Release: v28.0** +**Deprecated in**: v27.0**. + +**Disabled by default in release: v27.0** + +**Target for removal in release: v28.0** [Graphdriver plugins](https://github.com/docker/cli/blob/v26.1.4/docs/extend/plugins_graphdriver.md) were an experimental feature that allowed extending the Docker Engine with custom @@ -250,8 +313,10 @@ and a custom [snapshotter](https://github.com/containerd/containerd/tree/v1.7.18 ### API CORS headers -**Deprecated in Release: v27.0** -**Disabled by default in Release: v27.0** +**Deprecated in release: v27.0** + +**Disabled by default in release: v27.0** + **Removed in release: v28.0** The `api-cors-header` configuration option for the Docker daemon is insecure, @@ -271,8 +336,9 @@ If you need to access the API through a browser, use a reverse proxy. ### Unauthenticated TCP connections -**Deprecated in Release: v26.0** -**Target For Removal In Release: v28.0** +**Deprecated in release: v26.0** + +**Target for removal in release: v28.0** Configuring the Docker daemon to listen on a TCP address will require mandatory TLS verification. This change aims to ensure secure communication by preventing @@ -298,8 +364,9 @@ configuring TLS (or SSH) for the Docker daemon, refer to ### `Container` and `ContainerConfig` fields in Image inspect -**Deprecated in Release: v25.0** -**Removed In Release: v26.0** +**Deprecated in release: v25.0** + +**Removed in release: v26.0** The `Container` and `ContainerConfig` fields returned by `docker inspect` are mostly an implementation detail of the classic (non-BuildKit) image builder. @@ -311,8 +378,9 @@ you can obtain it from the `Config` field. ### Deprecate legacy API versions -**Deprecated in Release: v25.0** -**Target For Removal In Release: v26.0** +**Deprecated in release: v25.0** + +**Target for removal in release: v26.0** The Docker daemon provides a versioned API for backward compatibility with old clients. Docker clients can perform API-version negotiation to select the most @@ -368,8 +436,9 @@ old clients, and those clients must be supported. ### Container short ID in network Aliases field -**Deprecated in Release: v25.0** -**Removed In Release: v26.0** +**Deprecated in release: v25.0** + +**Removed in release: v26.0** The `Aliases` field returned by `docker inspect` contains the container short ID once the container is started. This behavior is deprecated in v25.0 but @@ -381,10 +450,32 @@ A new field `DNSNames` containing the container name (if one was specified), the hostname, the network aliases, as well as the container short ID, has been introduced in v25.0 and should be used instead of the `Aliases` field. +### Mount `bind-nonrecursive` option + +**Deprecated in release: v25.0** + +**Scheduled for removal in release: v29.0** + +The `bind-nonrecursive` option was replaced with the [`bind-recursive`] +option (see [cli-4316], [cli-4671]). The option was still accepted, but +printed a deprecation warning: + +```console +bind-nonrecursive is deprecated, use bind-recursive=disabled instead +``` + +In the v29.0 release, this warning will be removed and returned as an error. +Users should use the equivalent `bind-recursive=disabled` option instead. + +[`bind-recursive`]: https://docs.docker.com/engine/storage/bind-mounts/#recursive-mounts +[cli-4316]: https://github.com/docker/cli/pull/4316 +[cli-4671]: https://github.com/docker/cli/pull/4671 + ### IsAutomated field, and `is-automated` filter on `docker search` -**Deprecated in Release: v25.0** -**Removed In Release: v28.2** +**Deprecated in release: v25.0** + +**Removed in release: v28.2** The `is_automated` field has been deprecated by Docker Hub's search API. Consequently, the `IsAutomated` field in image search will always be set @@ -397,8 +488,9 @@ templating has been removed in v28.2. ### Logentries logging driver -**Deprecated in Release: v24.0** -**Removed in Release: v25.0** +**Deprecated in release: v24.0** + +**Removed in release: v25.0** The logentries service SaaS was shut down on November 15, 2022, rendering this logging driver non-functional. Users should no longer use this logging @@ -408,8 +500,9 @@ after upgrading. ### OOM-score adjust for the daemon -**Deprecated in Release: v24.0** -**Removed in Release: v25.0** +**Deprecated in release: v24.0** + +**Removed in release: v25.0** The `oom-score-adjust` option was added to prevent the daemon from being OOM-killed before other processes. This option was mostly added as a @@ -428,8 +521,9 @@ the daemon. ### BuildKit build information -**Deprecated in Release: v23.0** -**Removed in Release: v24.0** +**Deprecated in release: v23.0** + +**Removed in release: v24.0** [Build information](https://github.com/moby/buildkit/blob/v0.11/docs/buildinfo.md) structures have been introduced in [BuildKit v0.10.0](https://github.com/moby/buildkit/releases/tag/v0.10.0) @@ -440,7 +534,7 @@ information is also embedded into the image configuration if one is generated. ### Legacy builder for Linux images -**Deprecated in Release: v23.0** +**Deprecated in release: v23.0** Docker v23.0 now uses BuildKit by default to build Linux images, and uses the [Buildx](https://docs.docker.com/buildx/working-with-buildx/) CLI component for @@ -471,7 +565,7 @@ you to report issues in the [BuildKit issue tracker on GitHub](https://github.co ### Legacy builder fallback -**Deprecated in Release: v23.0** +**Deprecated in release: v23.0** [Docker v23.0 now uses BuildKit by default to build Linux images](#legacy-builder-for-linux-images), which requires the Buildx component to build images with BuildKit. There may be @@ -517,7 +611,7 @@ be possible in a future release. ### Btrfs storage driver on CentOS 7 and RHEL 7 -**Removed in Release: v23.0** +**Removed in release: v23.0** The `btrfs` storage driver on CentOS and RHEL was provided as a technology preview by CentOS and RHEL, but has been deprecated since the [Red Hat Enterprise Linux 7.4 release](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/storage_administration_guide/ch-btrfs), @@ -529,9 +623,9 @@ of Docker will no longer provide this driver. ### Support for encrypted TLS private keys -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** -**Removed in Release: v23.0** +**Removed in release: v23.0** Use of encrypted TLS private keys has been deprecated, and has been removed. Golang has deprecated support for legacy PEM encryption (as specified in @@ -545,8 +639,9 @@ to decrypt the private key, and store it un-encrypted to continue using it. ### Kubernetes stack and context support -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Following the deprecation of [Compose on Kubernetes](https://github.com/docker/compose-on-kubernetes), support for Kubernetes in the `stack` and `context` commands has been removed from @@ -574,8 +669,9 @@ CLI configuration file are no longer used, and ignored. ### Pulling images from non-compliant image registries -**Deprecated in Release: v20.10** -**Removed in Release: v28.2** +**Deprecated in release: v20.10** + +**Removed in release: v28.2** Docker Engine v20.10 and up includes optimizations to verify if images in the local image cache need updating before pulling, preventing the Docker Engine @@ -604,8 +700,9 @@ no longer needed. ### Linux containers on Windows (LCOW) (experimental) -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** The experimental feature to run Linux containers on Windows (LCOW) was introduced as a technical preview in Docker 17.09. While many enhancements were made after @@ -617,7 +714,7 @@ Developers who want to run Linux workloads on a Windows host are encouraged to u ### BLKIO weight options with cgroups v1 -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** Specifying blkio weight (`docker run --blkio-weight` and `docker run --blkio-weight-device`) is now marked as deprecated when using cgroups v1 because the corresponding features @@ -627,8 +724,9 @@ When using cgroups v2, the `--blkio-weight` options are implemented using ### Kernel memory limit -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Specifying kernel memory limit (`docker run --kernel-memory`) is no longer supported because the [Linux kernel deprecated `kmem.limit_in_bytes` in v5.4](https://github.com/torvalds/linux/commit/0158115f702b0ba208ab0b5adf44cae99b3ebcc7). @@ -654,8 +752,9 @@ take no effect. ### Classic Swarm and overlay networks using cluster store -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** Standalone ("classic") Swarm has been deprecated, and with that the use of overlay networks using an external key/value store. The corresponding`--cluster-advertise`, @@ -663,8 +762,9 @@ networks using an external key/value store. The corresponding`--cluster-advertis ### Support for legacy `~/.dockercfg` configuration files -**Deprecated in Release: v20.10** -**Removed in Release: v23.0** +**Deprecated in release: v20.10** + +**Removed in release: v23.0** The Docker CLI up until v1.7.0 used the `~/.dockercfg` file to store credentials after authenticating to a registry (`docker login`). Docker v1.7.0 replaced this @@ -679,9 +779,9 @@ been removed. ### Configuration options for experimental CLI features -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v23.0** +**Removed in release: v23.0** The `DOCKER_CLI_EXPERIMENTAL` environment variable and the corresponding `experimental` field in the CLI configuration file are deprecated. Experimental features are @@ -693,13 +793,13 @@ format. ### CLI plugins support -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** CLI Plugin API is now marked as deprecated. ### Dockerfile legacy `ENV name value` syntax -**Deprecated in Release: v20.10** +**Deprecated in release: v20.10** The Dockerfile `ENV` instruction allows values to be set using either `ENV name=value` or `ENV name value`. The latter (`ENV name value`) form can be ambiguous, for example, @@ -723,8 +823,9 @@ ENV ONE="" TWO="" THREE="world" ### `docker build --stream` flag (experimental) -**Deprecated in Release: v20.10** -**Removed in Release: v20.10** +**Deprecated in release: v20.10** + +**Removed in release: v20.10** Docker v17.07 introduced an experimental `--stream` flag on `docker build` which allowed the build-context to be incrementally sent to the daemon, instead of @@ -740,8 +841,9 @@ files. ### `fluentd-async-connect` log opt -**Deprecated in Release: v20.10** -**Removed in Release: v28.0** +**Deprecated in release: v20.10** + +**Removed in release: v28.0** The `--log-opt fluentd-async-connect` option for the fluentd logging driver is [deprecated in favor of `--log-opt fluentd-async`](https://github.com/moby/moby/pull/39086). @@ -756,11 +858,11 @@ for the old option has been removed. ### Pushing and pulling with image manifest v2 schema 1 -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Disabled by default in Release: v26.0** +**Disabled by default in release: v26.0** -**Removed in Release: v28.2** +**Removed in release: v28.2** The image manifest [v2 schema 1](https://distribution.github.io/distribution/spec/deprecated-schema-v1/) @@ -785,9 +887,9 @@ More information at https://docs.docker.com/go/deprecated-image-specs/ ### `docker engine` subcommands -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** The `docker engine activate`, `docker engine check`, and `docker engine update` provided an alternative installation method to upgrade Docker Community engines @@ -800,9 +902,9 @@ standard package managers. ### Top-level `docker deploy` subcommand (experimental) -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** The top-level `docker deploy` command (using the "Docker Application Bundle" (.dab) file format was introduced as an experimental feature in Docker 1.13 / @@ -811,9 +913,9 @@ subcommand. ### `docker stack deploy` using "dab" files (experimental) -**Deprecated in Release: v19.03** +**Deprecated in release: v19.03** -**Removed in Release: v20.10** +**Removed in release: v20.10** With no development being done on this feature, and no active use of the file format, support for the DAB file format and the top-level `docker deploy` command @@ -822,8 +924,9 @@ using compose files. ### Support for the `overlay2.override_kernel_check` storage option -**Deprecated in Release: v19.03** -**Removed in Release: v24.0** +**Deprecated in release: v19.03** + +**Removed in release: v24.0** This daemon configuration option disabled the Linux kernel version check used to detect if the kernel supported OverlayFS with multiple lower dirs, which is @@ -833,8 +936,9 @@ option was no longer used. ### AuFS storage driver -**Deprecated in Release: v19.03** -**Removed in Release: v24.0** +**Deprecated in release: v19.03** + +**Removed in release: v24.0** The `aufs` storage driver is deprecated in favor of `overlay2`, and has been removed in a Docker Engine v24.0. Users of the `aufs` storage driver must @@ -852,8 +956,9 @@ maintenance of the `aufs` storage driver. ### Legacy overlay storage driver -**Deprecated in Release: v18.09** -**Removed in Release: v24.0** +**Deprecated in release: v18.09** + +**Removed in release: v24.0** The `overlay` storage driver is deprecated in favor of the `overlay2` storage driver, which has all the benefits of `overlay`, without its limitations (excessive @@ -868,9 +973,11 @@ backported), there is no reason to keep maintaining the `overlay` storage driver ### Device mapper storage driver -**Deprecated in Release: v18.09** -**Disabled by default in Release: v23.0** -**Removed in Release: v25.0** +**Deprecated in release: v18.09** + +**Disabled by default in release: v23.0** + +**Removed in release: v25.0** The `devicemapper` storage driver is deprecated in favor of `overlay2`, and has been removed in Docker Engine v25.0. Users of the `devicemapper` storage driver @@ -886,9 +993,9 @@ is no reason to continue maintenance of the `devicemapper` storage driver. ### Use of reserved namespaces in engine labels -**Deprecated in Release: v18.06** +**Deprecated in release: v18.06** -**Removed In Release: v20.10** +**Removed in release: v20.10** The namespaces `com.docker.*`, `io.docker.*`, and `org.dockerproject.*` in engine labels were always documented to be reserved, but there was never any enforcement. @@ -900,7 +1007,7 @@ use, and will error instead in v20.10 and above. **Disabled In Release: v17.12** -**Removed In Release: v19.03** +**Removed in release: v19.03** The `--disable-legacy-registry` flag was disabled in Docker 17.12 and will print an error when used. For this error to be printed, the flag itself is still present, @@ -908,9 +1015,9 @@ but hidden. The flag has been removed in Docker 19.03. ### Interacting with V1 registries -**Disabled By Default In Release: v17.06** +**Disabled by default in release: v17.06** -**Removed In Release: v17.12** +**Removed in release: v17.12** Version 1.8.3 added a flag (`--disable-legacy-registry=false`) which prevents the Docker daemon from `pull`, `push`, and `login` operations against v1 @@ -927,7 +1034,7 @@ start when set. ### Asynchronous `service create` and `service update` as default -**Deprecated In Release: v17.05** +**Deprecated in release: v17.05** **Disabled by default in release: [v17.10](https://github.com/docker/docker-ce/releases/tag/v17.10.0-ce)** @@ -941,9 +1048,9 @@ and `docker service scale` in Docker 17.10. ### `-g` and `--graph` flags on `dockerd` -**Deprecated In Release: v17.05** +**Deprecated in release: v17.05** -**Removed In Release: v23.0** +**Removed in release: v23.0** The `-g` or `--graph` flag for the `dockerd` or `docker daemon` command was used to indicate the directory in which to store persistent data and resource @@ -952,9 +1059,9 @@ flag. These flags were deprecated and hidden in v17.05, and removed in v23.0. ### Top-level network properties in NetworkSettings -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Target For Removal In Release: v17.12** +**Target for removal in release: v17.12** When inspecting a container, `NetworkSettings` contains top-level information about the default ("bridge") network; @@ -971,18 +1078,18 @@ information. ### `filter` option for `/images/json` endpoint -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v20.10** +**Removed in release: v20.10** The `filter` option to filter the list of image by reference (name or name:tag) is now implemented as a regular filter, named `reference`. ### `repository:shortid` image references -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The `repository:shortid` syntax for referencing images is very little used, collides with tag references, and can be confused with digest references. @@ -992,32 +1099,32 @@ in Docker 17.12. ### `docker daemon` subcommand -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The daemon is moved to a separate binary (`dockerd`), and should be used instead. ### Duplicate keys with conflicting values in engine labels -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** When setting duplicate keys with conflicting values, an error will be produced, and the daemon will fail to start. ### `MAINTAINER` in Dockerfile -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** `MAINTAINER` was an early very limited form of `LABEL` which should be used instead. ### API calls without a version -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Target For Removal In Release: v17.12** +**Target for removal in release: v17.12** API versions should be supplied to all API calls to ensure compatibility with future Engine versions. Instead of just requesting, for example, the URL @@ -1025,9 +1132,9 @@ future Engine versions. Instead of just requesting, for example, the URL ### Backing filesystem without `d_type` support for overlay/overlay2 -**Deprecated In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Deprecated in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** -**Removed In Release: v17.12** +**Removed in release: v17.12** The overlay and overlay2 storage driver does not work as expected if the backing filesystem does not support `d_type`. For example, XFS does not support `d_type` @@ -1041,18 +1148,18 @@ Refer to [#27358](https://github.com/docker/docker/issues/27358) for details. ### `--automated` and `--stars` flags on `docker search` -**Deprecated in Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Deprecated in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** -**Removed In Release: v20.10** +**Removed in release: v20.10** The `docker search --automated` and `docker search --stars` options are deprecated. Use `docker search --filter=is-automated=` and `docker search --filter=stars=...` instead. ### `-h` shorthand for `--help` -**Deprecated In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Deprecated in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** -**Target For Removal In Release: v17.09** +**Target for removal in release: v17.09** The shorthand (`-h`) is less common than `--help` on Linux and cannot be used on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on @@ -1061,58 +1168,58 @@ on all subcommands (due to it conflicting with, e.g. `-h` / `--hostname` on ### `-e` and `--email` flags on `docker login` -**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** +**Deprecated in release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** -**Removed In Release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** +**Removed in release: [v17.06](https://github.com/docker/docker-ce/releases/tag/v17.06.0-ce)** The `docker login` no longer automatically registers an account with the target registry if the given username doesn't exist. Due to this change, the email flag is no longer required, and will be deprecated. ### Separator (`:`) of `--security-opt` flag on `docker run` -**Deprecated In Release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** +**Deprecated in release: [v1.11.0](https://github.com/docker/docker/releases/tag/v1.11.0)** -**Target For Removal In Release: v17.06** +**Target for removal in release: v17.06** The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`. ### Ambiguous event fields in API -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The fields `ID`, `Status` and `From` in the events API have been deprecated in favor of a more rich structure. See the events API documentation for the new format. ### `-f` flag on `docker tag` -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** To make tagging consistent across the various `docker` commands, the `-f` flag on the `docker tag` command is deprecated. It is no longer necessary to specify `-f` to move a tag from one image to another. Nor will `docker` generate an error if the `-f` flag is missing and the specified tag is already in use. ### HostConfig at API container start -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Passing an `HostConfig` to `POST /containers/{name}/start` is deprecated in favor of defining it at container creation (`POST /containers/create`). ### `--before` and `--since` flags on `docker ps` -**Deprecated In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The `docker ps --before` and `docker ps --since` options are deprecated. Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead. ### Driver-specific log tags -**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** +**Deprecated in release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Log tags are now generated in a standard way across different logging drivers. Because of which, the driver specific log tag options `syslog-tag`, `gelf-tag` and @@ -1124,9 +1231,9 @@ $ docker --log-driver=syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}" ### Docker Content Trust ENV passphrase variables name change -**Deprecated In Release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** +**Deprecated in release: [v1.9.0](https://github.com/docker/docker/releases/tag/v1.9.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the Tagging key has been renamed to Repository key. Due to this renaming, we're also changing the corresponding environment variables @@ -1135,25 +1242,25 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the ### `/containers/(id or name)/copy` endpoint -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The endpoint `/containers/(id or name)/copy` is deprecated in favor of `/containers/(id or name)/archive`. ### LXC built-in exec driver -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Removed in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The built-in LXC execution driver, the lxc-conf flag, and API fields have been removed. ### Old Command Line Options -**Deprecated In Release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** +**Deprecated in release: [v1.8.0](https://github.com/docker/docker/releases/tag/v1.8.0)** -**Removed In Release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** +**Removed in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)** The flags `-d` and `--daemon` are deprecated. Use the separate `dockerd` binary instead. @@ -1197,34 +1304,34 @@ The following double-dash options are deprecated and have no replacement: - `docker ps --before-id` - `docker search --trusted` -**Deprecated In Release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)** +**Deprecated in release: [v1.5.0](https://github.com/docker/docker/releases/tag/v1.5.0)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The single-dash (`-help`) was removed, in favor of the double-dash `--help` ### `--api-enable-cors` flag on `dockerd` -**Deprecated In Release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)** +**Deprecated in release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)** -**Removed In Release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)** +**Removed in release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)** The flag `--api-enable-cors` is deprecated since v1.6.0. Use the flag `--api-cors-header` instead. ### `--run` flag on `docker commit` -**Deprecated In Release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)** +**Deprecated in release: [v0.10.0](https://github.com/docker/docker/releases/tag/v0.10.0)** -**Removed In Release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** +**Removed in release: [v1.13.0](https://github.com/docker/docker/releases/tag/v1.13.0)** The flag `--run` of the `docker commit` command (and its short version `-run`) were deprecated in favor of the `--changes` flag that allows to pass `Dockerfile` commands. ### Three arguments form in `docker import` -**Deprecated In Release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)** +**Deprecated in release: [v0.6.7](https://github.com/docker/docker/releases/tag/v0.6.7)** -**Removed In Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** +**Removed in release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** The `docker import` command format `file|URL|- [REPOSITORY [TAG]]` is deprecated since November 2013. It's no longer supported. diff --git a/docs/reference/commandline/docker.md b/docs/reference/commandline/docker.md index 69b1c91303be..03dbddc8cced 100644 --- a/docs/reference/commandline/docker.md +++ b/docs/reference/commandline/docker.md @@ -74,7 +74,7 @@ The base command for the Docker CLI. | `--config` | `string` | `/root/.docker` | Location of client config files | | `-c`, `--context` | `string` | | Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with `docker context use`) | | `-D`, `--debug` | `bool` | | Enable debug mode | -| [`-H`](#host), [`--host`](#host) | `list` | | Daemon socket to connect to | +| [`-H`](#host), [`--host`](#host) | `string` | | Daemon socket to connect to | | `-l`, `--log-level` | `string` | `info` | Set the logging level (`debug`, `info`, `warn`, `error`, `fatal`) | | `--tls` | `bool` | | Use TLS; implied by --tlsverify | | `--tlscacert` | `string` | `/root/.docker/ca.pem` | Trust certs signed only by this CA | diff --git a/docs/reference/commandline/system_prune.md b/docs/reference/commandline/system_prune.md index 0d39d50feb99..39a58c54996c 100644 --- a/docs/reference/commandline/system_prune.md +++ b/docs/reference/commandline/system_prune.md @@ -72,7 +72,8 @@ my-network-a my-network-b Deleted Volumes: -named-vol +1e31bcd425e913d9f65ec0c3841e9c4ebb543aead2a1cfe0d95a7c5e88bb5026 +6a6ab3d6b8d740a1c1d4dbe36a9c5f043dd4bac5f78abfa7d1f2ae5789fe60b0 Deleted Images: untagged: my-curl:latest diff --git a/docs/reference/run.md b/docs/reference/run.md index db06ad71f669..9c8b8baf0f25 100644 --- a/docs/reference/run.md +++ b/docs/reference/run.md @@ -248,7 +248,7 @@ $ docker run -it --mount type=bind,source=[PATH],target=[PATH] busybox ``` In this case, the `--mount` flag takes three parameters. A type (`bind`), and -two paths. The `source` path is a the location on the host that you want to +two paths. The `source` path is the location on the host that you want to bind mount into the container. The `target` path is the mount destination inside the container. @@ -419,7 +419,7 @@ $ docker run -it -m 300M ubuntu:24.04 /bin/bash We set memory limit only, this means the processes in the container can use 300M memory and 300M swap memory, by default, the total virtual memory size -(--memory-swap) will be set as double of memory, in this case, memory + swap +(`--memory-swap`) will be set as double of memory, in this case, memory + swap would be 2*300M, so processes can use 300M swap memory as well. ```console @@ -1087,7 +1087,7 @@ Additionally, you can set any environment variable in the container by using one or more `-e` flags. You can even override the variables mentioned above, or variables defined using a Dockerfile `ENV` instruction when building the image. -If the you name an environment variable without specifying a value, the current +If you name an environment variable without specifying a value, the current value of the named variable on the host is propagated into the container's environment: diff --git a/e2e/cli-plugins/flags_test.go b/e2e/cli-plugins/flags_test.go index 69fe6d6ad3b4..0cb24ba2488e 100644 --- a/e2e/cli-plugins/flags_test.go +++ b/e2e/cli-plugins/flags_test.go @@ -1,6 +1,7 @@ package cliplugins import ( + "fmt" "os" "testing" @@ -91,9 +92,9 @@ func TestGlobalArgsOnlyParsedOnce(t *testing.T) { // This is checking the precondition wrt -H mentioned in the function comment name: "fails-if-H-used-twice", args: []string{"-H", dh, "-H", dh, "version", "-f", "{{.Client.Version}}"}, - expectedExitCode: 1, + expectedExitCode: 125, expectedOut: icmd.None, - expectedErr: "Specify only one -H", + expectedErr: fmt.Sprintf(`invalid argument %q for "-H, --host" flag: specify only one -H`, dh), }, { name: "builtin", diff --git a/e2e/internal/fixtures/fixtures.go b/e2e/internal/fixtures/fixtures.go index f6c49f50c794..3c47f8d7ff04 100644 --- a/e2e/internal/fixtures/fixtures.go +++ b/e2e/internal/fixtures/fixtures.go @@ -44,8 +44,7 @@ func SetupConfigWithNotaryURL(t *testing.T, path, notaryURL string) fs.Dir { "%s": { "auth": "ZWlhaXM6cGFzc3dvcmQK" } - }, - "experimental": "enabled" + } } `, notaryURL)), fs.WithDir("trust", fs.WithDir("private"))) return *dir diff --git a/e2e/testdata/Dockerfile.connhelper-ssh b/e2e/testdata/Dockerfile.connhelper-ssh index ccbe66da05a2..5fd05c3c858f 100644 --- a/e2e/testdata/Dockerfile.connhelper-ssh +++ b/e2e/testdata/Dockerfile.connhelper-ssh @@ -7,8 +7,8 @@ ARG ENGINE_VERSION=28 FROM docker:${ENGINE_VERSION}-dind # the openssh-client update is needed for security reasons when using docker:23.0-dind, currently maintained as an lts by mirantis -RUN apk --no-cache upgrade openssh-client && \ - apk --no-cache add shadow openssh-server && \ +RUN apk --no-cache add openssl openssh-client openssh-server shadow && \ + apk --no-cache upgrade openssl openssh-client openssh-server && \ # TODO(krissetto): `groupadd` can be removed once we only test against moby >= v24 # see https://github.com/docker-library/docker/pull/470 groupadd -f docker && \ diff --git a/e2e/testdata/Dockerfile.gencerts b/e2e/testdata/Dockerfile.gencerts index 390ed58e0b47..e084a1faff8d 100644 --- a/e2e/testdata/Dockerfile.gencerts +++ b/e2e/testdata/Dockerfile.gencerts @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG GO_VERSION=1.24.5 +ARG GO_VERSION=1.24.9 FROM golang:${GO_VERSION}-alpine AS generated ENV GOTOOLCHAIN=local diff --git a/e2e/testutils/plugins.go b/e2e/testutils/plugins.go index 1485f425b1fa..7406798fd764 100644 --- a/e2e/testutils/plugins.go +++ b/e2e/testutils/plugins.go @@ -32,7 +32,11 @@ func SetupPlugin(t *testing.T, ctx context.Context) *fs.Dir { }, Interface: types.PluginConfigInterface{ Socket: "basic.sock", - Types: []types.PluginInterfaceType{{Capability: "docker.dummy/1.0"}}, + Types: []types.PluginInterfaceType{{ + Capability: "dummy", + Prefix: "docker", + Version: "1.0", + }}, }, Entrypoint: []string{"/basic"}, } diff --git a/internal/jsonstream/display_test.go b/internal/jsonstream/display_test.go index 7b14db2a9fac..3a9c118ed813 100644 --- a/internal/jsonstream/display_test.go +++ b/internal/jsonstream/display_test.go @@ -30,10 +30,8 @@ func TestDisplay(t *testing.T) { return default: err := enc.Encode(JSONMessage{ - Status: "Downloading", - ID: fmt.Sprintf("id-%d", i), - TimeNano: time.Now().UnixNano(), - Time: time.Now().Unix(), + Status: "Downloading", + ID: fmt.Sprintf("id-%d", i), Progress: &JSONProgress{ Current: int64(i), Total: 100, diff --git a/internal/oauth/manager/manager.go b/internal/oauth/manager/manager.go index 115006436404..96deb61b1325 100644 --- a/internal/oauth/manager/manager.go +++ b/internal/oauth/manager/manager.go @@ -14,8 +14,8 @@ import ( "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/oauth" "github.com/docker/cli/internal/oauth/api" + "github.com/docker/cli/internal/registry" "github.com/docker/cli/internal/tui" - "github.com/docker/docker/registry" "github.com/morikuni/aec" "github.com/sirupsen/logrus" diff --git a/vendor/github.com/docker/docker/registry/auth.go b/internal/registry/auth.go similarity index 60% rename from vendor/github.com/docker/docker/registry/auth.go rename to internal/registry/auth.go index 1b0eeeed0b1c..4a042c9a17c4 100644 --- a/vendor/github.com/docker/docker/registry/auth.go +++ b/internal/registry/auth.go @@ -2,6 +2,7 @@ package registry import ( "context" + "fmt" "net/http" "net/url" "strings" @@ -12,7 +13,6 @@ import ( "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" ) // AuthClientID is used the ClientID used for the token server @@ -34,35 +34,6 @@ func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token strin lcs.authConfig.IdentityToken = token } -type staticCredentialStore struct { - auth *registry.AuthConfig -} - -// NewStaticCredentialStore returns a credential store -// which always returns the same credential values. -func NewStaticCredentialStore(auth *registry.AuthConfig) auth.CredentialStore { - return staticCredentialStore{ - auth: auth, - } -} - -func (scs staticCredentialStore) Basic(*url.URL) (string, string) { - if scs.auth == nil { - return "", "" - } - return scs.auth.Username, scs.auth.Password -} - -func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { - if scs.auth == nil { - return "" - } - return scs.auth.IdentityToken -} - -func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - // loginV2 tries to login to the v2 registry server. The given registry // endpoint will be pinged to get authorization challenges. These challenges // will be used to authenticate against the registry to validate credentials. @@ -96,7 +67,7 @@ func loginV2(ctx context.Context, authConfig *registry.AuthConfig, endpoint APIE if resp.StatusCode != http.StatusOK { // TODO(dmcgowan): Attempt to further interpret result, status code and error code string - return "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + return "", fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode)) } return credentialAuthConfig.IdentityToken, nil @@ -127,64 +98,19 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi }, nil } -// ConvertToHostname normalizes a registry URL which has http|https prepended -// to just its hostname. It is used to match credentials, which may be either -// stored as hostname or as hostname including scheme (in legacy configuration -// files). -func ConvertToHostname(url string) string { - stripped := url - if strings.HasPrefix(stripped, "http://") { - stripped = strings.TrimPrefix(stripped, "http://") - } else if strings.HasPrefix(stripped, "https://") { - stripped = strings.TrimPrefix(stripped, "https://") - } - stripped, _, _ = strings.Cut(stripped, "/") - return stripped -} - -// ResolveAuthConfig matches an auth configuration to a server address or a URL -func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig { - configKey := GetAuthConfigKey(index) - // First try the happy case - if c, found := authConfigs[configKey]; found || index.Official { - return c - } - - // Maybe they have a legacy config file, we will iterate the keys converting - // them to the new format and testing - for registryURL, ac := range authConfigs { - if configKey == ConvertToHostname(registryURL) { - return ac - } - } - - // When all else fails, return an empty auth config - return registry.AuthConfig{} -} - -// PingResponseError is used when the response from a ping -// was received but invalid. -type PingResponseError struct { - Err error -} - -func (err PingResponseError) Error() string { - return err.Err.Error() -} - // PingV2Registry attempts to ping a v2 registry and on success return a // challenge manager for the supported authentication types. // If a response is received but cannot be interpreted, a PingResponseError will be returned. -func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, error) { - pingClient := &http.Client{ - Transport: transport, - Timeout: 15 * time.Second, - } +func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) { endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/" req, err := http.NewRequest(http.MethodGet, endpointStr, http.NoBody) if err != nil { return nil, err } + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 15 * time.Second, + } resp, err := pingClient.Do(req) if err != nil { return nil, err @@ -193,9 +119,7 @@ func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.M challengeManager := challenge.NewSimpleManager() if err := challengeManager.AddResponse(resp); err != nil { - return nil, PingResponseError{ - Err: err, - } + return nil, err } return challengeManager, nil diff --git a/internal/registry/config.go b/internal/registry/config.go new file mode 100644 index 000000000000..04414cc88b36 --- /dev/null +++ b/internal/registry/config.go @@ -0,0 +1,310 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.23 + +package registry + +import ( + "context" + "fmt" + "net" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + + "github.com/containerd/log" + "github.com/distribution/reference" + "github.com/docker/docker/api/types/registry" +) + +// ServiceOptions holds command line options. +// +// TODO(thaJeztah): add CertsDir as option to replace the [CertsDir] function, which sets the location magically. +type ServiceOptions struct { + InsecureRegistries []string `json:"insecure-registries,omitempty"` +} + +// serviceConfig holds daemon configuration for the registry service. +// +// It's a reduced version of [registry.ServiceConfig] for the CLI. +type serviceConfig struct { + insecureRegistryCIDRs []*net.IPNet + indexConfigs map[string]*registry.IndexInfo +} + +// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains +// are here for historic reasons and backward-compatibility. These domains +// are still supported by Docker Hub (and will continue to be supported), but +// there are new domains already in use, and plans to consolidate all legacy +// domains to new "canonical" domains. Once those domains are decided on, we +// should update these consts (but making sure to preserve compatibility with +// existing installs, clients, and user configuration). +const ( + // DefaultNamespace is the default namespace + DefaultNamespace = "docker.io" + // IndexHostname is the index hostname, used for authentication and image search. + IndexHostname = "index.docker.io" + // IndexServer is used for user auth and image search + IndexServer = "https://index.docker.io/v1/" + // IndexName is the name of the index + IndexName = "docker.io" +) + +var ( + // DefaultV2Registry is the URI of the default (Docker Hub) registry + // used for pushing and pulling images. This hostname is hard-coded to handle + // the conversion from image references without registry name (e.g. "ubuntu", + // or "ubuntu:latest"), as well as references using the "docker.io" domain + // name, which is used as canonical reference for images on Docker Hub, but + // does not match the domain-name of Docker Hub's registry. + DefaultV2Registry = &url.URL{Scheme: "https", Host: "registry-1.docker.io"} + + validHostPortRegex = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`) + }) +) + +// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit], +// but inlining it to prevent adding that as a dependency for docker/cli. +// +// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 +func runningWithRootlessKit() bool { + return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != "" +} + +// CertsDir is the directory where certificates are stored. +// +// - Linux: "/etc/docker/certs.d/" +// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/" +// - Windows: "%PROGRAMDATA%/docker/certs.d/" +// +// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless. +func CertsDir() string { + certsDir := "/etc/docker/certs.d" + if runningWithRootlessKit() { + if configHome, _ := os.UserConfigDir(); configHome != "" { + certsDir = filepath.Join(configHome, "docker", "certs.d") + } + } else if runtime.GOOS == "windows" { + certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d") + } + return certsDir +} + +// newServiceConfig creates a new service config with the given options. +func newServiceConfig(registries []string) (*serviceConfig, error) { + if len(registries) == 0 { + return &serviceConfig{}, nil + } + // Localhost is by default considered as an insecure registry. This is a + // stop-gap for people who are running a private registry on localhost. + registries = append(registries, "::1/128", "127.0.0.0/8") + + var ( + insecureRegistryCIDRs = make([]*net.IPNet, 0) + indexConfigs = make(map[string]*registry.IndexInfo) + ) + +skip: + for _, r := range registries { + if scheme, host, ok := strings.Cut(r, "://"); ok { + switch strings.ToLower(scheme) { + case "http", "https": + log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme) + r = host + default: + // unsupported scheme + return nil, invalidParam(fmt.Errorf("insecure registry %s should not contain '://'", r)) + } + } + // Check if CIDR was passed to --insecure-registry + _, ipnet, err := net.ParseCIDR(r) + if err == nil { + // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. + for _, value := range insecureRegistryCIDRs { + if value.IP.String() == ipnet.IP.String() && value.Mask.String() == ipnet.Mask.String() { + continue skip + } + } + // ipnet is not found, add it in config.InsecureRegistryCIDRs + insecureRegistryCIDRs = append(insecureRegistryCIDRs, ipnet) + } else { + if err := validateHostPort(r); err != nil { + return nil, invalidParam(fmt.Errorf("insecure registry %s is not valid: %w", r, err)) + } + // Assume `host:port` if not CIDR. + indexConfigs[r] = ®istry.IndexInfo{ + Name: r, + Secure: false, + Official: false, + } + } + } + + // Configure public registry. + indexConfigs[IndexName] = ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + + return &serviceConfig{ + indexConfigs: indexConfigs, + insecureRegistryCIDRs: insecureRegistryCIDRs, + }, nil +} + +// isSecureIndex returns false if the provided indexName is part of the list of insecure registries +// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. +// +// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. +// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered +// insecure. +// +// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name +// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained +// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element +// of insecureRegistries. +func (config *serviceConfig) isSecureIndex(indexName string) bool { + // Check for configured index, first. This is needed in case isSecureIndex + // is called from anything besides newIndexInfo, in order to honor per-index configurations. + if index, ok := config.indexConfigs[indexName]; ok { + return index.Secure + } + + return !isCIDRMatch(config.insecureRegistryCIDRs, indexName) +} + +// for mocking in unit tests. +var lookupIP = net.LookupIP + +// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is a URL.Host ("host:port" or "host") +// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be +// resolved to IP addresses for matching. If resolution fails, false is returned. +func isCIDRMatch(cidrs []*net.IPNet, urlHost string) bool { + if len(cidrs) == 0 { + return false + } + + host, _, err := net.SplitHostPort(urlHost) + if err != nil { + // Assume urlHost is a host without port and go on. + host = urlHost + } + + var addresses []net.IP + if ip := net.ParseIP(host); ip != nil { + // Host is an IP-address. + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-address. + addresses, err = lookupIP(host) + if err != nil { + // We failed to resolve the host; assume there's no match. + return false + } + } + + for _, addr := range addresses { + for _, ipnet := range cidrs { + // check if the addr falls in the subnet + if ipnet.Contains(addr) { + return true + } + } + } + + return false +} + +func normalizeIndexName(val string) string { + if val == "index.docker.io" { + return "docker.io" + } + return val +} + +func validateHostPort(s string) error { + // Split host and port, and in case s can not be split, assume host only + host, port, err := net.SplitHostPort(s) + if err != nil { + host = s + port = "" + } + // If match against the `host:port` pattern fails, + // it might be `IPv6:port`, which will be captured by net.ParseIP(host) + if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil { + return invalidParamf("invalid host %q", host) + } + if port != "" { + v, err := strconv.Atoi(port) + if err != nil { + return err + } + if v < 0 || v > 65535 { + return invalidParamf("invalid port %q", port) + } + } + return nil +} + +// NewIndexInfo creates a new [registry.IndexInfo] or the given +// repository-name, and detects whether the registry is considered +// "secure" (non-localhost). +func NewIndexInfo(reposName reference.Named) *registry.IndexInfo { + indexName := normalizeIndexName(reference.Domain(reposName)) + if indexName == IndexName { + return ®istry.IndexInfo{ + Name: IndexName, + Secure: true, + Official: true, + } + } + + return ®istry.IndexInfo{ + Name: indexName, + Secure: !isInsecure(indexName), + } +} + +// isInsecure is used to detect whether a registry domain or IP-address is allowed +// to use an insecure (non-TLS, or self-signed cert) connection according to the +// defaults, which allows for insecure connections with registries running on a +// loopback address ("localhost", "::1/128", "127.0.0.0/8"). +// +// It is used in situations where we don't have access to the daemon's configuration, +// for example, when used from the client / CLI. +func isInsecure(hostNameOrIP string) bool { + // Attempt to strip port if present; this also strips brackets for + // IPv6 addresses with a port (e.g. "[::1]:5000"). + // + // This is best-effort; we'll continue using the address as-is if it fails. + if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { + hostNameOrIP = host + } + if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { + // Fast path; no need to resolve these, assuming nobody overrides + // "localhost" for anything else than a loopback address (sorry, not sorry). + return true + } + + var addresses []net.IP + if ip := net.ParseIP(hostNameOrIP); ip != nil { + addresses = append(addresses, ip) + } else { + // Try to resolve the host's IP-addresses. + addrs, _ := lookupIP(hostNameOrIP) + addresses = append(addresses, addrs...) + } + + for _, addr := range addresses { + if addr.IsLoopback() { + return true + } + } + return false +} diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go new file mode 100644 index 000000000000..9c21929f281b --- /dev/null +++ b/internal/registry/config_test.go @@ -0,0 +1,92 @@ +package registry + +import ( + "testing" + + "github.com/containerd/errdefs" + "gotest.tools/v3/assert" +) + +func TestLoadInsecureRegistries(t *testing.T) { + testCases := []struct { + registries []string + index string + err string + }{ + { + registries: []string{"127.0.0.1"}, + index: "127.0.0.1", + }, + { + registries: []string{"127.0.0.1:8080"}, + index: "127.0.0.1:8080", + }, + { + registries: []string{"2001:db8::1"}, + index: "2001:db8::1", + }, + { + registries: []string{"[2001:db8::1]:80"}, + index: "[2001:db8::1]:80", + }, + { + registries: []string{"http://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"https://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"HTTP://myregistry.example.com"}, + index: "myregistry.example.com", + }, + { + registries: []string{"svn://myregistry.example.com"}, + err: "insecure registry svn://myregistry.example.com should not contain '://'", + }, + { + registries: []string{`mytest-.com`}, + err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`, + }, + { + registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`}, + err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`, + }, + { + registries: []string{`myregistry.example.com:500000`}, + err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`, + }, + { + registries: []string{`"myregistry.example.com"`}, + err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`, + }, + { + registries: []string{`"myregistry.example.com:5000"`}, + err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`, + }, + } + for _, testCase := range testCases { + config, err := newServiceConfig(testCase.registries) + if testCase.err == "" { + if err != nil { + t.Fatalf("expect no error, got '%s'", err) + } + match := false + for index := range config.indexConfigs { + if index == testCase.index { + match = true + } + } + if !match { + t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.indexConfigs) + } + } else { + if err == nil { + t.Fatalf("expect error '%s', got no error", testCase.err) + } + assert.ErrorContains(t, err, testCase.err) + assert.Check(t, errdefs.IsInvalidArgument(err)) + } + } +} diff --git a/internal/registry/doc.go b/internal/registry/doc.go new file mode 100644 index 000000000000..0b6a24767c84 --- /dev/null +++ b/internal/registry/doc.go @@ -0,0 +1,12 @@ +// Package registry is a fork of [github.com/docker/docker/registry], taken +// at commit [moby@49306c6]. Git history was not preserved in this fork, +// but can be found using the URLs provided. +// +// This fork was created to remove the dependency on the "Moby" codebase, +// and because the CLI only needs a subset of its features. The original +// package was written specifically for use in the daemon code, and includes +// functionality that cannot be used in the CLI. +// +// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry +// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry +package registry diff --git a/internal/registry/errors.go b/internal/registry/errors.go new file mode 100644 index 000000000000..e27eb3e7a682 --- /dev/null +++ b/internal/registry/errors.go @@ -0,0 +1,51 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.23 + +package registry + +import ( + "errors" + "fmt" + "net/url" + + "github.com/docker/distribution/registry/api/errcode" +) + +func translateV2AuthError(err error) error { + var e *url.Error + if errors.As(err, &e) { + var e2 errcode.Error + if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) { + return unauthorizedErr{err} + } + } + return err +} + +func invalidParam(err error) error { + return invalidParameterErr{err} +} + +func invalidParamf(format string, args ...any) error { + return invalidParameterErr{fmt.Errorf(format, args...)} +} + +type unauthorizedErr struct{ error } + +func (unauthorizedErr) Unauthorized() {} + +func (e unauthorizedErr) Cause() error { + return e.error +} + +func (e unauthorizedErr) Unwrap() error { + return e.error +} + +type invalidParameterErr struct{ error } + +func (invalidParameterErr) InvalidParameter() {} + +func (e invalidParameterErr) Unwrap() error { + return e.error +} diff --git a/vendor/github.com/docker/docker/registry/registry.go b/internal/registry/registry.go similarity index 89% rename from vendor/github.com/docker/docker/registry/registry.go rename to internal/registry/registry.go index d3b3fbc9baef..97946d7fd879 100644 --- a/vendor/github.com/docker/docker/registry/registry.go +++ b/internal/registry/registry.go @@ -4,10 +4,13 @@ package registry import ( "context" "crypto/tls" + "fmt" "net" "net/http" "os" "path/filepath" + "runtime" + "strings" "time" "github.com/containerd/log" @@ -16,16 +19,15 @@ import ( "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) -// HostCertsDir returns the config directory for a specific host. -// -// Deprecated: this function was only used internally, and will be removed in a future release. -func HostCertsDir(hostname string) string { - return hostCertsDir(hostname) -} - // hostCertsDir returns the config directory for a specific host. -func hostCertsDir(hostname string) string { - return filepath.Join(CertsDir(), cleanPath(hostname)) +func hostCertsDir(hostnameAndPort string) string { + if runtime.GOOS == "windows" { + // Ensure that a directory name is valid; hostnameAndPort may contain + // a colon (:) if a port is included, and Windows does not allow colons + // in directory names. + hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", "")) + } + return filepath.Join(CertsDir(), hostnameAndPort) } // newTLSConfig constructs a client TLS configuration based on server defaults @@ -81,7 +83,7 @@ func loadTLSConfig(ctx context.Context, directory string, tlsConfig *tls.Config) if tlsConfig.RootCAs == nil { systemPool, err := tlsconfig.SystemCertPool() if err != nil { - return invalidParamWrapf(err, "unable to get system cert pool") + return invalidParam(fmt.Errorf("unable to get system cert pool: %w", err)) } tlsConfig.RootCAs = systemPool } diff --git a/internal/registry/registry_mock_test.go b/internal/registry/registry_mock_test.go new file mode 100644 index 000000000000..13c637852eb2 --- /dev/null +++ b/internal/registry/registry_mock_test.go @@ -0,0 +1,92 @@ +package registry + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/containerd/log" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" +) + +var testHTTPServer *httptest.Server + +func init() { + r := http.NewServeMux() + + // /v1/ + r.HandleFunc("/v1/_ping", handlerGetPing) + r.HandleFunc("/v1/search", handlerSearch) + + // /v2/ + r.HandleFunc("/v2/version", handlerGetPing) + + testHTTPServer = httptest.NewServer(handlerAccessLog(r)) +} + +func handlerAccessLog(handler http.Handler) http.Handler { + logHandler := func(w http.ResponseWriter, r *http.Request) { + log.G(context.TODO()).Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL) + handler.ServeHTTP(w, r) + } + return http.HandlerFunc(logHandler) +} + +func makeURL(req string) string { + return testHTTPServer.URL + req +} + +func writeHeaders(w http.ResponseWriter) { + h := w.Header() + h.Add("Server", "docker-tests/mock") + h.Add("Expires", "-1") + h.Add("Content-Type", "application/json") + h.Add("Pragma", "no-cache") + h.Add("Cache-Control", "no-cache") +} + +func writeResponse(w http.ResponseWriter, message any, code int) { + writeHeaders(w) + w.WriteHeader(code) + body, err := json.Marshal(message) + if err != nil { + _, _ = io.WriteString(w, err.Error()) + return + } + _, _ = w.Write(body) +} + +func handlerGetPing(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + writeResponse(w, true, http.StatusOK) +} + +func handlerSearch(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + result := ®istry.SearchResults{ + Query: "fakequery", + NumResults: 1, + Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}}, + } + writeResponse(w, result, http.StatusOK) +} + +func TestPing(t *testing.T) { + res, err := http.Get(makeURL("/v1/_ping")) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, res.StatusCode, http.StatusOK, "") + assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock") + _ = res.Body.Close() +} diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go new file mode 100644 index 000000000000..f1747aa86b62 --- /dev/null +++ b/internal/registry/registry_test.go @@ -0,0 +1,281 @@ +package registry + +import ( + "testing" + + "github.com/distribution/reference" + "github.com/docker/docker/api/types/registry" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +func TestNewIndexInfo(t *testing.T) { + type staticRepositoryInfo struct { + Index *registry.IndexInfo + RemoteName string + CanonicalName string + LocalName string + } + + tests := map[string]staticRepositoryInfo{ + "fooo/bar": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "fooo/bar", + LocalName: "fooo/bar", + CanonicalName: "docker.io/fooo/bar", + }, + "library/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "nonlibrary/ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "nonlibrary/ubuntu", + LocalName: "nonlibrary/ubuntu", + CanonicalName: "docker.io/nonlibrary/ubuntu", + }, + "ubuntu": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu", + LocalName: "ubuntu", + CanonicalName: "docker.io/library/ubuntu", + }, + "other/library": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "other/library", + LocalName: "other/library", + CanonicalName: "docker.io/other/library", + }, + "127.0.0.1:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "127.0.0.1:8000/private/moonbase", + CanonicalName: "127.0.0.1:8000/private/moonbase", + }, + "127.0.0.1:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "127.0.0.1:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "127.0.0.1:8000/privatebase", + CanonicalName: "127.0.0.1:8000/privatebase", + }, + "[::1]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "[::1]:8000/private/moonbase", + CanonicalName: "[::1]:8000/private/moonbase", + }, + "[::1]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::1]:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "[::1]:8000/privatebase", + CanonicalName: "[::1]:8000/privatebase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "[::2]:8000/private/moonbase", + CanonicalName: "[::2]:8000/private/moonbase", + }, + // IPv6 only has a single loopback address, so ::2 is not a loopback, + // hence not marked "insecure". + "[::2]:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "[::2]:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "[::2]:8000/privatebase", + CanonicalName: "[::2]:8000/privatebase", + }, + "localhost:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost:8000/private/moonbase", + CanonicalName: "localhost:8000/private/moonbase", + }, + "localhost:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost:8000", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost:8000/privatebase", + CanonicalName: "localhost:8000/privatebase", + }, + "example.com/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com/private/moonbase", + CanonicalName: "example.com/private/moonbase", + }, + "example.com/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com/privatebase", + CanonicalName: "example.com/privatebase", + }, + "example.com:8000/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "private/moonbase", + LocalName: "example.com:8000/private/moonbase", + CanonicalName: "example.com:8000/private/moonbase", + }, + "example.com:8000/privatebase": { + Index: ®istry.IndexInfo{ + Name: "example.com:8000", + Official: false, + Secure: true, + }, + RemoteName: "privatebase", + LocalName: "example.com:8000/privatebase", + CanonicalName: "example.com:8000/privatebase", + }, + "localhost/private/moonbase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "private/moonbase", + LocalName: "localhost/private/moonbase", + CanonicalName: "localhost/private/moonbase", + }, + "localhost/privatebase": { + Index: ®istry.IndexInfo{ + Name: "localhost", + Official: false, + Secure: false, + }, + RemoteName: "privatebase", + LocalName: "localhost/privatebase", + CanonicalName: "localhost/privatebase", + }, + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "index." + IndexName + "/public/moonbase": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "public/moonbase", + LocalName: "public/moonbase", + CanonicalName: "docker.io/public/moonbase", + }, + "ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + "index." + IndexName + "/ubuntu-12.04-base": { + Index: ®istry.IndexInfo{ + Name: IndexName, + Official: true, + Secure: true, + }, + RemoteName: "library/ubuntu-12.04-base", + LocalName: "ubuntu-12.04-base", + CanonicalName: "docker.io/library/ubuntu-12.04-base", + }, + } + + for reposName, expected := range tests { + t.Run(reposName, func(t *testing.T) { + named, err := reference.ParseNormalizedNamed(reposName) + assert.NilError(t, err) + + indexInfo := NewIndexInfo(named) + repoInfoName := reference.TrimNamed(named) + + assert.Check(t, is.DeepEqual(indexInfo, expected.Index)) + assert.Check(t, is.Equal(reference.Path(repoInfoName), expected.RemoteName)) + assert.Check(t, is.Equal(reference.FamiliarName(repoInfoName), expected.LocalName)) + assert.Check(t, is.Equal(repoInfoName.Name(), expected.CanonicalName)) + }) + } +} diff --git a/internal/registry/service.go b/internal/registry/service.go new file mode 100644 index 000000000000..a270b5855371 --- /dev/null +++ b/internal/registry/service.go @@ -0,0 +1,86 @@ +package registry + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/containerd/errdefs" + "github.com/containerd/log" + "github.com/docker/docker/api/types/registry" +) + +// Service is a registry service. It tracks configuration data such as a list +// of mirrors. +type Service struct { + config *serviceConfig +} + +// NewService returns a new instance of [Service] ready to be installed into +// an engine. +func NewService(options ServiceOptions) (*Service, error) { + config, err := newServiceConfig(options.InsecureRegistries) + if err != nil { + return nil, err + } + return &Service{config: config}, nil +} + +// Auth contacts the public registry with the provided credentials, +// and returns OK if authentication was successful. +// It can be used to verify the validity of a client's credentials. +func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (token string, _ error) { + registryHostName := IndexHostname + + if authConfig.ServerAddress != "" { + serverAddress := authConfig.ServerAddress + if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { + serverAddress = "https://" + serverAddress + } + u, err := url.Parse(serverAddress) + if err != nil { + return "", invalidParam(fmt.Errorf("unable to parse server address: %w", err)) + } + registryHostName = u.Host + } + + // Lookup endpoints for authentication. + endpoints, err := s.Endpoints(ctx, registryHostName) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return "", err + } + return "", invalidParam(err) + } + + var lastErr error + for _, endpoint := range endpoints { + authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errdefs.IsUnauthorized(err) { + // Failed to authenticate; don't continue with (non-TLS) endpoints. + return "", err + } + // Try next endpoint + log.G(ctx).WithFields(log.Fields{ + "error": err, + "endpoint": endpoint, + }).Infof("Error logging in to endpoint, trying next endpoint") + lastErr = err + continue + } + + return authToken, nil + } + + return "", lastErr +} + +// APIEndpoint represents a remote API endpoint +type APIEndpoint struct { + URL *url.URL + TLSConfig *tls.Config +} diff --git a/internal/registry/service_v2.go b/internal/registry/service_v2.go new file mode 100644 index 000000000000..ccfb5ac50959 --- /dev/null +++ b/internal/registry/service_v2.go @@ -0,0 +1,37 @@ +package registry + +import ( + "context" + "net/url" + + "github.com/docker/go-connections/tlsconfig" +) + +func (s *Service) Endpoints(ctx context.Context, hostname string) ([]APIEndpoint, error) { + if hostname == DefaultNamespace || hostname == IndexHostname { + return []APIEndpoint{{ + URL: DefaultV2Registry, + TLSConfig: tlsconfig.ServerDefault(), + }}, nil + } + + tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) + if err != nil { + return nil, err + } + + endpoints := []APIEndpoint{{ + URL: &url.URL{Scheme: "https", Host: hostname}, + TLSConfig: tlsConfig, + }} + + if tlsConfig.InsecureSkipVerify { + endpoints = append(endpoints, APIEndpoint{ + URL: &url.URL{Scheme: "http", Host: hostname}, + // used to check if supposed to be secure via InsecureSkipVerify + TLSConfig: tlsConfig, + }) + } + + return endpoints, nil +} diff --git a/cli/registry/client/client.go b/internal/registryclient/client.go similarity index 99% rename from cli/registry/client/client.go rename to internal/registryclient/client.go index f1fee951f305..6b88c5a2cb1e 100644 --- a/cli/registry/client/client.go +++ b/internal/registryclient/client.go @@ -1,4 +1,4 @@ -package client +package registryclient import ( "context" diff --git a/cli/registry/client/endpoint.go b/internal/registryclient/endpoint.go similarity index 81% rename from cli/registry/client/endpoint.go rename to internal/registryclient/endpoint.go index df0b190d56fa..59df91690a76 100644 --- a/cli/registry/client/endpoint.go +++ b/internal/registryclient/endpoint.go @@ -1,15 +1,17 @@ -package client +package registryclient import ( + "context" "net" "net/http" + "net/url" "time" "github.com/distribution/reference" + "github.com/docker/cli/internal/registry" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/transport" registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/registry" "github.com/pkg/errors" ) @@ -32,8 +34,7 @@ func (r repositoryEndpoint) BaseURL() string { func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) { repoName := reference.TrimNamed(ref) - repoInfo, _ := registry.ParseRepositoryInfo(ref) - indexInfo := repoInfo.Index + indexInfo := registry.NewIndexInfo(ref) endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure) if err != nil { @@ -54,7 +55,7 @@ func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEn if err != nil { return registry.APIEndpoint{}, err } - endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName)) + endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(repoName)) if err != nil { return registry.APIEndpoint{}, err } @@ -97,7 +98,7 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API if len(actions) == 0 { actions = []string{"pull"} } - creds := registry.NewStaticCredentialStore(&authConfig) + creds := &staticCredentialStore{authConfig: &authConfig} tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, actions...) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)) @@ -105,13 +106,6 @@ func getHTTPTransport(authConfig registrytypes.AuthConfig, endpoint registry.API return transport.NewTransport(base, modifiers...), nil } -// RepoNameForReference returns the repository name from a reference. -// -// Deprecated: this function is no longer used and will be removed in the next release. -func RepoNameForReference(ref reference.Named) (string, error) { - return reference.Path(reference.TrimNamed(ref)), nil -} - type existingTokenHandler struct { token string } @@ -124,3 +118,23 @@ func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, _ map[string func (*existingTokenHandler) Scheme() string { return "bearer" } + +type staticCredentialStore struct { + authConfig *registrytypes.AuthConfig +} + +func (scs staticCredentialStore) Basic(*url.URL) (string, string) { + if scs.authConfig == nil { + return "", "" + } + return scs.authConfig.Username, scs.authConfig.Password +} + +func (scs staticCredentialStore) RefreshToken(*url.URL, string) string { + if scs.authConfig == nil { + return "" + } + return scs.authConfig.IdentityToken +} + +func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {} diff --git a/cli/registry/client/fetcher.go b/internal/registryclient/fetcher.go similarity index 96% rename from cli/registry/client/fetcher.go rename to internal/registryclient/fetcher.go index f270d494324f..b36aab51cc85 100644 --- a/cli/registry/client/fetcher.go +++ b/internal/registryclient/fetcher.go @@ -1,4 +1,4 @@ -package client +package registryclient import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/manifest/types" + "github.com/docker/cli/internal/registry" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/ocischema" @@ -13,7 +14,6 @@ import ( "github.com/docker/distribution/registry/api/errcode" v2 "github.com/docker/distribution/registry/api/v2" distclient "github.com/docker/distribution/registry/client" - "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -221,8 +221,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, } repoName := reference.TrimNamed(namedRef) - repoInfo, _ := registry.ParseRepositoryInfo(namedRef) - indexInfo := repoInfo.Index + indexInfo := registry.NewIndexInfo(namedRef) confirmedTLSRegistries := make(map[string]bool) for _, endpoint := range endpoints { @@ -283,10 +282,9 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi } registryService, err := registry.NewService(serviceOpts) if err != nil { - return []registry.APIEndpoint{}, err + return nil, err } - repoInfo, _ := registry.ParseRepositoryInfo(namedRef) - endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name)) + endpoints, err := registryService.Endpoints(context.TODO(), reference.Domain(namedRef)) logrus.Debugf("endpoints for %s: %v", namedRef, endpoints) return endpoints, err } diff --git a/internal/test/cli.go b/internal/test/cli.go index 8b506e14149c..72591bac1baf 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -11,9 +11,9 @@ import ( "github.com/docker/cli/cli/context/docker" "github.com/docker/cli/cli/context/store" manifeststore "github.com/docker/cli/cli/manifest/store" - registryclient "github.com/docker/cli/cli/registry/client" "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" + "github.com/docker/cli/internal/registryclient" "github.com/docker/docker/api" "github.com/docker/docker/client" notaryclient "github.com/theupdateframework/notary/client" @@ -36,7 +36,6 @@ type FakeCli struct { notaryClientFunc NotaryClientFuncType manifestStore manifeststore.Store registryClient registryclient.RegistryClient - contentTrust bool contextStore store.Store currentContext string dockerEndpoint docker.Endpoint @@ -198,16 +197,6 @@ func (c *FakeCli) SetRegistryClient(registryClient registryclient.RegistryClient c.registryClient = registryClient } -// ContentTrustEnabled on the fake cli -func (c *FakeCli) ContentTrustEnabled() bool { - return c.contentTrust -} - -// EnableContentTrust on the fake cli -func EnableContentTrust(c *FakeCli) { - c.contentTrust = true -} - // BuildKitEnabled on the fake cli func (*FakeCli) BuildKitEnabled() (bool, error) { return true, nil diff --git a/internal/volumespec/types.go b/internal/volumespec/types.go new file mode 100644 index 000000000000..7eb9a5006ef0 --- /dev/null +++ b/internal/volumespec/types.go @@ -0,0 +1,40 @@ +package volumespec + +// VolumeConfig are references to a volume used by a service +type VolumeConfig struct { + Type string `yaml:",omitempty" json:"type,omitempty"` + Source string `yaml:",omitempty" json:"source,omitempty"` + Target string `yaml:",omitempty" json:"target,omitempty"` + ReadOnly bool `mapstructure:"read_only" yaml:"read_only,omitempty" json:"read_only,omitempty"` + Consistency string `yaml:",omitempty" json:"consistency,omitempty"` + Bind *BindOpts `yaml:",omitempty" json:"bind,omitempty"` + Volume *VolumeOpts `yaml:",omitempty" json:"volume,omitempty"` + Image *ImageOpts `yaml:",omitempty" json:"image,omitempty"` + Tmpfs *TmpFsOpts `yaml:",omitempty" json:"tmpfs,omitempty"` + Cluster *ClusterOpts `yaml:",omitempty" json:"cluster,omitempty"` +} + +// BindOpts are options for a service volume of type bind +type BindOpts struct { + Propagation string `yaml:",omitempty" json:"propagation,omitempty"` +} + +// VolumeOpts are options for a service volume of type volume +type VolumeOpts struct { + NoCopy bool `mapstructure:"nocopy" yaml:"nocopy,omitempty" json:"nocopy,omitempty"` + Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` +} + +// ImageOpts are options for a service volume of type image +type ImageOpts struct { + Subpath string `mapstructure:"subpath" yaml:"subpath,omitempty" json:"subpath,omitempty"` +} + +// TmpFsOpts are options for a service volume of type tmpfs +type TmpFsOpts struct { + Size int64 `yaml:",omitempty" json:"size,omitempty"` +} + +// ClusterOpts are options for a service volume of type cluster. +// Deliberately left blank for future options, but unused now. +type ClusterOpts struct{} diff --git a/cli/compose/loader/volume.go b/internal/volumespec/volumespec.go similarity index 82% rename from cli/compose/loader/volume.go rename to internal/volumespec/volumespec.go index f043f4aa57ff..0ae4f31c8b95 100644 --- a/cli/compose/loader/volume.go +++ b/internal/volumespec/volumespec.go @@ -1,20 +1,19 @@ -package loader +package volumespec import ( "strings" "unicode" "unicode/utf8" - "github.com/docker/cli/cli/compose/types" "github.com/docker/docker/api/types/mount" "github.com/pkg/errors" ) const endOfSpec = rune(0) -// ParseVolume parses a volume spec without any knowledge of the target platform -func ParseVolume(spec string) (types.ServiceVolumeConfig, error) { - volume := types.ServiceVolumeConfig{} +// Parse parses a volume spec without any knowledge of the target platform +func Parse(spec string) (VolumeConfig, error) { + volume := VolumeConfig{} switch len(spec) { case 0: @@ -49,7 +48,7 @@ func isWindowsDrive(buffer []rune, char rune) bool { return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) } -func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error { +func populateFieldFromBuffer(char rune, buffer []rune, volume *VolumeConfig) error { strBuffer := string(buffer) switch { case len(buffer) == 0: @@ -74,10 +73,10 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu case "rw": volume.ReadOnly = false case "nocopy": - volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} + volume.Volume = &VolumeOpts{NoCopy: true} default: if isBindOption(option) { - volume.Bind = &types.ServiceVolumeBind{Propagation: option} + volume.Bind = &BindOpts{Propagation: option} } // ignore unknown options } @@ -94,7 +93,7 @@ func isBindOption(option string) bool { return false } -func populateType(volume *types.ServiceVolumeConfig) { +func populateType(volume *VolumeConfig) { switch { // Anonymous volume case volume.Source == "": diff --git a/cli/compose/loader/volume_test.go b/internal/volumespec/volumespec_test.go similarity index 77% rename from cli/compose/loader/volume_test.go rename to internal/volumespec/volumespec_test.go index 54dc200fe8f6..b4d356fffaf7 100644 --- a/cli/compose/loader/volume_test.go +++ b/internal/volumespec/volumespec_test.go @@ -1,18 +1,17 @@ -package loader +package volumespec import ( "fmt" "testing" - "github.com/docker/cli/cli/compose/types" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestParseVolumeAnonymousVolume(t *testing.T) { for _, path := range []string{"/path", "/path/foo"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) } @@ -20,22 +19,22 @@ func TestParseVolumeAnonymousVolume(t *testing.T) { func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) } } func TestParseVolumeTooManyColons(t *testing.T) { - _, err := ParseVolume("/foo:/foo:ro:foo") + _, err := Parse("/foo:/foo:ro:foo") assert.Error(t, err, "invalid spec: /foo:/foo:ro:foo: too many colons") } func TestParseVolumeShortVolumes(t *testing.T) { for _, path := range []string{".", "/a"} { - volume, err := ParseVolume(path) - expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + volume, err := Parse(path) + expected := VolumeConfig{Type: "volume", Target: path} assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) } @@ -43,15 +42,15 @@ func TestParseVolumeShortVolumes(t *testing.T) { func TestParseVolumeMissingSource(t *testing.T) { for _, spec := range []string{":foo", "/foo::ro"} { - _, err := ParseVolume(spec) + _, err := Parse(spec) assert.ErrorContains(t, err, "empty section between colons") } } func TestParseVolumeBindMount(t *testing.T) { for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target") - expected := types.ServiceVolumeConfig{ + volume, err := Parse(path + ":/target") + expected := VolumeConfig{ Type: "bind", Source: path, Target: "/target", @@ -68,8 +67,8 @@ func TestParseVolumeRelativeBindMountWindows(t *testing.T) { "../other", "D:\\path", "/home/user", } { - volume, err := ParseVolume(path + ":d:\\target") - expected := types.ServiceVolumeConfig{ + volume, err := Parse(path + ":d:\\target") + expected := VolumeConfig{ Type: "bind", Source: path, Target: "d:\\target", @@ -80,42 +79,42 @@ func TestParseVolumeRelativeBindMountWindows(t *testing.T) { } func TestParseVolumeWithBindOptions(t *testing.T) { - volume, err := ParseVolume("/source:/target:slave") - expected := types.ServiceVolumeConfig{ + volume, err := Parse("/source:/target:slave") + expected := VolumeConfig{ Type: "bind", Source: "/source", Target: "/target", - Bind: &types.ServiceVolumeBind{Propagation: "slave"}, + Bind: &BindOpts{Propagation: "slave"}, } assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) } func TestParseVolumeWithBindOptionsWindows(t *testing.T) { - volume, err := ParseVolume("C:\\source\\foo:D:\\target:ro,rprivate") - expected := types.ServiceVolumeConfig{ + volume, err := Parse("C:\\source\\foo:D:\\target:ro,rprivate") + expected := VolumeConfig{ Type: "bind", Source: "C:\\source\\foo", Target: "D:\\target", ReadOnly: true, - Bind: &types.ServiceVolumeBind{Propagation: "rprivate"}, + Bind: &BindOpts{Propagation: "rprivate"}, } assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) } func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { - _, err := ParseVolume("name:/target:bogus") + _, err := Parse("name:/target:bogus") assert.NilError(t, err) } func TestParseVolumeWithVolumeOptions(t *testing.T) { - volume, err := ParseVolume("name:/target:nocopy") - expected := types.ServiceVolumeConfig{ + volume, err := Parse("name:/target:nocopy") + expected := VolumeConfig{ Type: "volume", Source: "name", Target: "/target", - Volume: &types.ServiceVolumeVolume{NoCopy: true}, + Volume: &VolumeOpts{NoCopy: true}, } assert.NilError(t, err) assert.Check(t, is.DeepEqual(expected, volume)) @@ -123,8 +122,8 @@ func TestParseVolumeWithVolumeOptions(t *testing.T) { func TestParseVolumeWithReadOnly(t *testing.T) { for _, path := range []string{"./foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target:ro") - expected := types.ServiceVolumeConfig{ + volume, err := Parse(path + ":/target:ro") + expected := VolumeConfig{ Type: "bind", Source: path, Target: "/target", @@ -137,8 +136,8 @@ func TestParseVolumeWithReadOnly(t *testing.T) { func TestParseVolumeWithRW(t *testing.T) { for _, path := range []string{"./foo", "/home/user"} { - volume, err := ParseVolume(path + ":/target:rw") - expected := types.ServiceVolumeConfig{ + volume, err := Parse(path + ":/target:rw") + expected := VolumeConfig{ Type: "bind", Source: path, Target: "/target", @@ -150,9 +149,9 @@ func TestParseVolumeWithRW(t *testing.T) { } func TestParseVolumeWindowsNamedPipe(t *testing.T) { - volume, err := ParseVolume(`\\.\pipe\docker_engine:\\.\pipe\inside`) + volume, err := Parse(`\\.\pipe\docker_engine:\\.\pipe\inside`) assert.NilError(t, err) - expected := types.ServiceVolumeConfig{ + expected := VolumeConfig{ Type: "bind", Source: `\\.\pipe\docker_engine`, Target: `\\.\pipe\inside`, @@ -206,7 +205,7 @@ func TestParseVolumeSplitCases(t *testing.T) { // Cover directories with one-character name {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, } { - parsed, _ := ParseVolume(x.input) + parsed, _ := Parse(x.input) expected := len(x.expected) > 1 msg := fmt.Sprintf("Case %d: %s", casenumber, x.input) @@ -215,16 +214,16 @@ func TestParseVolumeSplitCases(t *testing.T) { } func TestParseVolumeInvalidEmptySpec(t *testing.T) { - _, err := ParseVolume("") + _, err := Parse("") assert.ErrorContains(t, err, "invalid empty volume spec") } func TestParseVolumeInvalidSections(t *testing.T) { - _, err := ParseVolume("/foo::rw") + _, err := Parse("/foo::rw") assert.ErrorContains(t, err, "invalid spec") } func TestParseVolumeWithEmptySource(t *testing.T) { - _, err := ParseVolume(":/vol") + _, err := Parse(":/vol") assert.ErrorContains(t, err, "empty section between colons") } diff --git a/opts/env.go b/opts/env.go index 675ddda96229..2a21394ba5ba 100644 --- a/opts/env.go +++ b/opts/env.go @@ -7,13 +7,14 @@ import ( ) // ValidateEnv validates an environment variable and returns it. -// If no value is specified, it obtains its value from the current environment +// If no value is specified, it obtains its value from the current environment. // -// As on ParseEnvFile and related to #16585, environment variable names -// are not validated, and it's up to the application inside the container -// to validate them or not. +// Environment variable names are not validated, and it's up to the application +// inside the container to validate them (see [moby-16585]). The only validation +// here is to check if name is empty, per [moby-25099]. // -// The only validation here is to check if name is empty, per #25099 +// [moby-16585]: https://github.com/moby/moby/issues/16585 +// [moby-25099]: https://github.com/moby/moby/issues/25099 func ValidateEnv(val string) (string, error) { k, _, hasValue := strings.Cut(val, "=") if k == "" { diff --git a/opts/envfile_deprecated.go b/opts/envfile_deprecated.go new file mode 100644 index 000000000000..f2f10b1816fe --- /dev/null +++ b/opts/envfile_deprecated.go @@ -0,0 +1,14 @@ +package opts + +import ( + "os" + + "github.com/docker/cli/pkg/kvfile" +) + +// ParseEnvFile reads a file with environment variables enumerated by lines +// +// Deprecated: use [kvfile.Parse] and pass [os.LookupEnv] to lookup env-vars from the current environment. +func ParseEnvFile(filename string) ([]string, error) { + return kvfile.Parse(filename, os.LookupEnv) +} diff --git a/opts/hosts.go b/opts/hosts.go index 552ab6b4a552..87e1a1da755b 100644 --- a/opts/hosts.go +++ b/opts/hosts.go @@ -34,7 +34,7 @@ const ( // ValidateHost validates that the specified string is a valid host and returns it. // -// TODO(thaJeztah): ValidateHost appears to be unused; deprecate it. +// Deprecated: this function is no longer used, and will be removed in the next release. func ValidateHost(val string) (string, error) { host := strings.TrimSpace(val) // The empty string means default and is not handled by parseDockerDaemonHost diff --git a/opts/opts.go b/opts/opts.go index 1a885db30eff..c555c2d18884 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -60,6 +60,8 @@ func (opts *ListOpts) Set(value string) error { } // Delete removes the specified element from the slice. +// +// Deprecated: this method is no longer used and will be removed in the next release. func (opts *ListOpts) Delete(key string) { for i, k := range *opts.values { if k == key { @@ -134,6 +136,8 @@ func (opts *ListOpts) WithValidator(validator ValidatorFctType) *ListOpts { // NamedOption is an interface that list and map options // with names implement. +// +// Deprecated: NamedOption is no longer used and will be removed in the next release. type NamedOption interface { Name() string } @@ -141,6 +145,8 @@ type NamedOption interface { // NamedListOpts is a ListOpts with a configuration name. // This struct is useful to keep reference to the assigned // field name in the internal configuration struct. +// +// Deprecated: NamedListOpts is no longer used and will be removed in the next release. type NamedListOpts struct { name string ListOpts @@ -149,6 +155,8 @@ type NamedListOpts struct { var _ NamedOption = &NamedListOpts{} // NewNamedListOptsRef creates a reference to a new NamedListOpts struct. +// +// Deprecated: NewNamedListOptsRef is no longer used and will be removed in the next release. func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts { return &NamedListOpts{ name: name, @@ -157,6 +165,8 @@ func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctTy } // Name returns the name of the NamedListOpts in the configuration. +// +// Deprecated: NamedListOpts is no longer used and will be removed in the next release. func (o *NamedListOpts) Name() string { return o.name } @@ -210,6 +220,8 @@ func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts { // NamedMapOpts is a MapOpts struct with a configuration name. // This struct is useful to keep reference to the assigned // field name in the internal configuration struct. +// +// Deprecated: NamedMapOpts is no longer used and will be removed in the next release. type NamedMapOpts struct { name string MapOpts @@ -218,6 +230,8 @@ type NamedMapOpts struct { var _ NamedOption = &NamedMapOpts{} // NewNamedMapOpts creates a reference to a new NamedMapOpts struct. +// +// Deprecated: NamedMapOpts is no longer used and will be removed in the next release. func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts { return &NamedMapOpts{ name: name, @@ -226,6 +240,8 @@ func NewNamedMapOpts(name string, values map[string]string, validator ValidatorF } // Name returns the name of the NamedMapOpts in the configuration. +// +// Deprecated: NamedMapOpts is no longer used and will be removed in the next release. func (o *NamedMapOpts) Name() string { return o.name } @@ -250,6 +266,8 @@ func ValidateIPAddress(val string) (string, error) { } // ValidateMACAddress validates a MAC address. +// +// Deprecated: use [net.ParseMAC]. This function will be removed in the next release. func ValidateMACAddress(val string) (string, error) { _, err := net.ParseMAC(strings.TrimSpace(val)) if err != nil { diff --git a/opts/opts_test.go b/opts/opts_test.go index 7dc87cad9e83..f363a2a6440a 100644 --- a/opts/opts_test.go +++ b/opts/opts_test.go @@ -142,18 +142,14 @@ func TestListOptsWithoutValidator(t *testing.T) { if o.Get("baz") { t.Error(`o.Get("baz") == true`) } - o.Delete("foo") - if o.String() != "[bar bar]" { - t.Errorf("%s != [bar bar]", o.String()) + if listOpts := o.GetSlice(); len(listOpts) != 3 || listOpts[0] != "foo" || listOpts[1] != "bar" || listOpts[2] != "bar" { + t.Errorf("Expected [[foo bar bar]], got [%v]", listOpts) } - if listOpts := o.GetAll(); len(listOpts) != 2 || listOpts[0] != "bar" || listOpts[1] != "bar" { - t.Errorf("Expected [[bar bar]], got [%v]", listOpts) + if listOpts := o.GetAll(); len(listOpts) != 3 || listOpts[0] != "foo" || listOpts[1] != "bar" || listOpts[2] != "bar" { + t.Errorf("Expected [[foo bar bar]], got [%v]", listOpts) } - if listOpts := o.GetSlice(); len(listOpts) != 2 || listOpts[0] != "bar" || listOpts[1] != "bar" { - t.Errorf("Expected [[bar bar]], got [%v]", listOpts) - } - if mapListOpts := o.GetMap(); len(mapListOpts) != 1 { - t.Errorf("Expected [map[bar:{}]], got [%v]", mapListOpts) + if mapListOpts := o.GetMap(); len(mapListOpts) != 2 { + t.Errorf("Expected [map[bar:{} foo:{}]], got [%v]", mapListOpts) } } @@ -186,9 +182,8 @@ func TestListOptsWithValidator(t *testing.T) { if o.Get("baz") { t.Error(`o.Get("baz") == true`) } - o.Delete("valid-option2=2") - if o.String() != "" { - t.Errorf(`%s != ""`, o.String()) + if expected := "[valid-option2=2]"; o.String() != expected { + t.Errorf(`%s != %q`, o.String(), expected) } } @@ -396,20 +391,6 @@ func TestNamedMapOpts(t *testing.T) { } } -func TestValidateMACAddress(t *testing.T) { - if _, err := ValidateMACAddress(`92:d0:c6:0a:29:33`); err != nil { - t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:29:33`) got %s", err) - } - - if _, err := ValidateMACAddress(`92:d0:c6:0a:33`); err == nil { - t.Fatalf("ValidateMACAddress(`92:d0:c6:0a:33`) succeeded; expected failure on invalid MAC") - } - - if _, err := ValidateMACAddress(`random invalid string`); err == nil { - t.Fatalf("ValidateMACAddress(`random invalid string`) succeeded; expected failure on invalid MAC") - } -} - func TestValidateLink(t *testing.T) { valid := []string{ "name", diff --git a/opts/quotedstring.go b/opts/quotedstring.go index eb2ac7fbc8af..d1d8b09a1fec 100644 --- a/opts/quotedstring.go +++ b/opts/quotedstring.go @@ -2,6 +2,8 @@ package opts // QuotedString is a string that may have extra quotes around the value. The // quotes are stripped from the value. +// +// Deprecated: This option type is no longer used and will be removed in the next release. type QuotedString struct { value *string } @@ -35,6 +37,8 @@ func trimQuotes(value string) string { } // NewQuotedString returns a new quoted string option +// +// Deprecated: This option type is no longer used and will be removed in the next release. func NewQuotedString(value *string) *QuotedString { return &QuotedString{value: value} } diff --git a/opts/swarmopts/port_test.go b/opts/swarmopts/port_test.go index c13c6cd4d000..a9e2f4a293a9 100644 --- a/opts/swarmopts/port_test.go +++ b/opts/swarmopts/port_test.go @@ -309,7 +309,8 @@ func TestPortOptInvalidSimpleSyntax(t *testing.T) { }, { value: "", - expectedError: "no port specified: ", + expectedError: "invalid proto: ", + // expectedError: "no port specified: ", // FIXME(thaJeztah): re-enable once https://github.com/docker/go-connections/pull/143 is in a go-connections release. }, { value: "1.1.1.1:80:80", diff --git a/templates/templates.go b/templates/templates.go index f0726eec95c5..4af4496d19a1 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -71,7 +71,7 @@ var HeaderFunctions = template.FuncMap{ // Parse creates a new anonymous template with the basic functions // and parses the given format. func Parse(format string) (*template.Template, error) { - return NewParse("", format) + return template.New("").Funcs(basicFunctions).Parse(format) } // New creates a new empty template with the provided tag and built-in @@ -82,8 +82,10 @@ func New(tag string) *template.Template { // NewParse creates a new tagged template with the basic functions // and parses the given format. +// +// Deprecated: this function is unused and will be removed in the next release. Use [New] if you need to set a tag, or [Parse] instead. func NewParse(tag, format string) (*template.Template, error) { - return New(tag).Parse(format) + return template.New(tag).Funcs(basicFunctions).Parse(format) } // padWithSpace adds whitespace to the input if the input is non-empty diff --git a/templates/templates_test.go b/templates/templates_test.go index 608fe72a2004..e9dbaefd0e5e 100644 --- a/templates/templates_test.go +++ b/templates/templates_test.go @@ -30,7 +30,7 @@ func TestParseStringFunctions(t *testing.T) { } func TestNewParse(t *testing.T) { - tm, err := NewParse("foo", "this is a {{ . }}") + tm, err := New("foo").Parse("this is a {{ . }}") assert.NilError(t, err) var b bytes.Buffer diff --git a/vendor.mod b/vendor.mod index 68d14b00251c..336c2fd26069 100644 --- a/vendor.mod +++ b/vendor.mod @@ -7,17 +7,18 @@ module github.com/docker/cli go 1.23.0 require ( - dario.cat/mergo v1.0.1 + dario.cat/mergo v1.0.2 github.com/containerd/errdefs v1.0.0 + github.com/containerd/log v0.1.0 github.com/containerd/platforms v1.0.0-rc.1 github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/creack/pty v1.1.24 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.1+incompatible + github.com/docker/docker v28.5.1+incompatible github.com/docker/docker-credential-helpers v0.9.3 - github.com/docker/go-connections v0.5.0 + github.com/docker/go-connections v0.6.0 github.com/docker/go-units v0.5.0 github.com/fvbommel/sortorder v1.1.0 github.com/go-jose/go-jose/v4 v4.0.5 @@ -34,10 +35,11 @@ require ( github.com/moby/sys/capability v0.4.0 github.com/moby/sys/sequential v0.6.0 github.com/moby/sys/signal v0.7.1 + github.com/moby/sys/symlink v0.3.0 github.com/moby/term v0.5.2 github.com/morikuni/aec v1.0.0 github.com/opencontainers/go-digest v1.0.0 - github.com/opencontainers/image-spec v1.1.0 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 @@ -46,6 +48,7 @@ require ( github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 github.com/xeipuuv/gojsonschema v1.2.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 @@ -53,7 +56,7 @@ require ( go.opentelemetry.io/otel/sdk v1.35.0 go.opentelemetry.io/otel/sdk/metric v1.35.0 go.opentelemetry.io/otel/trace v1.35.0 - golang.org/x/sync v0.14.0 + golang.org/x/sync v0.16.0 golang.org/x/sys v0.33.0 golang.org/x/term v0.31.0 golang.org/x/text v0.24.0 @@ -69,9 +72,8 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect - github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -83,7 +85,6 @@ require ( github.com/klauspost/compress v1.18.0 // indirect github.com/miekg/pkcs11 v1.1.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/sys/symlink v0.3.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -97,7 +98,6 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.etcd.io/etcd/raft/v3 v3.5.16 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect diff --git a/vendor.sum b/vendor.sum index 3aa9706f4a08..0ccf6c1a6d39 100644 --- a/vendor.sum +++ b/vendor.sum @@ -1,5 +1,5 @@ -dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= -dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -57,17 +57,17 @@ 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.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY= -github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.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= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk= +github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= @@ -209,8 +209,8 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -348,8 +348,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/vendor/dario.cat/mergo/FUNDING.json b/vendor/dario.cat/mergo/FUNDING.json new file mode 100644 index 000000000000..0585e1fe13f7 --- /dev/null +++ b/vendor/dario.cat/mergo/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "ethereum": { + "ownedBy": "0x6160020e7102237aC41bdb156e94401692D76930" + } + } +} diff --git a/vendor/dario.cat/mergo/README.md b/vendor/dario.cat/mergo/README.md index 0b3c488893b0..0e4a59afd9ac 100644 --- a/vendor/dario.cat/mergo/README.md +++ b/vendor/dario.cat/mergo/README.md @@ -85,7 +85,6 @@ Mergo is used by [thousands](https://deps.dev/go/dario.cat%2Fmergo/v1.0.0/depend * [goreleaser/goreleaser](https://github.com/goreleaser/goreleaser) * [go-micro/go-micro](https://github.com/go-micro/go-micro) * [grafana/loki](https://github.com/grafana/loki) -* [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) * [masterminds/sprig](github.com/Masterminds/sprig) * [moby/moby](https://github.com/moby/moby) * [slackhq/nebula](https://github.com/slackhq/nebula) @@ -191,10 +190,6 @@ func main() { } ``` -Note: if test are failing due missing package, please execute: - - go get gopkg.in/yaml.v3 - ### Transformers Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? diff --git a/vendor/dario.cat/mergo/SECURITY.md b/vendor/dario.cat/mergo/SECURITY.md index a5de61f77ba7..3788fcc1c212 100644 --- a/vendor/dario.cat/mergo/SECURITY.md +++ b/vendor/dario.cat/mergo/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 0.3.x | :white_check_mark: | -| < 0.3 | :x: | +| 1.x.x | :white_check_mark: | +| < 1.0 | :x: | ## Security contact information diff --git a/vendor/github.com/docker/docker/api/swagger.yaml b/vendor/github.com/docker/docker/api/swagger.yaml index 8d6a8f9356a4..6ca2c2b0863f 100644 --- a/vendor/github.com/docker/docker/api/swagger.yaml +++ b/vendor/github.com/docker/docker/api/swagger.yaml @@ -81,7 +81,6 @@ info: { "username": "string", "password": "string", - "email": "string", "serveraddress": "string" } ``` @@ -637,6 +636,9 @@ definitions: by the default (runc) runtime. This field is omitted when empty. + + **Deprecated**: This field is deprecated as kernel 6.12 has deprecated `memory.kmem.tcp.limit_in_bytes` field + for cgroups v1. This field will be removed in a future release. type: "integer" format: "int64" MemoryReservation: @@ -1531,37 +1533,6 @@ definitions: items: type: "string" example: ["/bin/sh", "-c"] - # FIXME(thaJeztah): temporarily using a full example to remove some "omitempty" fields. Remove once the fields are removed. - example: - "User": "web:web" - "ExposedPorts": { - "80/tcp": {}, - "443/tcp": {} - } - "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] - "Cmd": ["/bin/sh"] - "Healthcheck": { - "Test": ["string"], - "Interval": 0, - "Timeout": 0, - "Retries": 0, - "StartPeriod": 0, - "StartInterval": 0 - } - "ArgsEscaped": true - "Volumes": { - "/app/data": {}, - "/app/config": {} - } - "WorkingDir": "/public/" - "Entrypoint": [] - "OnBuild": [] - "Labels": { - "com.example.some-label": "some-value", - "com.example.some-other-label": "some-other-value" - } - "StopSignal": "SIGTERM" - "Shell": ["/bin/sh", "-c"] NetworkingConfig: description: | @@ -1608,6 +1579,8 @@ definitions: Bridge: description: | Name of the default bridge interface when dockerd's --bridge flag is set. + + Deprecated: This field is only set when the daemon is started with the --bridge flag specified. type: "string" example: "docker0" SandboxID: @@ -1965,6 +1938,11 @@ definitions: Depending on how the image was created, this field may be empty and is only set for images that were built/created locally. This field is empty if the image was pulled from an image registry. + + > **Deprecated**: This field is only set when using the deprecated + > legacy builder. It is included in API responses for informational + > purposes, but should not be depended on as it will be omitted + > once the legacy builder is removed. type: "string" x-nullable: false example: "" @@ -1990,6 +1968,11 @@ definitions: The version of Docker that was used to build the image. Depending on how the image was created, this field may be empty. + + > **Deprecated**: This field is only set when using the deprecated + > legacy builder. It is included in API responses for informational + > purposes, but should not be depended on as it will be omitted + > once the legacy builder is removed. type: "string" x-nullable: false example: "27.0.1" @@ -2034,14 +2017,6 @@ definitions: format: "int64" x-nullable: false example: 1239828 - VirtualSize: - description: | - Total size of the image including all layers it is composed of. - - Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - type: "integer" - format: "int64" - example: 1239828 GraphDriver: $ref: "#/definitions/DriverData" RootFS: @@ -2174,14 +2149,6 @@ definitions: format: "int64" x-nullable: false example: 1239828 - VirtualSize: - description: |- - Total size of the image including all layers it is composed of. - - Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead. - type: "integer" - format: "int64" - example: 172064416 Labels: description: "User-defined key/value metadata." type: "object" @@ -2234,6 +2201,10 @@ definitions: password: type: "string" email: + description: | + Email is an optional value associated with the username. + + > **Deprecated**: This field is deprecated since docker 1.11 (API v1.23) and will be removed in a future release. type: "string" serveraddress: type: "string" @@ -2913,7 +2884,8 @@ definitions: be used. If multiple endpoints have the same priority, endpoints are lexicographically sorted based on their network name, and the one that sorts first is picked. - type: "number" + type: "integer" + format: "int64" example: - 10 @@ -3170,10 +3142,15 @@ definitions: - Args properties: DockerVersion: - description: "Docker Version used to create the plugin" + description: |- + Docker Version used to create the plugin. + + Depending on how the plugin was created, this field may be empty or omitted. + + Deprecated: this field is no longer set, and will be removed in the next API version. type: "string" x-nullable: false - example: "17.06.0-ce" + x-omitempty: true Description: type: "string" x-nullable: false @@ -4391,6 +4368,7 @@ definitions: A counter that triggers an update even if no relevant parameters have been changed. type: "integer" + format: "uint64" Runtime: description: | Runtime is the type of runtime specified for the task executor. @@ -6374,6 +6352,8 @@ definitions: Kernel memory TCP limits are not supported when using cgroups v2, which does not support the corresponding `memory.kmem.tcp.limit_in_bytes` cgroup. + + **Deprecated**: This field is deprecated as kernel 6.12 has deprecated kernel memory TCP accounting. type: "boolean" example: true CpuCfsPeriod: @@ -6411,29 +6391,6 @@ definitions: description: "Indicates IPv4 forwarding is enabled." type: "boolean" example: true - BridgeNfIptables: - description: | - Indicates if `bridge-nf-call-iptables` is available on the host when - the daemon was started. - -


- - > **Deprecated**: netfilter module is now loaded on-demand and no longer - > during daemon startup, making this field obsolete. This field is always - > `false` and will be removed in a API v1.49. - type: "boolean" - example: false - BridgeNfIp6tables: - description: | - Indicates if `bridge-nf-call-ip6tables` is available on the host. - -


- - > **Deprecated**: netfilter module is now loaded on-demand, and no longer - > during daemon startup, making this field obsolete. This field is always - > `false` and will be removed in a API v1.49. - type: "boolean" - example: false Debug: description: | Indicates if the daemon is running in debug-mode / with debug-level diff --git a/vendor/github.com/docker/docker/api/types/build/disk_usage.go b/vendor/github.com/docker/docker/api/types/build/disk_usage.go index e969b6d615f2..cfd7333272c4 100644 --- a/vendor/github.com/docker/docker/api/types/build/disk_usage.go +++ b/vendor/github.com/docker/docker/api/types/build/disk_usage.go @@ -1,6 +1,8 @@ package build // CacheDiskUsage contains disk usage for the build cache. +// +// Deprecated: this type is no longer used and will be removed in the next release. type CacheDiskUsage struct { TotalSize int64 Reclaimable int64 diff --git a/vendor/github.com/docker/docker/api/types/container/disk_usage.go b/vendor/github.com/docker/docker/api/types/container/disk_usage.go index 05b6cbe9c709..d77538c2ab55 100644 --- a/vendor/github.com/docker/docker/api/types/container/disk_usage.go +++ b/vendor/github.com/docker/docker/api/types/container/disk_usage.go @@ -1,6 +1,8 @@ package container // DiskUsage contains disk usage for containers. +// +// Deprecated: this type is no longer used and will be removed in the next release. type DiskUsage struct { TotalSize int64 Reclaimable int64 diff --git a/vendor/github.com/docker/docker/api/types/container/hostconfig.go b/vendor/github.com/docker/docker/api/types/container/hostconfig.go index f63f049c7c25..7a41436cc702 100644 --- a/vendor/github.com/docker/docker/api/types/container/hostconfig.go +++ b/vendor/github.com/docker/docker/api/types/container/hostconfig.go @@ -394,7 +394,12 @@ type Resources struct { // KernelMemory specifies the kernel memory limit (in bytes) for the container. // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes. - KernelMemory int64 `json:",omitempty"` + KernelMemory int64 `json:",omitempty"` + // Hard limit for kernel TCP buffer memory (in bytes). + // + // Deprecated: This field is deprecated and will be removed in the next release. + // Starting with 6.12, the kernel has deprecated kernel memory tcp accounting + // for cgroups v1. KernelMemoryTCP int64 `json:",omitempty"` // Hard limit for kernel TCP buffer memory (in bytes) MemoryReservation int64 // Memory soft limit (in bytes) MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap diff --git a/vendor/github.com/docker/docker/api/types/container/network_settings.go b/vendor/github.com/docker/docker/api/types/container/network_settings.go index afec0e54323e..687145f2953a 100644 --- a/vendor/github.com/docker/docker/api/types/container/network_settings.go +++ b/vendor/github.com/docker/docker/api/types/container/network_settings.go @@ -13,8 +13,11 @@ type NetworkSettings struct { } // NetworkSettingsBase holds networking state for a container when inspecting it. +// +// Deprecated: Most fields in NetworkSettingsBase are deprecated. Fields which aren't deprecated will move to +// NetworkSettings in v29.0, and this struct will be removed. type NetworkSettingsBase struct { - Bridge string // Bridge contains the name of the default bridge interface iff it was set through the daemon --bridge flag. + Bridge string // Deprecated: This field is only set when the daemon is started with the --bridge flag specified. SandboxID string // SandboxID uniquely represents a container's network stack SandboxKey string // SandboxKey identifies the sandbox Ports nat.PortMap // Ports is a collection of PortBinding indexed by Port @@ -35,18 +38,44 @@ type NetworkSettingsBase struct { SecondaryIPv6Addresses []network.Address // Deprecated: This field is never set and will be removed in a future release. } -// DefaultNetworkSettings holds network information -// during the 2 release deprecation period. -// It will be removed in Docker 1.11. +// DefaultNetworkSettings holds the networking state for the default bridge, if the container is connected to that +// network. +// +// Deprecated: this struct is deprecated since Docker v1.11 and will be removed in v29. You should look for the default +// network in NetworkSettings.Networks instead. type DefaultNetworkSettings struct { - EndpointID string // EndpointID uniquely represents a service endpoint in a Sandbox - Gateway string // Gateway holds the gateway address for the network - GlobalIPv6Address string // GlobalIPv6Address holds network's global IPv6 address - GlobalIPv6PrefixLen int // GlobalIPv6PrefixLen represents mask length of network's global IPv6 address - IPAddress string // IPAddress holds the IPv4 address for the network - IPPrefixLen int // IPPrefixLen represents mask length of network's IPv4 address - IPv6Gateway string // IPv6Gateway holds gateway address specific for IPv6 - MacAddress string // MacAddress holds the MAC address for the network + // EndpointID uniquely represents a service endpoint in a Sandbox + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + EndpointID string + // Gateway holds the gateway address for the network + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + Gateway string + // GlobalIPv6Address holds network's global IPv6 address + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + GlobalIPv6Address string + // GlobalIPv6PrefixLen represents mask length of network's global IPv6 address + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + GlobalIPv6PrefixLen int + // IPAddress holds the IPv4 address for the network + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + IPAddress string + // IPPrefixLen represents mask length of network's IPv4 address + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + IPPrefixLen int + // IPv6Gateway holds gateway address specific for IPv6 + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + IPv6Gateway string + // MacAddress holds the MAC address for the network + // + // Deprecated: This field will be removed in v29. You should look for the default network in NetworkSettings.Networks instead. + MacAddress string } // NetworkSettingsSummary provides a summary of container's networks diff --git a/vendor/github.com/docker/docker/api/types/filters/filters_deprecated.go b/vendor/github.com/docker/docker/api/types/filters/filters_deprecated.go new file mode 100644 index 000000000000..4504cd7a7fef --- /dev/null +++ b/vendor/github.com/docker/docker/api/types/filters/filters_deprecated.go @@ -0,0 +1,61 @@ +package filters + +import ( + "encoding/json" + + "github.com/docker/docker/api/types/versions" +) + +// ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22 +// then the encoded format will use an older legacy format where the values are a +// list of strings, instead of a set. +// +// Deprecated: do not use in any new code; use ToJSON instead +func ToParamWithVersion(version string, a Args) (string, error) { + out, err := ToJSON(a) + if out == "" || err != nil { + return "", nil + } + if version != "" && versions.LessThan(version, "1.22") { + return encodeLegacyFilters(out) + } + return out, nil +} + +// encodeLegacyFilters encodes Args in the legacy format as used in API v1.21 and older. +// where values are a list of strings, instead of a set. +// +// Don't use in any new code; use [filters.ToJSON]] instead. +func encodeLegacyFilters(currentFormat string) (string, error) { + // The Args.fields field is not exported, but used to marshal JSON, + // so we'll marshal to the new format, then unmarshal to get the + // fields, and marshal again. + // + // This is far from optimal, but this code is only used for deprecated + // API versions, so should not be hit commonly. + var argsFields map[string]map[string]bool + err := json.Unmarshal([]byte(currentFormat), &argsFields) + if err != nil { + return "", err + } + + buf, err := json.Marshal(convertArgsToSlice(argsFields)) + if err != nil { + return "", err + } + return string(buf), nil +} + +func convertArgsToSlice(f map[string]map[string]bool) map[string][]string { + m := map[string][]string{} + for k, v := range f { + values := []string{} + for kk := range v { + if v[kk] { + values = append(values, kk) + } + } + m[k] = values + } + return m +} diff --git a/vendor/github.com/docker/docker/api/types/filters/parse.go b/vendor/github.com/docker/docker/api/types/filters/parse.go index 86f4bdb28e17..396657bb1921 100644 --- a/vendor/github.com/docker/docker/api/types/filters/parse.go +++ b/vendor/github.com/docker/docker/api/types/filters/parse.go @@ -8,8 +8,6 @@ import ( "encoding/json" "regexp" "strings" - - "github.com/docker/docker/api/types/versions" ) // Args stores a mapping of keys to a set of multiple values. @@ -63,24 +61,6 @@ func ToJSON(a Args) (string, error) { return string(buf), err } -// ToParamWithVersion encodes Args as a JSON string. If version is less than 1.22 -// then the encoded format will use an older legacy format where the values are a -// list of strings, instead of a set. -// -// Deprecated: do not use in any new code; use ToJSON instead -func ToParamWithVersion(version string, a Args) (string, error) { - if a.Len() == 0 { - return "", nil - } - - if version != "" && versions.LessThan(version, "1.22") { - buf, err := json.Marshal(convertArgsToSlice(a.fields)) - return string(buf), err - } - - return ToJSON(a) -} - // FromJSON decodes a JSON encoded string into Args func FromJSON(p string) (Args, error) { args := NewArgs() @@ -320,17 +300,3 @@ func deprecatedArgs(d map[string][]string) map[string]map[string]bool { } return m } - -func convertArgsToSlice(f map[string]map[string]bool) map[string][]string { - m := map[string][]string{} - for k, v := range f { - values := []string{} - for kk := range v { - if v[kk] { - values = append(values, kk) - } - } - m[k] = values - } - return m -} diff --git a/vendor/github.com/docker/docker/api/types/image/disk_usage.go b/vendor/github.com/docker/docker/api/types/image/disk_usage.go index b29d925cac48..e847386a8d13 100644 --- a/vendor/github.com/docker/docker/api/types/image/disk_usage.go +++ b/vendor/github.com/docker/docker/api/types/image/disk_usage.go @@ -1,6 +1,8 @@ package image // DiskUsage contains disk usage for images. +// +// Deprecated: this type is no longer used and will be removed in the next release. type DiskUsage struct { TotalSize int64 Reclaimable int64 diff --git a/vendor/github.com/docker/docker/api/types/image/image_inspect.go b/vendor/github.com/docker/docker/api/types/image/image_inspect.go index 3bdb474287c0..1bec0b72b287 100644 --- a/vendor/github.com/docker/docker/api/types/image/image_inspect.go +++ b/vendor/github.com/docker/docker/api/types/image/image_inspect.go @@ -48,6 +48,8 @@ type InspectResponse struct { // Depending on how the image was created, this field may be empty and // is only set for images that were built/created locally. This field // is empty if the image was pulled from an image registry. + // + // Deprecated: this field is deprecated, and will be removed in the next release. Parent string // Comment is an optional message that can be set when committing or @@ -80,6 +82,8 @@ type InspectResponse struct { // DockerVersion is the version of Docker that was used to build the image. // // Depending on how the image was created, this field may be empty. + // + // Deprecated: this field is deprecated, and will be removed in the next release. DockerVersion string // Author is the name of the author that was specified when committing the diff --git a/vendor/github.com/docker/docker/api/types/network/endpoint.go b/vendor/github.com/docker/docker/api/types/network/endpoint.go index 167ac70ab56a..cdc06c6c9001 100644 --- a/vendor/github.com/docker/docker/api/types/network/endpoint.go +++ b/vendor/github.com/docker/docker/api/types/network/endpoint.go @@ -4,8 +4,6 @@ import ( "errors" "fmt" "net" - - "github.com/docker/docker/internal/multierror" ) // EndpointSettings stores the network endpoint details @@ -99,7 +97,7 @@ func (cfg *EndpointIPAMConfig) IsInRange(v4Subnets []NetworkSubnet, v6Subnets [] errs = append(errs, err) } - return multierror.Join(errs...) + return errJoin(errs...) } func validateEndpointIPAddress(epAddr string, ipamSubnets []NetworkSubnet) error { @@ -149,5 +147,5 @@ func (cfg *EndpointIPAMConfig) Validate() error { } } - return multierror.Join(errs...) + return errJoin(errs...) } diff --git a/vendor/github.com/docker/docker/api/types/network/ipam.go b/vendor/github.com/docker/docker/api/types/network/ipam.go index f319e1402b08..f9a9ff9b358f 100644 --- a/vendor/github.com/docker/docker/api/types/network/ipam.go +++ b/vendor/github.com/docker/docker/api/types/network/ipam.go @@ -4,8 +4,7 @@ import ( "errors" "fmt" "net/netip" - - "github.com/docker/docker/internal/multierror" + "strings" ) // IPAM represents IP Address Management @@ -72,7 +71,7 @@ func ValidateIPAM(ipam *IPAM, enableIPv6 bool) error { } } - if err := multierror.Join(errs...); err != nil { + if err := errJoin(errs...); err != nil { return fmt.Errorf("invalid network config:\n%w", err) } @@ -132,3 +131,43 @@ func validateAddress(address string, subnet netip.Prefix, subnetFamily ipFamily) return nil } + +func errJoin(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} + +type joinError struct { + errs []error +} + +func (e *joinError) Error() string { + if len(e.errs) == 1 { + return strings.TrimSpace(e.errs[0].Error()) + } + stringErrs := make([]string, 0, len(e.errs)) + for _, subErr := range e.errs { + stringErrs = append(stringErrs, strings.ReplaceAll(subErr.Error(), "\n", "\n\t")) + } + return "* " + strings.Join(stringErrs, "\n* ") +} + +func (e *joinError) Unwrap() []error { + return e.errs +} diff --git a/vendor/github.com/docker/docker/api/types/plugin.go b/vendor/github.com/docker/docker/api/types/plugin.go index abae48b9ab01..a9eff28a0485 100644 --- a/vendor/github.com/docker/docker/api/types/plugin.go +++ b/vendor/github.com/docker/docker/api/types/plugin.go @@ -42,7 +42,11 @@ type PluginConfig struct { // Required: true Description string `json:"Description"` - // Docker Version used to create the plugin + // Docker Version used to create the plugin. + // + // Depending on how the plugin was created, this field may be empty or omitted. + // + // Deprecated: this field is no longer set, and will be removed in the next API version. DockerVersion string `json:"DockerVersion,omitempty"` // documentation diff --git a/vendor/github.com/docker/docker/api/types/registry/authconfig.go b/vendor/github.com/docker/docker/api/types/registry/authconfig.go index 70f732007211..4c6d7ab2badf 100644 --- a/vendor/github.com/docker/docker/api/types/registry/authconfig.go +++ b/vendor/github.com/docker/docker/api/types/registry/authconfig.go @@ -32,8 +32,8 @@ type AuthConfig struct { Auth string `json:"auth,omitempty"` // Email is an optional value associated with the username. - // This field is deprecated and will be removed in a later - // version of docker. + // + // Deprecated: This field is deprecated since docker 1.11 (API v1.23) and will be removed in the next release. Email string `json:"email,omitempty"` ServerAddress string `json:"serveraddress,omitempty"` @@ -83,6 +83,8 @@ func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) { // Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an // error occurs. It is up to the caller to decide if authentication is required, // and if the error can be ignored. +// +// Deprecated: this function is no longer used and will be removed in the next release. func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) { return decodeAuthConfigFromReader(rdr) } diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime.go b/vendor/github.com/docker/docker/api/types/swarm/runtime.go index 8a28320f7b85..3fda4ca65d1d 100644 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime.go +++ b/vendor/github.com/docker/docker/api/types/swarm/runtime.go @@ -1,5 +1,7 @@ package swarm +import "github.com/docker/docker/api/types/swarm/runtime" + // RuntimeType is the type of runtime used for the TaskSpec type RuntimeType string @@ -25,3 +27,11 @@ const ( type NetworkAttachmentSpec struct { ContainerID string } + +// RuntimeSpec defines the base payload which clients can specify for creating +// a service with the plugin runtime. +type RuntimeSpec = runtime.PluginSpec + +// RuntimePrivilege describes a permission the user has to accept +// upon installing a plugin. +type RuntimePrivilege = runtime.PluginPrivilege diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go b/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go deleted file mode 100644 index 90e572cf9c90..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/gen.go +++ /dev/null @@ -1,3 +0,0 @@ -//go:generate protoc --gogofaster_out=import_path=github.com/docker/docker/api/types/swarm/runtime:. plugin.proto - -package runtime diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go b/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go deleted file mode 100644 index 32aaf0d51990..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.pb.go +++ /dev/null @@ -1,808 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: plugin.proto - -package runtime - -import ( - fmt "fmt" - proto "github.com/gogo/protobuf/proto" - io "io" - math "math" - math_bits "math/bits" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package - -// PluginSpec defines the base payload which clients can specify for creating -// a service with the plugin runtime. -type PluginSpec struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Remote string `protobuf:"bytes,2,opt,name=remote,proto3" json:"remote,omitempty"` - Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges,proto3" json:"privileges,omitempty"` - Disabled bool `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"` - Env []string `protobuf:"bytes,5,rep,name=env,proto3" json:"env,omitempty"` -} - -func (m *PluginSpec) Reset() { *m = PluginSpec{} } -func (m *PluginSpec) String() string { return proto.CompactTextString(m) } -func (*PluginSpec) ProtoMessage() {} -func (*PluginSpec) Descriptor() ([]byte, []int) { - return fileDescriptor_22a625af4bc1cc87, []int{0} -} -func (m *PluginSpec) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PluginSpec) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_PluginSpec.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *PluginSpec) XXX_Merge(src proto.Message) { - xxx_messageInfo_PluginSpec.Merge(m, src) -} -func (m *PluginSpec) XXX_Size() int { - return m.Size() -} -func (m *PluginSpec) XXX_DiscardUnknown() { - xxx_messageInfo_PluginSpec.DiscardUnknown(m) -} - -var xxx_messageInfo_PluginSpec proto.InternalMessageInfo - -func (m *PluginSpec) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *PluginSpec) GetRemote() string { - if m != nil { - return m.Remote - } - return "" -} - -func (m *PluginSpec) GetPrivileges() []*PluginPrivilege { - if m != nil { - return m.Privileges - } - return nil -} - -func (m *PluginSpec) GetDisabled() bool { - if m != nil { - return m.Disabled - } - return false -} - -func (m *PluginSpec) GetEnv() []string { - if m != nil { - return m.Env - } - return nil -} - -// PluginPrivilege describes a permission the user has to accept -// upon installing a plugin. -type PluginPrivilege struct { - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` - Value []string `protobuf:"bytes,3,rep,name=value,proto3" json:"value,omitempty"` -} - -func (m *PluginPrivilege) Reset() { *m = PluginPrivilege{} } -func (m *PluginPrivilege) String() string { return proto.CompactTextString(m) } -func (*PluginPrivilege) ProtoMessage() {} -func (*PluginPrivilege) Descriptor() ([]byte, []int) { - return fileDescriptor_22a625af4bc1cc87, []int{1} -} -func (m *PluginPrivilege) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PluginPrivilege) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_PluginPrivilege.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *PluginPrivilege) XXX_Merge(src proto.Message) { - xxx_messageInfo_PluginPrivilege.Merge(m, src) -} -func (m *PluginPrivilege) XXX_Size() int { - return m.Size() -} -func (m *PluginPrivilege) XXX_DiscardUnknown() { - xxx_messageInfo_PluginPrivilege.DiscardUnknown(m) -} - -var xxx_messageInfo_PluginPrivilege proto.InternalMessageInfo - -func (m *PluginPrivilege) GetName() string { - if m != nil { - return m.Name - } - return "" -} - -func (m *PluginPrivilege) GetDescription() string { - if m != nil { - return m.Description - } - return "" -} - -func (m *PluginPrivilege) GetValue() []string { - if m != nil { - return m.Value - } - return nil -} - -func init() { - proto.RegisterType((*PluginSpec)(nil), "PluginSpec") - proto.RegisterType((*PluginPrivilege)(nil), "PluginPrivilege") -} - -func init() { proto.RegisterFile("plugin.proto", fileDescriptor_22a625af4bc1cc87) } - -var fileDescriptor_22a625af4bc1cc87 = []byte{ - // 225 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xc8, 0x29, 0x4d, - 0xcf, 0xcc, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x9a, 0xc1, 0xc8, 0xc5, 0x15, 0x00, 0x16, - 0x08, 0x2e, 0x48, 0x4d, 0x16, 0x12, 0xe2, 0x62, 0xc9, 0x4b, 0xcc, 0x4d, 0x95, 0x60, 0x54, 0x60, - 0xd4, 0xe0, 0x0c, 0x02, 0xb3, 0x85, 0xc4, 0xb8, 0xd8, 0x8a, 0x52, 0x73, 0xf3, 0x4b, 0x52, 0x25, - 0x98, 0xc0, 0xa2, 0x50, 0x9e, 0x90, 0x01, 0x17, 0x57, 0x41, 0x51, 0x66, 0x59, 0x66, 0x4e, 0x6a, - 0x7a, 0x6a, 0xb1, 0x04, 0xb3, 0x02, 0xb3, 0x06, 0xb7, 0x91, 0x80, 0x1e, 0xc4, 0xb0, 0x00, 0x98, - 0x44, 0x10, 0x92, 0x1a, 0x21, 0x29, 0x2e, 0x8e, 0x94, 0xcc, 0xe2, 0xc4, 0xa4, 0x9c, 0xd4, 0x14, - 0x09, 0x16, 0x05, 0x46, 0x0d, 0x8e, 0x20, 0x38, 0x5f, 0x48, 0x80, 0x8b, 0x39, 0x35, 0xaf, 0x4c, - 0x82, 0x55, 0x81, 0x59, 0x83, 0x33, 0x08, 0xc4, 0x54, 0x8a, 0xe5, 0xe2, 0x47, 0x33, 0x0c, 0xab, - 0xf3, 0x14, 0xb8, 0xb8, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, 0x0b, 0x4a, 0x32, 0xf3, 0xf3, 0xa0, - 0x6e, 0x44, 0x16, 0x12, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x05, 0xbb, 0x91, 0x33, - 0x08, 0xc2, 0x71, 0x92, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, - 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x24, 0x36, - 0x70, 0xd0, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x37, 0xea, 0xe2, 0xca, 0x2a, 0x01, 0x00, - 0x00, -} - -func (m *PluginSpec) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *PluginSpec) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *PluginSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Env) > 0 { - for iNdEx := len(m.Env) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Env[iNdEx]) - copy(dAtA[i:], m.Env[iNdEx]) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Env[iNdEx]))) - i-- - dAtA[i] = 0x2a - } - } - if m.Disabled { - i-- - if m.Disabled { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x20 - } - if len(m.Privileges) > 0 { - for iNdEx := len(m.Privileges) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Privileges[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintPlugin(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x1a - } - } - if len(m.Remote) > 0 { - i -= len(m.Remote) - copy(dAtA[i:], m.Remote) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Remote))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func (m *PluginPrivilege) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *PluginPrivilege) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *PluginPrivilege) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Value) > 0 { - for iNdEx := len(m.Value) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Value[iNdEx]) - copy(dAtA[i:], m.Value[iNdEx]) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Value[iNdEx]))) - i-- - dAtA[i] = 0x1a - } - } - if len(m.Description) > 0 { - i -= len(m.Description) - copy(dAtA[i:], m.Description) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Description))) - i-- - dAtA[i] = 0x12 - } - if len(m.Name) > 0 { - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarintPlugin(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0xa - } - return len(dAtA) - i, nil -} - -func encodeVarintPlugin(dAtA []byte, offset int, v uint64) int { - offset -= sovPlugin(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *PluginSpec) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - l = len(m.Remote) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - if len(m.Privileges) > 0 { - for _, e := range m.Privileges { - l = e.Size() - n += 1 + l + sovPlugin(uint64(l)) - } - } - if m.Disabled { - n += 2 - } - if len(m.Env) > 0 { - for _, s := range m.Env { - l = len(s) - n += 1 + l + sovPlugin(uint64(l)) - } - } - return n -} - -func (m *PluginPrivilege) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Name) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - l = len(m.Description) - if l > 0 { - n += 1 + l + sovPlugin(uint64(l)) - } - if len(m.Value) > 0 { - for _, s := range m.Value { - l = len(s) - n += 1 + l + sovPlugin(uint64(l)) - } - } - return n -} - -func sovPlugin(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozPlugin(x uint64) (n int) { - return sovPlugin(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *PluginSpec) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: PluginSpec: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: PluginSpec: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Remote", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Remote = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Privileges", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Privileges = append(m.Privileges, &PluginPrivilege{}) - if err := m.Privileges[len(m.Privileges)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Disabled", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Disabled = bool(v != 0) - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Env", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Env = append(m.Env, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipPlugin(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthPlugin - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *PluginPrivilege) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: PluginPrivilege: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: PluginPrivilege: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Name = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Description", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Description = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Value", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowPlugin - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthPlugin - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthPlugin - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Value = append(m.Value, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipPlugin(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthPlugin - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipPlugin(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowPlugin - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLengthPlugin - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroupPlugin - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLengthPlugin - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowPlugin = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroupPlugin = fmt.Errorf("proto: unexpected end of group") -) diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto b/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto deleted file mode 100644 index e311b36ba2cf..000000000000 --- a/vendor/github.com/docker/docker/api/types/swarm/runtime/plugin.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -// PluginSpec defines the base payload which clients can specify for creating -// a service with the plugin runtime. -message PluginSpec { - string name = 1; - string remote = 2; - repeated PluginPrivilege privileges = 3; - bool disabled = 4; - repeated string env = 5; -} - -// PluginPrivilege describes a permission the user has to accept -// upon installing a plugin. -message PluginPrivilege { - string name = 1; - string description = 2; - repeated string value = 3; -} diff --git a/vendor/github.com/docker/docker/api/types/swarm/runtime/runtime.go b/vendor/github.com/docker/docker/api/types/swarm/runtime/runtime.go new file mode 100644 index 000000000000..95176b268186 --- /dev/null +++ b/vendor/github.com/docker/docker/api/types/swarm/runtime/runtime.go @@ -0,0 +1,27 @@ +package runtime + +import "fmt" + +// PluginSpec defines the base payload which clients can specify for creating +// a service with the plugin runtime. +type PluginSpec struct { + Name string `json:"name,omitempty"` + Remote string `json:"remote,omitempty"` + Privileges []*PluginPrivilege `json:"privileges,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Env []string `json:"env,omitempty"` +} + +// PluginPrivilege describes a permission the user has to accept +// upon installing a plugin. +type PluginPrivilege struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Value []string `json:"value,omitempty"` +} + +var ( + ErrInvalidLengthPlugin = fmt.Errorf("proto: negative length found during unmarshaling") // Deprecated: this error was only used internally and is no longer used. + ErrIntOverflowPlugin = fmt.Errorf("proto: integer overflow") // Deprecated: this error was only used internally and is no longer used. + ErrUnexpectedEndOfGroupPlugin = fmt.Errorf("proto: unexpected end of group") // Deprecated: this error was only used internally and is no longer used. +) diff --git a/vendor/github.com/docker/docker/api/types/swarm/task.go b/vendor/github.com/docker/docker/api/types/swarm/task.go index 4dc95e8b1dde..e143f844fa8f 100644 --- a/vendor/github.com/docker/docker/api/types/swarm/task.go +++ b/vendor/github.com/docker/docker/api/types/swarm/task.go @@ -4,7 +4,6 @@ import ( "time" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm/runtime" ) // TaskState represents the state of a task. @@ -77,7 +76,7 @@ type TaskSpec struct { // NetworkAttachmentSpec is used if the `Runtime` field is set to // `attachment`. ContainerSpec *ContainerSpec `json:",omitempty"` - PluginSpec *runtime.PluginSpec `json:",omitempty"` + PluginSpec *RuntimeSpec `json:",omitempty"` NetworkAttachmentSpec *NetworkAttachmentSpec `json:",omitempty"` Resources *ResourceRequirements `json:",omitempty"` diff --git a/vendor/github.com/docker/docker/api/types/system/disk_usage.go b/vendor/github.com/docker/docker/api/types/system/disk_usage.go deleted file mode 100644 index 99078cf196d0..000000000000 --- a/vendor/github.com/docker/docker/api/types/system/disk_usage.go +++ /dev/null @@ -1,17 +0,0 @@ -package system - -import ( - "github.com/docker/docker/api/types/build" - "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/image" - "github.com/docker/docker/api/types/volume" -) - -// DiskUsage contains response of Engine API for API 1.49 and greater: -// GET "/system/df" -type DiskUsage struct { - Images *image.DiskUsage - Containers *container.DiskUsage - Volumes *volume.DiskUsage - BuildCache *build.CacheDiskUsage -} diff --git a/vendor/github.com/docker/docker/api/types/system/info.go b/vendor/github.com/docker/docker/api/types/system/info.go index 047639ed91e2..0f39099d8ab5 100644 --- a/vendor/github.com/docker/docker/api/types/system/info.go +++ b/vendor/github.com/docker/docker/api/types/system/info.go @@ -9,19 +9,23 @@ import ( // Info contains response of Engine API: // GET "/info" type Info struct { - ID string - Containers int - ContainersRunning int - ContainersPaused int - ContainersStopped int - Images int - Driver string - DriverStatus [][2]string - SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API - Plugins PluginsInfo - MemoryLimit bool - SwapLimit bool - KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes + ID string + Containers int + ContainersRunning int + ContainersPaused int + ContainersStopped int + Images int + Driver string + DriverStatus [][2]string + SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API + Plugins PluginsInfo + MemoryLimit bool + SwapLimit bool + KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes + // KernelMemoryLimit is not supported on cgroups v2. + // + // Deprecated: This field is deprecated and will be removed in the next release. + // Starting with kernel 6.12, the kernel has deprecated kernel memory tcp accounting KernelMemoryTCP bool `json:",omitempty"` // KernelMemoryTCP is not supported on cgroups v2. CPUCfsPeriod bool `json:"CpuCfsPeriod"` CPUCfsQuota bool `json:"CpuCfsQuota"` diff --git a/vendor/github.com/docker/docker/api/types/types_deprecated.go b/vendor/github.com/docker/docker/api/types/types_deprecated.go index 8456a45607e2..c9c20b873604 100644 --- a/vendor/github.com/docker/docker/api/types/types_deprecated.go +++ b/vendor/github.com/docker/docker/api/types/types_deprecated.go @@ -46,15 +46,16 @@ type NetworkSettings = container.NetworkSettings // NetworkSettingsBase holds networking state for a container when inspecting it. // -// Deprecated: use [container.NetworkSettingsBase]. -type NetworkSettingsBase = container.NetworkSettingsBase +// Deprecated: [container.NetworkSettingsBase] will be removed in v29. Prefer +// accessing the fields it contains through [container.NetworkSettings]. +type NetworkSettingsBase = container.NetworkSettingsBase //nolint:staticcheck // ignore SA1019: NetworkSettingsBase is deprecated in v28.4. // DefaultNetworkSettings holds network information // during the 2 release deprecation period. // It will be removed in Docker 1.11. // // Deprecated: use [container.DefaultNetworkSettings]. -type DefaultNetworkSettings = container.DefaultNetworkSettings +type DefaultNetworkSettings = container.DefaultNetworkSettings //nolint:staticcheck // ignore SA1019: DefaultNetworkSettings is deprecated in v28.4. // SummaryNetworkSettings provides a summary of container's networks // in /containers/json. diff --git a/vendor/github.com/docker/docker/api/types/volume/disk_usage.go b/vendor/github.com/docker/docker/api/types/volume/disk_usage.go index 3d716c6e00d9..88974303a0b0 100644 --- a/vendor/github.com/docker/docker/api/types/volume/disk_usage.go +++ b/vendor/github.com/docker/docker/api/types/volume/disk_usage.go @@ -1,6 +1,8 @@ package volume // DiskUsage contains disk usage for volumes. +// +// Deprecated: this type is no longer used and will be removed in the next release. type DiskUsage struct { TotalSize int64 Reclaimable int64 diff --git a/vendor/github.com/docker/docker/client/client.go b/vendor/github.com/docker/docker/client/client.go index d6e014dddf81..8acfb7f490e4 100644 --- a/vendor/github.com/docker/docker/client/client.go +++ b/vendor/github.com/docker/docker/client/client.go @@ -463,7 +463,9 @@ func (cli *Client) dialer() func(context.Context) (net.Conn, error) { case "unix": return net.Dial(cli.proto, cli.addr) case "npipe": - return sockets.DialPipe(cli.addr, 32*time.Second) + ctx, cancel := context.WithTimeout(ctx, 32*time.Second) + defer cancel() + return dialPipeContext(ctx, cli.addr) default: if tlsConfig := cli.tlsConfig(); tlsConfig != nil { return tls.Dial(cli.proto, cli.addr, tlsConfig) diff --git a/vendor/github.com/docker/docker/client/client_unix.go b/vendor/github.com/docker/docker/client/client_unix.go index e5b921b40642..1fb9fbfb9e55 100644 --- a/vendor/github.com/docker/docker/client/client_unix.go +++ b/vendor/github.com/docker/docker/client/client_unix.go @@ -2,6 +2,17 @@ package client +import ( + "context" + "net" + "syscall" +) + // DefaultDockerHost defines OS-specific default host if the DOCKER_HOST // (EnvOverrideHost) environment variable is unset or empty. const DefaultDockerHost = "unix:///var/run/docker.sock" + +// dialPipeContext connects to a Windows named pipe. It is not supported on non-Windows. +func dialPipeContext(_ context.Context, _ string) (net.Conn, error) { + return nil, syscall.EAFNOSUPPORT +} diff --git a/vendor/github.com/docker/docker/client/client_windows.go b/vendor/github.com/docker/docker/client/client_windows.go index 19b954b2fd78..b471c0612403 100644 --- a/vendor/github.com/docker/docker/client/client_windows.go +++ b/vendor/github.com/docker/docker/client/client_windows.go @@ -1,5 +1,17 @@ package client +import ( + "context" + "net" + + "github.com/Microsoft/go-winio" +) + // DefaultDockerHost defines OS-specific default host if the DOCKER_HOST // (EnvOverrideHost) environment variable is unset or empty. const DefaultDockerHost = "npipe:////./pipe/docker_engine" + +// dialPipeContext connects to a Windows named pipe. It is not supported on non-Windows. +func dialPipeContext(ctx context.Context, addr string) (net.Conn, error) { + return winio.DialPipeContext(ctx, addr) +} diff --git a/vendor/github.com/docker/docker/client/container_stats.go b/vendor/github.com/docker/docker/client/container_stats.go index 2244e0f4b94a..076954f4c3e8 100644 --- a/vendor/github.com/docker/docker/client/container_stats.go +++ b/vendor/github.com/docker/docker/client/container_stats.go @@ -28,7 +28,7 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea return container.StatsResponseReader{ Body: resp.Body, - OSType: getDockerOS(resp.Header.Get("Server")), + OSType: resp.Header.Get("Ostype"), }, nil } @@ -51,6 +51,6 @@ func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string return container.StatsResponseReader{ Body: resp.Body, - OSType: getDockerOS(resp.Header.Get("Server")), + OSType: resp.Header.Get("Ostype"), }, nil } diff --git a/vendor/github.com/docker/docker/client/image_build.go b/vendor/github.com/docker/docker/client/image_build.go index 66ca75e4af28..1ed0878bfdb2 100644 --- a/vendor/github.com/docker/docker/client/image_build.go +++ b/vendor/github.com/docker/docker/client/image_build.go @@ -40,7 +40,7 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio return build.ImageBuildResponse{ Body: resp.Body, - OSType: getDockerOS(resp.Header.Get("Server")), + OSType: resp.Header.Get("Ostype"), }, nil } diff --git a/vendor/github.com/docker/docker/client/image_push.go b/vendor/github.com/docker/docker/client/image_push.go index cbbe9a25d609..8dbe0b1e5caa 100644 --- a/vendor/github.com/docker/docker/client/image_push.go +++ b/vendor/github.com/docker/docker/client/image_push.go @@ -66,7 +66,16 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu } func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/images/"+imageID+"/push", query, nil, http.Header{ + // Always send a body (which may be an empty JSON document ("{}")) to prevent + // EOF errors on older daemons which had faulty fallback code for handling + // authentication in the body when no auth-header was set, resulting in; + // + // Error response from daemon: bad parameters and missing X-Registry-Auth: invalid X-Registry-Auth header: EOF + // + // We use [http.NoBody], which gets marshaled to an empty JSON document. + // + // see: https://github.com/moby/moby/commit/ea29dffaa541289591aa44fa85d2a596ce860e16 + return cli.post(ctx, "/images/"+imageID+"/push", query, http.NoBody, http.Header{ registry.AuthHeader: {registryAuth}, }) } diff --git a/vendor/github.com/docker/docker/client/utils.go b/vendor/github.com/docker/docker/client/utils.go index 67e1e6934b59..7b82f185ac58 100644 --- a/vendor/github.com/docker/docker/client/utils.go +++ b/vendor/github.com/docker/docker/client/utils.go @@ -8,12 +8,9 @@ import ( cerrdefs "github.com/containerd/errdefs" "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/internal/lazyregexp" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -var headerRegexp = lazyregexp.New(`\ADocker/.+\s\((.+)\)\z`) - type emptyIDError string func (e emptyIDError) InvalidParameter() {} @@ -31,16 +28,6 @@ func trimID(objType, id string) (string, error) { return id, nil } -// getDockerOS returns the operating system based on the server header from the daemon. -func getDockerOS(serverHeader string) string { - var osType string - matches := headerRegexp.FindStringSubmatch(serverHeader) - if len(matches) > 0 { - osType = matches[1] - } - return osType -} - // getFiltersQuery returns a url query with "filters" query term, based on the // filters provided. func getFiltersQuery(f filters.Args) (url.Values, error) { diff --git a/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go b/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go deleted file mode 100644 index 6334edb60dca..000000000000 --- a/vendor/github.com/docker/docker/internal/lazyregexp/lazyregexp.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Code below was largely copied from golang.org/x/mod@v0.22; -// https://github.com/golang/mod/blob/v0.22.0/internal/lazyregexp/lazyre.go -// with some additional methods added. - -// Package lazyregexp is a thin wrapper over regexp, allowing the use of global -// regexp variables without forcing them to be compiled at init. -package lazyregexp - -import ( - "os" - "regexp" - "strings" - "sync" -) - -// Regexp is a wrapper around [regexp.Regexp], where the underlying regexp will be -// compiled the first time it is needed. -type Regexp struct { - str string - once sync.Once - rx *regexp.Regexp -} - -func (r *Regexp) re() *regexp.Regexp { - r.once.Do(r.build) - return r.rx -} - -func (r *Regexp) build() { - r.rx = regexp.MustCompile(r.str) - r.str = "" -} - -func (r *Regexp) FindSubmatch(s []byte) [][]byte { - return r.re().FindSubmatch(s) -} - -func (r *Regexp) FindAllStringSubmatch(s string, n int) [][]string { - return r.re().FindAllStringSubmatch(s, n) -} - -func (r *Regexp) FindStringSubmatch(s string) []string { - return r.re().FindStringSubmatch(s) -} - -func (r *Regexp) FindStringSubmatchIndex(s string) []int { - return r.re().FindStringSubmatchIndex(s) -} - -func (r *Regexp) ReplaceAllString(src, repl string) string { - return r.re().ReplaceAllString(src, repl) -} - -func (r *Regexp) FindString(s string) string { - return r.re().FindString(s) -} - -func (r *Regexp) FindAllString(s string, n int) []string { - return r.re().FindAllString(s, n) -} - -func (r *Regexp) MatchString(s string) bool { - return r.re().MatchString(s) -} - -func (r *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string { - return r.re().ReplaceAllStringFunc(src, repl) -} - -func (r *Regexp) SubexpNames() []string { - return r.re().SubexpNames() -} - -var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test") - -// New creates a new lazy regexp, delaying the compiling work until it is first -// needed. If the code is being run as part of tests, the regexp compiling will -// happen immediately. -func New(str string) *Regexp { - lr := &Regexp{str: str} - if inTest { - // In tests, always compile the regexps early. - lr.re() - } - return lr -} diff --git a/vendor/github.com/docker/docker/internal/multierror/multierror.go b/vendor/github.com/docker/docker/internal/multierror/multierror.go deleted file mode 100644 index e899f4de85c9..000000000000 --- a/vendor/github.com/docker/docker/internal/multierror/multierror.go +++ /dev/null @@ -1,46 +0,0 @@ -package multierror - -import ( - "strings" -) - -// Join is a drop-in replacement for errors.Join with better formatting. -func Join(errs ...error) error { - n := 0 - for _, err := range errs { - if err != nil { - n++ - } - } - if n == 0 { - return nil - } - e := &joinError{ - errs: make([]error, 0, n), - } - for _, err := range errs { - if err != nil { - e.errs = append(e.errs, err) - } - } - return e -} - -type joinError struct { - errs []error -} - -func (e *joinError) Error() string { - if len(e.errs) == 1 { - return strings.TrimSpace(e.errs[0].Error()) - } - stringErrs := make([]string, 0, len(e.errs)) - for _, subErr := range e.errs { - stringErrs = append(stringErrs, strings.ReplaceAll(subErr.Error(), "\n", "\n\t")) - } - return "* " + strings.Join(stringErrs, "\n* ") -} - -func (e *joinError) Unwrap() []error { - return e.errs -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir.go b/vendor/github.com/docker/docker/pkg/homedir/homedir.go deleted file mode 100644 index c0ab3f5bf359..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir.go +++ /dev/null @@ -1,28 +0,0 @@ -package homedir - -import ( - "os" - "os/user" - "runtime" -) - -// Get returns the home directory of the current user with the help of -// environment variables depending on the target operating system. -// Returned path should be used with "path/filepath" to form new paths. -// -// On non-Windows platforms, it falls back to nss lookups, if the home -// directory cannot be obtained from environment-variables. -// -// If linking statically with cgo enabled against glibc, ensure the -// osusergo build tag is used. -// -// If needing to do nss lookups, do not disable cgo or set osusergo. -func Get() string { - home, _ := os.UserHomeDir() - if home == "" && runtime.GOOS != "windows" { - if u, err := user.Current(); err == nil { - return u.HomeDir - } - } - return home -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go deleted file mode 100644 index 469395f16e2e..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_linux.go +++ /dev/null @@ -1,105 +0,0 @@ -package homedir - -import ( - "errors" - "os" - "path/filepath" - "strings" -) - -// GetRuntimeDir returns XDG_RUNTIME_DIR. -// XDG_RUNTIME_DIR is typically configured via pam_systemd. -// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetRuntimeDir() (string, error) { - if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" { - return xdgRuntimeDir, nil - } - return "", errors.New("could not get XDG_RUNTIME_DIR") -} - -// StickRuntimeDirContents sets the sticky bit on files that are under -// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system. -// -// StickyRuntimeDir returns slice of sticked files. -// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func StickRuntimeDirContents(files []string) ([]string, error) { - runtimeDir, err := GetRuntimeDir() - if err != nil { - // ignore error if runtimeDir is empty - return nil, nil - } - runtimeDir, err = filepath.Abs(runtimeDir) - if err != nil { - return nil, err - } - var sticked []string - for _, f := range files { - f, err = filepath.Abs(f) - if err != nil { - return sticked, err - } - if strings.HasPrefix(f, runtimeDir+"/") { - if err = stick(f); err != nil { - return sticked, err - } - sticked = append(sticked, f) - } - } - return sticked, nil -} - -func stick(f string) error { - st, err := os.Stat(f) - if err != nil { - return err - } - m := st.Mode() - m |= os.ModeSticky - return os.Chmod(f, m) -} - -// GetDataHome returns XDG_DATA_HOME. -// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set. -// If HOME and XDG_DATA_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetDataHome() (string, error) { - if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { - return xdgDataHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_DATA_HOME or HOME") - } - return filepath.Join(home, ".local", "share"), nil -} - -// GetConfigHome returns XDG_CONFIG_HOME. -// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set. -// If HOME and XDG_CONFIG_HOME are not set, getpwent(3) is consulted to determine the users home directory. -// -// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html -func GetConfigHome() (string, error) { - if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { - return xdgConfigHome, nil - } - home := Get() - if home == "" { - return "", errors.New("could not get either XDG_CONFIG_HOME or HOME") - } - return filepath.Join(home, ".config"), nil -} - -// GetLibHome returns $HOME/.local/lib -// If HOME is not set, getpwent(3) is consulted to determine the users home directory. -func GetLibHome() (string, error) { - home := Get() - if home == "" { - return "", errors.New("could not get HOME") - } - return filepath.Join(home, ".local/lib"), nil -} diff --git a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go b/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go deleted file mode 100644 index 1e41e6aab51f..000000000000 --- a/vendor/github.com/docker/docker/pkg/homedir/homedir_others.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build !linux - -package homedir - -import ( - "errors" -) - -// GetRuntimeDir is unsupported on non-linux system. -func GetRuntimeDir() (string, error) { - return "", errors.New("homedir.GetRuntimeDir() is not supported on this system") -} - -// StickRuntimeDirContents is unsupported on non-linux system. -func StickRuntimeDirContents(files []string) ([]string, error) { - return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system") -} - -// GetDataHome is unsupported on non-linux system. -func GetDataHome() (string, error) { - return "", errors.New("homedir.GetDataHome() is not supported on this system") -} - -// GetConfigHome is unsupported on non-linux system. -func GetConfigHome() (string, error) { - return "", errors.New("homedir.GetConfigHome() is not supported on this system") -} - -// GetLibHome is unsupported on non-linux system. -func GetLibHome() (string, error) { - return "", errors.New("homedir.GetLibHome() is not supported on this system") -} diff --git a/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go b/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go index 3d072808f1de..13df228c622c 100644 --- a/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go +++ b/vendor/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go @@ -151,9 +151,9 @@ type JSONMessage struct { // Deprecated: this field is deprecated since docker v0.7.1 / API v1.8. Use the information in [Progress] instead. This field will be omitted in a future release. ProgressMessage string `json:"progress,omitempty"` ID string `json:"id,omitempty"` - From string `json:"from,omitempty"` - Time int64 `json:"time,omitempty"` - TimeNano int64 `json:"timeNano,omitempty"` + From string `json:"from,omitempty"` // Deprecated: this field is no longer set in stream responses and should not be used. + Time int64 `json:"time,omitempty"` // Deprecated: this field is no longer set in stream responses and should not be used. + TimeNano int64 `json:"timeNano,omitempty"` // Deprecated: this field is no longer set in stream responses and should not be used. Error *JSONError `json:"errorDetail,omitempty"` // ErrorMessage contains errors encountered during the operation. diff --git a/vendor/github.com/docker/docker/pkg/process/doc.go b/vendor/github.com/docker/docker/pkg/process/doc.go deleted file mode 100644 index dae536d7dbb0..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package process provides a set of basic functions to manage individual -// processes. -package process diff --git a/vendor/github.com/docker/docker/pkg/process/process_unix.go b/vendor/github.com/docker/docker/pkg/process/process_unix.go deleted file mode 100644 index 13298bbdccb7..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/process_unix.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build !windows - -package process - -import ( - "bytes" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "strconv" - - "golang.org/x/sys/unix" -) - -// Alive returns true if process with a given pid is running. It only considers -// positive PIDs; 0 (all processes in the current process group), -1 (all processes -// with a PID larger than 1), and negative (-n, all processes in process group -// "n") values for pid are never considered to be alive. -func Alive(pid int) bool { - if pid < 1 { - return false - } - switch runtime.GOOS { - case "darwin": - // OS X does not have a proc filesystem. Use kill -0 pid to judge if the - // process exists. From KILL(2): https://www.freebsd.org/cgi/man.cgi?query=kill&sektion=2&manpath=OpenDarwin+7.2.1 - // - // Sig may be one of the signals specified in sigaction(2) or it may - // be 0, in which case error checking is performed but no signal is - // actually sent. This can be used to check the validity of pid. - err := unix.Kill(pid, 0) - - // Either the PID was found (no error) or we get an EPERM, which means - // the PID exists, but we don't have permissions to signal it. - return err == nil || errors.Is(err, unix.EPERM) - default: - _, err := os.Stat(filepath.Join("/proc", strconv.Itoa(pid))) - return err == nil - } -} - -// Kill force-stops a process. It only considers positive PIDs; 0 (all processes -// in the current process group), -1 (all processes with a PID larger than 1), -// and negative (-n, all processes in process group "n") values for pid are -// ignored. Refer to [KILL(2)] for details. -// -// [KILL(2)]: https://man7.org/linux/man-pages/man2/kill.2.html -func Kill(pid int) error { - if pid < 1 { - return fmt.Errorf("invalid PID (%d): only positive PIDs are allowed", pid) - } - err := unix.Kill(pid, unix.SIGKILL) - if err != nil && !errors.Is(err, unix.ESRCH) { - return err - } - return nil -} - -// Zombie return true if process has a state with "Z". It only considers positive -// PIDs; 0 (all processes in the current process group), -1 (all processes with -// a PID larger than 1), and negative (-n, all processes in process group "n") -// values for pid are ignored. Refer to [PROC(5)] for details. -// -// [PROC(5)]: https://man7.org/linux/man-pages/man5/proc.5.html -func Zombie(pid int) (bool, error) { - if pid < 1 { - return false, nil - } - data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - if cols := bytes.SplitN(data, []byte(" "), 4); len(cols) >= 3 && string(cols[2]) == "Z" { - return true, nil - } - return false, nil -} diff --git a/vendor/github.com/docker/docker/pkg/process/process_windows.go b/vendor/github.com/docker/docker/pkg/process/process_windows.go deleted file mode 100644 index 2dd57e825452..000000000000 --- a/vendor/github.com/docker/docker/pkg/process/process_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -package process - -import ( - "os" - - "golang.org/x/sys/windows" -) - -// Alive returns true if process with a given pid is running. -func Alive(pid int) bool { - h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) - if err != nil { - return false - } - var c uint32 - err = windows.GetExitCodeProcess(h, &c) - _ = windows.CloseHandle(h) - if err != nil { - // From the GetExitCodeProcess function (processthreadsapi.h) API docs: - // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess - // - // The GetExitCodeProcess function returns a valid error code defined by the - // application only after the thread terminates. Therefore, an application should - // not use STILL_ACTIVE (259) as an error code (STILL_ACTIVE is a macro for - // STATUS_PENDING (minwinbase.h)). If a thread returns STILL_ACTIVE (259) as - // an error code, then applications that test for that value could interpret it - // to mean that the thread is still running, and continue to test for the - // completion of the thread after the thread has terminated, which could put - // the application into an infinite loop. - return c == uint32(windows.STATUS_PENDING) - } - return true -} - -// Kill force-stops a process. -func Kill(pid int) error { - p, err := os.FindProcess(pid) - if err == nil { - err = p.Kill() - if err != nil && err != os.ErrProcessDone { - return err - } - } - return nil -} diff --git a/vendor/github.com/docker/docker/registry/config.go b/vendor/github.com/docker/docker/registry/config.go deleted file mode 100644 index 218a12683a63..000000000000 --- a/vendor/github.com/docker/docker/registry/config.go +++ /dev/null @@ -1,481 +0,0 @@ -package registry - -import ( - "context" - "net" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/internal/lazyregexp" - "github.com/docker/docker/pkg/homedir" -) - -// ServiceOptions holds command line options. -type ServiceOptions struct { - Mirrors []string `json:"registry-mirrors,omitempty"` - InsecureRegistries []string `json:"insecure-registries,omitempty"` -} - -// serviceConfig holds daemon configuration for the registry service. -type serviceConfig registry.ServiceConfig - -// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains -// are here for historic reasons and backward-compatibility. These domains -// are still supported by Docker Hub (and will continue to be supported), but -// there are new domains already in use, and plans to consolidate all legacy -// domains to new "canonical" domains. Once those domains are decided on, we -// should update these consts (but making sure to preserve compatibility with -// existing installs, clients, and user configuration). -const ( - // DefaultNamespace is the default namespace - DefaultNamespace = "docker.io" - // DefaultRegistryHost is the hostname for the default (Docker Hub) registry - // used for pushing and pulling images. This hostname is hard-coded to handle - // the conversion from image references without registry name (e.g. "ubuntu", - // or "ubuntu:latest"), as well as references using the "docker.io" domain - // name, which is used as canonical reference for images on Docker Hub, but - // does not match the domain-name of Docker Hub's registry. - DefaultRegistryHost = "registry-1.docker.io" - // IndexHostname is the index hostname, used for authentication and image search. - IndexHostname = "index.docker.io" - // IndexServer is used for user auth and image search - IndexServer = "https://" + IndexHostname + "/v1/" - // IndexName is the name of the index - IndexName = "docker.io" -) - -var ( - // DefaultV2Registry is the URI of the default (Docker Hub) registry. - DefaultV2Registry = &url.URL{ - Scheme: "https", - Host: DefaultRegistryHost, - } - - validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`) - - // certsDir is used to override defaultCertsDir when running with rootlessKit. - // - // TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir] - // TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed. - setCertsDirOnce sync.Once - certsDir string -) - -func setCertsDir(dir string) string { - setCertsDirOnce.Do(func() { - if dir != "" { - certsDir = dir - return - } - if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" { - // Configure registry.CertsDir() when running in rootless-mode - // This is the equivalent of [rootless.RunningWithRootlessKit], - // but inlining it to prevent adding that as a dependency - // for docker/cli. - // - // [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8 - if configHome, _ := homedir.GetConfigHome(); configHome != "" { - certsDir = filepath.Join(configHome, "docker/certs.d") - return - } - } - certsDir = defaultCertsDir - }) - return certsDir -} - -// SetCertsDir allows the default certs directory to be changed. This function -// is used at daemon startup to set the correct location when running in -// rootless mode. -// -// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually. -func SetCertsDir(path string) { - setCertsDir(path) -} - -// CertsDir is the directory where certificates are stored. -func CertsDir() string { - // call setCertsDir with an empty path to synchronise with [SetCertsDir] - return setCertsDir("") -} - -// newServiceConfig returns a new instance of ServiceConfig -func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { - config := &serviceConfig{} - if err := config.loadMirrors(options.Mirrors); err != nil { - return nil, err - } - if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil { - return nil, err - } - - return config, nil -} - -// copy constructs a new ServiceConfig with a copy of the configuration in config. -func (config *serviceConfig) copy() *registry.ServiceConfig { - ic := make(map[string]*registry.IndexInfo) - for key, value := range config.IndexConfigs { - ic[key] = value - } - return ®istry.ServiceConfig{ - InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), - IndexConfigs: ic, - Mirrors: append([]string(nil), config.Mirrors...), - } -} - -// loadMirrors loads mirrors to config, after removing duplicates. -// Returns an error if mirrors contains an invalid mirror. -func (config *serviceConfig) loadMirrors(mirrors []string) error { - mMap := map[string]struct{}{} - unique := []string{} - - for _, mirror := range mirrors { - m, err := ValidateMirror(mirror) - if err != nil { - return err - } - if _, exist := mMap[m]; !exist { - mMap[m] = struct{}{} - unique = append(unique, m) - } - } - - config.Mirrors = unique - - // Configure public registry since mirrors may have changed. - config.IndexConfigs = map[string]*registry.IndexInfo{ - IndexName: { - Name: IndexName, - Mirrors: unique, - Secure: true, - Official: true, - }, - } - - return nil -} - -// loadInsecureRegistries loads insecure registries to config -func (config *serviceConfig) loadInsecureRegistries(registries []string) error { - // Localhost is by default considered as an insecure registry. This is a - // stop-gap for people who are running a private registry on localhost. - registries = append(registries, "::1/128", "127.0.0.0/8") - - var ( - insecureRegistryCIDRs = make([]*registry.NetIPNet, 0) - indexConfigs = make(map[string]*registry.IndexInfo) - ) - -skip: - for _, r := range registries { - // validate insecure registry - if _, err := ValidateIndexName(r); err != nil { - return err - } - if strings.HasPrefix(strings.ToLower(r), "http://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'http://' and 'http://' has been removed from the insecure registry config", r) - r = r[7:] - } else if strings.HasPrefix(strings.ToLower(r), "https://") { - log.G(context.TODO()).Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r) - r = r[8:] - } else if hasScheme(r) { - return invalidParamf("insecure registry %s should not contain '://'", r) - } - // Check if CIDR was passed to --insecure-registry - _, ipnet, err := net.ParseCIDR(r) - if err == nil { - // Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip. - data := (*registry.NetIPNet)(ipnet) - for _, value := range insecureRegistryCIDRs { - if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() { - continue skip - } - } - // ipnet is not found, add it in config.InsecureRegistryCIDRs - insecureRegistryCIDRs = append(insecureRegistryCIDRs, data) - } else { - if err := validateHostPort(r); err != nil { - return invalidParamWrapf(err, "insecure registry %s is not valid", r) - } - // Assume `host:port` if not CIDR. - indexConfigs[r] = ®istry.IndexInfo{ - Name: r, - Mirrors: []string{}, - Secure: false, - Official: false, - } - } - } - - // Configure public registry. - indexConfigs[IndexName] = ®istry.IndexInfo{ - Name: IndexName, - Mirrors: config.Mirrors, - Secure: true, - Official: true, - } - config.InsecureRegistryCIDRs = insecureRegistryCIDRs - config.IndexConfigs = indexConfigs - - return nil -} - -// isSecureIndex returns false if the provided indexName is part of the list of insecure registries -// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs. -// -// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet. -// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered -// insecure. -// -// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name -// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained -// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element -// of insecureRegistries. -func (config *serviceConfig) isSecureIndex(indexName string) bool { - // Check for configured index, first. This is needed in case isSecureIndex - // is called from anything besides newIndexInfo, in order to honor per-index configurations. - if index, ok := config.IndexConfigs[indexName]; ok { - return index.Secure - } - - return !isCIDRMatch(config.InsecureRegistryCIDRs, indexName) -} - -// for mocking in unit tests. -var lookupIP = net.LookupIP - -// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`) -// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be -// resolved to IP addresses for matching. If resolution fails, false is returned. -func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool { - if len(cidrs) == 0 { - return false - } - - host, _, err := net.SplitHostPort(URLHost) - if err != nil { - // Assume URLHost is a host without port and go on. - host = URLHost - } - - var addresses []net.IP - if ip := net.ParseIP(host); ip != nil { - // Host is an IP-address. - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-address. - addresses, err = lookupIP(host) - if err != nil { - // We failed to resolve the host; assume there's no match. - return false - } - } - - for _, addr := range addresses { - for _, ipnet := range cidrs { - // check if the addr falls in the subnet - if (*net.IPNet)(ipnet).Contains(addr) { - return true - } - } - } - - return false -} - -// ValidateMirror validates and normalizes an HTTP(S) registry mirror. It -// returns an error if the given mirrorURL is invalid, or the normalized -// format for the URL otherwise. -// -// It is used by the daemon to validate the daemon configuration. -func ValidateMirror(mirrorURL string) (string, error) { - // Fast path for missing scheme, as url.Parse splits by ":", which can - // cause the hostname to be considered the "scheme" when using "hostname:port". - if scheme, _, ok := strings.Cut(mirrorURL, "://"); !ok || scheme == "" { - return "", invalidParamf("invalid mirror: no scheme specified for %q: must use either 'https://' or 'http://'", mirrorURL) - } - uri, err := url.Parse(mirrorURL) - if err != nil { - return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", mirrorURL) - } - if uri.Scheme != "http" && uri.Scheme != "https" { - return "", invalidParamf("invalid mirror: unsupported scheme %q in %q: must use either 'https://' or 'http://'", uri.Scheme, uri) - } - if uri.RawQuery != "" || uri.Fragment != "" { - return "", invalidParamf("invalid mirror: query or fragment at end of the URI %q", uri) - } - if uri.User != nil { - // strip password from output - uri.User = url.UserPassword(uri.User.Username(), "xxxxx") - return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri) - } - return strings.TrimSuffix(mirrorURL, "/") + "/", nil -} - -// ValidateIndexName validates an index name. It is used by the daemon to -// validate the daemon configuration. -func ValidateIndexName(val string) (string, error) { - val = normalizeIndexName(val) - if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") { - return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val) - } - return val, nil -} - -func normalizeIndexName(val string) string { - // TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/". - // TODO: upstream this to check to reference package - if val == "index.docker.io" { - return "docker.io" - } - return val -} - -func hasScheme(reposName string) bool { - return strings.Contains(reposName, "://") -} - -func validateHostPort(s string) error { - // Split host and port, and in case s can not be split, assume host only - host, port, err := net.SplitHostPort(s) - if err != nil { - host = s - port = "" - } - // If match against the `host:port` pattern fails, - // it might be `IPv6:port`, which will be captured by net.ParseIP(host) - if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil { - return invalidParamf("invalid host %q", host) - } - if port != "" { - v, err := strconv.Atoi(port) - if err != nil { - return err - } - if v < 0 || v > 65535 { - return invalidParamf("invalid port %q", port) - } - } - return nil -} - -// newIndexInfo returns IndexInfo configuration from indexName -func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo { - indexName = normalizeIndexName(indexName) - - // Return any configured index info, first. - if index, ok := config.IndexConfigs[indexName]; ok { - return index - } - - // Construct a non-configured index info. - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: config.isSecureIndex(indexName), - } -} - -// GetAuthConfigKey special-cases using the full index address of the official -// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. -func GetAuthConfigKey(index *registry.IndexInfo) string { - if index.Official { - return IndexServer - } - return index.Name -} - -// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo -func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo { - index := newIndexInfo(config, reference.Domain(name)) - var officialRepo bool - if index.Official { - // RepositoryInfo.Official indicates whether the image repository - // is an official (docker library official images) repository. - // - // We only need to check this if the image-repository is on Docker Hub. - officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/') - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(name), - Index: index, - Official: officialRepo, - } -} - -// ParseRepositoryInfo performs the breakdown of a repository name into a -// [RepositoryInfo], but lacks registry configuration. -// -// It is used by the Docker cli to interact with registry-related endpoints. -func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) { - indexName := normalizeIndexName(reference.Domain(reposName)) - if indexName == IndexName { - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, - Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'), - }, nil - } - - return &RepositoryInfo{ - Name: reference.TrimNamed(reposName), - Index: ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, - }, nil -} - -// isInsecure is used to detect whether a registry domain or IP-address is allowed -// to use an insecure (non-TLS, or self-signed cert) connection according to the -// defaults, which allows for insecure connections with registries running on a -// loopback address ("localhost", "::1/128", "127.0.0.0/8"). -// -// It is used in situations where we don't have access to the daemon's configuration, -// for example, when used from the client / CLI. -func isInsecure(hostNameOrIP string) bool { - // Attempt to strip port if present; this also strips brackets for - // IPv6 addresses with a port (e.g. "[::1]:5000"). - // - // This is best-effort; we'll continue using the address as-is if it fails. - if host, _, err := net.SplitHostPort(hostNameOrIP); err == nil { - hostNameOrIP = host - } - if hostNameOrIP == "127.0.0.1" || hostNameOrIP == "::1" || strings.EqualFold(hostNameOrIP, "localhost") { - // Fast path; no need to resolve these, assuming nobody overrides - // "localhost" for anything else than a loopback address (sorry, not sorry). - return true - } - - var addresses []net.IP - if ip := net.ParseIP(hostNameOrIP); ip != nil { - addresses = append(addresses, ip) - } else { - // Try to resolve the host's IP-addresses. - addrs, _ := lookupIP(hostNameOrIP) - addresses = append(addresses, addrs...) - } - - for _, addr := range addresses { - if addr.IsLoopback() { - return true - } - } - return false -} diff --git a/vendor/github.com/docker/docker/registry/config_unix.go b/vendor/github.com/docker/docker/registry/config_unix.go deleted file mode 100644 index 6aa6cdcca391..000000000000 --- a/vendor/github.com/docker/docker/registry/config_unix.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !windows - -package registry - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -const defaultCertsDir = "/etc/docker/certs.d" - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:/index.docker.io/v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return s -} diff --git a/vendor/github.com/docker/docker/registry/config_windows.go b/vendor/github.com/docker/docker/registry/config_windows.go deleted file mode 100644 index fd13bffde0f0..000000000000 --- a/vendor/github.com/docker/docker/registry/config_windows.go +++ /dev/null @@ -1,20 +0,0 @@ -package registry - -import ( - "os" - "path/filepath" - "strings" -) - -// defaultCertsDir is the platform-specific default directory where certificates -// are stored. On Linux, it may be overridden through certsDir, for example, when -// running in rootless mode. -var defaultCertsDir = os.Getenv("programdata") + `\docker\certs.d` - -// cleanPath is used to ensure that a directory name is valid on the target -// platform. It will be passed in something *similar* to a URL such as -// https:\index.docker.io\v1. Not all platforms support directory names -// which contain those characters (such as : on Windows) -func cleanPath(s string) string { - return filepath.FromSlash(strings.ReplaceAll(s, ":", "")) -} diff --git a/vendor/github.com/docker/docker/registry/errors.go b/vendor/github.com/docker/docker/registry/errors.go deleted file mode 100644 index cc3a37da6e92..000000000000 --- a/vendor/github.com/docker/docker/registry/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package registry - -import ( - "net/url" - - "github.com/docker/distribution/registry/api/errcode" - "github.com/pkg/errors" -) - -func translateV2AuthError(err error) error { - switch e := err.(type) { - case *url.Error: - switch e2 := e.Err.(type) { - case errcode.Error: - switch e2.Code { - case errcode.ErrorCodeUnauthorized: - return unauthorizedErr{err} - } - } - } - - return err -} - -func invalidParam(err error) error { - return invalidParameterErr{err} -} - -func invalidParamf(format string, args ...interface{}) error { - return invalidParameterErr{errors.Errorf(format, args...)} -} - -func invalidParamWrapf(err error, format string, args ...interface{}) error { - return invalidParameterErr{errors.Wrapf(err, format, args...)} -} - -type unauthorizedErr struct{ error } - -func (unauthorizedErr) Unauthorized() {} - -func (e unauthorizedErr) Cause() error { - return e.error -} - -func (e unauthorizedErr) Unwrap() error { - return e.error -} - -type invalidParameterErr struct{ error } - -func (invalidParameterErr) InvalidParameter() {} - -func (e invalidParameterErr) Unwrap() error { - return e.error -} - -type systemErr struct{ error } - -func (systemErr) System() {} - -func (e systemErr) Unwrap() error { - return e.error -} - -type errUnknown struct{ error } - -func (errUnknown) Unknown() {} - -func (e errUnknown) Unwrap() error { - return e.error -} diff --git a/vendor/github.com/docker/docker/registry/search.go b/vendor/github.com/docker/docker/registry/search.go deleted file mode 100644 index 26a14298ac6c..000000000000 --- a/vendor/github.com/docker/docker/registry/search.go +++ /dev/null @@ -1,170 +0,0 @@ -package registry - -import ( - "context" - "net/http" - "strconv" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -var acceptedSearchFilterTags = map[string]bool{ - "is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future. - "is-official": true, - "stars": true, -} - -// Search queries the public registry for repositories matching the specified -// search term and filters. -func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) { - if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil { - return nil, err - } - - isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false) - if err != nil { - return nil, err - } - - // "is-automated" is deprecated and filtering for `true` will yield no results. - if isAutomated { - return []registry.SearchResult{}, nil - } - - isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false) - if err != nil { - return nil, err - } - - hasStarFilter := 0 - if searchFilters.Contains("stars") { - hasStars := searchFilters.Get("stars") - for _, hasStar := range hasStars { - iHasStar, err := strconv.Atoi(hasStar) - if err != nil { - return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)} - } - if iHasStar > hasStarFilter { - hasStarFilter = iHasStar - } - } - } - - unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers) - if err != nil { - return nil, err - } - - filteredResults := []registry.SearchResult{} - for _, result := range unfilteredResult.Results { - if searchFilters.Contains("is-official") { - if isOfficial != result.IsOfficial { - continue - } - } - if searchFilters.Contains("stars") { - if result.StarCount < hasStarFilter { - continue - } - } - // "is-automated" is deprecated and the value in Docker Hub search - // results is untrustworthy. Force it to false so as to not mislead our - // clients. - result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated) - filteredResults = append(filteredResults, result) - } - - return filteredResults, nil -} - -func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) { - if hasScheme(term) { - return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term) - } - - indexName, remoteName := splitReposSearchTerm(term) - - // Search is a long-running operation, just lock s.config to avoid block others. - s.mu.RLock() - index := newIndexInfo(s.config, indexName) - s.mu.RUnlock() - if index.Official { - // If pull "library/foo", it's stored locally under "foo" - remoteName = strings.TrimPrefix(remoteName, "library/") - } - - endpoint, err := newV1Endpoint(ctx, index, headers) - if err != nil { - return nil, err - } - - var client *http.Client - if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { - creds := NewStaticCredentialStore(authConfig) - - // TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac) - modifiers := Headers(headers.Get("User-Agent"), nil) - v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{ - auth.RegistryScope{Name: "catalog", Actions: []string{"search"}}, - }) - if err != nil { - return nil, err - } - // Copy non transport http client features - v2Client.Timeout = endpoint.client.Timeout - v2Client.CheckRedirect = endpoint.client.CheckRedirect - v2Client.Jar = endpoint.client.Jar - - log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL) - client = v2Client - } else { - client = endpoint.client - if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil { - return nil, err - } - } - - return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit) -} - -// splitReposSearchTerm breaks a search term into an index name and remote name -func splitReposSearchTerm(reposName string) (string, string) { - nameParts := strings.SplitN(reposName, "/", 2) - if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") && - !strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") { - // This is a Docker Hub repository (ex: samalba/hipache or ubuntu), - // use the default Docker Hub registry (docker.io) - return IndexName, reposName - } - return nameParts[0], nameParts[1] -} - -// ParseSearchIndexInfo will use repository name to get back an indexInfo. -// -// TODO(thaJeztah) this function is only used by the CLI, and used to get -// information of the registry (to provide credentials if needed). We should -// move this function (or equivalent) to the CLI, as it's doing too much just -// for that. -func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) { - indexName, _ := splitReposSearchTerm(reposName) - indexName = normalizeIndexName(indexName) - if indexName == IndexName { - return ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, nil - } - - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, nil -} diff --git a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go b/vendor/github.com/docker/docker/registry/search_endpoint_v1.go deleted file mode 100644 index 2ac3cee8296d..000000000000 --- a/vendor/github.com/docker/docker/registry/search_endpoint_v1.go +++ /dev/null @@ -1,207 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "net/http" - "net/url" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types/registry" -) - -// v1PingResult contains the information returned when pinging a registry. It -// indicates whether the registry claims to be a standalone registry. -type v1PingResult struct { - // Standalone is set to true if the registry indicates it is a - // standalone registry in the X-Docker-Registry-Standalone - // header - Standalone bool `json:"standalone"` -} - -// v1Endpoint stores basic information about a V1 registry endpoint. -type v1Endpoint struct { - client *http.Client - URL *url.URL - IsSecure bool -} - -// newV1Endpoint parses the given address to return a registry endpoint. -// TODO: remove. This is only used by search. -func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) { - tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure) - if err != nil { - return nil, err - } - - endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers) - if err != nil { - return nil, err - } - - if endpoint.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fall back to http in case of error) - return endpoint, nil - } - - // Try HTTPS ping to registry - endpoint.URL.Scheme = "https" - if _, err := endpoint.ping(ctx); err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - if endpoint.IsSecure { - // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` - // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP. - return nil, invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host) - } - - // registry is insecure and HTTPS failed, fallback to HTTP. - log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint) - endpoint.URL.Scheme = "http" - if _, err2 := endpoint.ping(ctx); err2 != nil { - return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) - } - } - - return endpoint, nil -} - -// trimV1Address trims the "v1" version suffix off the address and returns -// the trimmed address. It returns an error on "v2" endpoints. -func trimV1Address(address string) (string, error) { - trimmed := strings.TrimSuffix(address, "/") - if strings.HasSuffix(trimmed, "/v2") { - return "", invalidParamf("search is not supported on v2 endpoints: %s", address) - } - return strings.TrimSuffix(trimmed, "/v1"), nil -} - -func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) { - if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { - address = "https://" + address - } - - address, err := trimV1Address(address) - if err != nil { - return nil, err - } - - uri, err := url.Parse(address) - if err != nil { - return nil, invalidParam(err) - } - - // TODO(tiborvass): make sure a ConnectTimeout transport is used - tr := newTransport(tlsConfig) - - return &v1Endpoint{ - IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, - URL: uri, - client: httpClient(transport.NewTransport(tr, Headers("", headers)...)), - }, nil -} - -// Get the formatted URL for the root of this registry Endpoint -func (e *v1Endpoint) String() string { - return e.URL.String() + "/v1/" -} - -// ping returns a v1PingResult which indicates whether the registry is standalone or not. -func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) { - if e.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fallback to http in case of error) - return v1PingResult{}, nil - } - - pingURL := e.String() + "_ping" - log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody) - if err != nil { - return v1PingResult{}, invalidParam(err) - } - - resp, err := e.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return v1PingResult{}, err - } - return v1PingResult{}, invalidParam(err) - } - - defer resp.Body.Close() - - if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" { - info := v1PingResult{} - // Accepted values are "1", and "true" (case-insensitive). - if v == "1" || strings.EqualFold(v, "true") { - info.Standalone = true - } - log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone) - return info, nil - } - - // If the header is absent, we assume true for compatibility with earlier - // versions of the registry. default to true - info := v1PingResult{ - Standalone: true, - } - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - log.G(ctx).WithError(err).Debug("error unmarshaling _ping response") - // don't stop here. Just assume sane defaults - } - - log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone) - return info, nil -} - -// httpClient returns an HTTP client structure which uses the given transport -// and contains the necessary headers for redirected requests -func httpClient(transport http.RoundTripper) *http.Client { - return &http.Client{ - Transport: transport, - CheckRedirect: addRequiredHeadersToRedirectedRequests, - } -} - -func trustedLocation(req *http.Request) bool { - var ( - trusteds = []string{"docker.com", "docker.io"} - hostname = strings.SplitN(req.Host, ":", 2)[0] - ) - if req.URL.Scheme != "https" { - return false - } - - for _, trusted := range trusteds { - if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { - return true - } - } - return false -} - -// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers -// for redirected requests -func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { - if len(via) != 0 && via[0] != nil { - if trustedLocation(req) && trustedLocation(via[0]) { - req.Header = via[0].Header - return nil - } - for k, v := range via[0].Header { - if k != "Authorization" { - for _, vv := range v { - req.Header.Add(k, vv) - } - } - } - } - return nil -} diff --git a/vendor/github.com/docker/docker/registry/search_session.go b/vendor/github.com/docker/docker/registry/search_session.go deleted file mode 100644 index f2886b7d3859..000000000000 --- a/vendor/github.com/docker/docker/registry/search_session.go +++ /dev/null @@ -1,247 +0,0 @@ -package registry - -import ( - // this is required for some certificates - "context" - _ "crypto/sha512" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "net/url" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// A session is used to communicate with a V1 registry -type session struct { - indexEndpoint *v1Endpoint - client *http.Client -} - -type authTransport struct { - base http.RoundTripper - authConfig *registry.AuthConfig - - alwaysSetBasicAuth bool - token []string - - mu sync.Mutex // guards modReq - modReq map[*http.Request]*http.Request // original -> modified -} - -// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official) -// -// For private v1 registries, set alwaysSetBasicAuth to true. -// -// For the official v1 registry, if there isn't already an Authorization header in the request, -// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. -// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing -// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent -// requests. -// -// If the server sends a token without the client having requested it, it is ignored. -// -// This RoundTripper also has a CancelRequest method important for correct timeout handling. -func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport { - if base == nil { - base = http.DefaultTransport - } - return &authTransport{ - base: base, - authConfig: authConfig, - alwaysSetBasicAuth: alwaysSetBasicAuth, - modReq: make(map[*http.Request]*http.Request), - } -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header, len(r.Header)) - for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) - } - - return r2 -} - -// onEOFReader wraps an io.ReadCloser and a function -// the function will run at the end of file or close the file. -type onEOFReader struct { - Rc io.ReadCloser - Fn func() -} - -func (r *onEOFReader) Read(p []byte) (int, error) { - n, err := r.Rc.Read(p) - if err == io.EOF { - r.runFunc() - } - return n, err -} - -// Close closes the file and run the function. -func (r *onEOFReader) Close() error { - err := r.Rc.Close() - r.runFunc() - return err -} - -func (r *onEOFReader) runFunc() { - if fn := r.Fn; fn != nil { - fn() - r.Fn = nil - } -} - -// RoundTrip changes an HTTP request's headers to add the necessary -// authentication-related headers -func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { - // Authorization should not be set on 302 redirect for untrusted locations. - // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. - // As the authorization logic is currently implemented in RoundTrip, - // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. - // This is safe as Docker doesn't set Referrer in other scenarios. - if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { - return tr.base.RoundTrip(orig) - } - - req := cloneRequest(orig) - tr.mu.Lock() - tr.modReq[orig] = req - tr.mu.Unlock() - - if tr.alwaysSetBasicAuth { - if tr.authConfig == nil { - return nil, errors.New("unexpected error: empty auth config") - } - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - return tr.base.RoundTrip(req) - } - - // Don't override - if req.Header.Get("Authorization") == "" { - if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" { - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - } else if len(tr.token) > 0 { - req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) - } - } - resp, err := tr.base.RoundTrip(req) - if err != nil { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - return nil, err - } - if len(resp.Header["X-Docker-Token"]) > 0 { - tr.token = resp.Header["X-Docker-Token"] - } - resp.Body = &onEOFReader{ - Rc: resp.Body, - Fn: func() { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - }, - } - return resp, nil -} - -// CancelRequest cancels an in-flight request by closing its connection. -func (tr *authTransport) CancelRequest(req *http.Request) { - type canceler interface { - CancelRequest(*http.Request) - } - if cr, ok := tr.base.(canceler); ok { - tr.mu.Lock() - modReq := tr.modReq[req] - delete(tr.modReq, req) - tr.mu.Unlock() - cr.CancelRequest(modReq) - } -} - -func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error { - var alwaysSetBasicAuth bool - - // If we're working with a standalone private registry over HTTPS, send Basic Auth headers - // alongside all our requests. - if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { - info, err := endpoint.ping(ctx) - if err != nil { - return err - } - if info.Standalone && authConfig != nil { - log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth") - alwaysSetBasicAuth = true - } - } - - // Annotate the transport unconditionally so that v2 can - // properly fallback on v1 when an image is not found. - client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) - - jar, err := cookiejar.New(nil) - if err != nil { - return systemErr{errors.New("cookiejar.New is not supposed to return an error")} - } - client.Jar = jar - - return nil -} - -func newSession(client *http.Client, endpoint *v1Endpoint) *session { - return &session{ - client: client, - indexEndpoint: endpoint, - } -} - -// defaultSearchLimit is the default value for maximum number of returned search results. -const defaultSearchLimit = 25 - -// searchRepositories performs a search against the remote repository -func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) { - if limit == 0 { - limit = defaultSearchLimit - } - if limit < 1 || limit > 100 { - return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit) - } - u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) - log.G(ctx).WithField("url", u).Debug("searchRepositories") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) - if err != nil { - return nil, invalidParamWrapf(err, "error building request") - } - // Have the AuthTransport send authentication, when logged in. - req.Header.Set("X-Docker-Token", "true") - res, err := r.client.Do(req) - if err != nil { - return nil, systemErr{err} - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - // TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286). - // TODO(thaJeztah): handle other status-codes to return correct error-type - return nil, errUnknown{fmt.Errorf("Unexpected status code %d", res.StatusCode)} - } - result := ®istry.SearchResults{} - err = json.NewDecoder(res.Body).Decode(result) - if err != nil { - return nil, systemErr{errors.Wrap(err, "error decoding registry search results")} - } - return result, nil -} diff --git a/vendor/github.com/docker/docker/registry/service.go b/vendor/github.com/docker/docker/registry/service.go deleted file mode 100644 index 85299be32ec1..000000000000 --- a/vendor/github.com/docker/docker/registry/service.go +++ /dev/null @@ -1,174 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "errors" - "net/url" - "strings" - "sync" - - cerrdefs "github.com/containerd/errdefs" - "github.com/containerd/log" - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" -) - -// Service is a registry service. It tracks configuration data such as a list -// of mirrors. -type Service struct { - config *serviceConfig - mu sync.RWMutex -} - -// NewService returns a new instance of [Service] ready to be installed into -// an engine. -func NewService(options ServiceOptions) (*Service, error) { - config, err := newServiceConfig(options) - if err != nil { - return nil, err - } - - return &Service{config: config}, err -} - -// ServiceConfig returns a copy of the public registry service's configuration. -func (s *Service) ServiceConfig() *registry.ServiceConfig { - s.mu.RLock() - defer s.mu.RUnlock() - return s.config.copy() -} - -// ReplaceConfig prepares a transaction which will atomically replace the -// registry service's configuration when the returned commit function is called. -func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) { - config, err := newServiceConfig(options) - if err != nil { - return nil, err - } - return func() { - s.mu.Lock() - defer s.mu.Unlock() - s.config = config - }, nil -} - -// Auth contacts the public registry with the provided credentials, -// and returns OK if authentication was successful. -// It can be used to verify the validity of a client's credentials. -func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (statusMessage, token string, _ error) { - // TODO Use ctx when searching for repositories - registryHostName := IndexHostname - - if authConfig.ServerAddress != "" { - serverAddress := authConfig.ServerAddress - if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") { - serverAddress = "https://" + serverAddress - } - u, err := url.Parse(serverAddress) - if err != nil { - return "", "", invalidParamWrapf(err, "unable to parse server address") - } - registryHostName = u.Host - } - - // Lookup endpoints for authentication but exclude mirrors to prevent - // sending credentials of the upstream registry to a mirror. - s.mu.RLock() - endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) - s.mu.RUnlock() - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return "", "", err - } - return "", "", invalidParam(err) - } - - var lastErr error - for _, endpoint := range endpoints { - authToken, err := loginV2(ctx, authConfig, endpoint, userAgent) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || cerrdefs.IsUnauthorized(err) { - // Failed to authenticate; don't continue with (non-TLS) endpoints. - return "", "", err - } - // Try next endpoint - log.G(ctx).WithFields(log.Fields{ - "error": err, - "endpoint": endpoint, - }).Infof("Error logging in to endpoint, trying next endpoint") - lastErr = err - continue - } - - // TODO(thaJeztah): move the statusMessage to the API endpoint; we don't need to produce that here? - return "Login Succeeded", authToken, nil - } - - return "", "", lastErr -} - -// ResolveRepository splits a repository name into its components -// and configuration of the associated registry. -// -// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release. -func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) { - s.mu.RLock() - defer s.mu.RUnlock() - // TODO(thaJeztah): remove error return as it's no longer used. - return newRepositoryInfo(s.config, name), nil -} - -// ResolveAuthConfig looks up authentication for the given reference from the -// given authConfigs. -// -// IMPORTANT: This function is for internal use and should not be used by external projects. -func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig { - s.mu.RLock() - defer s.mu.RUnlock() - // Simplified version of "newIndexInfo" without handling of insecure - // registries and mirrors, as we don't need that information to resolve - // the auth-config. - indexName := normalizeIndexName(reference.Domain(ref)) - registryInfo, ok := s.config.IndexConfigs[indexName] - if !ok { - registryInfo = ®istry.IndexInfo{Name: indexName} - } - return ResolveAuthConfig(authConfigs, registryInfo) -} - -// APIEndpoint represents a remote API endpoint -type APIEndpoint struct { - Mirror bool - URL *url.URL - AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release. - Official bool // Deprecated: this field was only used internally, and will be removed in the next release. - TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release. - TLSConfig *tls.Config -} - -// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference. -// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP. -func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.lookupV2Endpoints(context.TODO(), hostname, true) -} - -// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. -// It gives preference to HTTPS over plain HTTP. Mirrors are not included. -func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.lookupV2Endpoints(context.TODO(), hostname, false) -} - -// IsInsecureRegistry returns true if the registry at given host is configured as -// insecure registry. -func (s *Service) IsInsecureRegistry(host string) bool { - s.mu.RLock() - defer s.mu.RUnlock() - return !s.config.isSecureIndex(host) -} diff --git a/vendor/github.com/docker/docker/registry/service_v2.go b/vendor/github.com/docker/docker/registry/service_v2.go deleted file mode 100644 index 6b25a41dc327..000000000000 --- a/vendor/github.com/docker/docker/registry/service_v2.go +++ /dev/null @@ -1,74 +0,0 @@ -package registry - -import ( - "context" - "net/url" - "strings" - - "github.com/docker/go-connections/tlsconfig" -) - -func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includeMirrors bool) ([]APIEndpoint, error) { - var endpoints []APIEndpoint - if hostname == DefaultNamespace || hostname == IndexHostname { - if includeMirrors { - for _, mirror := range s.config.Mirrors { - if ctx.Err() != nil { - return nil, ctx.Err() - } - if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") { - mirror = "https://" + mirror - } - mirrorURL, err := url.Parse(mirror) - if err != nil { - return nil, invalidParam(err) - } - // TODO(thaJeztah); this should all be memoized when loading the config. We're resolving mirrors and loading TLS config every time. - mirrorTLSConfig, err := newTLSConfig(ctx, mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host)) - if err != nil { - return nil, err - } - endpoints = append(endpoints, APIEndpoint{ - URL: mirrorURL, - Mirror: true, - TLSConfig: mirrorTLSConfig, - }) - } - } - endpoints = append(endpoints, APIEndpoint{ - URL: DefaultV2Registry, - Official: true, - TLSConfig: tlsconfig.ServerDefault(), - }) - - return endpoints, nil - } - - tlsConfig, err := newTLSConfig(ctx, hostname, s.config.isSecureIndex(hostname)) - if err != nil { - return nil, err - } - - endpoints = []APIEndpoint{ - { - URL: &url.URL{ - Scheme: "https", - Host: hostname, - }, - TLSConfig: tlsConfig, - }, - } - - if tlsConfig.InsecureSkipVerify { - endpoints = append(endpoints, APIEndpoint{ - URL: &url.URL{ - Scheme: "http", - Host: hostname, - }, - // used to check if supposed to be secure via InsecureSkipVerify - TLSConfig: tlsConfig, - }) - } - - return endpoints, nil -} diff --git a/vendor/github.com/docker/docker/registry/types.go b/vendor/github.com/docker/docker/registry/types.go deleted file mode 100644 index bb081d5638a2..000000000000 --- a/vendor/github.com/docker/docker/registry/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package registry - -import ( - "github.com/distribution/reference" - "github.com/docker/docker/api/types/registry" -) - -// RepositoryInfo describes a repository -type RepositoryInfo struct { - Name reference.Named - // Index points to registry information - Index *registry.IndexInfo - // Official indicates whether the repository is considered official. - // If the registry is official, and the normalized name does not - // contain a '/' (e.g. "foo"), then it is considered an official repo. - // - // Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead. - Official bool - // Class represents the class of the repository, such as "plugin" - // or "image". - // - // Deprecated: this field is no longer used, and will be removed in the next release. - Class string -} diff --git a/vendor/github.com/docker/go-connections/nat/nat.go b/vendor/github.com/docker/go-connections/nat/nat.go index 4049d780c54a..1ffe0355dc15 100644 --- a/vendor/github.com/docker/go-connections/nat/nat.go +++ b/vendor/github.com/docker/go-connections/nat/nat.go @@ -2,6 +2,7 @@ package nat import ( + "errors" "fmt" "net" "strconv" @@ -43,19 +44,19 @@ func NewPort(proto, port string) (Port, error) { // ParsePort parses the port number string and returns an int func ParsePort(rawPort string) (int, error) { - if len(rawPort) == 0 { + if rawPort == "" { return 0, nil } port, err := strconv.ParseUint(rawPort, 10, 16) if err != nil { - return 0, err + return 0, fmt.Errorf("invalid port '%s': %w", rawPort, errors.Unwrap(err)) } return int(port), nil } // ParsePortRangeToInt parses the port range string and returns start/end ints func ParsePortRangeToInt(rawPort string) (int, int, error) { - if len(rawPort) == 0 { + if rawPort == "" { return 0, 0, nil } start, end, err := ParsePortRange(rawPort) @@ -91,29 +92,31 @@ func (p Port) Range() (int, int, error) { return ParsePortRangeToInt(p.Port()) } -// SplitProtoPort splits a port in the format of proto/port -func SplitProtoPort(rawPort string) (string, string) { - parts := strings.Split(rawPort, "/") - l := len(parts) - if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 { +// SplitProtoPort splits a port(range) and protocol, formatted as "/[]" +// "/[]". It returns an empty string for both if +// no port(range) is provided. If a port(range) is provided, but no protocol, +// the default ("tcp") protocol is returned. +// +// SplitProtoPort does not validate or normalize the returned values. +func SplitProtoPort(rawPort string) (proto string, port string) { + port, proto, _ = strings.Cut(rawPort, "/") + if port == "" { return "", "" } - if l == 1 { - return "tcp", rawPort + if proto == "" { + proto = "tcp" } - if len(parts[1]) == 0 { - return "tcp", parts[0] - } - return parts[1], parts[0] + return proto, port } -func validateProto(proto string) bool { - for _, availableProto := range []string{"tcp", "udp", "sctp"} { - if availableProto == proto { - return true - } +func validateProto(proto string) error { + switch proto { + case "tcp", "udp", "sctp": + // All good + return nil + default: + return errors.New("invalid proto: " + proto) } - return false } // ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses @@ -123,22 +126,18 @@ func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, exposedPorts = make(map[Port]struct{}, len(ports)) bindings = make(map[Port][]PortBinding) ) - for _, rawPort := range ports { - portMappings, err := ParsePortSpec(rawPort) + for _, p := range ports { + portMappings, err := ParsePortSpec(p) if err != nil { return nil, nil, err } - for _, portMapping := range portMappings { - port := portMapping.Port - if _, exists := exposedPorts[port]; !exists { + for _, pm := range portMappings { + port := pm.Port + if _, ok := exposedPorts[port]; !ok { exposedPorts[port] = struct{}{} } - bslice, exists := bindings[port] - if !exists { - bslice = []PortBinding{} - } - bindings[port] = append(bslice, portMapping.Binding) + bindings[port] = append(bindings[port], pm.Binding) } } return exposedPorts, bindings, nil @@ -150,28 +149,34 @@ type PortMapping struct { Binding PortBinding } -func splitParts(rawport string) (string, string, string) { +func (p *PortMapping) String() string { + return net.JoinHostPort(p.Binding.HostIP, p.Binding.HostPort+":"+string(p.Port)) +} + +func splitParts(rawport string) (hostIP, hostPort, containerPort string) { parts := strings.Split(rawport, ":") - n := len(parts) - containerPort := parts[n-1] - switch n { + switch len(parts) { case 1: - return "", "", containerPort + return "", "", parts[0] case 2: - return "", parts[0], containerPort + return "", parts[0], parts[1] case 3: - return parts[0], parts[1], containerPort + return parts[0], parts[1], parts[2] default: - return strings.Join(parts[:n-2], ":"), parts[n-2], containerPort + n := len(parts) + return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1] } } // ParsePortSpec parses a port specification string into a slice of PortMappings func ParsePortSpec(rawPort string) ([]PortMapping, error) { - var proto string ip, hostPort, containerPort := splitParts(rawPort) - proto, containerPort = SplitProtoPort(containerPort) + proto, containerPort := SplitProtoPort(containerPort) + proto = strings.ToLower(proto) + if err := validateProto(proto); err != nil { + return nil, err + } if ip != "" && ip[0] == '[' { // Strip [] from IPV6 addresses @@ -182,7 +187,7 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { ip = rawIP } if ip != "" && net.ParseIP(ip) == nil { - return nil, fmt.Errorf("invalid IP address: %s", ip) + return nil, errors.New("invalid IP address: " + ip) } if containerPort == "" { return nil, fmt.Errorf("no port specified: %s", rawPort) @@ -190,51 +195,43 @@ func ParsePortSpec(rawPort string) ([]PortMapping, error) { startPort, endPort, err := ParsePortRange(containerPort) if err != nil { - return nil, fmt.Errorf("invalid containerPort: %s", containerPort) + return nil, errors.New("invalid containerPort: " + containerPort) } - var startHostPort, endHostPort uint64 = 0, 0 - if len(hostPort) > 0 { + var startHostPort, endHostPort uint64 + if hostPort != "" { startHostPort, endHostPort, err = ParsePortRange(hostPort) if err != nil { - return nil, fmt.Errorf("invalid hostPort: %s", hostPort) + return nil, errors.New("invalid hostPort: " + hostPort) } - } - - if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) { - // Allow host port range iff containerPort is not a range. - // In this case, use the host port range as the dynamic - // host port range to allocate into. - if endPort != startPort { - return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + if (endPort - startPort) != (endHostPort - startHostPort) { + // Allow host port range iff containerPort is not a range. + // In this case, use the host port range as the dynamic + // host port range to allocate into. + if endPort != startPort { + return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) + } } } - if !validateProto(strings.ToLower(proto)) { - return nil, fmt.Errorf("invalid proto: %s", proto) - } - - ports := []PortMapping{} - for i := uint64(0); i <= (endPort - startPort); i++ { - containerPort = strconv.FormatUint(startPort+i, 10) - if len(hostPort) > 0 { - hostPort = strconv.FormatUint(startHostPort+i, 10) - } - // Set hostPort to a range only if there is a single container port - // and a dynamic host port. - if startPort == endPort && startHostPort != endHostPort { - hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10)) - } - port, err := NewPort(strings.ToLower(proto), containerPort) - if err != nil { - return nil, err - } + count := endPort - startPort + 1 + ports := make([]PortMapping, 0, count) - binding := PortBinding{ - HostIP: ip, - HostPort: hostPort, + for i := uint64(0); i < count; i++ { + cPort := Port(strconv.FormatUint(startPort+i, 10) + "/" + proto) + hPort := "" + if hostPort != "" { + hPort = strconv.FormatUint(startHostPort+i, 10) + // Set hostPort to a range only if there is a single container port + // and a dynamic host port. + if count == 1 && startHostPort != endHostPort { + hPort += "-" + strconv.FormatUint(endHostPort, 10) + } } - ports = append(ports, PortMapping{Port: port, Binding: binding}) + ports = append(ports, PortMapping{ + Port: cPort, + Binding: PortBinding{HostIP: ip, HostPort: hPort}, + }) } return ports, nil } diff --git a/vendor/github.com/docker/go-connections/nat/parse.go b/vendor/github.com/docker/go-connections/nat/parse.go index e4b53e8a3242..64affa2a904c 100644 --- a/vendor/github.com/docker/go-connections/nat/parse.go +++ b/vendor/github.com/docker/go-connections/nat/parse.go @@ -1,7 +1,7 @@ package nat import ( - "fmt" + "errors" "strconv" "strings" ) @@ -9,7 +9,7 @@ import ( // ParsePortRange parses and validates the specified string as a port-range (8000-9000) func ParsePortRange(ports string) (uint64, uint64, error) { if ports == "" { - return 0, 0, fmt.Errorf("empty string specified for ports") + return 0, 0, errors.New("empty string specified for ports") } if !strings.Contains(ports, "-") { start, err := strconv.ParseUint(ports, 10, 16) @@ -27,7 +27,7 @@ func ParsePortRange(ports string) (uint64, uint64, error) { return 0, 0, err } if end < start { - return 0, 0, fmt.Errorf("invalid range specified for port: %s", ports) + return 0, 0, errors.New("invalid range specified for port: " + ports) } return start, end, nil } diff --git a/vendor/github.com/docker/go-connections/sockets/README.md b/vendor/github.com/docker/go-connections/sockets/README.md deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/vendor/github.com/docker/go-connections/sockets/proxy.go b/vendor/github.com/docker/go-connections/sockets/proxy.go index c897cb02adea..f04980e40a5a 100644 --- a/vendor/github.com/docker/go-connections/sockets/proxy.go +++ b/vendor/github.com/docker/go-connections/sockets/proxy.go @@ -9,6 +9,8 @@ import ( // GetProxyEnv allows access to the uppercase and the lowercase forms of // proxy-related variables. See the Go specification for details on these // variables. https://golang.org/pkg/net/http/ +// +// Deprecated: this function was used as helper for [DialerFromEnvironment] and is no longer used. It will be removed in the next release. func GetProxyEnv(key string) string { proxyValue := os.Getenv(strings.ToUpper(key)) if proxyValue == "" { @@ -19,10 +21,11 @@ func GetProxyEnv(key string) string { // DialerFromEnvironment was previously used to configure a net.Dialer to route // connections through a SOCKS proxy. -// DEPRECATED: SOCKS proxies are now supported by configuring only +// +// Deprecated: SOCKS proxies are now supported by configuring only // http.Transport.Proxy, and no longer require changing http.Transport.Dial. -// Therefore, only sockets.ConfigureTransport() needs to be called, and any -// sockets.DialerFromEnvironment() calls can be dropped. +// Therefore, only [sockets.ConfigureTransport] needs to be called, and any +// [sockets.DialerFromEnvironment] calls can be dropped. func DialerFromEnvironment(direct *net.Dialer) (*net.Dialer, error) { return direct, nil } diff --git a/vendor/github.com/docker/go-connections/sockets/sockets.go b/vendor/github.com/docker/go-connections/sockets/sockets.go index b0eae239d2c5..6117297860db 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets.go @@ -2,13 +2,19 @@ package sockets import ( + "context" "errors" + "fmt" "net" "net/http" + "syscall" "time" ) -const defaultTimeout = 10 * time.Second +const ( + defaultTimeout = 10 * time.Second + maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) +) // ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system. var ErrProtocolNotAvailable = errors.New("protocol not available") @@ -35,3 +41,26 @@ func ConfigureTransport(tr *http.Transport, proto, addr string) error { } return nil } + +// DialPipe connects to a Windows named pipe. It is not supported on +// non-Windows platforms. +// +// Deprecated: use [github.com/Microsoft/go-winio.DialPipe] or [github.com/Microsoft/go-winio.DialPipeContext]. +func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { + return dialPipe(addr, timeout) +} + +func configureUnixTransport(tr *http.Transport, proto, addr string) error { + if len(addr) > maxUnixSocketPathSize { + return fmt.Errorf("unix socket path %q is too long", addr) + } + // No need for compression in local communications. + tr.DisableCompression = true + dialer := &net.Dialer{ + Timeout: defaultTimeout, + } + tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { + return dialer.DialContext(ctx, proto, addr) + } + return nil +} diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go index 78a34a980d28..913d2f00dd2f 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_unix.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_unix.go @@ -3,37 +3,16 @@ package sockets import ( - "context" - "fmt" "net" "net/http" "syscall" "time" ) -const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) - -func configureUnixTransport(tr *http.Transport, proto, addr string) error { - if len(addr) > maxUnixSocketPathSize { - return fmt.Errorf("unix socket path %q is too long", addr) - } - // No need for compression in local communications. - tr.DisableCompression = true - dialer := &net.Dialer{ - Timeout: defaultTimeout, - } - tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { - return dialer.DialContext(ctx, proto, addr) - } - return nil -} - func configureNpipeTransport(tr *http.Transport, proto, addr string) error { return ErrProtocolNotAvailable } -// DialPipe connects to a Windows named pipe. -// This is not supported on other OSes. -func DialPipe(_ string, _ time.Duration) (net.Conn, error) { +func dialPipe(_ string, _ time.Duration) (net.Conn, error) { return nil, syscall.EAFNOSUPPORT } diff --git a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go index 7acafc5a2ad8..6d6beb3855c0 100644 --- a/vendor/github.com/docker/go-connections/sockets/sockets_windows.go +++ b/vendor/github.com/docker/go-connections/sockets/sockets_windows.go @@ -9,10 +9,6 @@ import ( "github.com/Microsoft/go-winio" ) -func configureUnixTransport(tr *http.Transport, proto, addr string) error { - return ErrProtocolNotAvailable -} - func configureNpipeTransport(tr *http.Transport, proto, addr string) error { // No need for compression in local communications. tr.DisableCompression = true @@ -22,7 +18,6 @@ func configureNpipeTransport(tr *http.Transport, proto, addr string) error { return nil } -// DialPipe connects to a Windows named pipe. -func DialPipe(addr string, timeout time.Duration) (net.Conn, error) { +func dialPipe(addr string, timeout time.Duration) (net.Conn, error) { return winio.DialPipe(addr, &timeout) } diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket.go b/vendor/github.com/docker/go-connections/sockets/unix_socket.go index b9233521e49a..e736f71d38b1 100644 --- a/vendor/github.com/docker/go-connections/sockets/unix_socket.go +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket.go @@ -1,5 +1,3 @@ -//go:build !windows - /* Package sockets is a simple unix domain socket wrapper. @@ -57,26 +55,6 @@ import ( // SockOption sets up socket file's creating option type SockOption func(string) error -// WithChown modifies the socket file's uid and gid -func WithChown(uid, gid int) SockOption { - return func(path string) error { - if err := os.Chown(path, uid, gid); err != nil { - return err - } - return nil - } -} - -// WithChmod modifies socket file's access mode. -func WithChmod(mask os.FileMode) SockOption { - return func(path string) error { - if err := os.Chmod(path, mask); err != nil { - return err - } - return nil - } -} - // NewUnixSocketWithOpts creates a unix socket with the specified options. // By default, socket permissions are 0000 (i.e.: no access for anyone); pass // WithChmod() and WithChown() to set the desired ownership and permissions. @@ -90,22 +68,7 @@ func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error return nil, err } - // net.Listen does not allow for permissions to be set. As a result, when - // specifying custom permissions ("WithChmod()"), there is a short time - // between creating the socket and applying the permissions, during which - // the socket permissions are Less restrictive than desired. - // - // To work around this limitation of net.Listen(), we temporarily set the - // umask to 0777, which forces the socket to be created with 000 permissions - // (i.e.: no access for anyone). After that, WithChmod() must be used to set - // the desired permissions. - // - // We don't use "defer" here, to reset the umask to its original value as soon - // as possible. Ideally we'd be able to detect if WithChmod() was passed as - // an option, and skip changing umask if default permissions are used. - origUmask := syscall.Umask(0o777) - l, err := net.Listen("unix", path) - syscall.Umask(origUmask) + l, err := listenUnix(path) if err != nil { return nil, err } @@ -119,8 +82,3 @@ func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error return l, nil } - -// NewUnixSocket creates a unix socket with the specified path and group. -func NewUnixSocket(path string, gid int) (net.Listener, error) { - return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) -} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go new file mode 100644 index 000000000000..a41a71654742 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_unix.go @@ -0,0 +1,54 @@ +//go:build !windows + +package sockets + +import ( + "net" + "os" + "syscall" +) + +// WithChown modifies the socket file's uid and gid +func WithChown(uid, gid int) SockOption { + return func(path string) error { + if err := os.Chown(path, uid, gid); err != nil { + return err + } + return nil + } +} + +// WithChmod modifies socket file's access mode. +func WithChmod(mask os.FileMode) SockOption { + return func(path string) error { + if err := os.Chmod(path, mask); err != nil { + return err + } + return nil + } +} + +// NewUnixSocket creates a unix socket with the specified path and group. +func NewUnixSocket(path string, gid int) (net.Listener, error) { + return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) +} + +func listenUnix(path string) (net.Listener, error) { + // net.Listen does not allow for permissions to be set. As a result, when + // specifying custom permissions ("WithChmod()"), there is a short time + // between creating the socket and applying the permissions, during which + // the socket permissions are Less restrictive than desired. + // + // To work around this limitation of net.Listen(), we temporarily set the + // umask to 0777, which forces the socket to be created with 000 permissions + // (i.e.: no access for anyone). After that, WithChmod() must be used to set + // the desired permissions. + // + // We don't use "defer" here, to reset the umask to its original value as soon + // as possible. Ideally we'd be able to detect if WithChmod() was passed as + // an option, and skip changing umask if default permissions are used. + origUmask := syscall.Umask(0o777) + l, err := net.Listen("unix", path) + syscall.Umask(origUmask) + return l, err +} diff --git a/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go new file mode 100644 index 000000000000..5ec29e059e78 --- /dev/null +++ b/vendor/github.com/docker/go-connections/sockets/unix_socket_windows.go @@ -0,0 +1,7 @@ +package sockets + +import "net" + +func listenUnix(path string) (net.Listener, error) { + return net.Listen("unix", path) +} diff --git a/vendor/github.com/docker/go-connections/tlsconfig/config.go b/vendor/github.com/docker/go-connections/tlsconfig/config.go index 606c98a38b51..8b0264f68b75 100644 --- a/vendor/github.com/docker/go-connections/tlsconfig/config.go +++ b/vendor/github.com/docker/go-connections/tlsconfig/config.go @@ -34,51 +34,37 @@ type Options struct { // the system pool will be used. ExclusiveRootPools bool MinVersion uint16 - // If Passphrase is set, it will be used to decrypt a TLS private key - // if the key is encrypted. - // - // Deprecated: Use of encrypted TLS private keys has been deprecated, and - // will be removed in a future release. Golang has deprecated support for - // legacy PEM encryption (as specified in RFC 1423), as it is insecure by - // design (see https://go-review.googlesource.com/c/go/+/264159). - Passphrase string -} - -// Extra (server-side) accepted CBC cipher suites - will phase out in the future -var acceptedCBCCiphers = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, } // DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls // options struct but wants to use a commonly accepted set of TLS cipher suites, with // known weak algorithms removed. -var DefaultServerAcceptedCiphers = append(clientCipherSuites, acceptedCBCCiphers...) +var DefaultServerAcceptedCiphers = defaultCipherSuites + +// defaultCipherSuites is shared by both client and server as the default set. +var defaultCipherSuites = []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, +} // ServerDefault returns a secure-enough TLS configuration for the server TLS configuration. func ServerDefault(ops ...func(*tls.Config)) *tls.Config { - tlsConfig := &tls.Config{ - // Avoid fallback by default to SSL protocols < TLS1.2 - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - CipherSuites: DefaultServerAcceptedCiphers, - } - - for _, op := range ops { - op(tlsConfig) - } - - return tlsConfig + return defaultConfig(ops...) } // ClientDefault returns a secure-enough TLS configuration for the client TLS configuration. func ClientDefault(ops ...func(*tls.Config)) *tls.Config { + return defaultConfig(ops...) +} + +// defaultConfig is the default config used by both client and server TLS configuration. +func defaultConfig(ops ...func(*tls.Config)) *tls.Config { tlsConfig := &tls.Config{ - // Prefer TLS1.2 as the client minimum + // Avoid fallback by default to SSL protocols < TLS1.2 MinVersion: tls.VersionTLS12, - CipherSuites: clientCipherSuites, + CipherSuites: defaultCipherSuites, } for _, op := range ops { @@ -92,13 +78,13 @@ func ClientDefault(ops ...func(*tls.Config)) *tls.Config { func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { // If we should verify the server, we need to load a trusted ca var ( - certPool *x509.CertPool - err error + pool *x509.CertPool + err error ) if exclusivePool { - certPool = x509.NewCertPool() + pool = x509.NewCertPool() } else { - certPool, err = SystemCertPool() + pool, err = SystemCertPool() if err != nil { return nil, fmt.Errorf("failed to read system certificates: %v", err) } @@ -107,10 +93,10 @@ func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { if err != nil { return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err) } - if !certPool.AppendCertsFromPEM(pemData) { + if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) } - return certPool, nil + return pool, nil } // allTLSVersions lists all the TLS versions and is used by the code that validates @@ -144,34 +130,32 @@ func adjustMinVersion(options Options, config *tls.Config) error { return nil } -// IsErrEncryptedKey returns true if the 'err' is an error of incorrect -// password when trying to decrypt a TLS private key. +// errEncryptedKeyDeprecated is produced when we encounter an encrypted +// (password-protected) key. From https://go-review.googlesource.com/c/go/+/264159; // -// Deprecated: Use of encrypted TLS private keys has been deprecated, and -// will be removed in a future release. Golang has deprecated support for -// legacy PEM encryption (as specified in RFC 1423), as it is insecure by -// design (see https://go-review.googlesource.com/c/go/+/264159). -func IsErrEncryptedKey(err error) bool { - return errors.Is(err, x509.IncorrectPasswordError) -} +// > Legacy PEM encryption as specified in RFC 1423 is insecure by design. Since +// > it does not authenticate the ciphertext, it is vulnerable to padding oracle +// > attacks that can let an attacker recover the plaintext +// > +// > It's unfortunate that we don't implement PKCS#8 encryption so we can't +// > recommend an alternative but PEM encryption is so broken that it's worth +// > deprecating outright. +// +// Also see https://docs.docker.com/go/deprecated/ +var errEncryptedKeyDeprecated = errors.New("private key is encrypted; encrypted private keys are obsolete, and not supported") // getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format. -// If the private key is encrypted, 'passphrase' is used to decrypted the -// private key. -func getPrivateKey(keyBytes []byte, passphrase string) ([]byte, error) { +// It returns an error if the file could not be decoded or was protected by +// a passphrase. +func getPrivateKey(keyBytes []byte) ([]byte, error) { // this section makes some small changes to code from notary/tuf/utils/x509.go pemBlock, _ := pem.Decode(keyBytes) if pemBlock == nil { return nil, fmt.Errorf("no valid private key found") } - var err error if x509.IsEncryptedPEMBlock(pemBlock) { //nolint:staticcheck // Ignore SA1019 (IsEncryptedPEMBlock is deprecated) - keyBytes, err = x509.DecryptPEMBlock(pemBlock, []byte(passphrase)) //nolint:staticcheck // Ignore SA1019 (DecryptPEMBlock is deprecated) - if err != nil { - return nil, fmt.Errorf("private key is encrypted, but could not decrypt it: %w", err) - } - keyBytes = pem.EncodeToMemory(&pem.Block{Type: pemBlock.Type, Bytes: keyBytes}) + return nil, errEncryptedKeyDeprecated } return keyBytes, nil @@ -195,7 +179,7 @@ func getCert(options Options) ([]tls.Certificate, error) { return nil, err } - prKeyBytes, err = getPrivateKey(prKeyBytes, options.Passphrase) + prKeyBytes, err = getPrivateKey(prKeyBytes) if err != nil { return nil, err } @@ -210,7 +194,7 @@ func getCert(options Options) ([]tls.Certificate, error) { // Client returns a TLS configuration meant to be used by a client. func Client(options Options) (*tls.Config, error) { - tlsConfig := ClientDefault() + tlsConfig := defaultConfig() tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify if !options.InsecureSkipVerify && options.CAFile != "" { CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) @@ -235,7 +219,7 @@ func Client(options Options) (*tls.Config, error) { // Server returns a TLS configuration meant to be used by a server. func Server(options Options) (*tls.Config, error) { - tlsConfig := ServerDefault() + tlsConfig := defaultConfig() tlsConfig.ClientAuth = options.ClientAuth tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) if err != nil { diff --git a/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go b/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go deleted file mode 100644 index a82f9fa52e2e..000000000000 --- a/vendor/github.com/docker/go-connections/tlsconfig/config_client_ciphers.go +++ /dev/null @@ -1,14 +0,0 @@ -// Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. -package tlsconfig - -import ( - "crypto/tls" -) - -// Client TLS cipher suites (dropping CBC ciphers for client preferred suite set) -var clientCipherSuites = []uint16{ - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, -} diff --git a/vendor/github.com/docker/go-events/README.md b/vendor/github.com/docker/go-events/README.md index 0acafc279a38..662d855fb330 100644 --- a/vendor/github.com/docker/go-events/README.md +++ b/vendor/github.com/docker/go-events/README.md @@ -1,7 +1,6 @@ # Docker Events Package [![GoDoc](https://godoc.org/github.com/docker/go-events?status.svg)](https://godoc.org/github.com/docker/go-events) -[![Circle CI](https://circleci.com/gh/docker/go-events.svg?style=shield)](https://circleci.com/gh/docker/go-events) The Docker `events` package implements a composable event distribution package for Go. diff --git a/vendor/github.com/docker/go-events/SECURITY.md b/vendor/github.com/docker/go-events/SECURITY.md new file mode 100644 index 000000000000..610eef2c9e62 --- /dev/null +++ b/vendor/github.com/docker/go-events/SECURITY.md @@ -0,0 +1,36 @@ +# Security Policy + +The maintainers of the Docker Events package take security seriously. If you discover +a security issue, please bring it to their attention right away! + +## Reporting a Vulnerability + +Please **DO NOT** file a public issue, instead send your report privately +to [security@docker.com](mailto:security@docker.com). + +Reporter(s) can expect a response within 72 hours, acknowledging the issue was +received. + +## Review Process + +After receiving the report, an initial triage and technical analysis is +performed to confirm the report and determine its scope. We may request +additional information in this stage of the process. + +Once a reviewer has confirmed the relevance of the report, a draft security +advisory will be created on GitHub. The draft advisory will be used to discuss +the issue with maintainers, the reporter(s), and where applicable, other +affected parties under embargo. + +If the vulnerability is accepted, a timeline for developing a patch, public +disclosure, and patch release will be determined. If there is an embargo period +on public disclosure before the patch release, the reporter(s) are expected to +participate in the discussion of the timeline and abide by agreed upon dates +for public disclosure. + +## Accreditation + +Security reports are greatly appreciated and we will publicly thank you, +although we will keep your name confidential if you request it. We also like to +send gifts - if you're into swag, make sure to let us know. We do not currently +offer a paid security bounty program at this time. diff --git a/vendor/github.com/docker/go-events/vendor.mod b/vendor/github.com/docker/go-events/vendor.mod new file mode 100644 index 000000000000..66a2cd9e5c6c --- /dev/null +++ b/vendor/github.com/docker/go-events/vendor.mod @@ -0,0 +1,5 @@ +module github.com/docker/go-events + +go 1.13 + +require github.com/sirupsen/logrus v1.9.3 diff --git a/vendor/github.com/docker/go-events/vendor.sum b/vendor/github.com/docker/go-events/vendor.sum new file mode 100644 index 000000000000..9243c28735dd --- /dev/null +++ b/vendor/github.com/docker/go-events/vendor.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/opencontainers/image-spec/specs-go/version.go b/vendor/github.com/opencontainers/image-spec/specs-go/version.go index 7069ae44d715..c3897c7ca0c6 100644 --- a/vendor/github.com/opencontainers/image-spec/specs-go/version.go +++ b/vendor/github.com/opencontainers/image-spec/specs-go/version.go @@ -22,7 +22,7 @@ const ( // VersionMinor is for functionality in a backwards-compatible manner VersionMinor = 1 // VersionPatch is for backwards-compatible bug fixes - VersionPatch = 0 + VersionPatch = 1 // VersionDev indicates development branch. Releases will be empty string. VersionDev = "" diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index cfafed5b54c6..1d8cffae8cfc 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -12,8 +12,6 @@ package errgroup import ( "context" "fmt" - "runtime" - "runtime/debug" "sync" ) @@ -33,10 +31,6 @@ type Group struct { errOnce sync.Once err error - - mu sync.Mutex - panicValue any // = PanicError | PanicValue; non-nil if some Group.Go goroutine panicked. - abnormal bool // some Group.Go goroutine terminated abnormally (panic or goexit). } func (g *Group) done() { @@ -56,80 +50,47 @@ func WithContext(ctx context.Context) (*Group, context.Context) { return &Group{cancel: cancel}, ctx } -// Wait blocks until all function calls from the Go method have returned -// normally, then returns the first non-nil error (if any) from them. -// -// If any of the calls panics, Wait panics with a [PanicValue]; -// and if any of them calls [runtime.Goexit], Wait calls runtime.Goexit. +// Wait blocks until all function calls from the Go method have returned, then +// returns the first non-nil error (if any) from them. func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel(g.err) } - if g.panicValue != nil { - panic(g.panicValue) - } - if g.abnormal { - runtime.Goexit() - } return g.err } // Go calls the given function in a new goroutine. -// The first call to Go must happen before a Wait. -// It blocks until the new goroutine can be added without the number of -// active goroutines in the group exceeding the configured limit. // +// The first call to Go must happen before a Wait. // It blocks until the new goroutine can be added without the number of // goroutines in the group exceeding the configured limit. // -// The first goroutine in the group that returns a non-nil error, panics, or -// invokes [runtime.Goexit] will cancel the associated Context, if any. +// The first goroutine in the group that returns a non-nil error will +// cancel the associated Context, if any. The error will be returned +// by Wait. func (g *Group) Go(f func() error) { if g.sem != nil { g.sem <- token{} } - g.add(f) -} - -func (g *Group) add(f func() error) { g.wg.Add(1) go func() { defer g.done() - normalReturn := false - defer func() { - if normalReturn { - return - } - v := recover() - g.mu.Lock() - defer g.mu.Unlock() - if !g.abnormal { - if g.cancel != nil { - g.cancel(g.err) - } - g.abnormal = true - } - if v != nil && g.panicValue == nil { - switch v := v.(type) { - case error: - g.panicValue = PanicError{ - Recovered: v, - Stack: debug.Stack(), - } - default: - g.panicValue = PanicValue{ - Recovered: v, - Stack: debug.Stack(), - } - } - } - }() - err := f() - normalReturn = true - if err != nil { + // It is tempting to propagate panics from f() + // up to the goroutine that calls Wait, but + // it creates more problems than it solves: + // - it delays panics arbitrarily, + // making bugs harder to detect; + // - it turns f's panic stack into a mere value, + // hiding it from crash-monitoring tools; + // - it risks deadlocks that hide the panic entirely, + // if f's panic leaves the program in a state + // that prevents the Wait call from being reached. + // See #53757, #74275, #74304, #74306. + + if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { @@ -154,7 +115,19 @@ func (g *Group) TryGo(f func() error) bool { } } - g.add(f) + g.wg.Add(1) + go func() { + defer g.done() + + if err := f(); err != nil { + g.errOnce.Do(func() { + g.err = err + if g.cancel != nil { + g.cancel(g.err) + } + }) + } + }() return true } @@ -176,33 +149,3 @@ func (g *Group) SetLimit(n int) { } g.sem = make(chan token, n) } - -// PanicError wraps an error recovered from an unhandled panic -// when calling a function passed to Go or TryGo. -type PanicError struct { - Recovered error - Stack []byte // result of call to [debug.Stack] -} - -func (p PanicError) Error() string { - // A Go Error method conventionally does not include a stack dump, so omit it - // here. (Callers who care can extract it from the Stack field.) - return fmt.Sprintf("recovered from errgroup.Group: %v", p.Recovered) -} - -func (p PanicError) Unwrap() error { return p.Recovered } - -// PanicValue wraps a value that does not implement the error interface, -// recovered from an unhandled panic when calling a function passed to Go or -// TryGo. -type PanicValue struct { - Recovered any - Stack []byte // result of call to [debug.Stack] -} - -func (p PanicValue) String() string { - if len(p.Stack) > 0 { - return fmt.Sprintf("recovered from errgroup.Group: %v\n%s", p.Recovered, p.Stack) - } - return fmt.Sprintf("recovered from errgroup.Group: %v", p.Recovered) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 71c03ba05efe..422b0fe05b4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# dario.cat/mergo v1.0.1 +# dario.cat/mergo v1.0.2 ## explicit; go 1.13 dario.cat/mergo # github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c @@ -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.1+incompatible +# github.com/docker/docker v28.5.1+incompatible ## explicit github.com/docker/docker/api github.com/docker/docker/api/types @@ -89,18 +89,11 @@ github.com/docker/docker/api/types/system github.com/docker/docker/api/types/time github.com/docker/docker/api/types/versions github.com/docker/docker/api/types/volume -github.com/docker/docker/builder/remotecontext/git -github.com/docker/docker/builder/remotecontext/urlutil 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/jsonmessage -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/registry # github.com/docker/docker-credential-helpers v0.9.3 ## explicit; go 1.21 github.com/docker/docker-credential-helpers/client @@ -108,12 +101,12 @@ github.com/docker/docker-credential-helpers/credentials # github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c ## explicit github.com/docker/go/canonical/json -# github.com/docker/go-connections v0.5.0 +# github.com/docker/go-connections v0.6.0 ## explicit; go 1.18 github.com/docker/go-connections/nat github.com/docker/go-connections/sockets github.com/docker/go-connections/tlsconfig -# github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c +# github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 ## explicit github.com/docker/go-events # github.com/docker/go-metrics v0.0.1 @@ -250,7 +243,7 @@ github.com/munnerz/goautoneg # github.com/opencontainers/go-digest v1.0.0 ## explicit; go 1.13 github.com/opencontainers/go-digest -# github.com/opencontainers/image-spec v1.1.0 +# github.com/opencontainers/image-spec v1.1.1 ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 @@ -419,7 +412,7 @@ golang.org/x/net/idna golang.org/x/net/internal/httpcommon golang.org/x/net/internal/timeseries golang.org/x/net/trace -# golang.org/x/sync v0.14.0 +# golang.org/x/sync v0.16.0 ## explicit; go 1.23.0 golang.org/x/sync/errgroup # golang.org/x/sys v0.33.0