From 23aca88ad00b419c00e2553f79f89ff2e6fe422d Mon Sep 17 00:00:00 2001 From: apostasie Date: Sun, 1 Sep 2024 15:57:56 -0700 Subject: [PATCH] Refactor: cleanup prompting Signed-off-by: apostasie --- pkg/cmd/login/login.go | 59 +--------- pkg/cmd/login/prompt.go | 109 ++++++++++++++++++ .../login/{login_unix.go => prompt_unix.go} | 22 ++-- .../{login_windows.go => prompt_windows.go} | 13 +-- 4 files changed, 130 insertions(+), 73 deletions(-) create mode 100644 pkg/cmd/login/prompt.go rename pkg/cmd/login/{login_unix.go => prompt_unix.go} (73%) rename pkg/cmd/login/{login_windows.go => prompt_windows.go} (79%) diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go index 6d5f5a026d4..6f4492ae71b 100644 --- a/pkg/cmd/login/login.go +++ b/pkg/cmd/login/login.go @@ -17,18 +17,14 @@ package login import ( - "bufio" "context" "errors" "fmt" "io" "net/http" "net/url" - "os" - "strings" "golang.org/x/net/context/ctxhttp" - "golang.org/x/term" "github.com/containerd/containerd/v2/core/remotes/docker" "github.com/containerd/containerd/v2/core/remotes/docker/config" @@ -66,7 +62,7 @@ func Login(ctx context.Context, options types.LoginCommandOptions, stdout io.Wri } if err != nil || credentials.Username == "" || credentials.Password == "" { - err = configureAuthentication(credentials, options.Username, options.Password) + err = promptUserForAuthentication(credentials, options.Username, options.Password, stdout) if err != nil { return err } @@ -205,56 +201,3 @@ func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error { return errors.New("too many 401 (probably)") } - -func configureAuthentication(credentials *dockerconfigresolver.Credentials, username, password string) error { - if username = strings.TrimSpace(username); username == "" { - username = credentials.Username - } - if username == "" { - fmt.Print("Enter Username: ") - usr, err := readUsername() - if err != nil { - return err - } - username = usr - } - if username == "" { - return fmt.Errorf("error: Username is Required") - } - - if password == "" { - fmt.Print("Enter Password: ") - pwd, err := readPassword() - fmt.Println() - if err != nil { - return err - } - password = pwd - } - if password == "" { - return fmt.Errorf("error: Password is Required") - } - - credentials.Username = username - credentials.Password = password - - return nil -} - -func readUsername() (string, error) { - var fd *os.File - if term.IsTerminal(int(os.Stdin.Fd())) { - fd = os.Stdin - } else { - return "", fmt.Errorf("stdin is not a terminal (Hint: use `nerdctl login --username=USERNAME --password-stdin`)") - } - - reader := bufio.NewReader(fd) - username, err := reader.ReadString('\n') - if err != nil { - return "", fmt.Errorf("error reading username: %w", err) - } - username = strings.TrimSpace(username) - - return username, nil -} diff --git a/pkg/cmd/login/prompt.go b/pkg/cmd/login/prompt.go new file mode 100644 index 00000000000..db1f6554244 --- /dev/null +++ b/pkg/cmd/login/prompt.go @@ -0,0 +1,109 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package login + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "strings" + + "golang.org/x/term" + + "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" +) + +var ( + // User did not provide non-empty credentials when prompted for it + ErrUsernameIsRequired = errors.New("username is required") + ErrPasswordIsRequired = errors.New("password is required") + + // System errors - not a terminal, failure to read, etc + ErrReadingUsername = errors.New("unable to read username") + ErrReadingPassword = errors.New("unable to read password") + ErrNotATerminal = errors.New("stdin is not a terminal (Hint: use `nerdctl login --username=USERNAME --password-stdin`)") + ErrCannotAllocateTerminal = errors.New("error allocating terminal") +) + +// promptUserForAuthentication will prompt the user for credentials if needed +// It might error with any of the errors defined above. +func promptUserForAuthentication(credentials *dockerconfigresolver.Credentials, username, password string, stdout io.Writer) error { + var err error + + // If the provided username is empty... + if username = strings.TrimSpace(username); username == "" { + // Use the one we know of (from the store) + username = credentials.Username + // If the one from the store was empty as well, prompt and read the username + if username == "" { + _, _ = fmt.Fprint(stdout, "Enter Username: ") + username, err = readUsername() + if err != nil { + return err + } + + username = strings.TrimSpace(username) + // If it still is empty, that is an error + if username == "" { + return ErrUsernameIsRequired + } + } + } + + // If password was NOT passed along, ask for it + if password == "" { + _, _ = fmt.Fprint(stdout, "Enter Password: ") + password, err = readPassword() + if err != nil { + return err + } + + _, _ = fmt.Fprintln(stdout) + password = strings.TrimSpace(password) + + // If nothing was provided, error out + if password == "" { + return ErrPasswordIsRequired + } + } + + // Attach non-empty credentials to the auth object and return + credentials.Username = username + credentials.Password = password + + return nil +} + +// readUsername will try to read from user input +// It might error with: +// - ErrNotATerminal +// - ErrReadingUsername +func readUsername() (string, error) { + fd := os.Stdin + if !term.IsTerminal(int(fd.Fd())) { + return "", ErrNotATerminal + } + + username, err := bufio.NewReader(fd).ReadString('\n') + if err != nil { + return "", errors.Join(ErrReadingUsername, err) + } + + return strings.TrimSpace(username), nil +} diff --git a/pkg/cmd/login/login_unix.go b/pkg/cmd/login/prompt_unix.go similarity index 73% rename from pkg/cmd/login/login_unix.go rename to pkg/cmd/login/prompt_unix.go index c1eec8fdf01..69529473f6d 100644 --- a/pkg/cmd/login/login_unix.go +++ b/pkg/cmd/login/prompt_unix.go @@ -19,28 +19,34 @@ package login import ( - "fmt" + "errors" "os" "syscall" "golang.org/x/term" + + "github.com/containerd/log" ) func readPassword() (string, error) { - var fd int - if term.IsTerminal(syscall.Stdin) { - fd = syscall.Stdin - } else { + fd := syscall.Stdin + if !term.IsTerminal(fd) { tty, err := os.Open("/dev/tty") if err != nil { - return "", fmt.Errorf("error allocating terminal: %w", err) + return "", errors.Join(ErrCannotAllocateTerminal, err) } - defer tty.Close() + defer func() { + err = tty.Close() + if err != nil { + log.L.WithError(err).Error("failed closing tty") + } + }() fd = int(tty.Fd()) } + bytePassword, err := term.ReadPassword(fd) if err != nil { - return "", fmt.Errorf("error reading password: %w", err) + return "", errors.Join(ErrReadingPassword, err) } return string(bytePassword), nil diff --git a/pkg/cmd/login/login_windows.go b/pkg/cmd/login/prompt_windows.go similarity index 79% rename from pkg/cmd/login/login_windows.go rename to pkg/cmd/login/prompt_windows.go index 89c3834fb92..913e6ff5f98 100644 --- a/pkg/cmd/login/login_windows.go +++ b/pkg/cmd/login/prompt_windows.go @@ -17,22 +17,21 @@ package login import ( - "fmt" + "errors" "syscall" "golang.org/x/term" ) func readPassword() (string, error) { - var fd int - if term.IsTerminal(int(syscall.Stdin)) { - fd = int(syscall.Stdin) - } else { - return "", fmt.Errorf("error allocating terminal") + fd := int(syscall.Stdin) + if !term.IsTerminal(fd) { + return "", ErrNotATerminal } + bytePassword, err := term.ReadPassword(fd) if err != nil { - return "", fmt.Errorf("error reading password: %w", err) + return "", errors.Join(ErrReadingPassword, err) } return string(bytePassword), nil