Skip to content

Commit

Permalink
pkg/podman: Add error parsing method
Browse files Browse the repository at this point in the history
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.

#786
  • Loading branch information
HarryMichal committed Jul 3, 2021
1 parent 6c86cab commit cf70190
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 0 deletions.
114 changes: 114 additions & 0 deletions src/pkg/podman/error.go
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.SplitN(errMsg, "Error: ", 2)
// 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}
}
144 changes: 144 additions & 0 deletions src/pkg/podman/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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 input struct {
Msg string
}

type expect struct {
IsNil bool
Error string
Search string
Wrap []string
}

testCases := []struct {
name string
input input
expect expect
}{
{
name: "Empty error message",
input: input{
Msg: "",
},
expect: expect{
IsNil: true,
Error: "",
},
},
{
name: "Text with no prolog before an error message",
input: input{
Msg: "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: input{
Msg: "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: input{
Msg: "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: input{
Msg: "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: input{
Msg: "Error: foobar:\n err1\n err2\n err3",
},
expect: expect{
Error: "foobar: err1: err2: err3",
Search: "err2",
Wrap: []string{"foobar", "err1", "err2", "err3"},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := parseErrorMsg(bytes.NewBufferString(tc.input.Msg))

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:]
}
}
})
}
}

0 comments on commit cf70190

Please sign in to comment.