Compare commits
171 Commits
v28.0.2
...
v28.1.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| b8857225a0 | |||
| fc04a49c35 | |||
| 129ab99109 | |||
| 59a723bda6 | |||
| 6ca77b6529 | |||
| 50900c0da7 | |||
| 2dcc881d4d | |||
| e7a091eceb | |||
| e04b67f51d | |||
| 557d721299 | |||
| 219d3fe25a | |||
| 2b28bb649b | |||
| 1a502e91c9 | |||
| 1adc1583a7 | |||
| 785a12eeef | |||
| fc99fe2d08 | |||
| b501283743 | |||
| 3372bcf821 | |||
| c528504434 | |||
| adb0abaec5 | |||
| 18178e079f | |||
| e937b52210 | |||
| 6aa93d1f40 | |||
| a85062bcdc | |||
| 0d9d187f31 | |||
| 559c0121c8 | |||
| ec9e729f76 | |||
| 07b203e2f2 | |||
| f18e239a53 | |||
| 1f9a55de6a | |||
| c718d3f13c | |||
| 1a950db5ce | |||
| e2865628ae | |||
| e578f156c0 | |||
| b74b7b3c40 | |||
| ecde8c38a5 | |||
| b37d84fd10 | |||
| af85e1e2f7 | |||
| 8633197105 | |||
| 94afbc1116 | |||
| 4530417f6b | |||
| 23d7346f75 | |||
| 4c820d3ac0 | |||
| ced66f22d6 | |||
| 0b0fc106dc | |||
| 56c2fa6c0e | |||
| 1ed3859879 | |||
| 7fde1f799f | |||
| d5a8cd4093 | |||
| 01d8642c7e | |||
| a16c3a49c8 | |||
| d76057210a | |||
| 9a849ba00c | |||
| 481e6f1477 | |||
| bcd9c885e3 | |||
| e587e8a269 | |||
| 932574363f | |||
| ac375caa87 | |||
| 7cc6b8ebf4 | |||
| b8bcf6f5ad | |||
| d47d2338b7 | |||
| 410c0baadd | |||
| d83a1b777c | |||
| b515831508 | |||
| 2d3a81642a | |||
| 69d903e706 | |||
| e0fa0596a7 | |||
| a91d194d7f | |||
| 342a01a9ff | |||
| 6714b50288 | |||
| 2bf317ad5f | |||
| 91cbde67c5 | |||
| 49a36daebe | |||
| 90b48f8eb5 | |||
| 763be9b3f8 | |||
| 527998e6ee | |||
| 77f40b8e99 | |||
| 205241bcc6 | |||
| edd2f7d9fb | |||
| bd03f1154f | |||
| 2631d5ba99 | |||
| a2e179457e | |||
| 2c3cf8db0f | |||
| b65f52fd64 | |||
| 58fba25b09 | |||
| 64413c20ef | |||
| db44e59be7 | |||
| ce4b752274 | |||
| f66c5a33d0 | |||
| 6523832c73 | |||
| 3122b8e7f5 | |||
| ed0511251d | |||
| ce61ea015c | |||
| cda7235c81 | |||
| c8f9187157 | |||
| 0dabdd1a0d | |||
| 33494921b8 | |||
| c911ced1a4 | |||
| d726a9b4cd | |||
| 1c54b0ba66 | |||
| 4a3466eeb6 | |||
| 12d637c1b5 | |||
| 8f9fec11ab | |||
| 2328745f92 | |||
| b557e37a49 | |||
| 9b2479dca7 | |||
| 2b84421520 | |||
| 207a1a0dd8 | |||
| 850fea8023 | |||
| 8b222aedfa | |||
| 0c4912b0ec | |||
| 8e6de54d18 | |||
| fc817a1367 | |||
| 30c20d5c8c | |||
| 99a6126cfe | |||
| 491e8fdaf8 | |||
| 930173a2ab | |||
| 242422bbb3 | |||
| 3fe40e5ea9 | |||
| f1385df2a7 | |||
| 0e32baf115 | |||
| 9f4165ccb8 | |||
| 15b95beac7 | |||
| ee275d5733 | |||
| 80bca8eb1d | |||
| 1a14abb748 | |||
| b8034c0ed7 | |||
| 3e699a351f | |||
| f596202125 | |||
| ee2f787634 | |||
| d8432cdf23 | |||
| 60645d29f4 | |||
| 2b0631f45e | |||
| 84828b0eb8 | |||
| b5ca7e8e6b | |||
| cfaaeb0982 | |||
| 5a8120c809 | |||
| c27751fcfe | |||
| 7b348e4e94 | |||
| ff5fdfae35 | |||
| 9f19820f88 | |||
| 7607c3f945 | |||
| 61cd986723 | |||
| d97f65c4da | |||
| 30cac75693 | |||
| 255a5f630e | |||
| 535bb6c85c | |||
| 47775a8fa0 | |||
| 4a80c6da83 | |||
| b199ece92a | |||
| 48741f72ff | |||
| 4541df21e5 | |||
| eaf98b2202 | |||
| 98d0b0cc14 | |||
| 5ea072d936 | |||
| 08f86507b4 | |||
| 66eb27a487 | |||
| e002576821 | |||
| a9ac6fa376 | |||
| 23eadcd950 | |||
| 3b45f3c09a | |||
| 79141ce5eb | |||
| 082dfb7360 | |||
| f519a8648d | |||
| 353230d978 | |||
| 966b44183f | |||
| 091421f13f | |||
| d1a19d4476 | |||
| 40725aea3c | |||
| fdcfd229aa | |||
| abd02b6a23 |
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@ -63,7 +63,7 @@ jobs:
|
||||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.7"
|
||||
go-version: "1.23.8"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -66,7 +66,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23.7"
|
||||
go-version: "1.23.8"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
@ -7,6 +7,7 @@ linters:
|
||||
- dupword # Detects duplicate words.
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- forbidigo
|
||||
- gocritic # Metalinter; detects bugs, performance, and styling issues.
|
||||
- gocyclo
|
||||
- gofumpt # Detects whether code was gofumpt-ed.
|
||||
@ -44,7 +45,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.23.7"
|
||||
go: "1.23.8"
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
@ -66,6 +67,11 @@ linters-settings:
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
forbidigo:
|
||||
forbid:
|
||||
- pkg: ^regexp$
|
||||
p: ^regexp\.MustCompile
|
||||
msg: Use internal/lazyregexp.New instead.
|
||||
gocyclo:
|
||||
min-complexity: 16
|
||||
gosec:
|
||||
|
||||
@ -4,7 +4,7 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.23.7
|
||||
ARG GO_VERSION=1.23.8
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
ARG GOTESTSUM_VERSION=v1.12.0
|
||||
@ -12,8 +12,8 @@ ARG GOTESTSUM_VERSION=v1.12.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.20.1
|
||||
ARG COMPOSE_VERSION=v2.32.4
|
||||
ARG BUILDX_VERSION=0.23.0
|
||||
ARG COMPOSE_VERSION=v2.33.1
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
@ -117,7 +117,7 @@ COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docke
|
||||
COPY --link . .
|
||||
ENV DOCKER_BUILDKIT=1
|
||||
ENV PATH=/go/src/github.com/docker/cli/build:$PATH
|
||||
CMD ./scripts/test/e2e/entry
|
||||
CMD ["./scripts/test/e2e/entry"]
|
||||
|
||||
FROM build-base-${BASE_VARIANT} AS dev
|
||||
COPY --link . .
|
||||
|
||||
14
Makefile
14
Makefile
@ -67,20 +67,20 @@ dynbinary: ## build dynamically linked binary
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## build example CLI plugins
|
||||
./scripts/build/plugins
|
||||
scripts/build/plugins
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: ## update vendor with go modules
|
||||
rm -rf vendor
|
||||
./scripts/vendor update
|
||||
scripts/with-go-mod.sh scripts/vendor update
|
||||
|
||||
.PHONY: validate-vendor
|
||||
validate-vendor: ## validate vendor
|
||||
./scripts/vendor validate
|
||||
scripts/with-go-mod.sh scripts/vendor validate
|
||||
|
||||
.PHONY: mod-outdated
|
||||
mod-outdated: ## check outdated dependencies
|
||||
./scripts/vendor outdated
|
||||
scripts/with-go-mod.sh scripts/vendor outdated
|
||||
|
||||
.PHONY: authors
|
||||
authors: ## generate AUTHORS file from git history
|
||||
@ -115,15 +115,15 @@ shell-completion: ## generate shell-completion scripts
|
||||
|
||||
.PHONY: manpages
|
||||
manpages: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-man.sh
|
||||
|
||||
.PHONY: mddocs
|
||||
mddocs: ## generate markdown files from go source
|
||||
scripts/docs/generate-md.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-md.sh
|
||||
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/docs/generate-yaml.sh
|
||||
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
|
||||
@ -64,13 +64,10 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
return nil
|
||||
}
|
||||
|
||||
pluginDirs, err := getPluginDirs(cfg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
for pluginName, cfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(cfg, subCmdStr)
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -61,20 +61,16 @@ func IsNotFound(err error) bool {
|
||||
// 3. Platform-specific defaultSystemPluginDirs.
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
|
||||
func getPluginDirs(cfg *configfile.ConfigFile) []string {
|
||||
var pluginDirs []string
|
||||
|
||||
if cfg != nil {
|
||||
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
|
||||
}
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
pluginDirs = append(pluginDirs, pluginDir)
|
||||
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
|
||||
return pluginDirs, nil
|
||||
return pluginDirs
|
||||
}
|
||||
|
||||
func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
@ -116,10 +112,7 @@ func listPluginCandidates(dirs []string) map[string][]string {
|
||||
|
||||
// GetPlugin returns a plugin on the system by its name
|
||||
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCLI.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginDirs := getPluginDirs(dockerCLI.ConfigFile())
|
||||
return getPlugin(name, pluginDirs, rootcmd)
|
||||
}
|
||||
|
||||
@ -145,16 +138,20 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
|
||||
|
||||
// ListPlugins produces a list of the plugins available on the system
|
||||
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) {
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
candidates := listPluginCandidates(pluginDirs)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var plugins []Plugin
|
||||
var mu sync.Mutex
|
||||
eg, _ := errgroup.WithContext(context.TODO())
|
||||
ctx := rootcmd.Context()
|
||||
if ctx == nil {
|
||||
// Fallback, mostly for tests that pass a bare cobra.command
|
||||
ctx = context.Background()
|
||||
}
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
cmds := rootcmd.Commands()
|
||||
for _, paths := range candidates {
|
||||
func(paths []string) {
|
||||
@ -202,10 +199,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
|
||||
return nil, errPluginNotFound(name)
|
||||
}
|
||||
exename := addExeSuffix(metadata.NamePrefix + name)
|
||||
pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pluginDirs := getPluginDirs(dockerCli.ConfigFile())
|
||||
|
||||
for _, d := range pluginDirs {
|
||||
path := filepath.Join(d, exename)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -81,6 +82,12 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
assert.DeepEqual(t, candidates, exp)
|
||||
}
|
||||
|
||||
func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
|
||||
assert.Assert(t, len(candidates) == 0)
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
@ -166,14 +173,11 @@ func TestErrPluginNotFound(t *testing.T) {
|
||||
func TestGetPluginDirs(t *testing.T) {
|
||||
cli := test.NewFakeCli(nil)
|
||||
|
||||
pluginDir, err := config.Path("cli-plugins")
|
||||
assert.NilError(t, err)
|
||||
pluginDir := filepath.Join(config.Dir(), "cli-plugins")
|
||||
expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
|
||||
|
||||
var pluginDirs []string
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
assert.NilError(t, err)
|
||||
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
@ -182,7 +186,6 @@ func TestGetPluginDirs(t *testing.T) {
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
})
|
||||
pluginDirs, err = getPluginDirs(cli.ConfigFile())
|
||||
pluginDirs = getPluginDirs(cli.ConfigFile())
|
||||
assert.DeepEqual(t, expected, pluginDirs)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@ -8,14 +8,14 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
|
||||
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
|
||||
@ -23,3 +23,22 @@ func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// 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).
|
||||
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bake [OPTIONS] [TARGET...]",
|
||||
Short: "Build from a file",
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{
|
||||
// We want to show this command in the "top" category in --help
|
||||
// output, and not to be grouped under "management commands".
|
||||
"category-top": "5",
|
||||
"aliases": "docker buildx bake",
|
||||
"version": "1.31",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ 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"
|
||||
"github.com/docker/docker/errdefs"
|
||||
@ -69,7 +70,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allCacheWarning
|
||||
}
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@ -56,12 +56,12 @@ type Cli interface {
|
||||
CurrentContext() string
|
||||
DockerEndpoint() docker.Endpoint
|
||||
TelemetryClient
|
||||
DeprecatedNotaryClient
|
||||
DeprecatedManifestClient
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
// Instances of the client should be created using the [NewDockerCli]
|
||||
// constructor to make sure they are properly initialized with defaults
|
||||
// set.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
options *cliflags.ClientOptions
|
||||
@ -76,7 +76,7 @@ type DockerCli struct {
|
||||
init sync.Once
|
||||
initErr error
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig store.Config
|
||||
contextStoreConfig *store.Config
|
||||
initTimeout time.Duration
|
||||
res telemetryResource
|
||||
|
||||
@ -252,13 +252,33 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
return errors.New("conflicting options: cannot specify both --host and --context")
|
||||
}
|
||||
|
||||
if cli.contextStoreConfig == nil {
|
||||
// This path can be hit when calling Initialize on a DockerCli that's
|
||||
// not constructed through [NewDockerCli]. Using the default context
|
||||
// store without a config set will result in Endpoints from contexts
|
||||
// not being type-mapped correctly, and used as a generic "map[string]any",
|
||||
// instead of a [docker.EndpointMeta].
|
||||
//
|
||||
// When looking up the API endpoint (using [EndpointFromContext]), no
|
||||
// endpoint will be found, and a default, empty endpoint will be used
|
||||
// instead which in its turn, causes newAPIClientFromEndpoint to
|
||||
// be initialized with the default config instead of settings for
|
||||
// the current context (which may mean; connecting with the wrong
|
||||
// endpoint and/or TLS Config to be missing).
|
||||
//
|
||||
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
|
||||
if err := WithDefaultContextStoreConfig()(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
cli.options = opts
|
||||
cli.configFile = config.LoadDefaultConfigFile(cli.err)
|
||||
cli.currentContext = resolveContextName(cli.options, cli.configFile)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
|
||||
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
)
|
||||
|
||||
type DeprecatedNotaryClient interface {
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
//
|
||||
// Deprecated: use [trust.GetNotaryRepository] instead. This method is no longer used and will be removed in the next release.
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
}
|
||||
|
||||
type DeprecatedManifestClient interface {
|
||||
// ManifestStore returns a store for local manifests
|
||||
//
|
||||
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
|
||||
ManifestStore() manifeststore.Store
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry.
|
||||
//
|
||||
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
}
|
||||
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
}
|
||||
|
||||
// ManifestStore returns a store for local manifests
|
||||
//
|
||||
// Deprecated: use [manifeststore.NewStore] instead. This method is no longer used and will be removed in the next release.
|
||||
func (*DockerCli) ManifestStore() manifeststore.Store {
|
||||
return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// RegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
//
|
||||
// Deprecated: use [registryclient.NewRegistryClient]. This method is no longer used and will be removed in the next release.
|
||||
func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
|
||||
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
|
||||
return ResolveAuthConfig(cli.ConfigFile(), index)
|
||||
}
|
||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||
}
|
||||
@ -101,7 +101,8 @@ func WithContentTrust(enabled bool) CLIOption {
|
||||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||
func WithDefaultContextStoreConfig() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contextStoreConfig = DefaultContextStoreConfig()
|
||||
cfg := DefaultContextStoreConfig()
|
||||
cli.contextStoreConfig = &cfg
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,12 +19,10 @@ import (
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
@ -207,8 +205,8 @@ func TestInitializeFromClient(t *testing.T) {
|
||||
// Makes sure we don't hang forever on the initial connection.
|
||||
// https://github.com/docker/cli/issues/3652
|
||||
func TestInitializeFromClientHangs(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
socket := filepath.Join(dir, "my.sock")
|
||||
tmpDir := t.TempDir()
|
||||
socket := filepath.Join(tmpDir, "my.sock")
|
||||
l, err := net.Listen("unix", socket)
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -256,79 +254,40 @@ func TestInitializeFromClientHangs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The CLI no longer disables/hides experimental CLI features, however, we need
|
||||
// to verify that existing configuration files do not break
|
||||
func TestExperimentalCLI(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
testcases := []struct {
|
||||
doc string
|
||||
configfile string
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{}`,
|
||||
},
|
||||
{
|
||||
doc: "experimental",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
dir := fs.NewDir(t, tc.doc, fs.WithFile("config.json", tc.configfile))
|
||||
defer dir.Remove()
|
||||
apiclient := &fakeClient{
|
||||
version: defaultVersion,
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
|
||||
},
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
|
||||
config.SetDir(dir.Path())
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(
|
||||
WithContentTrust(true),
|
||||
)
|
||||
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
assert.Check(t, cli.Out() != nil)
|
||||
assert.Check(t, cli.Err() != nil)
|
||||
assert.Equal(t, cli.ContentTrustEnabled(), true)
|
||||
inputStream, err := io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "some input")
|
||||
|
||||
// Apply can modify a dockerCli after construction
|
||||
inbuf := bytes.NewBuffer([]byte("input"))
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(io.NopCloser(inbuf)),
|
||||
WithInputStream(io.NopCloser(strings.NewReader("input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check input stream
|
||||
inputStream, err := io.ReadAll(cli.In())
|
||||
inputStream, err = io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "input")
|
||||
// Check output stream
|
||||
fmt.Fprintf(cli.Out(), "output")
|
||||
_, err = fmt.Fprint(cli.Out(), "output")
|
||||
assert.NilError(t, err)
|
||||
outputStream, err := io.ReadAll(outbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(outputStream), "output")
|
||||
// Check error stream
|
||||
fmt.Fprintf(cli.Err(), "error")
|
||||
_, err = fmt.Fprint(cli.Err(), "error")
|
||||
assert.NilError(t, err)
|
||||
errStream, err := io.ReadAll(errbuf)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(errStream), "error")
|
||||
@ -345,6 +304,8 @@ func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
|
||||
func TestHooksEnabled(t *testing.T) {
|
||||
t.Run("disabled by default", func(t *testing.T) {
|
||||
// Make sure we don't depend on any existing ~/.docker/config.json
|
||||
config.SetDir(t.TempDir())
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -356,12 +317,11 @@ func TestHooksEnabled(t *testing.T) {
|
||||
"features": {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, cli.HooksEnabled())
|
||||
})
|
||||
|
||||
@ -371,12 +331,11 @@ func TestHooksEnabled(t *testing.T) {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HOOKS", "false")
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
|
||||
@ -386,12 +345,11 @@ func TestHooksEnabled(t *testing.T) {
|
||||
"hooks": "true"
|
||||
}}`
|
||||
t.Setenv("DOCKER_CLI_HINTS", "false")
|
||||
dir := fs.NewDir(t, "", fs.WithFile("config.json", configFile))
|
||||
defer dir.Remove()
|
||||
config.SetDir(t.TempDir())
|
||||
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
|
||||
assert.NilError(t, err)
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
config.SetDir(dir.Path())
|
||||
|
||||
assert.Check(t, !cli.HooksEnabled())
|
||||
})
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
system.NewInfoCommand(dockerCli),
|
||||
|
||||
// management commands
|
||||
builder.NewBakeStubCommand(dockerCli),
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
container.NewContainerCommand(dockerCli),
|
||||
|
||||
@ -13,8 +13,10 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
|
||||
type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
|
||||
// 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.
|
||||
@ -27,7 +29,7 @@ type APIClientProvider interface {
|
||||
}
|
||||
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
@ -47,7 +49,7 @@ func ImageNames(dockerCLI APIClientProvider, limit int) ValidArgsFn {
|
||||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) ValidArgsFn {
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
@ -80,7 +82,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
}
|
||||
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
@ -95,7 +97,7 @@ func VolumeNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
}
|
||||
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
|
||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
if err != nil {
|
||||
@ -133,7 +135,7 @@ func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobr
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
func FromList(options ...string) ValidArgsFn {
|
||||
func FromList(options ...string) cobra.CompletionFunc {
|
||||
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||
}
|
||||
|
||||
@ -150,7 +152,6 @@ func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCo
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
"linux",
|
||||
"linux/386",
|
||||
"linux/amd64",
|
||||
"linux/arm",
|
||||
@ -167,10 +168,8 @@ var commonPlatforms = []string{
|
||||
// Not yet supported
|
||||
"linux/riscv64",
|
||||
|
||||
"windows",
|
||||
"windows/amd64",
|
||||
|
||||
"wasip1",
|
||||
"wasip1/wasm",
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
|
||||
if err != nil {
|
||||
|
||||
@ -144,7 +144,7 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
||||
}
|
||||
|
||||
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
|
||||
func completeCgroupns() completion.ValidArgsFn {
|
||||
func completeCgroupns() cobra.CompletionFunc {
|
||||
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra
|
||||
|
||||
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
|
||||
// The completion is partly composite.
|
||||
func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completeIpc(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||
@ -175,7 +175,7 @@ func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command
|
||||
}
|
||||
|
||||
// completeLink implements shell completion for the `--link` option of `run` and `create`.
|
||||
func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
@ -184,7 +184,7 @@ func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Comman
|
||||
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
|
||||
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
|
||||
// of the build-in log drivers.
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
@ -206,7 +206,7 @@ func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.S
|
||||
}
|
||||
|
||||
// completePid implements shell completion for the `--pid` option of `run` and `create`.
|
||||
func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
func completePid(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
|
||||
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
|
||||
@ -277,7 +277,7 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
|
||||
}
|
||||
|
||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
|
||||
@ -16,8 +16,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@ -10,7 +10,8 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -74,7 +75,7 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.Tar(srcDir.Path(), archive.Uncompressed)
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return readCloser, container.PathStat{}, err
|
||||
},
|
||||
})
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
@ -13,13 +17,17 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/errdefs"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
@ -35,11 +43,12 @@ const (
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // always, missing, never
|
||||
quiet bool
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // always, missing, never
|
||||
quiet bool
|
||||
useAPISocket bool
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
@ -70,6 +79,8 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
|
||||
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
|
||||
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
@ -179,20 +190,20 @@ func (cid *cidFile) Write(id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newCIDFile(path string) (*cidFile, error) {
|
||||
if path == "" {
|
||||
func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
if cidPath == "" {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
|
||||
if _, err := os.Stat(cidPath); err == nil {
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(path)
|
||||
f, err := os.Create(cidPath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create the container ID file")
|
||||
}
|
||||
|
||||
return &cidFile{path: path, file: f}, nil
|
||||
return &cidFile{path: cidPath, file: f}, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
@ -239,6 +250,73 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
return nil
|
||||
}
|
||||
|
||||
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
|
||||
var apiSocketCreds map[string]types.AuthConfig
|
||||
|
||||
if options.useAPISocket {
|
||||
// We'll create two new mounts to handle this flag:
|
||||
// 1. Mount the actual docker socket.
|
||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
||||
|
||||
socket := dockerCli.DockerEndpoint().Host
|
||||
if !strings.HasPrefix(socket, "unix://") {
|
||||
return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
|
||||
}
|
||||
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
|
||||
|
||||
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
|
||||
Type: mount.TypeBind,
|
||||
Source: socket,
|
||||
Target: "/var/run/docker.sock",
|
||||
BindOptions: &mount.BindOptions{},
|
||||
})
|
||||
|
||||
/*
|
||||
|
||||
Ideally, we'd like to copy the config into a tmpfs but unfortunately,
|
||||
the mounts won't be in place until we start the container. This can
|
||||
leave around the config if the container doesn't get deleted.
|
||||
|
||||
We are using the most compose-secret-compatible approach,
|
||||
which is implemented at
|
||||
https://github.com/docker/compose/blob/main/pkg/compose/convergence.go#L737
|
||||
|
||||
// Prepare a tmpfs mount for our credentials so they go away after the
|
||||
// container exits. We'll copy into this mount after the container is
|
||||
// created.
|
||||
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
|
||||
Type: mount.TypeTmpfs,
|
||||
Target: "/docker/",
|
||||
TmpfsOptions: &mount.TmpfsOptions{
|
||||
SizeBytes: 1 << 20, // only need a small partition
|
||||
Mode: 0o600,
|
||||
},
|
||||
})
|
||||
*/
|
||||
|
||||
var envvarPresent bool
|
||||
for _, envvar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
|
||||
envvarPresent = true
|
||||
}
|
||||
}
|
||||
|
||||
// If the DOCKER_CONFIG env var is already present, we assume the client knows
|
||||
// what they're doing and don't inject the creds.
|
||||
if !envvarPresent {
|
||||
// Set our special little location for the config file.
|
||||
containerCfg.Config.Env = append(containerCfg.Config.Env,
|
||||
"DOCKER_CONFIG="+path.Dir(dockerConfigPathInContainer))
|
||||
|
||||
// Resolve this here for later, ensuring we error our before we create the container.
|
||||
creds, err := dockerCli.ConfigFile().GetAllCredentials()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving credentials failed: %w", err)
|
||||
}
|
||||
apiSocketCreds = creds // inject these after container creation.
|
||||
}
|
||||
}
|
||||
|
||||
var platform *specs.Platform
|
||||
// Engine API version 1.41 first introduced the option to specify platform on
|
||||
// create. It will produce an error if you try to set a platform on older API
|
||||
@ -286,11 +364,25 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
|
||||
if warn := localhostDNSWarning(*hostConfig); warn != "" {
|
||||
response.Warnings = append(response.Warnings, warn)
|
||||
}
|
||||
|
||||
containerID = response.ID
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
}
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return response.ID, err
|
||||
err = containerIDFile.Write(containerID)
|
||||
|
||||
if options.useAPISocket && apiSocketCreds != nil {
|
||||
// Create a new config file with just the auth.
|
||||
newConfig := &configfile.ConfigFile{
|
||||
AuthConfigs: apiSocketCreds,
|
||||
}
|
||||
|
||||
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
|
||||
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return containerID, err
|
||||
}
|
||||
|
||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||
@ -321,3 +413,39 @@ func validatePullOpt(val string) error {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// copyDockerConfigIntoContainer takes the client configuration and copies it
|
||||
// into the container.
|
||||
//
|
||||
// The path should be an absolute path in the container, commonly
|
||||
// /root/.docker/config.json.
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
var configBuf bytes.Buffer
|
||||
if err := config.SaveToWriter(&configBuf); err != nil {
|
||||
return fmt.Errorf("saving creds: %w", err)
|
||||
}
|
||||
|
||||
// We don't need to get super fancy with the tar creation.
|
||||
var tarBuf bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&tarBuf)
|
||||
tarWriter.WriteHeader(&tar.Header{
|
||||
Name: configPath,
|
||||
Size: int64(configBuf.Len()),
|
||||
Mode: 0o600,
|
||||
})
|
||||
|
||||
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
|
||||
return fmt.Errorf("writing config to tar file for config copy: %w", err)
|
||||
}
|
||||
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return fmt.Errorf("closing tar for config copy failed: %w", err)
|
||||
}
|
||||
|
||||
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
|
||||
&tarBuf, container.CopyToContainerOptions{}); err != nil {
|
||||
return fmt.Errorf("copying config.json into container failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -248,23 +248,25 @@ func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *specs.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := NewCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -41,27 +42,28 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) error {
|
||||
var output io.Writer
|
||||
if opts.output == "" {
|
||||
if dockerCLI.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
output = dockerCLI.Out()
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(opts.output); err != nil {
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
responseBody, err := clnt.ContainerExport(ctx, opts.container)
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if opts.output == "" {
|
||||
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
return command.CopyToFile(opts.output, responseBody)
|
||||
_, err = io.Copy(output, responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -42,8 +42,6 @@ func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"-o", "/dev/random", "container"})
|
||||
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err != nil)
|
||||
expected := `"/dev/random" must be a directory or a regular file`
|
||||
assert.ErrorContains(t, err, expected)
|
||||
const expected = `failed to export container: cannot write to a character device file`
|
||||
assert.Error(t, cmd.Execute(), expected)
|
||||
}
|
||||
|
||||
@ -8,12 +8,12 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
mounttypes "github.com/docker/docker/api/types/mount"
|
||||
@ -40,7 +40,7 @@ const (
|
||||
seccompProfileUnconfined = "unconfined"
|
||||
)
|
||||
|
||||
var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
|
||||
var deviceCgroupRuleRegexp = lazyregexp.New(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
|
||||
|
||||
// containerOptions is a data object with all the options for creating a container
|
||||
type containerOptions struct {
|
||||
|
||||
@ -65,7 +65,7 @@ func TestNewPortCommandOutput(t *testing.T) {
|
||||
}
|
||||
return ci, nil
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
})
|
||||
cmd := NewPortCommand(cli)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs([]string{"some_container", tc.port})
|
||||
|
||||
@ -7,6 +7,7 @@ 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/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
@ -56,7 +57,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container")
|
||||
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before running ("`+PullImageAlways+`", "`+PullImageMissing+`", "`+PullImageNever+`")`)
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
|
||||
flags.BoolVarP(&options.createOptions.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
@ -238,10 +239,16 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *runOption
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
case status := <-statusChan:
|
||||
// notify hijackedIOStreamer that we're exiting and wait
|
||||
// so that the terminal can be restored.
|
||||
cancelFun()
|
||||
<-errCh
|
||||
// If container exits, output stream processing may not be finished yet,
|
||||
// we need to keep the streamer running until all output is read.
|
||||
// However, if stdout or stderr is not attached, we can just exit.
|
||||
if !config.AttachStdout && !config.AttachStderr {
|
||||
// Notify hijackedIOStreamer that we're exiting and wait
|
||||
// so that the terminal can be restored.
|
||||
cancelFun()
|
||||
}
|
||||
<-errCh // Drain channel but don't care about result
|
||||
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
@ -156,8 +156,10 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
ID: "id",
|
||||
}, nil
|
||||
},
|
||||
containerKillFunc: func(ctx context.Context, containerID, signal string) error {
|
||||
killCh <- struct{}{}
|
||||
containerKillFunc: func(ctx context.Context, containerID, sig string) error {
|
||||
if sig == "TERM" {
|
||||
close(killCh)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
containerAttachFunc: func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
@ -172,7 +174,7 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
waitFunc: func(_ string) (<-chan container.WaitResponse, <-chan error) {
|
||||
responseChan := make(chan container.WaitResponse, 1)
|
||||
errChan := make(chan error)
|
||||
|
||||
<-killCh
|
||||
responseChan <- container.WaitResponse{
|
||||
StatusCode: 130,
|
||||
}
|
||||
@ -201,8 +203,7 @@ func TestRunAttachTermination(t *testing.T) {
|
||||
case <-attachCh:
|
||||
}
|
||||
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT))
|
||||
// end stream from "container" so that we'll detach
|
||||
assert.NilError(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM))
|
||||
conn.Close()
|
||||
|
||||
select {
|
||||
|
||||
@ -69,8 +69,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: rawMeta.Name,
|
||||
Current: isCurrent,
|
||||
Error: err.Error(),
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
continue
|
||||
}
|
||||
@ -85,8 +83,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Description: meta.Description,
|
||||
DockerEndpoint: dockerEndpoint.Host,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(meta.AdditionalFields, opts.format),
|
||||
}
|
||||
contexts = append(contexts, &desc)
|
||||
}
|
||||
@ -103,8 +99,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
Name: curContext,
|
||||
Current: true,
|
||||
Error: errMsg,
|
||||
|
||||
ContextType: getContextType(nil, opts.format),
|
||||
})
|
||||
}
|
||||
sort.Slice(contexts, func(i, j int) bool {
|
||||
@ -120,30 +114,6 @@ func runList(dockerCli command.Cli, opts *listOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getContextType sets the LegacyContextType field for compatibility with
|
||||
// Visual Studio, which depends on this field from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/contextmetadata.go#L28-L34
|
||||
// https://github.com/docker/compose-cli/blob/c156ce6da4c2b317174d42daf1b019efa87e9f92/api/context/store/store.go#L34-L51
|
||||
//
|
||||
// TODO(thaJeztah): remove this and [ClientContext.ContextType] once Visual Studio is updated to no longer depend on this.
|
||||
func getContextType(meta map[string]any, format string) string {
|
||||
if format != formatter.JSONFormat && format != formatter.JSONFormatKey {
|
||||
// We only need the ContextType field when formatting as JSON,
|
||||
// which is the format-string used by Visual Studio to detect the
|
||||
// context-type.
|
||||
return ""
|
||||
}
|
||||
if ct, ok := meta["Type"]; ok {
|
||||
// If the context on-disk has a context-type (ecs, aci), return it.
|
||||
return ct.(string)
|
||||
}
|
||||
|
||||
// Use the default context-type.
|
||||
return "moby"
|
||||
}
|
||||
|
||||
func format(dockerCli command.Cli, opts *listOptions, contexts []*formatter.ClientContext) error {
|
||||
contextCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
|
||||
10
cli/command/context/testdata/list-json.golden
vendored
10
cli/command/context/testdata/list-json.golden
vendored
@ -1,5 +1,5 @@
|
||||
{"Name":"context1","Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"aci"}
|
||||
{"Name":"context2","Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"ecs"}
|
||||
{"Name":"context3","Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Current":false,"Error":"","ContextType":"moby"}
|
||||
{"Name":"current","Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Current":true,"Error":"","ContextType":"moby"}
|
||||
{"Name":"default","Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Current":false,"Error":"","ContextType":"moby"}
|
||||
{"Current":false,"Description":"description of context1","DockerEndpoint":"https://someswarmserver.example.com","Error":"","Name":"context1"}
|
||||
{"Current":false,"Description":"description of context2","DockerEndpoint":"https://someswarmserver.example.com","Error":"","Name":"context2"}
|
||||
{"Current":false,"Description":"description of context3","DockerEndpoint":"https://someswarmserver.example.com","Error":"","Name":"context3"}
|
||||
{"Current":true,"Description":"description of current","DockerEndpoint":"https://someswarmserver.example.com","Error":"","Name":"current"}
|
||||
{"Current":false,"Description":"Current DOCKER_HOST based configuration","DockerEndpoint":"unix:///var/run/docker.sock","Error":"","Name":"default"}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package formatter
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
const (
|
||||
// ClientContextTableFormat is the default client context format.
|
||||
ClientContextTableFormat = "table {{.Name}}{{if .Current}} *{{end}}\t{{.Description}}\t{{.DockerEndpoint}}\t{{.Error}}"
|
||||
@ -30,13 +28,6 @@ type ClientContext struct {
|
||||
DockerEndpoint string
|
||||
Current bool
|
||||
Error string
|
||||
|
||||
// ContextType is a temporary field for compatibility with
|
||||
// Visual Studio, which depends on this from the "cloud integration"
|
||||
// wrapper.
|
||||
//
|
||||
// Deprecated: this type is only for backward-compatibility. Do not use.
|
||||
ContextType string `json:"ContextType,omitempty"`
|
||||
}
|
||||
|
||||
// ClientContextWrite writes formatted contexts using the Context
|
||||
@ -69,13 +60,6 @@ func newClientContextContext() *clientContextContext {
|
||||
}
|
||||
|
||||
func (c *clientContextContext) MarshalJSON() ([]byte, error) {
|
||||
if c.c.ContextType != "" {
|
||||
// We only have ContextType set for plain "json" or "{{json .}}" formatting,
|
||||
// so we should be able to just use the default json.Marshal with no
|
||||
// special handling.
|
||||
return json.Marshal(c.c)
|
||||
}
|
||||
// FIXME(thaJeztah): why do we need a special marshal function here?
|
||||
return MarshalJSON(c)
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@ -23,16 +22,16 @@ import (
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"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/archive"
|
||||
"github.com/docker/docker/pkg/idtools"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -269,7 +268,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
|
||||
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||
ExcludePatterns: excludes,
|
||||
ChownOpts: &idtools.Identity{UID: 0, GID: 0},
|
||||
ChownOpts: &archive.ChownOpts{UID: 0, GID: 0},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -433,7 +432,7 @@ func validateTag(rawRepo string) (string, error) {
|
||||
return rawRepo, nil
|
||||
}
|
||||
|
||||
var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
|
||||
var dockerfileFromLinePattern = lazyregexp.New(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
|
||||
|
||||
// resolvedTag records the repository, tag, and resolved digest reference
|
||||
// from a Dockerfile rewrite.
|
||||
|
||||
@ -15,11 +15,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/builder/remotecontext/git"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/patternmatcher"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -163,7 +164,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
tarArchive, err := archive.Tar(dockerfileDir, archive.Uncompressed)
|
||||
tarArchive, err := archive.Tar(dockerfileDir, compression.None)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@ -178,8 +179,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC
|
||||
// IsArchive checks for the magic bytes of a tar or any supported compression
|
||||
// algorithm.
|
||||
func IsArchive(header []byte) bool {
|
||||
compression := archive.DetectCompression(header)
|
||||
if compression != archive.Uncompressed {
|
||||
if compression.Detect(header) != compression.None {
|
||||
return true
|
||||
}
|
||||
r := tar.NewReader(bytes.NewBuffer(header))
|
||||
@ -427,7 +427,7 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip)
|
||||
compressWriter, err := compression.CompressStream(pipeWriter, archive.Gzip)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/patternmatcher"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -165,7 +166,7 @@ func TestGetContextFromReaderTar(t *testing.T) {
|
||||
contextDir := createTestTempDir(t)
|
||||
createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents)
|
||||
|
||||
tarStream, err := archive.Tar(contextDir, archive.Uncompressed)
|
||||
tarStream, err := archive.Tar(contextDir, compression.None)
|
||||
assert.NilError(t, err)
|
||||
|
||||
tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName)
|
||||
|
||||
@ -14,8 +14,8 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
"gotest.tools/v3/skip"
|
||||
@ -54,7 +54,7 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
|
||||
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
|
||||
|
||||
header := buffer.Bytes()[:10]
|
||||
assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
|
||||
assert.Equal(t, compression.Gzip, compression.Detect(header))
|
||||
}
|
||||
|
||||
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
@ -14,12 +15,14 @@ import (
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type inspectOptions struct {
|
||||
format string
|
||||
refs []string
|
||||
format string
|
||||
refs []string
|
||||
platform string
|
||||
}
|
||||
|
||||
// newInspectCommand creates a new cobra.Command for `docker image inspect`
|
||||
@ -39,14 +42,36 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
|
||||
// Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to
|
||||
// inspecting the image as-is. This also avoids forcing the platform selection
|
||||
// on older APIs which don't support it.
|
||||
flags.StringVar(&opts.platform, "platform", "", `Inspect a specific platform of the multi-platform image.
|
||||
If the image or the server is not multi-platform capable, the command will error out if the platform does not match.
|
||||
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
||||
flags.SetAnnotation("platform", "version", []string{"1.49"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
var platform *ocispec.Platform
|
||||
if opts.platform != "" {
|
||||
p, err := platforms.Parse(opts.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
return inspect.Inspect(dockerCLI.Out(), opts.refs, opts.format, func(ref string) (any, []byte, error) {
|
||||
var buf bytes.Buffer
|
||||
resp, err := apiClient.ImageInspect(ctx, ref, client.ImageInspectWithRawResponse(&buf))
|
||||
resp, err := apiClient.ImageInspect(ctx, ref,
|
||||
client.ImageInspectWithRawResponse(&buf),
|
||||
client.ImageInspectWithPlatform(platform),
|
||||
)
|
||||
if err != nil {
|
||||
return image.InspectResponse{}, nil, err
|
||||
}
|
||||
|
||||
@ -135,6 +135,14 @@ func runImages(ctx context.Context, dockerCLI command.Cli, options imagesOptions
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDangling is a copy of [formatter.isDangling].
|
||||
func isDangling(img image.Summary) bool {
|
||||
if len(img.RepoTags) == 0 && len(img.RepoDigests) == 0 {
|
||||
return true
|
||||
}
|
||||
return len(img.RepoTags) == 1 && img.RepoTags[0] == "<none>:<none>" && len(img.RepoDigests) == 1 && img.RepoDigests[0] == "<none>@<none>"
|
||||
}
|
||||
|
||||
// printAmbiguousHint prints an informational warning if the provided filter
|
||||
// argument is ambiguous.
|
||||
//
|
||||
|
||||
@ -9,6 +9,7 @@ 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/errdefs"
|
||||
units "github.com/docker/go-units"
|
||||
@ -70,7 +71,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allImageWarning
|
||||
}
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@ -15,7 +15,10 @@ import (
|
||||
)
|
||||
|
||||
// PullOptions defines what and how to pull
|
||||
type PullOptions struct {
|
||||
type PullOptions = pullOptions
|
||||
|
||||
// pullOptions defines what and how to pull.
|
||||
type pullOptions struct {
|
||||
remote string
|
||||
all bool
|
||||
platform string
|
||||
@ -25,7 +28,7 @@ type PullOptions struct {
|
||||
|
||||
// NewPullCommand creates a new `docker pull` command
|
||||
func NewPullCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts PullOptions
|
||||
var opts pullOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "pull [OPTIONS] NAME[:TAG|@DIGEST]",
|
||||
@ -33,7 +36,7 @@ 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",
|
||||
@ -57,6 +60,11 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
// 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)
|
||||
switch {
|
||||
case err != nil:
|
||||
|
||||
@ -45,7 +45,7 @@ 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",
|
||||
@ -73,8 +73,8 @@ Image index won't be pushed, meaning that other manifests, including attestation
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunPush performs a push against the engine based on the specified options
|
||||
func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error {
|
||||
// runPush performs a push against the engine based on the specified options.
|
||||
func runPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error {
|
||||
var platform *ocispec.Platform
|
||||
out := tui.NewOutput(dockerCli.Out())
|
||||
if opts.platform != "" {
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -29,7 +30,7 @@ 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",
|
||||
@ -47,16 +48,8 @@ func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunSave performs a save against the engine based on the specified options
|
||||
func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
|
||||
if err := command.ValidateOutputPath(opts.output); err != nil {
|
||||
return errors.Wrap(err, "failed to save image")
|
||||
}
|
||||
|
||||
// runSave performs a save against the engine based on the specified options
|
||||
func runSave(ctx context.Context, dockerCLI command.Cli, opts saveOptions) error {
|
||||
var options []client.ImageSaveOption
|
||||
if opts.platform != "" {
|
||||
p, err := platforms.Parse(opts.platform)
|
||||
@ -67,16 +60,27 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
|
||||
options = append(options, client.ImageSaveWithPlatforms(p))
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options...)
|
||||
var output io.Writer
|
||||
if opts.output == "" {
|
||||
if dockerCLI.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
output = dockerCLI.Out()
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to save image")
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
}
|
||||
|
||||
responseBody, err := dockerCLI.Client().ImageSave(ctx, opts.images, options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer responseBody.Close()
|
||||
|
||||
if opts.output == "" {
|
||||
_, err := io.Copy(dockerCli.Out(), responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
return command.CopyToFile(opts.output, responseBody)
|
||||
_, err = io.Copy(output, responseBody)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -44,12 +44,12 @@ func TestNewSaveCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "output directory does not exist",
|
||||
args: []string{"-o", "fakedir/out.tar", "arg1"},
|
||||
expectedError: "failed to save image: invalid output path: directory \"fakedir\" does not exist",
|
||||
expectedError: `failed to save image: invalid output path: stat fakedir: no such file or directory`,
|
||||
},
|
||||
{
|
||||
name: "output file is irregular",
|
||||
args: []string{"-o", "/dev/null", "arg1"},
|
||||
expectedError: "failed to save image: invalid output path: \"/dev/null\" must be a directory or a regular file",
|
||||
expectedError: `failed to save image: cannot write to a character device file`,
|
||||
},
|
||||
{
|
||||
name: "invalid platform",
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.22
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@ -38,6 +42,9 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !opts.all {
|
||||
images = slices.DeleteFunc(images, isDangling)
|
||||
}
|
||||
|
||||
view := treeView{
|
||||
images: make([]topImage, 0, len(images)),
|
||||
@ -54,6 +61,8 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||
var totalContent int64
|
||||
children := make([]subImage, 0, len(img.Manifests))
|
||||
for _, im := range img.Manifests {
|
||||
totalContent += im.Size.Content
|
||||
|
||||
if im.Kind == imagetypes.ManifestKindAttestation {
|
||||
attested[im.AttestationData.For] = true
|
||||
continue
|
||||
@ -78,7 +87,6 @@ func runTree(ctx context.Context, dockerCLI command.Cli, opts treeOptions) error
|
||||
details.InUse = true
|
||||
}
|
||||
|
||||
totalContent += im.Size.Content
|
||||
children = append(children, sub)
|
||||
|
||||
// Add extra spacing between images if there's at least one entry with children.
|
||||
|
||||
@ -41,34 +41,13 @@ func newNotaryClient(cli command.Streams, imgRefAndAuth trust.ImageRefAndAuth) (
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), command.UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), "pull")
|
||||
}
|
||||
|
||||
// TrustedPush handles content trust pushing of an image.
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func TrustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, options image.PushOptions) error {
|
||||
responseBody, err := cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer responseBody.Close()
|
||||
|
||||
return trust.PushTrustedReference(ctx, cli, repoInfo, ref, authConfig, responseBody, command.UserAgent())
|
||||
}
|
||||
|
||||
// PushTrustedReference pushes a canonical reference to the trust server.
|
||||
//
|
||||
// Deprecated: use [trust.PushTrustedReference] instead. this function was only used internally and will be removed in the next release.
|
||||
func PushTrustedReference(ctx context.Context, ioStreams command.Streams, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig registrytypes.AuthConfig, in io.Reader) error {
|
||||
return pushTrustedReference(ctx, ioStreams, repoInfo, ref, authConfig, in)
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
// trustedPull handles content trust pulling of an image
|
||||
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
|
||||
func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts pullOptions) error {
|
||||
refs, err := getTrustedPullTargets(cli, imgRefAndAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -90,7 +69,7 @@ func trustedPull(ctx context.Context, cli command.Cli, imgRefAndAuth trust.Image
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, PullOptions{
|
||||
if err := imagePullPrivileged(ctx, cli, updatedImgRefAndAuth, pullOptions{
|
||||
all: false,
|
||||
platform: opts.platform,
|
||||
quiet: opts.quiet,
|
||||
@ -165,7 +144,7 @@ func getTrustedPullTargets(cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth)
|
||||
}
|
||||
|
||||
// imagePullPrivileged pulls the image and displays it to the output
|
||||
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts PullOptions) error {
|
||||
func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth trust.ImageRefAndAuth, opts pullOptions) error {
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(*imgRefAndAuth.AuthConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -229,15 +208,6 @@ func convertTarget(t client.Target) (target, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TagTrusted tags a trusted ref. It is a shallow wrapper around APIClient.ImageTag
|
||||
// that updates the given image references to their familiar format for tagging
|
||||
// and printing.
|
||||
//
|
||||
// Deprecated: this function was only used internally, and will be removed in the next release.
|
||||
func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
|
||||
return trust.TagTrusted(ctx, cli.Client(), cli.Err(), trustedRef, ref)
|
||||
}
|
||||
|
||||
// AuthResolver returns an auth resolver function from a command.Cli
|
||||
func AuthResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
return func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
@ -52,12 +53,12 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !r {
|
||||
return "", errdefs.Cancelled(errors.New("network prune cancelled has been cancelled"))
|
||||
return "", errdefs.Cancelled(errors.New("network prune has been cancelled"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ 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/docker/api/types/network"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/spf13/cobra"
|
||||
@ -49,7 +50,7 @@ func runRemove(ctx context.Context, dockerCLI command.Cli, networks []string, op
|
||||
for _, name := range networks {
|
||||
nw, _, err := apiClient.NetworkInspectWithRaw(ctx, name, network.InspectOptions{})
|
||||
if err == nil && nw.Ingress {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), ingressWarning)
|
||||
r, err := prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), ingressWarning)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
// Set DOCKER_COMPLETION_SHOW_NODE_IDS=yes to also complete IDs.
|
||||
//
|
||||
// TODO(thaJeztah): add support for filters.
|
||||
func completeNodeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
// https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43
|
||||
showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_NODE_IDS") == "yes"
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
@ -12,7 +12,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@ -99,14 +100,14 @@ func runCreate(ctx context.Context, dockerCli command.Cli, options pluginCreateO
|
||||
return err
|
||||
}
|
||||
|
||||
compression := archive.Uncompressed
|
||||
comp := compression.None
|
||||
if options.compress {
|
||||
logrus.Debugf("compression enabled")
|
||||
compression = archive.Gzip
|
||||
comp = compression.Gzip
|
||||
}
|
||||
|
||||
createCtx, err := archive.TarWithOptions(absContextDir, &archive.TarOptions{
|
||||
Compression: compression,
|
||||
Compression: comp,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
@ -133,12 +134,12 @@ func runInstall(ctx context.Context, dockerCLI command.Cli, opts pluginOptions)
|
||||
return nil
|
||||
}
|
||||
|
||||
func acceptPrivileges(dockerCLI command.Cli, name string) func(ctx context.Context, privileges types.PluginPrivileges) (bool, error) {
|
||||
func acceptPrivileges(dockerCLI command.Streams, name string) func(ctx context.Context, privileges types.PluginPrivileges) (bool, error) {
|
||||
return func(ctx context.Context, privileges types.PluginPrivileges) (bool, error) {
|
||||
_, _ = fmt.Fprintf(dockerCLI.Out(), "Plugin %q is requesting the following privileges:\n", name)
|
||||
for _, privilege := range privileges {
|
||||
_, _ = fmt.Fprintf(dockerCLI.Out(), " - %s: %v\n", privilege.Name, privilege.Value)
|
||||
}
|
||||
return command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), "Do you grant the above permissions?")
|
||||
return prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), "Do you grant the above permissions?")
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -64,7 +65,7 @@ func runUpgrade(ctx context.Context, dockerCLI command.Cli, opts pluginOptions)
|
||||
|
||||
_, _ = fmt.Fprintf(dockerCLI.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote))
|
||||
if !opts.skipRemoteCheck && remote.String() != old.String() {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), "Plugin images do not match, are you sure?")
|
||||
r, err := prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), "Plugin images do not match, are you sure?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -13,9 +13,9 @@ import (
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/hints"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -28,16 +28,22 @@ const (
|
||||
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
|
||||
)
|
||||
|
||||
// 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/"
|
||||
|
||||
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
||||
// for the given command.
|
||||
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
|
||||
configKey := getAuthConfigKey(index.Name)
|
||||
isDefaultRegistry := configKey == authConfigKey || index.Official
|
||||
return func(ctx context.Context) (string, error) {
|
||||
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
|
||||
indexServer := registry.GetAuthConfigKey(index)
|
||||
isDefaultRegistry := indexServer == registry.IndexServer
|
||||
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
|
||||
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
|
||||
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", authConfigKey, err)
|
||||
}
|
||||
|
||||
select {
|
||||
@ -46,7 +52,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||
default:
|
||||
}
|
||||
|
||||
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, indexServer)
|
||||
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, authConfigKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -63,7 +69,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
|
||||
func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
||||
configKey := index.Name
|
||||
if index.Official {
|
||||
configKey = registry.IndexServer
|
||||
configKey = authConfigKey
|
||||
}
|
||||
|
||||
a, _ := cfg.GetAuthConfig(configKey)
|
||||
@ -132,7 +138,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
|
||||
argUser = strings.TrimSpace(argUser)
|
||||
if argUser == "" {
|
||||
if serverAddress == registry.IndexServer {
|
||||
if serverAddress == authConfigKey {
|
||||
// When signing in to the default (Docker Hub) registry, we display
|
||||
// hints for creating an account, and (if hints are enabled), using
|
||||
// a token instead of a password.
|
||||
@ -143,16 +149,16 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
}
|
||||
}
|
||||
|
||||
var prompt string
|
||||
var msg string
|
||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||
if defaultUsername == "" {
|
||||
prompt = "Username: "
|
||||
msg = "Username: "
|
||||
} else {
|
||||
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
|
||||
msg = fmt.Sprintf("Username (%s): ", defaultUsername)
|
||||
}
|
||||
|
||||
var err error
|
||||
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
|
||||
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@ -166,7 +172,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
|
||||
argPassword = strings.TrimSpace(argPassword)
|
||||
if argPassword == "" {
|
||||
restoreInput, err := DisableInputEcho(cli.In())
|
||||
restoreInput, err := prompt.DisableInputEcho(cli.In())
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@ -183,7 +189,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
out := tui.NewOutput(cli.Err())
|
||||
out.PrintNote("A Personal Access Token (PAT) can be used instead.\n" +
|
||||
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
|
||||
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@ -225,9 +231,25 @@ func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (regis
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
|
||||
configKey := getAuthConfigKey(reference.Domain(registryRef))
|
||||
a, err := cfg.GetAuthConfig(configKey)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
return ResolveAuthConfig(cfg, repoInfo.Index), nil
|
||||
return registrytypes.AuthConfig(a), nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
func getAuthConfigKey(domainName string) string {
|
||||
if domainName == "docker.io" || domainName == "index.docker.io" {
|
||||
return authConfigKey
|
||||
}
|
||||
return domainName
|
||||
}
|
||||
|
||||
@ -263,13 +263,13 @@ func loginClientSide(ctx context.Context, auth registrytypes.AuthConfig) (*regis
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
||||
_, token, err := svc.Auth(ctx, &auth, command.UserAgent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ®istrytypes.AuthenticateOKBody{
|
||||
Status: status,
|
||||
Status: "Login Succeeded",
|
||||
IdentityToken: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/docker/cli/cli/command"
|
||||
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/test"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
@ -492,7 +492,7 @@ func TestLoginTermination(t *testing.T) {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timed out after 1 second. `runLogin` did not return")
|
||||
case err := <-runErr:
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
assert.ErrorIs(t, err, prompt.ErrTerminated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -80,3 +82,113 @@ func TestGetDefaultAuthConfig_HelperError(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(expectedAuthConfig, authconfig))
|
||||
assert.Check(t, is.ErrorContains(err, "docker-credential-fake-does-not-exist"))
|
||||
}
|
||||
|
||||
func TestRetrieveAuthTokenFromImage(t *testing.T) {
|
||||
// configFileContent contains a plain-text "username:password", as stored by
|
||||
// the plain-text store;
|
||||
// https://github.com/docker/cli/blob/v28.0.4/cli/config/configfile/file.go#L218-L229
|
||||
const configFileContent = `{"auths": {
|
||||
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"[::1]:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"127.0.0.1:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"localhost:5000": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry-1.docker.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
|
||||
"registry.hub.docker.com": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
|
||||
}
|
||||
}`
|
||||
cfg := configfile.ConfigFile{}
|
||||
err := cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent)))
|
||||
assert.NilError(t, err)
|
||||
|
||||
remoteRefs := []string{
|
||||
"ubuntu",
|
||||
"ubuntu:latest",
|
||||
"ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu",
|
||||
"library/ubuntu:latest",
|
||||
"library/ubuntu:latest@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
"library/ubuntu@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
prefix string
|
||||
expectedAddress string
|
||||
expectedAuthCfg registry.AuthConfig
|
||||
}{
|
||||
{
|
||||
prefix: "",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
prefix: "index.docker.io",
|
||||
expectedAddress: "https://index.docker.io/v1/",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "https://index.docker.io/v1/"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry-1.docker.io (the actual registry) is the odd one out, and is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry-1.docker.io",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry-1.docker.io"},
|
||||
},
|
||||
{
|
||||
// FIXME(thaJeztah): registry.hub.docker.com is stored separate from other URLs used for docker hub's registry
|
||||
prefix: "registry.hub.docker.com",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "registry.hub.docker.com"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]",
|
||||
expectedAddress: "[::1]",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]"},
|
||||
},
|
||||
{
|
||||
prefix: "[::1]:5000",
|
||||
expectedAddress: "[::1]:5000",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "[::1]:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "127.0.0.1",
|
||||
expectedAddress: "127.0.0.1",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "127.0.0.1"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost",
|
||||
expectedAddress: "localhost",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost"},
|
||||
},
|
||||
{
|
||||
prefix: "localhost:5000",
|
||||
expectedAddress: "localhost:5000",
|
||||
expectedAuthCfg: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "localhost:5000"},
|
||||
},
|
||||
{
|
||||
prefix: "no-auth.example.com",
|
||||
expectedAuthCfg: registry.AuthConfig{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tcName := tc.prefix
|
||||
if tc.prefix == "" {
|
||||
tcName = "no-prefix"
|
||||
}
|
||||
t.Run(tcName, func(t *testing.T) {
|
||||
for _, remoteRef := range remoteRefs {
|
||||
imageRef := path.Join(tc.prefix, remoteRef)
|
||||
actual, err := command.RetrieveAuthTokenFromImage(&cfg, imageRef)
|
||||
assert.NilError(t, err)
|
||||
ac, err := registry.DecodeAuthConfig(actual)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.DeepEqual(*ac, tc.expectedAuthCfg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ func NewSecretCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm secrets
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().SecretList(cmd.Context(), types.SecretListOptions{})
|
||||
if err != nil {
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"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/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -35,27 +31,3 @@ func NewServiceCommand(dockerCli command.Cli) *cobra.Command {
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CompletionFn offers completion for swarm service names and optional IDs.
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_SERVICE_IDS=yes to also complete IDs.
|
||||
func CompletionFn(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
// https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43
|
||||
showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_SERVICE_IDS") == "yes"
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ServiceList(cmd.Context(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(list))
|
||||
for _, service := range list {
|
||||
if showIDs {
|
||||
names = append(names, service.Spec.Name, service.ID)
|
||||
} else {
|
||||
names = append(names, service.Spec.Name)
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
33
cli/command/service/completion.go
Normal file
33
cli/command/service/completion.go
Normal file
@ -0,0 +1,33 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// completeServiceNames offers completion for swarm service names and optional IDs.
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_SERVICE_IDS=yes to also complete IDs.
|
||||
func completeServiceNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
// https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43
|
||||
showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_SERVICE_IDS") == "yes"
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ServiceList(cmd.Context(), types.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(list))
|
||||
for _, service := range list {
|
||||
if showIDs {
|
||||
names = append(names, service.Spec.Name, service.ID)
|
||||
} else {
|
||||
names = append(names, service.Spec.Name)
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@ -41,9 +41,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
return runInspect(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -51,10 +51,8 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
opts.target = args[0]
|
||||
return runLogs(cmd.Context(), dockerCli, &opts)
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.29"},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.29"},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -39,9 +39,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options.services = args
|
||||
return runPS(cmd.Context(), dockerCli, options)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli),
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
|
||||
@ -19,9 +19,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCli, args)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli),
|
||||
}
|
||||
cmd.Flags()
|
||||
|
||||
|
||||
@ -23,10 +23,8 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRollback(cmd.Context(), dockerCli, options, args[0])
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
ValidArgsFunction: completeServiceNames(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -29,9 +29,7 @@ func newScaleCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runScale(cmd.Context(), dockerCli, options, args)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCli)(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completeScaleArgs(dockerCli),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -117,3 +115,19 @@ func runServiceScale(ctx context.Context, apiClient client.ServiceAPIClient, ser
|
||||
}
|
||||
return response.Warnings, nil
|
||||
}
|
||||
|
||||
// completeScaleArgs returns a completion function for the args of the scale command.
|
||||
// It completes service names followed by "=", suppressing the trailing space.
|
||||
func completeScaleArgs(dockerCli command.Cli) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// reuse the existing logic for configurable completion of service names and IDs.
|
||||
completions, directive := completeServiceNames(dockerCli)(cmd, args, toComplete)
|
||||
if directive == cobra.ShellCompDirectiveError {
|
||||
return completions, directive
|
||||
}
|
||||
for i, v := range completions {
|
||||
completions[i] = v + "="
|
||||
}
|
||||
return completions, directive | cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,9 +35,7 @@ func newUpdateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), options, args[0])
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return CompletionFn(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completeServiceNames(dockerCLI),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
@ -46,7 +46,7 @@ func NewStackCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// completeNames offers completion for swarm stacks
|
||||
func completeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := swarm.GetStacks(cmd.Context(), dockerCLI.Client())
|
||||
if err != nil {
|
||||
|
||||
@ -85,7 +85,7 @@ var (
|
||||
)
|
||||
|
||||
// completeEventFilters provides completion for the filters that can be used with `--filter`.
|
||||
func completeEventFilters(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
|
||||
func completeEventFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@ -19,9 +18,11 @@ import (
|
||||
"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/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"
|
||||
@ -97,46 +98,67 @@ func runInfo(ctx context.Context, cmd *cobra.Command, dockerCli command.Cli, opt
|
||||
info.ClientErrors = append(info.ClientErrors, err.Error())
|
||||
}
|
||||
|
||||
var serverConnErr error
|
||||
if needsServerInfo(opts.format, info) {
|
||||
if dinfo, err := dockerCli.Client().Info(ctx); err == nil {
|
||||
info.Info = &dinfo
|
||||
} else {
|
||||
info.ServerErrors = append(info.ServerErrors, err.Error())
|
||||
if opts.format == "" {
|
||||
// reset the server info to prevent printing "empty" Server info
|
||||
// and warnings, but don't reset it if a custom format was specified
|
||||
// to prevent errors from Go's template parsing during format.
|
||||
info.Info = nil
|
||||
} else {
|
||||
// if a format is provided, print the error, as it may be hidden
|
||||
// otherwise if the template doesn't include the ServerErrors field.
|
||||
fprintln(dockerCli.Err(), err)
|
||||
}
|
||||
}
|
||||
serverConnErr = addServerInfo(ctx, dockerCli, opts.format, &info)
|
||||
}
|
||||
|
||||
if opts.format == "" {
|
||||
info.UserName = dockerCli.ConfigFile().AuthConfigs[registry.IndexServer].Username
|
||||
info.ClientInfo.APIVersion = dockerCli.CurrentVersion()
|
||||
return prettyPrintInfo(dockerCli, info)
|
||||
return errors.Join(prettyPrintInfo(dockerCli, info), serverConnErr)
|
||||
}
|
||||
return formatInfo(dockerCli.Out(), info, opts.format)
|
||||
|
||||
return errors.Join(serverConnErr, formatInfo(dockerCli.Out(), info, opts.format))
|
||||
}
|
||||
|
||||
// addServerInfo retrieves the server information and adds it to the dockerInfo struct.
|
||||
// if a connection error occurs, it will be returned as an error.
|
||||
// other errors are appended to the info.ServerErrors field.
|
||||
func addServerInfo(ctx context.Context, dockerCli command.Cli, format string, info *dockerInfo) error {
|
||||
dinfo, err := dockerCli.Client().Info(ctx)
|
||||
if err != nil {
|
||||
// if no format is provided and we have an error, don't print the server info
|
||||
if format == "" {
|
||||
info.Info = nil
|
||||
}
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
info.ServerErrors = append(info.ServerErrors, err.Error())
|
||||
if format != "" {
|
||||
// if a format is provided, print the error, as it may be hidden
|
||||
// if the format template doesn't include the ServerErrors field.
|
||||
fprintln(dockerCli.Err(), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// on a server connection error, we want to return an error
|
||||
return err
|
||||
}
|
||||
|
||||
// only assign the server info if we have no error
|
||||
info.Info = &dinfo
|
||||
return nil
|
||||
}
|
||||
|
||||
// placeHolders does a rudimentary match for possible placeholders in a
|
||||
// template, matching a '.', followed by an letter (a-z/A-Z).
|
||||
var placeHolders = regexp.MustCompile(`\.[a-zA-Z]`)
|
||||
var placeHolders = lazyregexp.New(`\.[a-zA-Z]`)
|
||||
|
||||
// needsServerInfo detects if the given template uses any server information.
|
||||
// If only client-side information is used in the template, we can skip
|
||||
// connecting to the daemon. This allows (e.g.) to only get cli-plugin
|
||||
// information, without also making a (potentially expensive) API call.
|
||||
func needsServerInfo(template string, info dockerInfo) bool {
|
||||
if len(template) == 0 || placeHolders.FindString(template) == "" {
|
||||
// The template is empty, or does not contain formatting fields
|
||||
// (e.g. `table` or `raw` or `{{ json .}}`). Assume we need server-side
|
||||
// information to render it.
|
||||
switch template {
|
||||
case "", formatter.JSONFormat, formatter.JSONFormatKey:
|
||||
// No format (default) and full JSON output need server-side info.
|
||||
return true
|
||||
default:
|
||||
if placeHolders.FindString(template) == "" {
|
||||
// The template does not contain formatting fields; assume we
|
||||
// need server-side information to render it, and return early.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// A template is provided and has at least one field set.
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
@ -77,7 +78,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
return errors.New(`ERROR: The "until" filter is not supported with "--volumes"`)
|
||||
}
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), confirmationMessage(dockerCli, options))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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}}
|
||||
|
||||
@ -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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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}}
|
||||
|
||||
@ -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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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}}
|
||||
|
||||
@ -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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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}}
|
||||
|
||||
@ -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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":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,"BridgeNfIptables":false,"BridgeNfIp6tables":false,"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}}
|
||||
|
||||
@ -5,12 +5,12 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary"
|
||||
@ -41,7 +41,7 @@ func newKeyGenerateCommand(dockerCli command.Streams) *cobra.Command {
|
||||
}
|
||||
|
||||
// key names can use lowercase alphanumeric + _ + - characters
|
||||
var validKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
var validKeyName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
|
||||
// validate that all of the key names are unique and are alphanumeric + _ + -
|
||||
// and that we do not already have public key files in the target dir on disk
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"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/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -44,7 +45,7 @@ func revokeTrust(ctx context.Context, dockerCLI command.Cli, remote string, opti
|
||||
return errors.New("cannot use a digest reference for IMAGE:TAG")
|
||||
}
|
||||
if imgRefAndAuth.Tag() == "" && !options.forceYes {
|
||||
deleteRemote, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Confirm you would like to delete all signature data for %s?", remote))
|
||||
deleteRemote, err := prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), fmt.Sprintf("Confirm you would like to delete all signature data for %s?", remote))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -6,13 +6,13 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -45,7 +45,7 @@ func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
var validSignerName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
||||
|
||||
func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error {
|
||||
signerName := options.signer
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
@ -82,11 +83,7 @@ func maybePromptForSignerRemoval(ctx context.Context, dockerCLI command.Cli, rep
|
||||
"Are you sure you want to continue?",
|
||||
signerName, repoName, repoName,
|
||||
)
|
||||
removeSigner, err := command.PromptForConfirmation(ctx, dockerCLI.In(), dockerCLI.Out(), message)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return removeSigner, nil
|
||||
return prompt.Confirm(ctx, dockerCLI.In(), dockerCLI.Out(), message)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@ -4,67 +4,42 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/moby/term"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// CopyToFile writes the content of the reader to the specified file
|
||||
//
|
||||
// Deprecated: use [atomicwriter.New].
|
||||
func CopyToFile(outfile string, r io.Reader) error {
|
||||
// We use sequential file access here to avoid depleting the standby list
|
||||
// on Windows. On Linux, this is a call directly to os.CreateTemp
|
||||
tmpFile, err := sequential.CreateTemp(filepath.Dir(outfile), ".docker_temp_")
|
||||
writer, err := atomicwriter.New(outfile, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpPath := tmpFile.Name()
|
||||
|
||||
_, err = io.Copy(tmpFile, r)
|
||||
tmpFile.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = os.Rename(tmpPath, outfile); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
defer writer.Close()
|
||||
_, err = io.Copy(writer, r)
|
||||
return err
|
||||
}
|
||||
|
||||
var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))
|
||||
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.
|
||||
func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
|
||||
oldState, err := term.SaveState(ins.FD())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
restore = func() error {
|
||||
return term.RestoreTerminal(ins.FD(), oldState)
|
||||
}
|
||||
return restore, term.DisableEcho(ins.FD(), oldState)
|
||||
return prompt.DisableInputEcho(ins)
|
||||
}
|
||||
|
||||
// PromptForInput requests input from the user.
|
||||
@ -75,23 +50,7 @@ func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
|
||||
// the stack and close the io.Reader used for the prompt which will prevent the
|
||||
// background goroutine from blocking indefinitely.
|
||||
func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) {
|
||||
_, _ = fmt.Fprint(out, message)
|
||||
|
||||
result := make(chan string)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(in)
|
||||
if scanner.Scan() {
|
||||
result <- strings.TrimSpace(scanner.Text())
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_, _ = fmt.Fprintln(out, "")
|
||||
return "", ErrPromptTerminated
|
||||
case r := <-result:
|
||||
return r, nil
|
||||
}
|
||||
return prompt.ReadInput(ctx, in, out, message)
|
||||
}
|
||||
|
||||
// PromptForConfirmation requests and checks confirmation from the user.
|
||||
@ -105,68 +64,45 @@ func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message st
|
||||
// the stack and close the io.Reader used for the prompt which will prevent the
|
||||
// background goroutine from blocking indefinitely.
|
||||
func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) {
|
||||
if message == "" {
|
||||
message = "Are you sure you want to proceed?"
|
||||
}
|
||||
message += " [y/N] "
|
||||
|
||||
_, _ = fmt.Fprint(outs, message)
|
||||
|
||||
// On Windows, force the use of the regular OS stdin stream.
|
||||
if runtime.GOOS == "windows" {
|
||||
ins = streams.NewIn(os.Stdin)
|
||||
}
|
||||
|
||||
result := make(chan bool)
|
||||
|
||||
go func() {
|
||||
var res bool
|
||||
scanner := bufio.NewScanner(ins)
|
||||
if scanner.Scan() {
|
||||
answer := strings.TrimSpace(scanner.Text())
|
||||
if strings.EqualFold(answer, "y") {
|
||||
res = true
|
||||
}
|
||||
}
|
||||
result <- res
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_, _ = fmt.Fprintln(outs, "")
|
||||
return false, ErrPromptTerminated
|
||||
case r := <-result:
|
||||
return r, nil
|
||||
}
|
||||
return prompt.Confirm(ctx, ins, outs, message)
|
||||
}
|
||||
|
||||
// PruneFilters returns consolidated prune filters obtained from config.json and cli
|
||||
// PruneFilters merges prune filters specified in config.json with those specified
|
||||
// as command-line flags.
|
||||
//
|
||||
// CLI label filters have precedence over those specified in config.json. If a
|
||||
// label filter specified as flag conflicts with a label defined in config.json
|
||||
// (i.e., "label=some-value" conflicts with "label!=some-value", and vice versa),
|
||||
// then the filter defined in config.json is omitted.
|
||||
func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.Args {
|
||||
cfg := dockerCLI.ConfigFile()
|
||||
if cfg == nil {
|
||||
return pruneFilters
|
||||
}
|
||||
|
||||
// Merge filters provided through the CLI with default filters defined
|
||||
// in the CLI-configfile.
|
||||
for _, f := range cfg.PruneFilters {
|
||||
k, v, ok := strings.Cut(f, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if k == "label" {
|
||||
// CLI label filter supersede config.json.
|
||||
// If CLI label filter conflict with config.json,
|
||||
// skip adding label! filter in config.json.
|
||||
if pruneFilters.Contains("label!") && pruneFilters.ExactMatch("label!", v) {
|
||||
switch k {
|
||||
case "label":
|
||||
// "label != some-value" conflicts with "label = some-value"
|
||||
if pruneFilters.ExactMatch("label!", v) {
|
||||
continue
|
||||
}
|
||||
} else if k == "label!" {
|
||||
// CLI label! filter supersede config.json.
|
||||
// If CLI label! filter conflict with config.json,
|
||||
// skip adding label filter in config.json.
|
||||
if pruneFilters.Contains("label") && pruneFilters.ExactMatch("label", v) {
|
||||
pruneFilters.Add(k, v)
|
||||
case "label!":
|
||||
// "label != some-value" conflicts with "label = some-value"
|
||||
if pruneFilters.ExactMatch("label", v) {
|
||||
continue
|
||||
}
|
||||
pruneFilters.Add(k, v)
|
||||
default:
|
||||
pruneFilters.Add(k, v)
|
||||
}
|
||||
pruneFilters.Add(k, v)
|
||||
}
|
||||
|
||||
return pruneFilters
|
||||
@ -178,7 +114,7 @@ func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
|
||||
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
|
||||
}
|
||||
|
||||
// ValidateOutputPath validates the output paths of the `export` and `save` commands.
|
||||
// ValidateOutputPath validates the output paths of the "docker cp" command.
|
||||
func ValidateOutputPath(path string) error {
|
||||
dir := filepath.Dir(filepath.Clean(path))
|
||||
if dir != "" && dir != "." {
|
||||
@ -204,8 +140,8 @@ func ValidateOutputPath(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateOutputPathFileMode validates the output paths of the `cp` command and serves as a
|
||||
// helper to `ValidateOutputPath`
|
||||
// ValidateOutputPathFileMode validates the output paths of the "docker cp" command
|
||||
// and serves as a helper to [ValidateOutputPath]
|
||||
func ValidateOutputPathFileMode(fileMode os.FileMode) error {
|
||||
switch {
|
||||
case fileMode&os.ModeDevice != 0:
|
||||
|
||||
@ -1,23 +1,12 @@
|
||||
package command_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@ -54,171 +43,3 @@ func TestValidateOutputPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptForInput(t *testing.T) {
|
||||
t.Run("cancelling the context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
reader, _ := io.Pipe()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
wroteHook := make(chan struct{}, 1)
|
||||
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
wroteHook <- struct{}{}
|
||||
})
|
||||
|
||||
promptErr := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := command.PromptForInput(ctx, streams.NewIn(reader), streams.NewOut(promptOut), "Enter something")
|
||||
promptErr <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout waiting for prompt to write to buffer")
|
||||
case <-wroteHook:
|
||||
cancel()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("timeout waiting for prompt to be canceled")
|
||||
case err := <-promptErr:
|
||||
assert.ErrorIs(t, err, command.ErrPromptTerminated)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user input should be properly trimmed", func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
wroteHook := make(chan struct{}, 1)
|
||||
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
wroteHook <- struct{}{}
|
||||
})
|
||||
|
||||
go func() {
|
||||
<-wroteHook
|
||||
writer.Write([]byte(" foo \n"))
|
||||
}()
|
||||
|
||||
answer, err := command.PromptForInput(ctx, streams.NewIn(reader), streams.NewOut(promptOut), "Enter something")
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, answer, "foo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPromptForConfirmation(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
type promptResult struct {
|
||||
result bool
|
||||
err error
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
bufioWriter := bufio.NewWriter(buf)
|
||||
|
||||
var (
|
||||
promptWriter *io.PipeWriter
|
||||
promptReader *io.PipeReader
|
||||
)
|
||||
|
||||
defer func() {
|
||||
if promptWriter != nil {
|
||||
promptWriter.Close()
|
||||
}
|
||||
if promptReader != nil {
|
||||
promptReader.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
f func() error
|
||||
expected promptResult
|
||||
}{
|
||||
{"SIGINT", func() error {
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||
return nil
|
||||
}, promptResult{false, command.ErrPromptTerminated}},
|
||||
{"no", func() error {
|
||||
_, err := fmt.Fprintln(promptWriter, "n")
|
||||
return err
|
||||
}, promptResult{false, nil}},
|
||||
{"yes", func() error {
|
||||
_, err := fmt.Fprintln(promptWriter, "y")
|
||||
return err
|
||||
}, promptResult{true, nil}},
|
||||
{"any", func() error {
|
||||
_, err := fmt.Fprintln(promptWriter, "a")
|
||||
return err
|
||||
}, promptResult{false, nil}},
|
||||
{"with space", func() error {
|
||||
_, err := fmt.Fprintln(promptWriter, " y")
|
||||
return err
|
||||
}, promptResult{true, nil}},
|
||||
{"reader closed", func() error {
|
||||
return promptReader.Close()
|
||||
}, promptResult{false, nil}},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
notifyCtx, notifyCancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
|
||||
t.Cleanup(notifyCancel)
|
||||
|
||||
buf.Reset()
|
||||
promptReader, promptWriter = io.Pipe()
|
||||
|
||||
wroteHook := make(chan struct{}, 1)
|
||||
promptOut := test.NewWriterWithHook(bufioWriter, func(p []byte) {
|
||||
wroteHook <- struct{}{}
|
||||
})
|
||||
|
||||
result := make(chan promptResult, 1)
|
||||
go func() {
|
||||
r, err := command.PromptForConfirmation(notifyCtx, promptReader, promptOut, "")
|
||||
result <- promptResult{r, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-wroteHook:
|
||||
}
|
||||
|
||||
assert.NilError(t, bufioWriter.Flush())
|
||||
assert.Equal(t, strings.TrimSpace(buf.String()), "Are you sure you want to proceed? [y/N]")
|
||||
|
||||
// wait for the Prompt to write to the buffer
|
||||
drainChannel(ctx, wroteHook)
|
||||
|
||||
assert.NilError(t, tc.f())
|
||||
|
||||
select {
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
t.Fatal("timeout waiting for prompt result")
|
||||
case r := <-result:
|
||||
assert.Equal(t, r, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func drainChannel(ctx context.Context, ch <-chan struct{}) {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ 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"
|
||||
"github.com/docker/docker/errdefs"
|
||||
@ -77,7 +78,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
warning = allVolumesWarning
|
||||
}
|
||||
if !options.force {
|
||||
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -14,11 +16,21 @@ const (
|
||||
subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
|
||||
)
|
||||
|
||||
var defaultPattern = regexp.MustCompile(fmt.Sprintf(
|
||||
var defaultPattern = lazyregexp.New(fmt.Sprintf(
|
||||
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
|
||||
delimiter, delimiter, subst, subst,
|
||||
))
|
||||
|
||||
// regexper is an internal interface to allow passing a [lazyregexp.Regexp]
|
||||
// in places where a custom ("regular") [regexp.Regexp] is accepted. It defines
|
||||
// only the methods we currently use.
|
||||
type regexper interface {
|
||||
FindAllStringSubmatch(s string, n int) [][]string
|
||||
FindStringSubmatch(s string) []string
|
||||
ReplaceAllStringFunc(src string, repl func(string) string) string
|
||||
SubexpNames() []string
|
||||
}
|
||||
|
||||
// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
|
||||
var DefaultSubstituteFuncs = []SubstituteFunc{
|
||||
softDefault,
|
||||
@ -51,10 +63,16 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
|
||||
// SubstituteWith substitutes variables in the string with their values.
|
||||
// It accepts additional substitute function.
|
||||
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
|
||||
return substituteWith(template, mapping, pattern, subsFuncs...)
|
||||
}
|
||||
|
||||
// SubstituteWith substitutes variables in the string with their values.
|
||||
// It accepts additional substitute function.
|
||||
func substituteWith(template string, mapping Mapping, pattern regexper, subsFuncs ...SubstituteFunc) (string, error) {
|
||||
var err error
|
||||
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
|
||||
matches := pattern.FindStringSubmatch(substring)
|
||||
groups := matchGroups(matches, pattern)
|
||||
groups := matchGroups(matches, defaultPattern)
|
||||
if escaped := groups["escaped"]; escaped != "" {
|
||||
return escaped
|
||||
}
|
||||
@ -93,38 +111,42 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
|
||||
|
||||
// Substitute variables in the string with their values
|
||||
func Substitute(template string, mapping Mapping) (string, error) {
|
||||
return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
|
||||
return substituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
|
||||
}
|
||||
|
||||
// ExtractVariables returns a map of all the variables defined in the specified
|
||||
// composefile (dict representation) and their default value if any.
|
||||
func ExtractVariables(configDict map[string]any, pattern *regexp.Regexp) map[string]string {
|
||||
return extractVariables(configDict, pattern)
|
||||
}
|
||||
|
||||
func extractVariables(configDict map[string]any, pattern regexper) map[string]string {
|
||||
if pattern == nil {
|
||||
pattern = defaultPattern
|
||||
}
|
||||
return recurseExtract(configDict, pattern)
|
||||
}
|
||||
|
||||
func recurseExtract(value any, pattern *regexp.Regexp) map[string]string {
|
||||
func recurseExtract(value any, pattern regexper) map[string]string {
|
||||
m := map[string]string{}
|
||||
|
||||
switch value := value.(type) {
|
||||
switch val := value.(type) {
|
||||
case string:
|
||||
if values, is := extractVariable(value, pattern); is {
|
||||
if values, is := extractVariable(val, pattern); is {
|
||||
for _, v := range values {
|
||||
m[v.name] = v.value
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
for _, elem := range value {
|
||||
for _, elem := range val {
|
||||
submap := recurseExtract(elem, pattern)
|
||||
for key, value := range submap {
|
||||
m[key] = value
|
||||
for k, v := range submap {
|
||||
m[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
case []any:
|
||||
for _, elem := range value {
|
||||
for _, elem := range val {
|
||||
if values, is := extractVariable(elem, pattern); is {
|
||||
for _, v := range values {
|
||||
m[v.name] = v.value
|
||||
@ -141,7 +163,7 @@ type extractedValue struct {
|
||||
value string
|
||||
}
|
||||
|
||||
func extractVariable(value any, pattern *regexp.Regexp) ([]extractedValue, bool) {
|
||||
func extractVariable(value any, pattern regexper) ([]extractedValue, bool) {
|
||||
sValue, ok := value.(string)
|
||||
if !ok {
|
||||
return []extractedValue{}, false
|
||||
@ -227,7 +249,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
|
||||
func matchGroups(matches []string, pattern regexper) map[string]string {
|
||||
groups := make(map[string]string)
|
||||
for i, name := range pattern.SubexpNames()[1:] {
|
||||
groups[name] = matches[i+1]
|
||||
|
||||
@ -169,15 +169,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
|
||||
result, err := substituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("ok first", result))
|
||||
|
||||
result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
|
||||
result, err = substituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("ok ", result))
|
||||
|
||||
_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
|
||||
_, err = substituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
|
||||
assert.Check(t, is.ErrorContains(err, "required variable"))
|
||||
}
|
||||
|
||||
@ -278,7 +278,7 @@ func TestExtractVariables(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := ExtractVariables(tc.dict, defaultPattern)
|
||||
actual := extractVariables(tc.dict, defaultPattern)
|
||||
assert.Check(t, is.DeepEqual(actual, tc.expected))
|
||||
})
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ func resetConfigDir() {
|
||||
// getHomeDir is a copy of [pkg/homedir.Get] to prevent adding docker/docker
|
||||
// as dependency for consumers that only need to read the config-file.
|
||||
//
|
||||
// [pkg/homedir.Get]: https://pkg.go.dev/github.com/docker/docker@v26.1.4+incompatible/pkg/homedir#Get
|
||||
// [pkg/homedir.Get]: https://pkg.go.dev/github.com/docker/docker@v28.0.3+incompatible/pkg/homedir#Get
|
||||
func getHomeDir() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" && runtime.GOOS != "windows" {
|
||||
|
||||
@ -398,6 +398,32 @@ func TestLoadDefaultConfigFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// The CLI no longer disables/hides experimental CLI features, however, we need
|
||||
// to verify that existing configuration files do not break
|
||||
func TestLoadLegacyExperimental(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
configfile string
|
||||
}{
|
||||
{
|
||||
doc: "default",
|
||||
configfile: `{}`,
|
||||
},
|
||||
{
|
||||
doc: "experimental",
|
||||
configfile: `{
|
||||
"experimental": "enabled"
|
||||
}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
_, err := LoadFromReader(strings.NewReader(tc.configfile))
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPath(t *testing.T) {
|
||||
oldDir := Dir()
|
||||
|
||||
|
||||
@ -36,12 +36,14 @@ type ConfigFile struct {
|
||||
NodesFormat string `json:"nodesFormat,omitempty"`
|
||||
PruneFilters []string `json:"pruneFilters,omitempty"`
|
||||
Proxies map[string]ProxyConfig `json:"proxies,omitempty"`
|
||||
Experimental string `json:"experimental,omitempty"`
|
||||
CurrentContext string `json:"currentContext,omitempty"`
|
||||
CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"`
|
||||
Plugins map[string]map[string]string `json:"plugins,omitempty"`
|
||||
Aliases map[string]string `json:"aliases,omitempty"`
|
||||
Features map[string]string `json:"features,omitempty"`
|
||||
|
||||
// Deprecated: experimental CLI features are always enabled and this field is no longer used. Use [Features] instead for optional features. This field will be removed in a future release.
|
||||
Experimental string `json:"experimental,omitempty"`
|
||||
}
|
||||
|
||||
// ProxyConfig contains proxy configuration settings
|
||||
|
||||
@ -5,14 +5,14 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// LimitedReader is a fork of io.LimitedReader to override Read.
|
||||
type LimitedReader struct {
|
||||
// limitedReader is a fork of [io.LimitedReader] to override Read.
|
||||
type limitedReader struct {
|
||||
R io.Reader
|
||||
N int64 // max bytes remaining
|
||||
}
|
||||
|
||||
// Read is a fork of io.LimitedReader.Read that returns an error when limit exceeded.
|
||||
func (l *LimitedReader) Read(p []byte) (n int, err error) {
|
||||
// Read is a fork of [io.LimitedReader.Read] that returns an error when limit exceeded.
|
||||
func (l *limitedReader) Read(p []byte) (n int, err error) {
|
||||
if l.N < 0 {
|
||||
return 0, errors.New("read exceeds the defined limit")
|
||||
}
|
||||
|
||||
@ -15,10 +15,10 @@ func TestLimitReaderReadAll(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
|
||||
r = strings.NewReader("Test")
|
||||
_, err = io.ReadAll(&LimitedReader{R: r, N: 4})
|
||||
_, err = io.ReadAll(&limitedReader{R: r, N: 4})
|
||||
assert.NilError(t, err)
|
||||
|
||||
r = strings.NewReader("Test")
|
||||
_, err = io.ReadAll(&LimitedReader{R: r, N: 2})
|
||||
_, err = io.ReadAll(&limitedReader{R: r, N: 2})
|
||||
assert.Error(t, err, "read exceeds the defined limit")
|
||||
}
|
||||
|
||||
@ -12,8 +12,8 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/atomicwriter"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
@ -14,9 +14,9 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
@ -24,7 +24,7 @@ import (
|
||||
|
||||
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
|
||||
|
||||
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
|
||||
var restrictedNameRegEx = lazyregexp.New(restrictedNamePattern)
|
||||
|
||||
// Store provides a context store for easily remembering endpoints configuration
|
||||
type Store interface {
|
||||
@ -356,7 +356,7 @@ func isValidFilePath(p string) error {
|
||||
}
|
||||
|
||||
func importTar(name string, s Writer, reader io.Reader) error {
|
||||
tr := tar.NewReader(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
||||
tr := tar.NewReader(&limitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
||||
tlsData := ContextTLSData{
|
||||
Endpoints: map[string]EndpointTLSData{},
|
||||
}
|
||||
@ -406,7 +406,7 @@ func importTar(name string, s Writer, reader io.Reader) error {
|
||||
}
|
||||
|
||||
func importZip(name string, s Writer, reader io.Reader) error {
|
||||
body, err := io.ReadAll(&LimitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
||||
body, err := io.ReadAll(&limitedReader{R: reader, N: maxAllowedFileSizeToImport})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -434,7 +434,7 @@ func importZip(name string, s Writer, reader io.Reader) error {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(&LimitedReader{R: f, N: maxAllowedFileSizeToImport})
|
||||
data, err := io.ReadAll(&limitedReader{R: f, N: maxAllowedFileSizeToImport})
|
||||
defer f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/pkg/atomicwriter"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
@ -147,7 +147,7 @@ func (c *client) getRepositoryForReference(ctx context.Context, ref reference.Na
|
||||
|
||||
func (c *client) getHTTPTransportForRepoEndpoint(ctx context.Context, repoEndpoint repositoryEndpoint) (http.RoundTripper, error) {
|
||||
httpTransport, err := getHTTPTransport(
|
||||
c.authConfigResolver(ctx, repoEndpoint.info.Index),
|
||||
c.authConfigResolver(ctx, repoEndpoint.indexInfo),
|
||||
repoEndpoint.endpoint,
|
||||
repoEndpoint.Name(),
|
||||
c.userAgent,
|
||||
|
||||
@ -14,14 +14,15 @@ import (
|
||||
)
|
||||
|
||||
type repositoryEndpoint struct {
|
||||
info *registry.RepositoryInfo
|
||||
endpoint registry.APIEndpoint
|
||||
actions []string
|
||||
repoName reference.Named
|
||||
indexInfo *registrytypes.IndexInfo
|
||||
endpoint registry.APIEndpoint
|
||||
actions []string
|
||||
}
|
||||
|
||||
// Name returns the repository name
|
||||
func (r repositoryEndpoint) Name() string {
|
||||
return reference.Path(r.info.Name)
|
||||
return reference.Path(r.repoName)
|
||||
}
|
||||
|
||||
// BaseURL returns the endpoint url
|
||||
@ -30,32 +31,36 @@ func (r repositoryEndpoint) BaseURL() string {
|
||||
}
|
||||
|
||||
func newDefaultRepositoryEndpoint(ref reference.Named, insecure bool) (repositoryEndpoint, error) {
|
||||
repoName := reference.TrimNamed(ref)
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(ref)
|
||||
endpoint, err := getDefaultEndpointFromRepoInfo(repoInfo)
|
||||
indexInfo := repoInfo.Index
|
||||
|
||||
endpoint, err := getDefaultEndpoint(ref, !indexInfo.Secure)
|
||||
if err != nil {
|
||||
return repositoryEndpoint{}, err
|
||||
}
|
||||
if insecure {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return repositoryEndpoint{info: repoInfo, endpoint: endpoint}, nil
|
||||
return repositoryEndpoint{
|
||||
repoName: repoName,
|
||||
indexInfo: indexInfo,
|
||||
endpoint: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getDefaultEndpointFromRepoInfo(repoInfo *registry.RepositoryInfo) (registry.APIEndpoint, error) {
|
||||
var err error
|
||||
|
||||
options := registry.ServiceOptions{}
|
||||
registryService, err := registry.NewService(options)
|
||||
func getDefaultEndpoint(repoName reference.Named, insecure bool) (registry.APIEndpoint, error) {
|
||||
registryService, err := registry.NewService(registry.ServiceOptions{})
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
|
||||
endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoName))
|
||||
if err != nil {
|
||||
return registry.APIEndpoint{}, err
|
||||
}
|
||||
// Default to the highest priority endpoint to return
|
||||
endpoint := endpoints[0]
|
||||
if !repoInfo.Index.Secure {
|
||||
if insecure {
|
||||
for _, ep := range endpoints {
|
||||
if ep.URL.Scheme == "http" {
|
||||
endpoint = ep
|
||||
|
||||
@ -220,7 +220,9 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
|
||||
return err
|
||||
}
|
||||
|
||||
repoName := reference.TrimNamed(namedRef)
|
||||
repoInfo, _ := registry.ParseRepositoryInfo(namedRef)
|
||||
indexInfo := repoInfo.Index
|
||||
|
||||
confirmedTLSRegistries := make(map[string]bool)
|
||||
for _, endpoint := range endpoints {
|
||||
@ -234,7 +236,11 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
|
||||
if c.insecureRegistry {
|
||||
endpoint.TLSConfig.InsecureSkipVerify = true
|
||||
}
|
||||
repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo}
|
||||
repoEndpoint := repositoryEndpoint{
|
||||
repoName: repoName,
|
||||
indexInfo: indexInfo,
|
||||
endpoint: endpoint,
|
||||
}
|
||||
repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
|
||||
if err != nil {
|
||||
logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
|
||||
|
||||
@ -160,13 +160,12 @@ func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo
|
||||
Actions: actions,
|
||||
}
|
||||
creds := simpleCredentialStore{auth: *authConfig}
|
||||
tokenHandlerOptions := auth.TokenHandlerOptions{
|
||||
tokenHandler := auth.NewTokenHandlerWithOptions(auth.TokenHandlerOptions{
|
||||
Transport: authTransport,
|
||||
Credentials: creds,
|
||||
Scopes: []auth.Scope{scope},
|
||||
ClientID: registry.AuthClientID,
|
||||
}
|
||||
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
|
||||
})
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
tr := transport.NewTransport(base, modifiers...)
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
pluginmanager "github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -26,11 +26,11 @@ func processAliases(dockerCli command.Cli, cmd *cobra.Command, args, osArgs []st
|
||||
|
||||
for k, v := range aliasMap {
|
||||
if _, ok := allowedAliases[k]; !ok {
|
||||
return args, osArgs, envs, errors.Errorf("not allowed to alias %q (allowed: %#v)", k, allowedAliases)
|
||||
return args, osArgs, envs, fmt.Errorf("not allowed to alias %q (allowed: %#v)", k, allowedAliases)
|
||||
}
|
||||
if c, _, err := cmd.Find(strings.Split(v, " ")); err == nil {
|
||||
if !pluginmanager.IsPluginCommand(c) {
|
||||
return args, osArgs, envs, errors.Errorf("not allowed to alias with builtin %q as target", v)
|
||||
return args, osArgs, envs, fmt.Errorf("not allowed to alias with builtin %q as target", v)
|
||||
}
|
||||
}
|
||||
aliases = append(aliases, [2][]string{{k}, {v}})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -11,7 +12,6 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@ -29,6 +29,10 @@ const (
|
||||
buildxMissingError = `ERROR: BuildKit is enabled but the buildx component is missing or broken.
|
||||
Install the buildx component to build images with BuildKit:
|
||||
https://docs.docker.com/go/buildx/`
|
||||
|
||||
bakeMissingError = `ERROR: docker bake requires the buildx component but it is missing or broken.
|
||||
Install the buildx component to use bake:
|
||||
https://docs.docker.com/go/buildx/`
|
||||
)
|
||||
|
||||
func newBuilderError(errorMsg string, pluginLoadErr error) error {
|
||||
@ -51,7 +55,7 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return args, osargs, nil, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
return args, osargs, nil, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
||||
}
|
||||
if !enabled {
|
||||
buildKitDisabled = true
|
||||
@ -59,6 +63,10 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
useBuilder = true
|
||||
}
|
||||
}
|
||||
// docker bake always requires buildkit; ignore "DOCKER_BUILDKIT=0".
|
||||
if buildKitDisabled && len(args) > 0 && args[0] == "bake" {
|
||||
buildKitDisabled = false
|
||||
}
|
||||
|
||||
// if a builder alias is defined, use it instead
|
||||
// of the default one
|
||||
@ -105,6 +113,10 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
perr = plugin.Err
|
||||
}
|
||||
if perr != nil {
|
||||
// Using bake without buildx installed is always an error.
|
||||
if len(args) > 0 && args[0] == "bake" {
|
||||
return args, osargs, nil, newBuilderError(bakeMissingError, perr)
|
||||
}
|
||||
// if builder is enforced with DOCKER_BUILDKIT=1, cmd must fail
|
||||
// if the plugin is missing or broken.
|
||||
if useBuilder {
|
||||
@ -135,6 +147,11 @@ func processBuilder(dockerCli command.Cli, cmd *cobra.Command, args, osargs []st
|
||||
|
||||
func forwardBuilder(alias string, args, osargs []string) ([]string, []string, []string, bool) {
|
||||
aliases := [][3][]string{
|
||||
{
|
||||
{"bake"},
|
||||
{alias, "bake"},
|
||||
{},
|
||||
},
|
||||
{
|
||||
{"builder"},
|
||||
{alias},
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@ -21,7 +22,6 @@ import (
|
||||
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -203,7 +203,7 @@ func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) {
|
||||
return helpcmd.Run()
|
||||
}
|
||||
if !pluginmanager.IsNotFound(err) {
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
}
|
||||
if origRunE != nil {
|
||||
@ -247,6 +247,12 @@ func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(thaJeztah): need a better way for this; hiding the command here, so that it's present" by default for generating docs etc.
|
||||
if c, _, err := ccmd.Find([]string{"buildx"}); c == nil || err != nil {
|
||||
if b, _, _ := ccmd.Find([]string{"bake"}); b != nil {
|
||||
b.Hidden = true
|
||||
}
|
||||
}
|
||||
if err := isSupported(ccmd, dockerCli); err != nil {
|
||||
ccmd.Println(err)
|
||||
return
|
||||
@ -593,7 +599,7 @@ func isSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
}
|
||||
|
||||
func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
errs := []string{}
|
||||
var errs []error
|
||||
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
if !f.Changed || len(f.Annotations) == 0 {
|
||||
@ -608,11 +614,11 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
// See commit b39739123b845f872549e91be184cc583f5b387c for details.
|
||||
|
||||
if _, ok := f.Annotations["version"]; ok && !isVersionSupported(f, details.CurrentVersion()) {
|
||||
errs = append(errs, fmt.Sprintf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.CurrentVersion()))
|
||||
errs = append(errs, fmt.Errorf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.CurrentVersion()))
|
||||
return
|
||||
}
|
||||
if _, ok := f.Annotations["ostype"]; ok && !isOSTypeSupported(f, details.ServerInfo().OSType) {
|
||||
errs = append(errs, fmt.Sprintf(
|
||||
errs = append(errs, fmt.Errorf(
|
||||
`"--%s" is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s`,
|
||||
f.Name,
|
||||
getFlagAnnotation(f, "ostype"), details.ServerInfo().OSType),
|
||||
@ -620,14 +626,11 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
|
||||
return
|
||||
}
|
||||
if _, ok := f.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
|
||||
errs = append(errs, fmt.Sprintf(`"--%s" is only supported on a Docker daemon with experimental features enabled`, f.Name))
|
||||
errs = append(errs, fmt.Errorf(`"--%s" is only supported on a Docker daemon with experimental features enabled`, f.Name))
|
||||
}
|
||||
// buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case
|
||||
})
|
||||
if len(errs) > 0 {
|
||||
return errors.New(strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
|
||||
|
||||
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
@ -12,7 +13,6 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
platformsignals "github.com/docker/cli/cmd/docker/internal/signals"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@ -87,7 +87,7 @@ func TestUserTerminatedError(t *testing.T) {
|
||||
notifyCtx, cancelNotify := notifyContext(ctx, platformsignals.TerminationSignals...)
|
||||
t.Cleanup(cancelNotify)
|
||||
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGINT)
|
||||
assert.Check(t, syscall.Kill(syscall.Getpid(), syscall.SIGINT))
|
||||
|
||||
<-notifyCtx.Done()
|
||||
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
|
||||
@ -99,7 +99,7 @@ func TestUserTerminatedError(t *testing.T) {
|
||||
notifyCtx, cancelNotify = notifyContext(ctx, platformsignals.TerminationSignals...)
|
||||
t.Cleanup(cancelNotify)
|
||||
|
||||
syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
assert.Check(t, syscall.Kill(syscall.Getpid(), syscall.SIGTERM))
|
||||
|
||||
<-notifyCtx.Done()
|
||||
assert.ErrorIs(t, context.Cause(notifyCtx), errCtxSignalTerminated{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user