mirror of https://github.com/docker/cli.git
Merge pull request #5402 from laurazard/backport-27.x-login-not-interactive
[27.x backport] login: handle non-tty scenario consistently
This commit is contained in:
commit
6feee4ab35
|
@ -124,17 +124,6 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||||
cli.SetIn(streams.NewIn(os.Stdin))
|
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
|
isDefaultRegistry := serverAddress == registry.IndexServer
|
||||||
defaultUsername = strings.TrimSpace(defaultUsername)
|
defaultUsername = strings.TrimSpace(defaultUsername)
|
||||||
|
|
||||||
|
|
|
@ -176,6 +176,17 @@ func isOauthLoginDisabled() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginUser(ctx context.Context, dockerCli command.Cli, opts loginOptions, defaultUsername, serverAddress string) (*registrytypes.AuthenticateOKBody, error) {
|
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 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() {
|
if serverAddress == registry.IndexServer && opts.user == "" && opts.password == "" && !isOauthLoginDisabled() {
|
||||||
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
|
response, err := loginWithDeviceCodeFlow(ctx, dockerCli)
|
||||||
|
|
|
@ -313,6 +313,145 @@ func TestRunLogin(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestLoginTermination(t *testing.T) {
|
||||||
p, tty, err := pty.Open()
|
p, tty, err := pty.Open()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
Loading…
Reference in New Issue