Skip to content

Commit

Permalink
fix: ctx cancellation on login prompt
Browse files Browse the repository at this point in the history
Signed-off-by: Alano Terblanche <[email protected]>
  • Loading branch information
Benehiko committed Jun 18, 2024
1 parent 70b53a0 commit 623f59b
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 37 deletions.
48 changes: 12 additions & 36 deletions cli/command/registry.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package command

import (
"bufio"
"context"
"fmt"
"io"
"os"
"runtime"
"strings"
Expand All @@ -17,7 +15,6 @@ import (
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/registry"
"github.com/moby/term"
"github.com/pkg/errors"
)

Expand All @@ -43,7 +40,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default:
}

err = ConfigureAuth(cli, "", "", &authConfig, isDefaultRegistry)
err = ConfigureAuth(ctx, cli, "", "", &authConfig, isDefaultRegistry)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -89,7 +86,7 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
}

// ConfigureAuth handles prompting of user's username and password if needed
func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
// On Windows, force the use of the regular OS stdin stream.
//
// See:
Expand Down Expand Up @@ -124,9 +121,15 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
fmt.Fprintln(cli.Out())
}
}
promptWithDefault(cli.Out(), "Username", authconfig.Username)

var prompt string
if authconfig.Username == "" {
prompt = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", authconfig.Username)
}
var err error
flUser, err = readInput(cli.In())
flUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil {
return err
}
Expand All @@ -138,16 +141,8 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
return errors.Errorf("Error: Non-null Username Required")
}
if flPassword == "" {
oldState, err := term.SaveState(cli.In().FD())
if err != nil {
return err
}
fmt.Fprintf(cli.Out(), "Password: ")
_ = term.DisableEcho(cli.In().FD(), oldState)
defer func() {
_ = term.RestoreTerminal(cli.In().FD(), oldState)
}()
flPassword, err = readInput(cli.In())
var err error
flPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ", WithHideUserInput(true))
if err != nil {
return err
}
Expand All @@ -163,25 +158,6 @@ func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes
return nil
}

// readInput reads, and returns user input from in. It tries to return a
// single line, not including the end-of-line bytes, and trims leading
// and trailing whitespace.
func readInput(in io.Reader) (string, error) {
line, _, err := bufio.NewReader(in).ReadLine()
if err != nil {
return "", errors.Wrap(err, "error while reading input")
}
return strings.TrimSpace(string(line)), nil
}

func promptWithDefault(out io.Writer, prompt string, configDefault string) {
if configDefault == "" {
fmt.Fprintf(out, "%s: ", prompt)
} else {
fmt.Fprintf(out, "%s (%s): ", prompt, configDefault)
}
}

// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete
// image. The auth configuration is serialized as a base64url encoded RFC4648,
// section 5) JSON string for sending through the X-Registry-Auth header.
Expand Down
2 changes: 1 addition & 1 deletion cli/command/registry/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func runLogin(ctx context.Context, dockerCli command.Cli, opts loginOptions) err
response, err = loginWithCredStoreCreds(ctx, dockerCli, &authConfig)
}
if err != nil || authConfig.Username == "" || authConfig.Password == "" {
err = command.ConfigureAuth(dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
err = command.ConfigureAuth(ctx, dockerCli, opts.user, opts.password, &authConfig, isDefaultRegistry)
if err != nil {
return err
}
Expand Down
64 changes: 64 additions & 0 deletions cli/command/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/errdefs"
"github.com/moby/sys/sequential"
"github.com/moby/term"
"github.com/pkg/errors"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -76,6 +77,69 @@ func PrettyPrint(i any) string {

var ErrPromptTerminated = errdefs.Cancelled(errors.New("prompt terminated"))

type promptOptions struct {
disableEcho bool
}

type PromptOptions func(*promptOptions)

func WithHideUserInput(t bool) PromptOptions {
return func(p *promptOptions) {
p.disableEcho = t
}
}

// PromptForInput requests input from the user.
//
// If the user terminates the CLI with SIGINT or SIGTERM while the prompt is
// active, the prompt will return an empty string ("") with an ErrPromptTerminated error.
// When the prompt returns an error, the caller should propagate the error up
// the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely.
func PromptForInput(ctx context.Context, ins *streams.In, outs *streams.Out, message string, opts ...PromptOptions) (string, error) {
options := promptOptions{}
for _, o := range opts {
o(&options)
}

_, _ = fmt.Fprint(outs, message)

if options.disableEcho {
oldState, err := term.SaveState(ins.FD())
if err != nil {
return "", err
}
_ = term.DisableEcho(ins.FD(), oldState)
defer func() {
_ = term.RestoreTerminal(ins.FD(), oldState)
}()
}

// On Windows, force the use of the regular OS stdin stream.
if runtime.GOOS == "windows" {
ins = streams.NewIn(os.Stdin)
}

result := make(chan string)

go func() {
reader := bufio.NewReader(ins)
line, err := reader.ReadString('\n')
if err != nil {
result <- ""
}
result <- strings.TrimSpace(line)
}()

select {
case <-ctx.Done():
_, _ = fmt.Fprintln(outs, "")
return "", ErrPromptTerminated
case r := <-result:
return r, nil
}
}

// PromptForConfirmation requests and checks confirmation from the user.
// This will display the provided message followed by ' [y/N] '. If the user
// input 'y' or 'Y' it returns true otherwise false. If no message is provided,
Expand Down

0 comments on commit 623f59b

Please sign in to comment.