From fc636da9fac6ece52ef55f03ddcdf873d23f5927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= Date: Mon, 7 Jun 2021 21:46:45 +0200 Subject: [PATCH] [WIP] cmd/create, pkg/podman: Add capabilities for logging to registries https://github.com/containers/toolbox/pull/787 --- src/cmd/create.go | 90 +++++++++++++++++++++++++++++++++++++++- src/pkg/podman/podman.go | 73 +++++++++++++++++++++++++++++--- 2 files changed, 156 insertions(+), 7 deletions(-) diff --git a/src/cmd/create.go b/src/cmd/create.go index ad75e4983..a1653bf67 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -42,7 +42,9 @@ const ( var ( createFlags struct { + authfile string container string + creds string distro string image string release string @@ -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", @@ -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 @@ -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) @@ -720,10 +740,11 @@ 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() @@ -731,7 +752,72 @@ func pullImage(image, release string) (bool, error) { } 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 diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go index 3d2f25f84..ed7a20a8f 100644 --- a/src/pkg/podman/podman.go +++ b/src/pkg/podman/podman.go @@ -22,7 +22,7 @@ import ( "errors" "fmt" "io" - "sort" + "os" "strings" "github.com/HarryMichal/go-version" @@ -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). @@ -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