Skip to content

Commit

Permalink
Merge pull request containerd#3392 from apostasie/dev-login-2
Browse files Browse the repository at this point in the history
Login & authentication rework, part 2: prompt cleanup
  • Loading branch information
AkihiroSuda committed Sep 4, 2024
2 parents 1b94da9 + 23aca88 commit 53d898d
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 73 deletions.
59 changes: 1 addition & 58 deletions pkg/cmd/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
109 changes: 109 additions & 0 deletions pkg/cmd/login/prompt.go
Original file line number Diff line number Diff line change
@@ -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
}
22 changes: 14 additions & 8 deletions pkg/cmd/login/login_unix.go → pkg/cmd/login/prompt_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 53d898d

Please sign in to comment.