Compare commits

...

23 Commits

Author SHA1 Message Date
Sebastiaan van Stijn 69a91aec1e
Merge d3bafa5f3e into 0ab0eca8bd 2024-10-22 01:13:14 +02:00
Sebastiaan van Stijn 0ab0eca8bd
Merge pull request #5550 from thaJeztah/login_minor_refactor
cli/command: PromptUserForCredentials: assorted minor improvements and (linting) fixes
2024-10-21 23:23:06 +02:00
Sebastiaan van Stijn abb8e9b78a
Merge pull request #5546 from thaJeztah/hints_coverage
cli/hints: add tests
2024-10-21 18:08:28 +02:00
Laura Brehm 7029147458
Merge pull request #5557 from thaJeztah/minor_linting_issues 2024-10-21 17:00:40 +01:00
Paweł Gronowski d2b87a0a3b
Merge pull request #5553 from thaJeztah/login_idempotent
cli/config/credentials: skip saving config-file if credentials didn't change
2024-10-21 15:23:26 +02:00
Sebastiaan van Stijn 24ee5f228a
Merge pull request #5551 from thaJeztah/fix_ConfigureAuth_deprecation
cli/command: ConfigureAuth: fix deprecation comment
2024-10-21 14:28:43 +02:00
Sebastiaan van Stijn 8b6133a2b7
Merge pull request #5544 from thaJeztah/bump_engine_28
vendor: github.com/docker/docker 36a3bd090489 (master, v28.0-dev)
2024-10-21 13:28:35 +02:00
Sebastiaan van Stijn d3f6867e4d
cli/config/credentials: skip saving config-file if credentials didn't change
Before this change, the config-file was always updated, even if there
were no changes to save. This could cause issues when the config-file
already had credentials set and was read-only for the current user.

For example, on NixOS, this poses a problem because `config.json` is a
symlink to a write-protected file;

    $ readlink ~/.docker/config.json
    /home/username/.config/sops-nix/secrets/ghcr_auth

    $ readlink -f ~/.docker/config.json
    /run/user/1000/secrets.d/28/ghcr_auth

Which causes `docker login` to fail, even if no changes were to be made;

    Error saving credentials: rename /home/derek/.docker/config.json2180380217 /home/username/.config/sops-nix/secrets/ghcr_auth: invalid cross-device link

This patch updates the code to only update the config file if changes
were detected. It there's nothing to save, it skips updating the file,
as well as skips printing the warning about credentials being stored
insecurely.

With this patch applied:

    $ docker login -u yourname
    Password:

    WARNING! Your credentials are stored unencrypted in '/root/.docker/config.json'.
    Configure a credential helper to remove this warning. See
    https://docs.docker.com/go/credential-store/

    Login Succeeded

    $ docker login -u yourname
    Password:
    Login Succeeded

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-21 00:19:52 +02:00
Sebastiaan van Stijn 6b9083776f
cli/command: AddPlatformFlag: suppress unhandled error
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-20 17:51:36 +02:00
Sebastiaan van Stijn fb61156b05
cli/command/registry: fix minor linting issues
- fix camelCase naming of verifyLoginOptions
- suppress unhandled errors that can be ignored

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-20 17:51:12 +02:00
Sebastiaan van Stijn 4b7a1e4613
cli/command: PromptUserForCredentials: suppress unhandled errors
Keep the linters (and my IDE) happy; these errors should be safe to ignore.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 13:24:19 +02:00
Sebastiaan van Stijn 378a3d7d36
cli/command: PromptUserForCredentials: use consts for all hints
This message resulted in code-lines that were too long; move it to a
const together with the other hint. While at it, also suppress unhandled
error, and touch-up the code-comment.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 13:23:29 +02:00
Sebastiaan van Stijn 54e3685bcd
cli/command: ConfigureAuth: fix deprecation comment
Deprecation comments must have an empty line before them, otherwise tools
and linters may not recognise them. While fixing this, also updated the
reference to PromptUserForCredentials to be a docs-link to make it clickable.

Updates 6e4818e7d6.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 13:05:31 +02:00
Sebastiaan van Stijn 3d8b49523d
cli/command: PromptUserForCredentials: print error on terminal restore fail
If restoring the terminal state fails, "echo" no longer works, which means
that anything the user types is no longer shown. The login itself may already
have succeeded, so we should not fail the command, but it's good to inform
the user that this happened, which may give them a clue why things no longer
work as they expect them to work.

With this patch:

    docker login -u yourname
    Password:
    Error: failed to restore terminal state to echo input: something bad happened

    Login Succeeded

We should consider printing instructions how  to restore this manually (other
than restarting the shell). e.g., 'run stty echo' when in a Linux or macOS shell,
but PowerShell and CMD.exe may need different instructions.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:49:44 +02:00
Sebastiaan van Stijn a21a5f4243
cli/command: PromptUserForCredentials: always trim password
we don't support empty passwords; when prompting the user for a password,
we already trim the result, but we didn't do the same for a password that's
passed through stdin or through the `-p` / `--password` flag.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:10:46 +02:00
Sebastiaan van Stijn eda78e9cdc
cli/command: PromptUserForCredentials: move trimming where it's used
- move trimming defaultUsername inside the if-branch, as it's the only
  location where the result of the trimmed username is use.
- do the reverse for trimming argUser, because the result of trimming
  argUser is used outside of the if-branch (not just for the condition).
  putting it inside the condition makes it easy to assume the result is
  only used locally.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:07:51 +02:00
Sebastiaan van Stijn 581cf36bd4
cli/command: PromptUserForCredentials: move "post" check for empty name
move the "post" check for username being empty inside the branch
that's handling the username, as it's the only branch where username
is mutated after checking if it's empty.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:06:49 +02:00
Sebastiaan van Stijn a55cfe5f82
cli/command: PromptUserForCredentials: inline isDefaultRegistry
remove isDefaultRegistry and inline it where it's used; the code-comment
already outlines what we're looking for, so the intermediate var didn't
add much currently.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 11:58:42 +02:00
Sebastiaan van Stijn 3a8485085d
cli/command: PromptUserForCredentials: remove named output variables
This function has multiple conditional branches, which makes it harder
to see at a glance whether authConfig may be partially populated. This
patch instead returns a fresh instance for error returns to prevent any
confusion.

It also removes the named output variables, as they're now no longer used,
and the returned types should already be descriptive enough to understand
what's returned.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 11:46:21 +02:00
Sebastiaan van Stijn d3bafa5f3e
cli: deprecate Errors type
The Errors type is no longer used by the CLI itself, and this custom
"multi-error" implementation had both limitations (empty list not being
`nil`), as well as formatting not being great. All of this making it not
something to recommend, and better handled with Go's stdlib.

As far as I could find, there's no external consumers of this, but let's
deprecate first, and remove in the next release.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 10:32:55 +02:00
Sebastiaan van Stijn 71ebbb81ae
cli/command/plugins: use errors.Join instead of custom cli.Errors
This command was using a custom "multi-error" implementation, but it
had some limitations, and the formatting wasn't great.

This patch replaces it with Go's errors.Join.

Before:

    docker plugin remove one two three
    Error response from daemon: plugin "one" not found, Error response from daemon: plugin "two" not found, Error response from daemon: plugin "three" not found

After:

    docker plugin remove one two three
    Error response from daemon: plugin "one" not found
    Error response from daemon: plugin "two" not found
    Error response from daemon: plugin "three" not found

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 10:32:54 +02:00
Sebastiaan van Stijn 87acf77aef
cli/hints: add tests
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 00:48:16 +02:00
Sebastiaan van Stijn 9b525bc9d1
vendor: github.com/docker/docker 36a3bd090489 (master, v28.0-dev)
full diff: 164cae56ed...36a3bd0904

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-18 17:48:05 +02:00
23 changed files with 280 additions and 109 deletions

View File

@ -2,6 +2,7 @@ package plugin
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@ -36,17 +37,13 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
} }
func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error { func runRemove(ctx context.Context, dockerCli command.Cli, opts *rmOptions) error {
var errs cli.Errors var errs error
for _, name := range opts.plugins { for _, name := range opts.plugins {
if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil {
errs = append(errs, err) errs = errors.Join(errs, err)
continue continue
} }
fmt.Fprintln(dockerCli.Out(), name) _, _ = fmt.Fprintln(dockerCli.Out(), name)
} }
// Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. return errs
if errs != nil {
return errs
}
return nil
} }

View File

@ -18,20 +18,24 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const patSuggest = "You can log in with your password or a Personal Access " + const (
"Token (PAT). Using a limited-scope PAT grants better security and is required " + registerSuggest = "Log in with your Docker ID or email address to push and pull images from Docker Hub. " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/" "If you don't have a Docker ID, head over to https://hub.docker.com/ to create one."
patSuggest = "You can log in with your password or a Personal Access " +
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
)
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command. // for the given command.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig { func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
return func(ctx context.Context) (string, error) { return func(ctx context.Context) (string, error) {
fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName) _, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
indexServer := registry.GetAuthConfigKey(index) indexServer := registry.GetAuthConfigKey(index)
isDefaultRegistry := indexServer == registry.IndexServer isDefaultRegistry := indexServer == registry.IndexServer
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry) authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
if err != nil { 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", indexServer, err)
} }
select { select {
@ -86,7 +90,8 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
} }
// ConfigureAuth handles prompting of user's username and password if needed. // ConfigureAuth handles prompting of user's username and password if needed.
// Deprecated: use PromptUserForCredentials instead. //
// Deprecated: use [PromptUserForCredentials] instead.
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error {
defaultUsername := authConfig.Username defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress serverAddress := authConfig.ServerAddress
@ -110,7 +115,7 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// If defaultUsername is not empty, the username prompt includes that username // If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default // and the user can hit enter without inputting a username to use that default
// username. // username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) { func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
// On Windows, force the use of the regular OS stdin stream. // On Windows, force the use of the regular OS stdin stream.
// //
// See: // See:
@ -123,57 +128,71 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
cli.SetIn(streams.NewIn(os.Stdin)) cli.SetIn(streams.NewIn(os.Stdin))
} }
isDefaultRegistry := serverAddress == registry.IndexServer argUser = strings.TrimSpace(argUser)
defaultUsername = strings.TrimSpace(defaultUsername) if argUser == "" {
if serverAddress == registry.IndexServer {
if argUser = strings.TrimSpace(argUser); argUser == "" { // When signing in to the default (Docker Hub) registry, we display
if isDefaultRegistry { // hints for creating an account, and (if hints are enabled), using
// if this is a default registry (docker hub), then display the following message. // a token instead of a password.
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.") _, _ = fmt.Fprintln(cli.Out(), registerSuggest)
if hints.Enabled() { if hints.Enabled() {
fmt.Fprintln(cli.Out(), patSuggest) _, _ = fmt.Fprintln(cli.Out(), patSuggest)
fmt.Fprintln(cli.Out()) _, _ = fmt.Fprintln(cli.Out())
} }
} }
var prompt string var prompt string
defaultUsername = strings.TrimSpace(defaultUsername)
if defaultUsername == "" { if defaultUsername == "" {
prompt = "Username: " prompt = "Username: "
} else { } else {
prompt = fmt.Sprintf("Username (%s): ", defaultUsername) prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
} }
var err error
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt) argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil { if err != nil {
return authConfig, err return registrytypes.AuthConfig{}, err
} }
if argUser == "" { if argUser == "" {
argUser = defaultUsername argUser = defaultUsername
} }
if argUser == "" {
return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required")
}
} }
if argUser == "" {
return authConfig, errors.Errorf("Error: Non-null Username Required") argPassword = strings.TrimSpace(argPassword)
}
if argPassword == "" { if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In()) restoreInput, err := DisableInputEcho(cli.In())
if err != nil { if err != nil {
return authConfig, err return registrytypes.AuthConfig{}, err
} }
defer restoreInput() defer func() {
if err := restoreInput(); err != nil {
// TODO(thaJeztah): we should consider printing instructions how
// to restore this manually (other than restarting the shell).
// e.g., 'run stty echo' when in a Linux or macOS shell, but
// PowerShell and CMD.exe may need different instructions.
_, _ = fmt.Fprintln(cli.Err(), "Error: failed to restore terminal state to echo input:", err)
}
}()
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ") argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil { if err != nil {
return authConfig, err return registrytypes.AuthConfig{}, err
} }
fmt.Fprint(cli.Out(), "\n") _, _ = fmt.Fprintln(cli.Out())
if argPassword == "" { if argPassword == "" {
return authConfig, errors.Errorf("Error: Password Required") return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required")
} }
} }
authConfig.Username = argUser return registrytypes.AuthConfig{
authConfig.Password = argPassword Username: argUser,
authConfig.ServerAddress = serverAddress Password: argPassword,
return authConfig, nil ServerAddress: serverAddress,
}, nil
} }
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -58,9 +58,9 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
return cmd return cmd
} }
func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error { func verifyLoginOptions(dockerCli command.Cli, opts *loginOptions) error {
if opts.password != "" { if opts.password != "" {
fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") _, _ = fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
if opts.passwordStdin { if opts.passwordStdin {
return errors.New("--password and --password-stdin are mutually exclusive") return errors.New("--password and --password-stdin are mutually exclusive")
} }
@ -83,7 +83,7 @@ func verifyloginOptions(dockerCli command.Cli, opts *loginOptions) error {
} }
func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error { func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) error {
if err := verifyloginOptions(dockerCli, &opts); err != nil { if err := verifyLoginOptions(dockerCli, &opts); err != nil {
return err return err
} }
var ( var (
@ -174,7 +174,7 @@ func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, de
if !errors.Is(err, manager.ErrDeviceLoginStartFail) { if !errors.Is(err, manager.ErrDeviceLoginStartFail) {
return response, err return response, err
} }
fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n") _, _ = fmt.Fprint(dockerCli.Err(), "Failed to start web-based login - falling back to command line login...\n\n")
} }
return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress) return loginWithUsernameAndPassword(ctx, dockerCli, opts, defaultUsername, serverAddress)

View File

@ -199,7 +199,7 @@ func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args {
// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later. // AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
func AddPlatformFlag(flags *pflag.FlagSet, target *string) { func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
flags.SetAnnotation("platform", "version", []string{"1.32"}) _ = 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 `export` and `save` commands.

View File

@ -30,6 +30,10 @@ func NewFileStore(file store) Store {
// Erase removes the given credentials from the file store. // Erase removes the given credentials from the file store.
func (c *fileStore) Erase(serverAddress string) error { func (c *fileStore) Erase(serverAddress string) error {
if _, exists := c.file.GetAuthConfigs()[serverAddress]; !exists {
// nothing to do; no credentials found for the given serverAddress
return nil
}
delete(c.file.GetAuthConfigs(), serverAddress) delete(c.file.GetAuthConfigs(), serverAddress)
return c.file.Save() return c.file.Save()
} }
@ -70,9 +74,14 @@ https://docs.docker.com/go/credential-store/
// CLI invocation (no need to warn the user multiple times per command). // CLI invocation (no need to warn the user multiple times per command).
var alreadyPrinted atomic.Bool var alreadyPrinted atomic.Bool
// Store saves the given credentials in the file store. // Store saves the given credentials in the file store. This function is
// idempotent and does not update the file if credentials did not change.
func (c *fileStore) Store(authConfig types.AuthConfig) error { func (c *fileStore) Store(authConfig types.AuthConfig) error {
authConfigs := c.file.GetAuthConfigs() authConfigs := c.file.GetAuthConfigs()
if oldAuthConfig, ok := authConfigs[authConfig.ServerAddress]; ok && oldAuthConfig == authConfig {
// Credentials didn't change, so skip updating the configuration file.
return nil
}
authConfigs[authConfig.ServerAddress] = authConfig authConfigs[authConfig.ServerAddress] = authConfig
if err := c.file.Save(); err != nil { if err := c.file.Save(); err != nil {
return err return err

View File

@ -8,6 +8,8 @@ import (
// Errors is a list of errors. // Errors is a list of errors.
// Useful in a loop if you don't want to return the error right away and you want to display after the loop, // Useful in a loop if you don't want to return the error right away and you want to display after the loop,
// all the errors that happened during the loop. // all the errors that happened during the loop.
//
// Deprecated: use [errors.Join] instead; will be removed in the next release.
type Errors []error type Errors []error
func (errList Errors) Error() string { func (errList Errors) Error() string {

View File

@ -5,7 +5,9 @@ import (
"strconv" "strconv"
) )
// Enabled returns whether cli hints are enabled or not // Enabled returns whether cli hints are enabled or not. Hints are enabled by
// default, but can be disabled through the "DOCKER_CLI_HINTS" environment
// variable.
func Enabled() bool { func Enabled() bool {
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" { if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)

52
cli/hints/hints_test.go Normal file
View File

@ -0,0 +1,52 @@
package hints
import (
"testing"
"gotest.tools/v3/assert"
)
func TestEnabled(t *testing.T) {
tests := []struct {
doc string
env string
expected bool
}{
{
doc: "default",
expected: true,
},
{
doc: "DOCKER_CLI_HINTS=1",
env: "1",
expected: true,
},
{
doc: "DOCKER_CLI_HINTS=true",
env: "true",
expected: true,
},
{
doc: "DOCKER_CLI_HINTS=0",
env: "0",
expected: false,
},
{
doc: "DOCKER_CLI_HINTS=false",
env: "false",
expected: false,
},
{
doc: "DOCKER_CLI_HINTS=not-a-bool",
env: "not-a-bool",
expected: true,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
t.Setenv("DOCKER_CLI_HINTS", tc.env)
assert.Equal(t, Enabled(), tc.expected)
})
}
}

View File

@ -3,7 +3,7 @@ module github.com/docker/cli/docs/generate
// dummy go.mod to avoid dealing with dependencies specific // dummy go.mod to avoid dealing with dependencies specific
// to docs generation and not really part of the project. // to docs generation and not really part of the project.
go 1.16 go 1.22.0
//require ( //require (
// github.com/docker/cli v0.0.0+incompatible // github.com/docker/cli v0.0.0+incompatible

View File

@ -3,7 +3,7 @@ module github.com/docker/cli/man
// dummy go.mod to avoid dealing with dependencies specific // dummy go.mod to avoid dealing with dependencies specific
// to manpages generation and not really part of the project. // to manpages generation and not really part of the project.
go 1.16 go 1.12.0
//require ( //require (
// github.com/docker/cli v0.0.0+incompatible // github.com/docker/cli v0.0.0+incompatible

View File

@ -18,7 +18,7 @@ init() {
cat > go.mod <<EOL cat > go.mod <<EOL
module github.com/docker/cli module github.com/docker/cli
go 1.19 go 1.22.0
EOL EOL
} }

View File

@ -4,7 +4,7 @@ module github.com/docker/cli
// There is no 'go.mod' file, as that would imply opting in for all the rules // There is no 'go.mod' file, as that would imply opting in for all the rules
// around SemVer, which this repo cannot abide by as it uses CalVer. // around SemVer, which this repo cannot abide by as it uses CalVer.
go 1.21.0 go 1.22.0
require ( require (
dario.cat/mergo v1.0.1 dario.cat/mergo v1.0.1
@ -13,7 +13,7 @@ require (
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.8.0 github.com/docker/cli-docs-tool v0.8.0
github.com/docker/distribution v2.8.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v27.0.2-0.20240912171519-164cae56ed95+incompatible // master (v-next) github.com/docker/docker v27.0.2-0.20241018142220-36a3bd090489+incompatible // master (v-next)
github.com/docker/docker-credential-helpers v0.8.2 github.com/docker/docker-credential-helpers v0.8.2
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0

View File

@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.8.0/go.mod h1:8TQQ3E7mOXoYUs811LiPdUnAhXrcVsB
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v27.0.2-0.20240912171519-164cae56ed95+incompatible h1:HRK75BHG33htes7s+v/fJ8saCNw3B7f3spcgLsvbLRQ= github.com/docker/docker v27.0.2-0.20241018142220-36a3bd090489+incompatible h1:utxxyIvPGk7UmtlGHirUyNUP2Spf8yL660PCbmb7tsk=
github.com/docker/docker v27.0.2-0.20240912171519-164cae56ed95+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v27.0.2-0.20241018142220-36a3bd090489+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=

View File

@ -6005,7 +6005,7 @@ definitions:
accept un-encrypted (HTTP) and/or untrusted (HTTPS with certificates accept un-encrypted (HTTP) and/or untrusted (HTTPS with certificates
from unknown CAs) communication. from unknown CAs) communication.
By default, local registries (`127.0.0.0/8`) are configured as By default, local registries (`::1/128` and `127.0.0.0/8`) are configured as
insecure. All other registries are secure. Communicating with an insecure. All other registries are secure. Communicating with an
insecure registry is not possible if the daemon assumes that registry insecure registry is not possible if the daemon assumes that registry
is secure. is secure.
@ -6170,6 +6170,8 @@ definitions:
Expected: Expected:
description: | description: |
Commit ID of external tool expected by dockerd as set at build time. Commit ID of external tool expected by dockerd as set at build time.
**Deprecated**: This field is deprecated and will be omitted in a API v1.49.
type: "string" type: "string"
example: "2d41c047c83e09a6d61d464906feb2a2f3c52aa4" example: "2d41c047c83e09a6d61d464906feb2a2f3c52aa4"
@ -7881,10 +7883,12 @@ paths:
type: "string" type: "string"
- name: "h" - name: "h"
in: "query" in: "query"
required: true
description: "Height of the TTY session in characters" description: "Height of the TTY session in characters"
type: "integer" type: "integer"
- name: "w" - name: "w"
in: "query" in: "query"
required: true
description: "Width of the TTY session in characters" description: "Width of the TTY session in characters"
type: "integer" type: "integer"
tags: ["Container"] tags: ["Container"]
@ -10236,10 +10240,12 @@ paths:
type: "string" type: "string"
- name: "h" - name: "h"
in: "query" in: "query"
required: true
description: "Height of the TTY session in characters" description: "Height of the TTY session in characters"
type: "integer" type: "integer"
- name: "w" - name: "w"
in: "query" in: "query"
required: true
description: "Width of the TTY session in characters" description: "Width of the TTY session in characters"
type: "integer" type: "integer"
tags: ["Exec"] tags: ["Exec"]

View File

@ -137,8 +137,13 @@ type PluginsInfo struct {
// Commit holds the Git-commit (SHA1) that a binary was built from, as reported // Commit holds the Git-commit (SHA1) that a binary was built from, as reported
// in the version-string of external tools, such as containerd, or runC. // in the version-string of external tools, such as containerd, or runC.
type Commit struct { type Commit struct {
ID string // ID is the actual commit ID of external tool. // ID is the actual commit ID or version of external tool.
Expected string // Expected is the commit ID of external tool expected by dockerd as set at build time. ID string
// Expected is the commit ID of external tool expected by dockerd as set at build time.
//
// Deprecated: this field is no longer used in API v1.49, but kept for backward-compatibility with older API versions.
Expected string
} }
// NetworkAddressPool is a temp struct used by [Info] struct. // NetworkAddressPool is a temp struct used by [Info] struct.

View File

@ -172,4 +172,6 @@ type BuildCachePruneOptions struct {
All bool All bool
KeepStorage int64 KeepStorage int64
Filters filters.Args Filters filters.Args
// FIXME(thaJeztah): add new options; see https://github.com/moby/moby/issues/48639
} }

View File

@ -2,7 +2,7 @@
Package client is a Go client for the Docker Engine API. Package client is a Go client for the Docker Engine API.
For more information about the Engine API, see the documentation: For more information about the Engine API, see the documentation:
https://docs.docker.com/engine/api/ https://docs.docker.com/reference/api/engine/
# Usage # Usage

View File

@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"net/url" "net/url"
"path" "path"
"sort"
"strings"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
@ -12,12 +14,6 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
type configWrapper struct {
*container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
}
// ContainerCreate creates a new container based on the given configuration. // ContainerCreate creates a new container based on the given configuration.
// It can be associated with a name, but it's not mandatory. // It can be associated with a name, but it's not mandatory.
func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) { func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
@ -58,6 +54,9 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
// When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize // When using API under 1.42, the Linux daemon doesn't respect the ConsoleSize
hostConfig.ConsoleSize = [2]uint{0, 0} hostConfig.ConsoleSize = [2]uint{0, 0}
} }
hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd)
hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop)
} }
// Since API 1.44, the container-wide MacAddress is deprecated and will trigger a WARNING if it's specified. // Since API 1.44, the container-wide MacAddress is deprecated and will trigger a WARNING if it's specified.
@ -74,7 +73,7 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
query.Set("name", containerName) query.Set("name", containerName)
} }
body := configWrapper{ body := container.CreateRequest{
Config: config, Config: config,
HostConfig: hostConfig, HostConfig: hostConfig,
NetworkingConfig: networkingConfig, NetworkingConfig: networkingConfig,
@ -114,3 +113,42 @@ func hasEndpointSpecificMacAddress(networkingConfig *network.NetworkingConfig) b
} }
return false return false
} }
// allCapabilities is a magic value for "all capabilities"
const allCapabilities = "ALL"
// normalizeCapabilities normalizes capabilities to their canonical form,
// removes duplicates, and sorts the results.
//
// It is similar to [github.com/docker/docker/oci/caps.NormalizeLegacyCapabilities],
// but performs no validation based on supported capabilities.
func normalizeCapabilities(caps []string) []string {
var normalized []string
unique := make(map[string]struct{})
for _, c := range caps {
c = normalizeCap(c)
if _, ok := unique[c]; ok {
continue
}
unique[c] = struct{}{}
normalized = append(normalized, c)
}
sort.Strings(normalized)
return normalized
}
// normalizeCap normalizes a capability to its canonical format by upper-casing
// and adding a "CAP_" prefix (if not yet present). It also accepts the "ALL"
// magic-value.
func normalizeCap(cap string) string {
cap = strings.ToUpper(cap)
if cap == allCapabilities {
return cap
}
if !strings.HasPrefix(cap, "CAP_") {
cap = "CAP_" + cap
}
return cap
}

View File

@ -19,9 +19,10 @@ func (cli *Client) ContainerExecResize(ctx context.Context, execID string, optio
} }
func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error { func (cli *Client) resize(ctx context.Context, basePath string, height, width uint) error {
// FIXME(thaJeztah): the API / backend accepts uint32, but container.ResizeOptions uses uint.
query := url.Values{} query := url.Values{}
query.Set("h", strconv.Itoa(int(height))) query.Set("h", strconv.FormatUint(uint64(height), 10))
query.Set("w", strconv.Itoa(int(width))) query.Set("w", strconv.FormatUint(uint64(width), 10))
resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil) resp, err := cli.post(ctx, basePath+"/resize", query, nil, nil)
ensureReaderClosed(resp) ensureReaderClosed(resp)

View File

@ -12,6 +12,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
) )
// ImageBuild sends a request to the daemon to build images. // ImageBuild sends a request to the daemon to build images.
@ -44,10 +45,15 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
} }
func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.ImageBuildOptions) (url.Values, error) { func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.ImageBuildOptions) (url.Values, error) {
query := url.Values{ query := url.Values{}
"t": options.Tags, if len(options.Tags) > 0 {
"securityopt": options.SecurityOpt, query["t"] = options.Tags
"extrahosts": options.ExtraHosts, }
if len(options.SecurityOpt) > 0 {
query["securityopt"] = options.SecurityOpt
}
if len(options.ExtraHosts) > 0 {
query["extrahosts"] = options.ExtraHosts
} }
if options.SuppressOutput { if options.SuppressOutput {
query.Set("q", "1") query.Set("q", "1")
@ -58,9 +64,11 @@ func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.I
if options.NoCache { if options.NoCache {
query.Set("nocache", "1") query.Set("nocache", "1")
} }
if options.Remove { if !options.Remove {
query.Set("rm", "1") // only send value when opting out because the daemon's default is
} else { // to remove intermediate containers after a successful build,
//
// TODO(thaJeztah): deprecate "Remove" option, and provide a "NoRemove" or "Keep" option instead.
query.Set("rm", "0") query.Set("rm", "0")
} }
@ -83,42 +91,70 @@ func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.I
query.Set("isolation", string(options.Isolation)) query.Set("isolation", string(options.Isolation))
} }
query.Set("cpusetcpus", options.CPUSetCPUs) if options.CPUSetCPUs != "" {
query.Set("networkmode", options.NetworkMode) query.Set("cpusetcpus", options.CPUSetCPUs)
query.Set("cpusetmems", options.CPUSetMems)
query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
query.Set("memory", strconv.FormatInt(options.Memory, 10))
query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
query.Set("cgroupparent", options.CgroupParent)
query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
query.Set("dockerfile", options.Dockerfile)
query.Set("target", options.Target)
ulimitsJSON, err := json.Marshal(options.Ulimits)
if err != nil {
return query, err
} }
query.Set("ulimits", string(ulimitsJSON)) if options.NetworkMode != "" && options.NetworkMode != network.NetworkDefault {
query.Set("networkmode", options.NetworkMode)
buildArgsJSON, err := json.Marshal(options.BuildArgs)
if err != nil {
return query, err
} }
query.Set("buildargs", string(buildArgsJSON)) if options.CPUSetMems != "" {
query.Set("cpusetmems", options.CPUSetMems)
labelsJSON, err := json.Marshal(options.Labels)
if err != nil {
return query, err
} }
query.Set("labels", string(labelsJSON)) if options.CPUShares != 0 {
query.Set("cpushares", strconv.FormatInt(options.CPUShares, 10))
cacheFromJSON, err := json.Marshal(options.CacheFrom) }
if err != nil { if options.CPUQuota != 0 {
return query, err query.Set("cpuquota", strconv.FormatInt(options.CPUQuota, 10))
}
if options.CPUPeriod != 0 {
query.Set("cpuperiod", strconv.FormatInt(options.CPUPeriod, 10))
}
if options.Memory != 0 {
query.Set("memory", strconv.FormatInt(options.Memory, 10))
}
if options.MemorySwap != 0 {
query.Set("memswap", strconv.FormatInt(options.MemorySwap, 10))
}
if options.CgroupParent != "" {
query.Set("cgroupparent", options.CgroupParent)
}
if options.ShmSize != 0 {
query.Set("shmsize", strconv.FormatInt(options.ShmSize, 10))
}
if options.Dockerfile != "" {
query.Set("dockerfile", options.Dockerfile)
}
if options.Target != "" {
query.Set("target", options.Target)
}
if len(options.Ulimits) != 0 {
ulimitsJSON, err := json.Marshal(options.Ulimits)
if err != nil {
return query, err
}
query.Set("ulimits", string(ulimitsJSON))
}
if len(options.BuildArgs) != 0 {
buildArgsJSON, err := json.Marshal(options.BuildArgs)
if err != nil {
return query, err
}
query.Set("buildargs", string(buildArgsJSON))
}
if len(options.Labels) != 0 {
labelsJSON, err := json.Marshal(options.Labels)
if err != nil {
return query, err
}
query.Set("labels", string(labelsJSON))
}
if len(options.CacheFrom) != 0 {
cacheFromJSON, err := json.Marshal(options.CacheFrom)
if err != nil {
return query, err
}
query.Set("cachefrom", string(cacheFromJSON))
} }
query.Set("cachefrom", string(cacheFromJSON))
if options.SessionID != "" { if options.SessionID != "" {
query.Set("session", options.SessionID) query.Set("session", options.SessionID)
} }
@ -131,7 +167,9 @@ func (cli *Client) imageBuildOptionsToQuery(ctx context.Context, options types.I
if options.BuildID != "" { if options.BuildID != "" {
query.Set("buildid", options.BuildID) query.Set("buildid", options.BuildID)
} }
query.Set("version", string(options.Version)) if options.Version != "" {
query.Set("version", string(options.Version))
}
if options.Outputs != nil { if options.Outputs != nil {
outputsJSON, err := json.Marshal(options.Outputs) outputsJSON, err := json.Marshal(options.Outputs)

View File

@ -654,7 +654,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
ta.Buffer.Reset(ta.TarWriter) ta.Buffer.Reset(ta.TarWriter)
defer ta.Buffer.Reset(nil) defer ta.Buffer.Reset(nil)
_, err = io.Copy(ta.Buffer, file) _, err = pools.Copy(ta.Buffer, file)
file.Close() file.Close()
if err != nil { if err != nil {
return err return err
@ -705,7 +705,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
if err != nil { if err != nil {
return err return err
} }
if _, err := io.Copy(file, reader); err != nil { if _, err := pools.Copy(file, reader); err != nil {
file.Close() file.Close()
return err return err
} }
@ -1375,7 +1375,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
if err := tw.WriteHeader(hdr); err != nil { if err := tw.WriteHeader(hdr); err != nil {
return err return err
} }
if _, err := io.Copy(tw, srcF); err != nil { if _, err := pools.Copy(tw, srcF); err != nil {
return err return err
} }
return nil return nil

View File

@ -184,7 +184,7 @@ func (config *serviceConfig) loadMirrors(mirrors []string) error {
func (config *serviceConfig) loadInsecureRegistries(registries []string) error { func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
// Localhost is by default considered as an insecure registry. This is a // Localhost is by default considered as an insecure registry. This is a
// stop-gap for people who are running a private registry on localhost. // stop-gap for people who are running a private registry on localhost.
registries = append(registries, "127.0.0.0/8") registries = append(registries, "::1/128", "127.0.0.0/8")
var ( var (
insecureRegistryCIDRs = make([]*registry.NetIPNet, 0) insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)

2
vendor/modules.txt vendored
View File

@ -55,7 +55,7 @@ github.com/docker/distribution/registry/client/transport
github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory github.com/docker/distribution/registry/storage/cache/memory
github.com/docker/distribution/uuid github.com/docker/distribution/uuid
# github.com/docker/docker v27.0.2-0.20240912171519-164cae56ed95+incompatible # github.com/docker/docker v27.0.2-0.20241018142220-36a3bd090489+incompatible
## explicit ## explicit
github.com/docker/docker/api github.com/docker/docker/api
github.com/docker/docker/api/types github.com/docker/docker/api/types