toolbox/toolbox-Support-logging-into-a-registry-if-necessary.patch

668 lines
19 KiB
Diff
Raw Normal View History

From 62a022428231df50637495673354d4fdd2541c1a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Mon, 7 Jun 2021 14:11:27 +0200
Subject: [PATCH 1/7] pkg/podman: Support parsing 'podman pull' spew into a Go
error
This is meant to get a better understanding of a failed 'podman pull'
invocation to understand whether pulling an image requires logging into
the registry or not. Currently, 'podman pull' doesn't have a dedicated
exit code to denote authorization errors, so this is meant to be a
temporary workaround for that.
Parsing the error stream is inherently fragile and tricky because
there's no guarantee that the structure of the messages won't change,
and there's no clear definition of how the messages are laid out.
Therefore, this approach can't be treated as a generic solution for
getting detailed information about failed Podman invocations.
The error stream is used not only for dumping error messages, but also
for showing progress bars. Therefore, all lines are skipped until one
that starts with "Error: " is found. This is a heuristic based on how
Go programs written using the Cobra [1] library tend to report errors.
All subsequent lines are taken together and split around the ": "
sub-string, on the assumption that the ": " sub-string is used when a
new error message is prefixed to an inner error. Each sub-string
created from the split is treated as a potential member of the chain of
errors reported within Podman.
Some real world examples of the 'podman pull' error stream in the case
of authorization errors are:
* With Docker Hub (https://hub.docker.com/):
Trying to pull docker.io/library/foobar:latest...
Error: Error initializing source docker://foobar:latest: Error reading manifest latest in docker.io/library/foobar: errors:
denied: requested access to the resource is denied
unauthorized: authentication required
* With registry.redhat.io:
Trying to pull registry.redhat.io/foobar:latest...
Error: Error initializing source docker://registry.redhat.io/foobar:latest: unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
[1] https://github.com/spf13/cobra/
https://pkg.go.dev/github.com/spf13/cobra
https://github.com/containers/toolbox/pull/786
https://github.com/containers/toolbox/pull/787
---
src/pkg/podman/error.go | 109 +++++++++++++++++++++++++
src/pkg/podman/error_test.go | 152 +++++++++++++++++++++++++++++++++++
2 files changed, 261 insertions(+)
create mode 100644 src/pkg/podman/error.go
create mode 100644 src/pkg/podman/error_test.go
diff --git a/src/pkg/podman/error.go b/src/pkg/podman/error.go
new file mode 100644
index 000000000000..ea35de8d7ed8
--- /dev/null
+++ b/src/pkg/podman/error.go
@@ -0,0 +1,109 @@
+/*
+ * Copyright © 2021 Red Hat Inc.
+ *
+ * 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 podman
+
+import (
+ "bufio"
+ "bytes"
+ "strings"
+)
+
+// internalError serves for representing errors printed by Podman to stderr
+type internalError struct {
+ errors []string
+}
+
+func (e *internalError) Error() string {
+ if e.errors == nil || len(e.errors) == 0 {
+ return ""
+ }
+
+ var builder strings.Builder
+
+ for i, part := range e.errors {
+ if i != 0 {
+ builder.WriteString(": ")
+ }
+
+ builder.WriteString(part)
+ }
+
+ return builder.String()
+}
+
+// Is lexically compares errors
+//
+// The comparison is done for every part in the error chain not across.
+func (e *internalError) Is(target error) bool {
+ if target == nil {
+ return false
+ }
+
+ if e.errors == nil || len(e.errors) == 0 {
+ return false
+ }
+
+ for _, part := range e.errors {
+ if part == target.Error() {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (e *internalError) Unwrap() error {
+ if e.errors == nil || len(e.errors) <= 1 {
+ return nil
+ }
+
+ return &internalError{e.errors[1:]}
+}
+
+// parseErrorMsg serves for converting error output of Podman into an error
+// that can be further used in Go
+func parseErrorMsg(stderr *bytes.Buffer) error {
+ // Stderr is not used only for error messages but also for things like
+ // progress bars. We're only interested in the error messages.
+
+ var errMsgFound bool
+ var errMsgParts []string
+
+ scanner := bufio.NewScanner(stderr)
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if strings.HasPrefix(line, "Error: ") {
+ line = strings.TrimPrefix(line, "Error: ")
+ errMsgFound = true
+ }
+
+ if errMsgFound {
+ line = strings.TrimSpace(line)
+ line = strings.Trim(line, ":")
+
+ parts := strings.Split(line, ": ")
+ errMsgParts = append(errMsgParts, parts...)
+ }
+ }
+
+ if !errMsgFound {
+ return nil
+ }
+
+ return &internalError{errMsgParts}
+}
diff --git a/src/pkg/podman/error_test.go b/src/pkg/podman/error_test.go
new file mode 100644
index 000000000000..11652c709bdd
--- /dev/null
+++ b/src/pkg/podman/error_test.go
@@ -0,0 +1,152 @@
+/*
+ * Copyright © 2021 Red Hat Inc.
+ *
+ * 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 podman
+
+import (
+ "bytes"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestInternalError(t *testing.T) {
+ type expect struct {
+ IsNil bool
+ Error string
+ Search string
+ Wrap []string
+ }
+
+ testCases := []struct {
+ name string
+ input string
+ expect expect
+ }{
+ {
+ name: "Empty",
+ input: "",
+ expect: expect{
+ IsNil: true,
+ Error: "",
+ },
+ },
+ {
+ name: "Text with only a prolog and no error message",
+ input: "There is only a prolog and no error message",
+ expect: expect{
+ IsNil: true,
+ Error: "",
+ },
+ },
+ {
+ name: "Text with only a prolog and no error message",
+ input: "There is only a prolog Error: not an error message",
+ expect: expect{
+ IsNil: true,
+ Error: "",
+ },
+ },
+ {
+ name: "Text with a prolog before the error message",
+ input: `There is a prolog
+Error: an error message`,
+ expect: expect{
+ Error: "an error message",
+ Search: "an error message",
+ },
+ },
+ {
+ name: "Error message with several wrapped errors",
+ input: "Error: level 1: level 2: level 3: level 4",
+ expect: expect{
+ Error: "level 1: level 2: level 3: level 4",
+ Search: "level 4",
+ Wrap: []string{"level 1", "level 2", "level 3", "level 4"},
+ },
+ },
+ {
+ name: "Error message with a bullet list",
+ input: `Error: an error message:
+ err1
+ err2
+ err3`,
+ expect: expect{
+ Error: "an error message: err1: err2: err3",
+ Search: "err2",
+ Wrap: []string{"an error message", "err1", "err2", "err3"},
+ },
+ },
+ {
+ name: "Error message from 'podman pull' - unauthorized (Docker Hub)",
+ input: `Trying to pull docker.io/library/foobar:latest...
+Error: Error initializing source docker://foobar:latest: Error reading manifest latest in docker.io/library/foobar: errors:
+denied: requested access to the resource is denied
+unauthorized: authentication required`,
+ expect: expect{
+ Error: "Error initializing source docker://foobar:latest: Error reading manifest latest in docker.io/library/foobar: errors: denied: requested access to the resource is denied: unauthorized: authentication required",
+ },
+ },
+ {
+ name: "Error message from 'podman pull' - unauthorized (Red Hat Registry)",
+ input: `Trying to pull registry.redhat.io/foobar:latest...
+Error: Error initializing source docker://registry.redhat.io/foobar:latest: unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
+`,
+ expect: expect{
+ Error: "Error initializing source docker://registry.redhat.io/foobar:latest: unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication",
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ err := parseErrorMsg(bytes.NewBufferString(tc.input))
+
+ if tc.expect.IsNil {
+ assert.Nil(t, err)
+ return
+ } else {
+ assert.NotNil(t, err)
+ }
+
+ errInternal := err.(*internalError)
+ assert.Equal(t, tc.expect.Error, errInternal.Error())
+
+ if tc.expect.Search != "" {
+ assert.True(t, errInternal.Is(errors.New(tc.expect.Search)))
+ }
+
+ if len(tc.expect.Wrap) != 0 {
+ for {
+ assert.Equal(t, len(tc.expect.Wrap), len(errInternal.errors))
+
+ for i, part := range tc.expect.Wrap {
+ assert.Equal(t, part, errInternal.errors[i])
+ }
+
+ err = errInternal.Unwrap()
+ if err == nil {
+ assert.Equal(t, len(tc.expect.Wrap), 1)
+ break
+ }
+ errInternal = err.(*internalError)
+ tc.expect.Wrap = tc.expect.Wrap[1:]
+ }
+ }
+ })
+ }
+}
--
2.31.1
From 146c93b431941d21b3c686734a7b83a737072004 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Sat, 3 Jul 2021 22:06:30 +0200
Subject: [PATCH 2/7] pkg/shell: Allow extracting the stderr even when it's
shown to the user
This way the standard error stream of the spawned binaries can be
inspected to get a better understanding of the failure, while still
being shown to the user when run with the '--verbose' flag.
Unfortunately, this breaks the progress bar in 'podman pull' because
the standard error stream is no longer connected to a file descriptor
that's a terminal device.
https://github.com/containers/toolbox/pull/787
https://github.com/containers/toolbox/pull/823
---
src/pkg/shell/shell.go | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/src/pkg/shell/shell.go b/src/pkg/shell/shell.go
index 272dcc9ca693..c63b85bc8745 100644
--- a/src/pkg/shell/shell.go
+++ b/src/pkg/shell/shell.go
@@ -40,15 +40,12 @@ func Run(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string)
}
func RunWithExitCode(name string, stdin io.Reader, stdout, stderr io.Writer, arg ...string) (int, error) {
- logLevel := logrus.GetLevel()
- if stderr == nil && logLevel >= logrus.DebugLevel {
- stderr = os.Stderr
- }
+ stderrWrapper := getStderrWrapper(stderr)
cmd := exec.Command(name, arg...)
cmd.Stdin = stdin
cmd.Stdout = stdout
- cmd.Stderr = stderr
+ cmd.Stderr = stderrWrapper
if err := cmd.Run(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
@@ -66,3 +63,18 @@ func RunWithExitCode(name string, stdin io.Reader, stdout, stderr io.Writer, arg
return 0, nil
}
+
+func getStderrWrapper(buffer io.Writer) io.Writer {
+ var stderr io.Writer
+
+ logLevel := logrus.GetLevel()
+ if logLevel < logrus.DebugLevel {
+ stderr = buffer
+ } else if buffer == nil {
+ stderr = os.Stderr
+ } else {
+ stderr = io.MultiWriter(buffer, os.Stderr)
+ }
+
+ return stderr
+}
--
2.31.1
From 5c3249033224c6f107a2d39ab84e519303d74072 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Sun, 4 Jul 2021 18:15:16 +0200
Subject: [PATCH 3/7] pkg/podman: Try to parse the error from 'podman pull'
into a Go error
https://github.com/containers/toolbox/pull/787
---
src/pkg/podman/podman.go | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go
index 9099df1eaf2a..8e7d5068fb97 100644
--- a/src/pkg/podman/podman.go
+++ b/src/pkg/podman/podman.go
@@ -228,10 +228,13 @@ func IsToolboxImage(image string) (bool, error) {
// Pull pulls an image
func Pull(imageName string) error {
+ var stderr bytes.Buffer
+
logLevelString := LogLevel.String()
args := []string{"--log-level", logLevelString, "pull", imageName}
- if err := shell.Run("podman", nil, nil, nil, args...); err != nil {
+ if err := shell.Run("podman", nil, nil, &stderr, args...); err != nil {
+ err := parseErrorMsg(&stderr)
return err
}
--
2.31.1
From 4b8b007201cd019909829d73afdcf919753943a3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Sun, 4 Jul 2021 18:16:12 +0200
Subject: [PATCH 4/7] pkg/podman: Add error for 'podman pull' failing due to no
authorization
https://github.com/containers/toolbox/pull/787
---
src/pkg/podman/podman.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go
index 8e7d5068fb97..521538c9abfb 100644
--- a/src/pkg/podman/podman.go
+++ b/src/pkg/podman/podman.go
@@ -19,6 +19,7 @@ package podman
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
@@ -32,7 +33,8 @@ var (
)
var (
- LogLevel = logrus.ErrorLevel
+ ErrUnauthorized = errors.New("unauthorized")
+ LogLevel = logrus.ErrorLevel
)
// CheckVersion compares provided version with the version of Podman.
--
2.31.1
From ef3bebac89cd833a663dc63fffba8b04ca944dde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Sat, 3 Jul 2021 02:25:49 +0200
Subject: [PATCH 5/7] pkg/podman: Wrap 'podman login'
https://github.com/containers/toolbox/pull/787
---
src/pkg/podman/podman.go | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/pkg/podman/podman.go b/src/pkg/podman/podman.go
index 521538c9abfb..8538bd7c617a 100644
--- a/src/pkg/podman/podman.go
+++ b/src/pkg/podman/podman.go
@@ -228,6 +228,18 @@ func IsToolboxImage(image string) (bool, error) {
return true, nil
}
+func Login(registry, username, password string) error {
+ logLevelString := LogLevel.String()
+ args := []string{"--log-level", logLevelString, "login", registry, "--password-stdin", "--username", username}
+ stdin := bytes.NewBufferString(password)
+
+ if err := shell.Run("podman", stdin, nil, nil, args...); err != nil {
+ return err
+ }
+
+ return nil
+}
+
// Pull pulls an image
func Pull(imageName string) error {
var stderr bytes.Buffer
--
2.31.1
From 49cd3c48c26d37c85aba5dbdcf04a995593ae673 Mon Sep 17 00:00:00 2001
From: Debarshi Ray <rishi@fedoraproject.org>
Date: Sun, 4 Jul 2021 18:11:04 +0200
Subject: [PATCH 6/7] cmd/create: Split out the spinner around 'podman pull'
A subsequent commit will use this when retrying the 'podman pull'
after logging the user into the registry for images that require
authorization.
https://github.com/containers/toolbox/pull/787
---
src/cmd/create.go | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/cmd/create.go b/src/cmd/create.go
index e3245b4c5ae3..6a3005f06041 100644
--- a/src/cmd/create.go
+++ b/src/cmd/create.go
@@ -731,6 +731,14 @@ func pullImage(image, release string) (bool, error) {
return false, nil
}
+ if _, err := pullImageWithSpinner(imageFull); err != nil {
+ return false, fmt.Errorf("failed to pull image %s", imageFull)
+ }
+
+ return true, nil
+}
+
+func pullImageWithSpinner(imageFull string) (bool, error) {
logrus.Debugf("Pulling image %s", imageFull)
stdoutFd := os.Stdout.Fd()
@@ -744,7 +752,7 @@ func pullImage(image, release string) (bool, error) {
}
if err := podman.Pull(imageFull); err != nil {
- return false, fmt.Errorf("failed to pull image %s", imageFull)
+ return false, err
}
return true, nil
--
2.31.1
From 19bb0f08118536c32ff2d2f571d92758dff00dcb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20M=C3=ADchal?= <harrymichal@seznam.cz>
Date: Sun, 4 Jul 2021 18:11:28 +0200
Subject: [PATCH 7/7] cmd/create: Support logging into a registry if necessary
Some registries contain private repositories of images and require the
user to log in first to gain access. With this Toolbox tries to
recognize errors when pulling images and offers the user the means to
log in.
Some changes by Debarshi Ray.
https://github.com/containers/toolbox/pull/787
---
src/cmd/create.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 71 insertions(+), 1 deletion(-)
diff --git a/src/cmd/create.go b/src/cmd/create.go
index 6a3005f06041..7cf03e8a4cd2 100644
--- a/src/cmd/create.go
+++ b/src/cmd/create.go
@@ -17,6 +17,7 @@
package cmd
import (
+ "bufio"
"errors"
"fmt"
"os"
@@ -668,6 +669,65 @@ func isPathReadWrite(path string) (bool, error) {
return false, nil
}
+func logIntoRegistry(imageFull, registry string) (bool, error) {
+ fmt.Printf("Image %s requires log-in.\n", imageFull)
+
+ scanner := bufio.NewScanner(os.Stdin)
+
+ stdinFd := os.Stdin.Fd()
+ stdinFdInt := int(stdinFd)
+
+ if terminal.IsTerminal(stdinFdInt) {
+ fmt.Printf("Username: ")
+ }
+
+ if !scanner.Scan() {
+ if err := scanner.Err(); err != nil {
+ logrus.Debugf("Logging into registry: failed to read username: %s", err)
+ }
+
+ return false, errors.New("failed to read username")
+ }
+
+ username := scanner.Text()
+
+ var password string
+
+ if terminal.IsTerminal(stdinFdInt) {
+ logrus.Debug("Reading password from a terminal input")
+
+ fmt.Printf("Password: ")
+
+ passwordBytes, err := terminal.ReadPassword(stdinFdInt)
+ if err != nil {
+ logrus.Debugf("Logging into registry: failed to read password: %s", err)
+ return false, errors.New("failed to read password")
+ }
+
+ password = string(passwordBytes)
+ fmt.Println("")
+ } else {
+ logrus.Debug("Reading password from a non-terminal input")
+
+ if !scanner.Scan() {
+ if err := scanner.Err(); err != nil {
+ logrus.Debugf("Logging into registry: failed to read password: %s", err)
+ }
+
+ return false, errors.New("failed to read password")
+ }
+
+ password = scanner.Text()
+ }
+
+ if err := podman.Login(registry, username, password); err != nil {
+ logrus.Debugf("Logging into registry %s failed: %s", registry, err)
+ return false, fmt.Errorf("failed to log into registry %s", registry)
+ }
+
+ return true, nil
+}
+
func pullImage(image, release string) (bool, error) {
if _, err := utils.ImageReferenceCanBeID(image); err == nil {
logrus.Debugf("Looking for image %s", image)
@@ -732,7 +792,17 @@ func pullImage(image, release string) (bool, error) {
}
if _, err := pullImageWithSpinner(imageFull); err != nil {
- return false, fmt.Errorf("failed to pull image %s", imageFull)
+ if !errors.Is(err, podman.ErrUnauthorized) {
+ return false, fmt.Errorf("failed to pull image %s", imageFull)
+ }
+
+ if _, err := logIntoRegistry(imageFull, domain); err != nil {
+ return false, err
+ }
+
+ if _, err := pullImageWithSpinner(imageFull); err != nil {
+ return false, fmt.Errorf("failed to pull image %s", imageFull)
+ }
}
return true, nil
--
2.31.1