forked from containers/toolbox
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pkg/podman: Add error parsing method
This new helper type serves for parsing the output of Podman in stderr into a form that can be further analyzed. The most significant part is the method Is() that lexically compares error strings. This is needed because there are no defined errors to catch errors coming from Podman. containers#786
- Loading branch information
1 parent
6c86cab
commit 328a9ab
Showing
2 changed files
with
311 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* Copyright © 2019 – 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" | ||
"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 { | ||
if e.errors[0] == target.Error() { | ||
return true | ||
} | ||
|
||
err := e.Unwrap() | ||
if err == nil { | ||
return false | ||
} | ||
e = err.(*internalError) | ||
} | ||
} | ||
|
||
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 { | ||
errMsg := stderr.String() | ||
errMsg = strings.TrimSpace(errMsg) | ||
if errMsg == "" { | ||
return nil | ||
} | ||
|
||
// Stderr is not used only for error messages but also for e.g. loading | ||
// bars | ||
errMsgSplit := strings.Split(errMsg, "Error: ") | ||
// We're only interested in the part with an error | ||
errMsg = errMsgSplit[len(errMsgSplit)-1] | ||
// Sometimes an error contains a newline (e.g., responses from Docker | ||
// registry). Normalize them into further parseable error message. | ||
errMsg = strings.ReplaceAll(errMsg, "\n", ": ") | ||
// Wrapped error messages are usually separated by a colon followed by | ||
// a single space character | ||
errMsgPartsRaw := strings.Split(errMsg, ": ") | ||
// The parts of the err message still can have a whitespace at the | ||
// beginning ar at the end. Trim them. | ||
var errMsgParts []string | ||
for _, part := range errMsgPartsRaw { | ||
part = strings.TrimSpace(part) | ||
part = strings.Trim(part, ":") | ||
// Podman error messages are usually prepended with the "Error: " string | ||
// Sometimes the error contains errors in a bullet list. This list is | ||
// usually prepended with a message equal to "errors:" | ||
if part == "Error" || part == "errors" { | ||
continue | ||
} | ||
|
||
errMsgParts = append(errMsgParts, part) | ||
} | ||
|
||
return &internalError{errMsgParts} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* | ||
* Copyright © 2019 – 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 error message", | ||
input: "", | ||
expect: expect{ | ||
IsNil: true, | ||
Error: "", | ||
}, | ||
}, | ||
{ | ||
name: "Text with no prolog before an error message", | ||
input: "There is no prolog before the error message.", | ||
expect: expect{ | ||
Error: "There is no prolog before the error message.", | ||
}, | ||
}, | ||
{ | ||
name: "Text with prolog before an error message", | ||
input: "There is the prolog.Error: an error message", | ||
expect: expect{ | ||
Error: "an error message", | ||
Search: "an error message", | ||
}, | ||
}, | ||
{ | ||
name: "Text with prolog before an error message (separated by newline)", | ||
input: "There is the prolog.\nError: an error message", | ||
expect: expect{ | ||
Error: "an error message", | ||
Search: "an error message", | ||
}, | ||
}, | ||
{ | ||
name: "Error message with several wrapped errors (prepended with \"Error:\")", | ||
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 newline and with errors in \"bullet\" list", | ||
input: "Error: foobar:\n err1\n err2\n err3", | ||
expect: expect{ | ||
Error: "foobar: err1: err2: err3", | ||
Search: "err2", | ||
Wrap: []string{"foobar", "err1", "err2", "err3"}, | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman pull' - unknown registry", | ||
input: `Trying to pull foobar.com/foo:latest... | ||
Error: Error initializing source docker://foobar.com/foo:latest: error pinging docker registry foobar.com: Get "https://foobar.com/v2/": x509: certificate has expired or is not yet valid: current time 2021-07-04T00:45:46+02:00 is after 2019-09-26T23:59:59Z`, | ||
expect: expect{ | ||
Error: "Error initializing source docker://foobar.com/foo:latest: error pinging docker registry foobar.com: Get \"https://foobar.com/v2/\": x509: certificate has expired or is not yet valid: current time 2021-07-04T00:45:46+02:00 is after 2019-09-26T23:59:59Z", | ||
}, | ||
}, | ||
{ | ||
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: 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", | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman pull' - unknwon image (Fedora Registry)", | ||
input: `Trying to pull registry.fedoraproject.org/foobar:latest... | ||
Error: Error initializing source docker://registry.fedoraproject.org/foobar:latest: Error reading manifest latest in registry.fedoraproject.org/foobar: manifest unknown: manifest unknown`, | ||
expect: expect{ | ||
Error: "Error initializing source docker://registry.fedoraproject.org/foobar:latest: Error reading manifest latest in registry.fedoraproject.org/foobar: manifest unknown: manifest unknown", | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman pull' - unknown image (Docker Hub)", | ||
input: `Trying to pull docker.io/library/busybox:foobar... | ||
Error: Error initializing source docker://busybox:foobar: Error reading manifest foobar in docker.io/library/busybox: manifest unknown: manifest unknown`, | ||
expect: expect{ | ||
Error: "Error initializing source docker://busybox:foobar: Error reading manifest foobar in docker.io/library/busybox: manifest unknown: manifest unknown", | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman pull' - unknown image (Red Hat Registry)", | ||
input: `Trying to pull registry.redhat.io/foobar:latest... | ||
time="2021-07-04T01:08:22+02:00" level=warning msg="failed, retrying in 1s ... (1/3). Error: Error initializing source docker://registry.redhat.io/foobar:latest: Error reading manifest latest in registry.redhat.io/foobar: unknown: Not Found" | ||
time="2021-07-04T01:08:25+02:00" level=warning msg="failed, retrying in 1s ... (2/3). Error: Error initializing source docker://registry.redhat.io/foobar:latest: Error reading manifest latest in registry.redhat.io/foobar: unknown: Not Found" | ||
time="2021-07-04T01:08:27+02:00" level=warning msg="failed, retrying in 1s ... (3/3). Error: Error initializing source docker://registry.redhat.io/foobar:latest: Error reading manifest latest in registry.redhat.io/foobar: unknown: Not Found" | ||
Error: Error initializing source docker://registry.redhat.io/foobar:latest: Error reading manifest latest in registry.redhat.io/foobar: unknown: Not Found | ||
`, | ||
expect: expect{ | ||
Error: "Error initializing source docker://registry.redhat.io/foobar:latest: Error reading manifest latest in registry.redhat.io/foobar: unknown: Not Found", | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman login' - no such host", | ||
input: `Error: authenticating creds for "foobar": error pinging docker registry foobar: Get "https://foobar/v2/": dial tcp: lookup foobar: no such host`, | ||
expect: expect{ | ||
Error: `authenticating creds for "foobar": error pinging docker registry foobar: Get "https://foobar/v2/": dial tcp: lookup foobar: no such host`, | ||
}, | ||
}, | ||
{ | ||
name: "Error message from 'podman login' - invalid username/password", | ||
input: `Error: error logging into "registry.redhat.io": invalid username/password`, | ||
expect: expect{ | ||
Error: `error logging into "registry.redhat.io": invalid username/password`, | ||
}, | ||
}, | ||
} | ||
|
||
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:] | ||
} | ||
} | ||
}) | ||
} | ||
} |