Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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