cmd/acr/login.go (124 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/Azure/acr-cli/auth/oras"
"github.com/moby/term"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry/remote"
)
const (
newLoginCmdLongMessage = `Login to a container registry, obtaining credentials or writing them to the config file`
loginExampleMessage = ` - Log in to an Azure Container Registry named "example"
acr login -u username -p password example.azurecr.io
- Log in to an Azure Container Registry named "example" getting the password from stdin
acr login example.azurecr.io -u username --password-stdin
- Log in to an Azure Container Registry named "example" from prompt
acr login example.azurecr.io`
)
type loginOpts struct {
hostname string
username string
password string
configs []string
debug bool
fromStdin bool
}
// newLoginCmd is used when the program is used locally and not inside a container.
// It is based on the login.go file found on https://github.com/deislabs/oras/blob/master/cmd/oras/login.go
// This will login into any ACR, the login info will be written in a config.json file inside the .docker folder.
func newLoginCmd() *cobra.Command {
var opts loginOpts
cmd := &cobra.Command{
Use: "login",
Short: "Login to a container registry",
Long: newLoginCmdLongMessage,
Example: loginExampleMessage,
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
opts.hostname = args[0]
return runLogin(opts)
},
}
cmd.Flags().BoolVarP(&opts.debug, "debug", "d", false, "debug mode")
cmd.Flags().StringArrayVarP(&opts.configs, "config", "c", nil, "auth config paths")
cmd.Flags().StringVarP(&opts.username, "username", "u", "", "the registry username")
cmd.Flags().StringVarP(&opts.password, "password", "p", "", "the registry password or identity token")
cmd.Flags().BoolVarP(&opts.fromStdin, "password-stdin", "", false, "read password or identity token from stdin")
return cmd
}
func runLogin(opts loginOpts) error {
if opts.debug {
logrus.SetLevel(logrus.DebugLevel)
}
store, err := oras.NewStore(opts.configs...)
if err != nil {
return err
}
var username string
var passwordBytes []byte
if opts.fromStdin {
passwordBytes, err = io.ReadAll(os.Stdin)
if err != nil {
return err
}
opts.password = strings.TrimSuffix(string(passwordBytes), "\n")
opts.password = strings.TrimSuffix(opts.password, "\r")
} else if opts.password == "" {
if opts.username == "" {
username, err = readLine("Username: ", false)
if err != nil {
return err
}
opts.username = strings.TrimSpace(username)
}
if opts.password, err = readLine("Password: ", true); err != nil {
return err
} else if opts.password == "" {
return errors.New("password required")
}
} else {
fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
}
// Ping to ensure credential is valid
remote, err := remote.NewRegistry(opts.hostname)
if err != nil {
return err
}
cred := oras.Credential(opts.username, opts.password)
remote.Client = oras.NewClient(oras.ClientOptions{
Credential: cred,
Debug: opts.debug,
})
if err = remote.Ping(context.Background()); err != nil {
return err
}
// Store the validated credential
if err := store.Store(opts.hostname, cred); err != nil {
return err
}
fmt.Println("Login Succeeded")
return nil
}
func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt)
if silent {
fd := os.Stdin.Fd()
state, err := term.SaveState(fd)
if err != nil {
return "", err
}
term.DisableEcho(fd, state)
defer term.RestoreTerminal(fd, state)
}
reader := bufio.NewReader(os.Stdin)
line, _, err := reader.ReadLine()
if err != nil {
return "", err
}
if silent {
fmt.Println()
}
return string(line), nil
}