Skip to content

Commit

Permalink
[WIP] cmd/create, pkg/podman: Add capabilities for logging to registries
Browse files Browse the repository at this point in the history
  • Loading branch information
HarryMichal committed Jun 7, 2021
1 parent afd427d commit fc636da
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 7 deletions.
90 changes: 88 additions & 2 deletions src/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ const (

var (
createFlags struct {
authfile string
container string
creds string
distro string
image string
release string
Expand All @@ -66,12 +68,22 @@ var createCmd = &cobra.Command{
func init() {
flags := createCmd.Flags()

flags.StringVar(&createFlags.authfile,
"authfile",
"",
"Path of the authentication file")

flags.StringVarP(&createFlags.container,
"container",
"c",
"",
"Assign a different name to the toolbox container")

flags.StringVar(&createFlags.creds,
"creds",
"",
"Credentials (USERNAME:PASSWORD) to use for authenticating to a registry")

flags.StringVarP(&createFlags.distro,
"distro",
"d",
Expand Down Expand Up @@ -115,6 +127,12 @@ func create(cmd *cobra.Command, args []string) error {
return errors.New("options --image and --release cannot be used together")
}

if cmd.Flag("creds").Changed {
if strings.Count(createFlags.creds, ":") != 1 {
return errors.New("option --creds accepts values in format USERNAME:PASSWORD")
}
}

var container string
var containerArg string

Expand Down Expand Up @@ -656,6 +674,8 @@ func isPathReadWrite(path string) (bool, error) {
}

func pullImage(image, release string) (bool, error) {
var didLogin bool = false

if _, err := utils.ImageReferenceCanBeID(image); err == nil {
logrus.Debugf("Looking for image %s", image)

Expand Down Expand Up @@ -720,18 +740,84 @@ func pullImage(image, release string) (bool, error) {

logrus.Debugf("Pulling image %s", imageFull)

pull_image:
stdoutFd := os.Stdout.Fd()
stdoutFdInt := int(stdoutFd)
var s *spinner.Spinner = spinner.New(spinner.CharSets[9], 500*time.Millisecond)
if logLevel := logrus.GetLevel(); logLevel < logrus.DebugLevel && terminal.IsTerminal(stdoutFdInt) {
s := spinner.New(spinner.CharSets[9], 500*time.Millisecond)
s.Prefix = fmt.Sprintf("Pulling %s: ", imageFull)
s.Writer = os.Stdout
s.Start()
defer s.Stop()
}

if err := podman.Pull(imageFull); err != nil {
return false, fmt.Errorf("failed to pull image %s", imageFull)
if errors.Is(err, podman.ErrUnknownImage) {
var builder strings.Builder
fmt.Fprintf(&builder, "The requested image does not seem to exist\n")
fmt.Fprintf(&builder, "Make sure the image URI is correct.")
errMsg := errors.New(builder.String())
return false, errMsg
}

if errors.Is(err, podman.ErrNotAuthorized) {
var builder strings.Builder

// This error should not be encountered for the second time after
// logging into a registry
if didLogin {
return false, fmt.Errorf("failed to pull image %s: unexpected unauthorized access", imageFull)
}

fmt.Fprintf(&builder, "Could not pull image %s\n", imageFull)
fmt.Fprintf(&builder, "The registry might require logging in but not necessarily.\n")

if rootFlags.assumeYes {
// We don't want to block
if createFlags.creds == "" {
fmt.Fprintf(&builder, "See 'podman login' on how to login into a registry.")
errMsg := errors.New(builder.String())
return false, errMsg
}

creds := strings.Split(createFlags.creds, ":")
args := []string{"--username", creds[0], "--password", creds[1]}
if err = podman.Login(domain, args...); err == nil {
didLogin = true
goto pull_image
}
}

if !rootFlags.assumeYes {
s.Stop()
fmt.Fprintf(os.Stderr, builder.String())

if !utils.AskForConfirmation("Do you want to try to log into the registry and try to pull the image again? [y/N]") {
return false, nil
}

var args []string

if createFlags.authfile != "" {
args = append(args, "--authfile", createFlags.authfile)
}

if createFlags.creds != "" {
creds := strings.Split(createFlags.creds, ":")
args = append(args, "--username", creds[0], "--password", creds[1])
}

if err = podman.Login(domain, args...); err != nil {
return false, err
}

fmt.Fprintf(os.Stderr, "Retrying to pull image %s\n", imageFull)
didLogin = true
goto pull_image
}
}

return false, fmt.Errorf("failed to pull image %s: %w", imageFull, err)
}

return true, nil
Expand Down
73 changes: 68 additions & 5 deletions src/pkg/podman/podman.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
"errors"
"fmt"
"io"
"sort"
"os"
"strings"

"github.com/HarryMichal/go-version"
Expand All @@ -38,6 +38,11 @@ var (
LogLevel = logrus.ErrorLevel
)

var (
ErrNotAuthorized = errors.New("possibly not authorized to registry")
ErrUnknownImage = errors.New("the pulled image is not known")
)

// CheckVersion compares provided version with the version of Podman.
//
// Takes in one string parameter that should be in the format that is used for versioning (eg. 1.0.0, 2.5.1-dev).
Expand Down Expand Up @@ -229,13 +234,71 @@ func IsToolboxImage(image string) (bool, error) {
return true, nil
}

// Pull pulls an image
func Login(registry string, extraArgs ...string) error {
var stderr bytes.Buffer

logLevelString := LogLevel.String()
args := []string{"--log-level", logLevelString, "login", registry}
args = append(args, extraArgs...)

if err := shell.Run("podman", os.Stdin, os.Stdout, &stderr, args...); err != nil {
if stderr.Len() == 0 {
return fmt.Errorf("failed to login to registry %s: %w", registry, err)
}

err := parseErrorMsg(&stderr)
return fmt.Errorf("failed to login to registry %s: %w", registry, err.Err)
}

return nil
}

// Pull pulls an image. Wraps around command 'podman pull'.
//
// The image pull can fail for many reasons. Few of those are:
// - unknown image (ErrUnknownImage)
// - unauthorized access to registry (ErrNotAuthorized)
// - general network issue
//
// The wrapper tries to discern these errors to some extent and provide means
// for their handling. The provided errors can not be relied on completely as
// they are created based on error message parsing that differs based on used
// registry.
func Pull(imageName string) error {
var stderr bytes.Buffer

logLevelString := LogLevel.String()
args := []string{"--log-level", logLevelString, "pull", imageName}
args := []string{"--log-level", logLevelString, "pull", imageName, "--quiet"}

if err := shell.Run("podman", nil, nil, nil, args...); err != nil {
return err
if err := shell.Run("podman", nil, nil, &stderr, args...); err != nil {
if stderr.Len() == 0 {
return fmt.Errorf("failed to pull image %s: %w", imageName, err)
}

err := parseErrorMsg(&stderr)
// The following error handling is very fragile as it relies on
// specific strings in error messages which may change in the future.
// Some registries provide so useless error message that the cause of
// the problem is close to impossible to discern.

// Unknown image handling
// "name unknown" and "Repo not found" are part of error message when
// trying to pull an unknown image from registry.access.redhat.com
// "manifest unknown" is part of error message when trying to pull an
// unknown image from registry.fedoraproject.org
if err.Is(errors.New("name unknown")) || err.Is(errors.New("Repo not found")) ||
err.Is(errors.New("manifest unknown")) {
return ErrUnknownImage
}

// Unathorized access handling
// "unauthorized" is part of error message when trying to pull from registry.redhat.io
// "authentication required" is part of error message when trying to pull from docker.io
if err.Is(errors.New("unauthorized")) || err.Is(errors.New("authentication required")) {
return ErrNotAuthorized
}

return fmt.Errorf("failed to pull image %s: %w", imageName, err.Err)
}

return nil
Expand Down

0 comments on commit fc636da

Please sign in to comment.