Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e34c9bb39 | |||
| 324cdbca40 | |||
| b5290d4e0b | |||
| 3db9538748 | |||
| 1ab89e71fa | |||
| 667d9fd4df | |||
| 41e61c45d9 | |||
| 869df10064 | |||
| 6feee4ab35 | |||
| d0c1a80617 | |||
| 383c428451 | |||
| 5f8416e541 | |||
| 5bf5cb9ff6 | |||
| 3a15d5a640 | |||
| 1dfd11acc0 | |||
| de2b49b074 | |||
| c5d846735c | |||
| 6274754e66 | |||
| 7a50cd0f01 | |||
| 074dfc0f88 | |||
| 92423287cc | |||
| 1a0b6a7a44 | |||
| 8fcfc0b803 | |||
| 28d2fed463 | |||
| 83072c0232 | |||
| 40109aa45f | |||
| 32aadc9902 |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -74,7 +74,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21.13
|
||||
go-version: 1.22.7
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
|
||||
@ -4,8 +4,8 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG XX_VERSION=1.4.0
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG XX_VERSION=1.5.0
|
||||
ARG GOVERSIONINFO_VERSION=v1.3.0
|
||||
ARG GOTESTSUM_VERSION=v1.10.0
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
|
||||
@ -124,17 +124,6 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
cli.SetIn(streams.NewIn(os.Stdin))
|
||||
}
|
||||
|
||||
// Some links documenting this:
|
||||
// - https://code.google.com/archive/p/mintty/issues/56
|
||||
// - https://github.com/docker/docker/issues/15272
|
||||
// - https://mintty.github.io/ (compatibility)
|
||||
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||
// will hit this if you attempt docker login from mintty where stdin
|
||||
// is a pipe, not a character based console.
|
||||
if argPassword == "" && !cli.In().IsTerminal() {
|
||||
return authConfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||
}
|
||||
|
||||
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||
|
||||
|
||||
@ -39,8 +39,8 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "login [OPTIONS] [SERVER]",
|
||||
Short: "Log in to a registry",
|
||||
Long: "Log in to a registry.\nIf no server is specified, the default is defined by the daemon.",
|
||||
Short: "Authenticate to a registry",
|
||||
Long: "Authenticate to a registry.\nDefaults to Docker Hub if no server is specified.",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
@ -111,9 +111,7 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
|
||||
serverAddress string
|
||||
response *registrytypes.AuthenticateOKBody
|
||||
)
|
||||
if opts.serverAddress != "" &&
|
||||
opts.serverAddress != registry.DefaultNamespace &&
|
||||
opts.serverAddress != registry.DefaultRegistryHost {
|
||||
if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace {
|
||||
serverAddress = opts.serverAddress
|
||||
} else {
|
||||
serverAddress = registry.IndexServer
|
||||
@ -129,7 +127,7 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
|
||||
// if we failed to authenticate with stored credentials (or didn't have stored credentials),
|
||||
// prompt the user for new credentials
|
||||
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
|
||||
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, serverAddress)
|
||||
response, err = loginUser(ctx, dockerCli, opts, authConfig.Username, authConfig.ServerAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -178,6 +176,17 @@ func isOauthLoginDisabled() bool {
|
||||
}
|
||||
|
||||
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
||||
// Some links documenting this:
|
||||
// - https://code.google.com/archive/p/mintty/issues/56
|
||||
// - https://github.com/docker/docker/issues/15272
|
||||
// - https://mintty.github.io/ (compatibility)
|
||||
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
||||
// will hit this if you attempt docker login from mintty where stdin
|
||||
// is a pipe, not a character based console.
|
||||
if (opts.user == "" || opts.password == "") && !dockerCli.In().IsTerminal() {
|
||||
return nil, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
||||
}
|
||||
|
||||
// If we're logging into the index server and the user didn't provide a username or password, use the device flow
|
||||
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
|
||||
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
|
||||
|
||||
@ -16,6 +16,7 @@ import (
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@ -83,88 +84,207 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRunLogin(t *testing.T) {
|
||||
const (
|
||||
storedServerAddress = "reg1"
|
||||
validUsername = "u1"
|
||||
validPassword = "p1"
|
||||
validPassword2 = "p2"
|
||||
)
|
||||
|
||||
validAuthConfig := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: validPassword,
|
||||
}
|
||||
expiredAuthConfig := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: expiredPassword,
|
||||
}
|
||||
validIdentityToken := configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
IdentityToken: useToken,
|
||||
}
|
||||
testCases := []struct {
|
||||
doc string
|
||||
inputLoginOption loginOptions
|
||||
inputStoredCred *configtypes.AuthConfig
|
||||
expectedErr string
|
||||
expectedSavedCred configtypes.AuthConfig
|
||||
doc string
|
||||
priorCredentials map[string]configtypes.AuthConfig
|
||||
input loginOptions
|
||||
expectedCredentials map[string]configtypes.AuthConfig
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "valid auth from store",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedSavedCred: validAuthConfig,
|
||||
},
|
||||
{
|
||||
doc: "expired auth",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
inputStoredCred: &expiredAuthConfig,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "valid username and password",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
user: validUsername,
|
||||
password: validPassword2,
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedSavedCred: configtypes.AuthConfig{
|
||||
ServerAddress: storedServerAddress,
|
||||
Username: validUsername,
|
||||
Password: validPassword2,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown user",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
doc: "expired auth from store",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: expiredPassword,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "store valid username and password",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: "p2",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "p2",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "unknown user w/ prior credentials",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: unknownUser,
|
||||
password: validPassword,
|
||||
password: "a-password",
|
||||
},
|
||||
expectedErr: errUnknownUser,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "a-password",
|
||||
Password: "a-password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
inputStoredCred: &validAuthConfig,
|
||||
expectedErr: errUnknownUser,
|
||||
},
|
||||
{
|
||||
doc: "valid token",
|
||||
inputLoginOption: loginOptions{
|
||||
serverAddress: storedServerAddress,
|
||||
user: validUsername,
|
||||
doc: "unknown user w/o prior credentials",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: unknownUser,
|
||||
password: "a-password",
|
||||
},
|
||||
expectedErr: errUnknownUser,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{},
|
||||
},
|
||||
{
|
||||
doc: "store valid token",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: useToken,
|
||||
},
|
||||
inputStoredCred: &validIdentityToken,
|
||||
expectedSavedCred: validIdentityToken,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
IdentityToken: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "valid token from store",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
IdentityToken: useToken,
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "no registry specified defaults to index server",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
user: "my-username",
|
||||
password: "my-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
registry.IndexServer: {
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: registry.IndexServer,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "registry-1.docker.io",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io",
|
||||
user: "my-username",
|
||||
password: "my-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Regression test for https://github.com/docker/cli/issues/5382
|
||||
{
|
||||
doc: "sanitizes server address to remove repo",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io/bork/test",
|
||||
user: "my-username",
|
||||
password: "a-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
// Regression test for https://github.com/docker/cli/issues/5382
|
||||
{
|
||||
doc: "updates credential if server address includes repo",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "a-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
input: loginOptions{
|
||||
serverAddress: "registry-1.docker.io/bork/test",
|
||||
user: "my-username",
|
||||
password: "new-password",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"registry-1.docker.io": {
|
||||
Username: "my-username",
|
||||
Password: "new-password",
|
||||
ServerAddress: "registry-1.docker.io",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
@ -172,23 +292,166 @@ func TestRunLogin(t *testing.T) {
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
|
||||
if tc.inputStoredCred != nil {
|
||||
cred := *tc.inputStoredCred
|
||||
assert.NilError(t, configfile.GetCredentialsStore(cred.ServerAddress).Store(cred))
|
||||
for _, priorCred := range tc.priorCredentials {
|
||||
assert.NilError(t, configfile.GetCredentialsStore(priorCred.ServerAddress).Store(priorCred))
|
||||
}
|
||||
loginErr := runLogin(context.Background(), cli, tc.inputLoginOption)
|
||||
storedCreds, err := configfile.GetAllCredentials()
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, storedCreds, tc.priorCredentials)
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, tc.input)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
savedCred, credStoreErr := configfile.GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress)
|
||||
assert.Check(t, credStoreErr)
|
||||
assert.DeepEqual(t, tc.expectedSavedCred, savedCred)
|
||||
|
||||
outputCreds, err := configfile.GetAllCredentials()
|
||||
assert.Check(t, err)
|
||||
assert.DeepEqual(t, outputCreds, tc.expectedCredentials)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginNonInteractive(t *testing.T) {
|
||||
t.Run("no prior credentials", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
username bool
|
||||
password bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success - w/ user w/ password",
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/o pass ",
|
||||
username: false,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
// "" meaning default registry
|
||||
registries := []string{"", "my-registry.com"}
|
||||
|
||||
for _, registry := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
options := loginOptions{
|
||||
serverAddress: registry,
|
||||
}
|
||||
if tc.username {
|
||||
options.user = "my-username"
|
||||
}
|
||||
if tc.password {
|
||||
options.password = "my-password"
|
||||
}
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, options)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("w/ prior credentials", func(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
username bool
|
||||
password bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "success - w/ user w/ password",
|
||||
username: true,
|
||||
password: true,
|
||||
},
|
||||
{
|
||||
doc: "success - w/o user w/o pass ",
|
||||
username: false,
|
||||
password: false,
|
||||
},
|
||||
{
|
||||
doc: "error - w/ user w/o pass",
|
||||
username: true,
|
||||
password: false,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
{
|
||||
doc: "error - w/o user w/ pass",
|
||||
username: false,
|
||||
password: true,
|
||||
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
|
||||
},
|
||||
}
|
||||
|
||||
// "" meaning default registry
|
||||
registries := []string{"", "my-registry.com"}
|
||||
|
||||
for _, registry := range registries {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
tmpFile := fs.NewFile(t, "test-run-login")
|
||||
defer tmpFile.Remove()
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
configfile := cli.ConfigFile()
|
||||
configfile.Filename = tmpFile.Path()
|
||||
serverAddress := registry
|
||||
if serverAddress == "" {
|
||||
serverAddress = "https://index.docker.io/v1/"
|
||||
}
|
||||
assert.NilError(t, configfile.GetCredentialsStore(serverAddress).Store(configtypes.AuthConfig{
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
ServerAddress: serverAddress,
|
||||
}))
|
||||
|
||||
options := loginOptions{
|
||||
serverAddress: registry,
|
||||
}
|
||||
if tc.username {
|
||||
options.user = "my-username"
|
||||
}
|
||||
if tc.password {
|
||||
options.password = "my-password"
|
||||
}
|
||||
|
||||
loginErr := runLogin(context.Background(), cli, options)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, loginErr, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
assert.NilError(t, loginErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoginTermination(t *testing.T) {
|
||||
p, tty, err := pty.Open()
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -45,14 +45,14 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "ssh host connection is not valid")
|
||||
}
|
||||
sshFlags = addSSHTimeout(sshFlags)
|
||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||
return &ConnectionHelper{
|
||||
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
args := []string{"docker"}
|
||||
if sp.Path != "" {
|
||||
args = append(args, "--host", "unix://"+sp.Path)
|
||||
}
|
||||
sshFlags = addSSHTimeout(sshFlags)
|
||||
sshFlags = disablePseudoTerminalAllocation(sshFlags)
|
||||
args = append(args, "system", "dial-stdio")
|
||||
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
|
||||
},
|
||||
|
||||
@ -96,18 +96,32 @@ func tryDecodeOAuthError(resp *http.Response) error {
|
||||
// authenticated or we have reached the time limit for authenticating (based on
|
||||
// the response from GetDeviceCode).
|
||||
func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
ticker := time.NewTicker(state.IntervalDuration())
|
||||
// Ticker for polling tenant for login – based on the interval
|
||||
// specified by the tenant response.
|
||||
ticker := time.NewTimer(state.IntervalDuration())
|
||||
defer ticker.Stop()
|
||||
timeout := time.After(state.ExpiryDuration())
|
||||
// The tenant tells us for as long as we can poll it for credentials
|
||||
// while the user logs in through their browser. Timeout if we don't get
|
||||
// credentials within this period.
|
||||
timeout := time.NewTimer(state.ExpiryDuration())
|
||||
defer timeout.Stop()
|
||||
|
||||
for {
|
||||
resetTimer(ticker, state.IntervalDuration())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// user canceled login
|
||||
return TokenResponse{}, ctx.Err()
|
||||
case <-ticker.C:
|
||||
// tick, check for user login
|
||||
res, err := a.getDeviceToken(ctx, state)
|
||||
if err != nil {
|
||||
return res, err
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// if the caller canceled the context, continue
|
||||
// and let the select hit the ctx.Done() branch
|
||||
continue
|
||||
}
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
@ -119,14 +133,33 @@ func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse
|
||||
}
|
||||
|
||||
return res, nil
|
||||
case <-timeout:
|
||||
case <-timeout.C:
|
||||
// login timed out
|
||||
return TokenResponse{}, ErrTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimer is a helper function thatstops, drains and resets the timer.
|
||||
// This is necessary in go versions <1.23, since the timer isn't stopped +
|
||||
// the timer's channel isn't drained on timer.Reset.
|
||||
// See: https://go-review.googlesource.com/c/go/+/568341
|
||||
// FIXME: remove/simplify this after we update to go1.23
|
||||
func resetTimer(t *time.Timer, d time.Duration) {
|
||||
if !t.Stop() {
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
t.Reset(d)
|
||||
}
|
||||
|
||||
// getToken calls the token endpoint of Auth0 and returns the response.
|
||||
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
|
||||
|
||||
@ -198,7 +198,7 @@ func TestWaitForDeviceToken(t *testing.T) {
|
||||
state := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
Interval: 1,
|
||||
Interval: 5,
|
||||
ExpiresIn: 1,
|
||||
}
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.Aut
|
||||
return nil, ErrDeviceLoginStartFail
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB BASED LOGIN"))
|
||||
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN"))
|
||||
_, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u <username>'")
|
||||
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
|
||||
_, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
variable "GO_VERSION" {
|
||||
default = "1.21.13"
|
||||
default = "1.22.7"
|
||||
}
|
||||
variable "VERSION" {
|
||||
default = ""
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
ARG BUILDX_VERSION=0.16.1
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GOLANGCI_LINT_VERSION=v1.59.1
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ The base command for the Docker CLI.
|
||||
| [`inspect`](inspect.md) | Return low-level information on Docker objects |
|
||||
| [`kill`](kill.md) | Kill one or more running containers |
|
||||
| [`load`](load.md) | Load an image from a tar archive or STDIN |
|
||||
| [`login`](login.md) | Log in to a registry |
|
||||
| [`login`](login.md) | Authenticate to a registry |
|
||||
| [`logout`](logout.md) | Log out from a registry |
|
||||
| [`logs`](logs.md) | Fetch the logs of a container |
|
||||
| [`manifest`](manifest.md) | Manage Docker image manifests and manifest lists |
|
||||
|
||||
@ -1,60 +1,51 @@
|
||||
# login
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Log in to a registry.
|
||||
If no server is specified, the default is defined by the daemon.
|
||||
Authenticate to a registry.
|
||||
Defaults to Docker Hub if no server is specified.
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------------------------|:---------|:--------|:-----------------------------|
|
||||
| `-p`, `--password` | `string` | | Password |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin |
|
||||
| `-u`, `--username` | `string` | | Username |
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------------------------------|:---------|:--------|:-----------------------------|
|
||||
| `-p`, `--password` | `string` | | Password |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the password from stdin |
|
||||
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
## Description
|
||||
|
||||
Log in to a registry.
|
||||
Authenticate to a registry.
|
||||
|
||||
## Examples
|
||||
You can authenticate to any public or private registry for which you have
|
||||
credentials. Authentication may be required for pulling and pushing images.
|
||||
Other commands, such as `docker scout` and `docker build`, may also require
|
||||
authentication to access subscription-only features or data related to your
|
||||
Docker organization.
|
||||
|
||||
### Login to a self-hosted registry
|
||||
Authentication credentials are stored in the configured [credential
|
||||
store](#credential-stores). If you use Docker Desktop, credentials are
|
||||
automatically saved to the native keychain of your operating system. If you're
|
||||
not using Docker Desktop, you can configure the credential store in the Docker
|
||||
configuration file, which is located at `$HOME/.docker/config.json` on Linux or
|
||||
`%USERPROFILE%/.docker/config.json` on Windows. If you don't configure a
|
||||
credential store, Docker stores credentials in the `config.json` file in a
|
||||
base64-encoded format. This method is less secure than configuring and using a
|
||||
credential store.
|
||||
|
||||
If you want to log in to a self-hosted registry you can specify this by
|
||||
adding the server name.
|
||||
`docker login` also supports [credential helpers](#credential-helpers) to help
|
||||
you handle credentials for specific registries.
|
||||
|
||||
```console
|
||||
$ docker login localhost:8080
|
||||
```
|
||||
### Authentication methods
|
||||
|
||||
### <a name="password-stdin"></a> Provide a password using STDIN (--password-stdin)
|
||||
|
||||
To run the `docker login` command non-interactively, you can set the
|
||||
`--password-stdin` flag to provide a password through `STDIN`. Using
|
||||
`STDIN` prevents the password from ending up in the shell's history,
|
||||
or log-files.
|
||||
|
||||
The following example reads a password from a file, and passes it to the
|
||||
`docker login` command using `STDIN`:
|
||||
|
||||
```console
|
||||
$ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
```
|
||||
|
||||
### Privileged user requirement
|
||||
|
||||
`docker login` requires you to use `sudo` or be `root`, except when:
|
||||
|
||||
- Connecting to a remote daemon, such as a `docker-machine` provisioned `docker engine`.
|
||||
- The user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/engine/security/#docker-daemon-attack-surface) for details.
|
||||
|
||||
You can log in to any public or private repository for which you have
|
||||
credentials. When you log in, the command stores credentials in
|
||||
`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on
|
||||
Windows, via the procedure described below.
|
||||
You can authenticate to a registry using a username and access token or
|
||||
password. Docker Hub also supports a web-based sign-in flow, which signs you in
|
||||
to your Docker account without entering your password. For Docker Hub, the
|
||||
`docker login` command uses a device code flow by default, unless the
|
||||
`--username` flag is specified. The device code flow is a secure way to sign
|
||||
in. See [Authenticate to Docker Hub using device code](#authenticate-to-docker-hub-using-device-code).
|
||||
|
||||
### Credential stores
|
||||
|
||||
@ -75,6 +66,10 @@ Helpers are available for the following credential stores:
|
||||
- Microsoft Windows Credential Manager
|
||||
- [pass](https://www.passwordstore.org/)
|
||||
|
||||
With Docker Desktop, the credential store is already installed and configured
|
||||
for you. Unless you want to change the credential store used by Docker Desktop,
|
||||
you can skip the following steps.
|
||||
|
||||
#### Configure the credential store
|
||||
|
||||
You need to specify the credential store in `$HOME/.docker/config.json`
|
||||
@ -94,22 +89,22 @@ the credentials from the file and run `docker login` again.
|
||||
#### Default behavior
|
||||
|
||||
By default, Docker looks for the native binary on each of the platforms, i.e.
|
||||
"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special
|
||||
case is that on Linux, Docker will fall back to the "secretservice" binary if
|
||||
it cannot find the "pass" binary. If none of these binaries are present, it
|
||||
stores the credentials (i.e. password) in base64 encoding in the config files
|
||||
described above.
|
||||
`osxkeychain` on macOS, `wincred` on Windows, and `pass` on Linux. A special
|
||||
case is that on Linux, Docker will fall back to the `secretservice` binary if
|
||||
it cannot find the `pass` binary. If none of these binaries are present, it
|
||||
stores the base64-encoded credentials in the `config.json` configuration file.
|
||||
|
||||
#### Credential helper protocol
|
||||
|
||||
Credential helpers can be any program or script that follows a very simple protocol.
|
||||
This protocol is heavily inspired by Git, but it differs in the information shared.
|
||||
Credential helpers can be any program or script that implements the credential
|
||||
helper protocol. This protocol is inspired by Git, but differs in the
|
||||
information shared.
|
||||
|
||||
The helpers always use the first argument in the command to identify the action.
|
||||
There are only three possible values for that argument: `store`, `get`, and `erase`.
|
||||
|
||||
The `store` command takes a JSON payload from the standard input. That payload carries
|
||||
the server address, to identify the credential, the user name, and either a password
|
||||
the server address, to identify the credential, the username, and either a password
|
||||
or an identity token.
|
||||
|
||||
```json
|
||||
@ -149,10 +144,10 @@ will show if there was an issue.
|
||||
|
||||
### Credential helpers
|
||||
|
||||
Credential helpers are similar to the credential store above, but act as the
|
||||
designated programs to handle credentials for specific registries. The default
|
||||
credential store (`credsStore` or the config file itself) will not be used for
|
||||
operations concerning credentials of the specified registries.
|
||||
Credential helpers are similar to [credential stores](#credential-stores), but
|
||||
act as the designated programs to handle credentials for specific registries.
|
||||
The default credential store will not be used for operations concerning
|
||||
credentials of the specified registries.
|
||||
|
||||
#### Configure credential helpers
|
||||
|
||||
@ -162,19 +157,93 @@ the credentials from the default store.
|
||||
Credential helpers are specified in a similar way to `credsStore`, but
|
||||
allow for multiple helpers to be configured at a time. Keys specify the
|
||||
registry domain, and values specify the suffix of the program to use
|
||||
(i.e. everything after `docker-credential-`).
|
||||
For example:
|
||||
(i.e. everything after `docker-credential-`). For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"credHelpers": {
|
||||
"registry.example.com": "registryhelper",
|
||||
"awesomereg.example.org": "hip-star",
|
||||
"unicorn.example.io": "vcbait"
|
||||
"myregistry.example.com": "secretservice",
|
||||
"docker.internal.example": "pass",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Authenticate to Docker Hub with web-based login
|
||||
|
||||
By default, the `docker login` command authenticates to Docker Hub, using a
|
||||
device code flow. This flow lets you authenticate to Docker Hub without
|
||||
entering your password. Instead, you visit a URL in your web browser, enter a
|
||||
code, and authenticate.
|
||||
|
||||
```console
|
||||
$ docker login
|
||||
|
||||
USING WEB-BASED LOGIN
|
||||
To sign in with credentials on the command line, use 'docker login -u <username>'
|
||||
|
||||
Your one-time device confirmation code is: LNFR-PGCJ
|
||||
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate
|
||||
|
||||
Waiting for authentication in the browser…
|
||||
```
|
||||
|
||||
After entering the code in your browser, you are authenticated to Docker Hub
|
||||
using the account you're currently signed in with on the Docker Hub website or
|
||||
in Docker Desktop. If you aren't signed in, you are prompted to sign in after
|
||||
entering the device code.
|
||||
|
||||
### Authenticate to a self-hosted registry
|
||||
|
||||
If you want to authenticate to a self-hosted registry you can specify this by
|
||||
adding the server name.
|
||||
|
||||
```console
|
||||
$ docker login registry.example.com
|
||||
```
|
||||
|
||||
By default, the `docker login` command assumes that the registry listens on
|
||||
port 443 or 80. If the registry listens on a different port, you can specify it
|
||||
by adding the port number to the server name.
|
||||
|
||||
```console
|
||||
$ docker login registry.example.com:1337
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Registry addresses should not include URL path components, only the hostname
|
||||
> and (optionally) the port. Registry addresses with URL path components may
|
||||
> result in an error. For example, `docker login registry.example.com/foo/`
|
||||
> is incorrect, while `docker login registry.example.com` is correct.
|
||||
>
|
||||
> The exception to this rule is the Docker Hub registry, which may use the
|
||||
> `/v1/` path component in the address for historical reasons.
|
||||
|
||||
### <a name="username"></a> Authenticate to a registry with a username and password
|
||||
|
||||
To authenticate to a registry with a username and password, you can use the
|
||||
`--username` or `-u` flag. The following example authenticates to Docker Hub
|
||||
with the username `moby`. The password is entered interactively.
|
||||
|
||||
```console
|
||||
$ docker login -u moby
|
||||
```
|
||||
|
||||
### <a name="password-stdin"></a> Provide a password using STDIN (--password-stdin)
|
||||
|
||||
To run the `docker login` command non-interactively, you can set the
|
||||
`--password-stdin` flag to provide a password through `STDIN`. Using
|
||||
`STDIN` prevents the password from ending up in the shell's history,
|
||||
or log-files.
|
||||
|
||||
The following example reads a password from a file, and passes it to the
|
||||
`docker login` command using `STDIN`:
|
||||
|
||||
```console
|
||||
$ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
```
|
||||
|
||||
## Related commands
|
||||
|
||||
* [logout](logout.md)
|
||||
|
||||
@ -30,7 +30,7 @@ func TestOauthLogin(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
|
||||
output, _ := io.ReadAll(p)
|
||||
assert.Check(t, strings.Contains(string(output), "USING WEB BASED LOGIN"), string(output))
|
||||
assert.Check(t, strings.Contains(string(output), "USING WEB-BASED LOGIN"), string(output))
|
||||
}
|
||||
|
||||
func TestLoginWithEscapeHatch(t *testing.T) {
|
||||
|
||||
2
e2e/testdata/Dockerfile.gencerts
vendored
2
e2e/testdata/Dockerfile.gencerts
vendored
@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.13
|
||||
ARG GO_VERSION=1.22.7
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine AS generated
|
||||
ENV GOTOOLCHAIN=local
|
||||
|
||||
@ -5,6 +5,12 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# Disable CGO - we don't need it for these plugins.
|
||||
#
|
||||
# Important: this must be done before sourcing "./scripts/build/.variables",
|
||||
# because some other variables are conditionally set whether CGO is enabled.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
source ./scripts/build/.variables
|
||||
|
||||
for p in cli-plugins/examples/* "$@" ; do
|
||||
@ -15,5 +21,5 @@ for p in cli-plugins/examples/* "$@" ; do
|
||||
mkdir -p "$(dirname "${TARGET_PLUGIN}")"
|
||||
|
||||
echo "Building $GO_LINKMODE $(basename "${TARGET_PLUGIN}")"
|
||||
(set -x ; CGO_ENABLED=0 GO111MODULE=auto go build -o "${TARGET_PLUGIN}" -tags "${GO_BUILDTAGS}" -ldflags "${GO_LDFLAGS}" ${GO_BUILDMODE} "github.com/docker/cli/${p}")
|
||||
(set -x ; GO111MODULE=auto go build -o "${TARGET_PLUGIN}" -tags "${GO_BUILDTAGS}" -ldflags "${GO_LDFLAGS}" ${GO_BUILDMODE} "github.com/docker/cli/${p}")
|
||||
done
|
||||
|
||||
@ -13,7 +13,7 @@ require (
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.8.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible // 27.x branch (v27.2.0-dev)
|
||||
github.com/docker/docker v27.2.1-0.20240906095740-8b539b8df240+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.8.2
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/docker/go-units v0.5.0
|
||||
@ -32,6 +32,7 @@ require (
|
||||
github.com/morikuni/aec v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.8.1
|
||||
@ -80,7 +81,6 @@ require (
|
||||
github.com/moby/sys/symlink v0.2.0 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
|
||||
@ -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.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible h1:EuHKrI999zfPX8J2mF6AcHVAMQvSTkawrSGh81s5ncU=
|
||||
github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.2.1-0.20240906095740-8b539b8df240+incompatible h1:Ge62WWQ9qg3GHZltYdb0zH1Uq/LVUi8b2/zZ3L1uaAE=
|
||||
github.com/docker/docker v27.2.1-0.20240906095740-8b539b8df240+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/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
|
||||
2
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
2
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
@ -5347,7 +5347,7 @@ definitions:
|
||||
The version Go used to compile the daemon, and the version of the Go
|
||||
runtime in use.
|
||||
type: "string"
|
||||
example: "go1.21.13"
|
||||
example: "go1.22.7"
|
||||
Os:
|
||||
description: |
|
||||
The operating system that the daemon is running on ("linux" or "windows")
|
||||
|
||||
5
vendor/github.com/docker/docker/api/types/container/hostconfig.go
generated
vendored
5
vendor/github.com/docker/docker/api/types/container/hostconfig.go
generated
vendored
@ -1,6 +1,7 @@
|
||||
package container // import "github.com/docker/docker/api/types/container"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@ -325,12 +326,12 @@ func ValidateRestartPolicy(policy RestartPolicy) error {
|
||||
if policy.MaximumRetryCount < 0 {
|
||||
msg += " and cannot be negative"
|
||||
}
|
||||
return &errInvalidParameter{fmt.Errorf(msg)}
|
||||
return &errInvalidParameter{errors.New(msg)}
|
||||
}
|
||||
return nil
|
||||
case RestartPolicyOnFailure:
|
||||
if policy.MaximumRetryCount < 0 {
|
||||
return &errInvalidParameter{fmt.Errorf("invalid restart policy: maximum retry count cannot be negative")}
|
||||
return &errInvalidParameter{errors.New("invalid restart policy: maximum retry count cannot be negative")}
|
||||
}
|
||||
return nil
|
||||
case "":
|
||||
|
||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -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/memory
|
||||
github.com/docker/distribution/uuid
|
||||
# github.com/docker/docker v27.2.0-rc.1.0.20240827140014-3ab5c7d0036c+incompatible
|
||||
# github.com/docker/docker v27.2.1-0.20240906095740-8b539b8df240+incompatible
|
||||
## explicit
|
||||
github.com/docker/docker/api
|
||||
github.com/docker/docker/api/types
|
||||
|
||||
Reference in New Issue
Block a user