forked from getporter/porter
-
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.
Allow mixins to ignore a failed command (getporter#1846)
* sync go mod Signed-off-by: Carolyn Van Slyck <[email protected]> * Allow mixins to ignore a failed command Add the HasErrorHandling interface to the shared pkg/exec/builder package used by all the mixins. When implemented, a mixin can examine a failed command and optionaly handle it. The exec mixin now supports ignoring failed commands, which demonstrates how to use builder.IgnoreErrorHandler struct to use the same error handling behavior. Closes getporter#592 Signed-off-by: Carolyn Van Slyck <[email protected]>
- Loading branch information
Showing
11 changed files
with
403 additions
and
22 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
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
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
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,93 @@ | ||
package builder | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
portercontext "get.porter.sh/porter/pkg/context" | ||
) | ||
|
||
var _ HasErrorHandling = IgnoreErrorHandler{} | ||
|
||
// IgnoreErrorHandler implements HasErrorHandling for the exec mixin | ||
// and can be used by any other mixin to get the same error handling behavior. | ||
type IgnoreErrorHandler struct { | ||
// All ignores any error that happens when the command is run. | ||
All bool `yaml:"all,omitempty"` | ||
|
||
// ExitCodes ignores any exit codes in the list. | ||
ExitCodes []int `yaml:"exitCodes,omitempty"` | ||
|
||
// Output determines if the error should be ignored based on the command | ||
// output. | ||
Output IgnoreErrorWithOutput `yaml:"output,omitempty"` | ||
} | ||
|
||
type IgnoreErrorWithOutput struct { | ||
// Contains specifies that the error is ignored when stderr contains the | ||
// specified substring. | ||
Contains []string `yaml:"contains,omitempty"` | ||
|
||
// Regex specifies that the error is ignored when stderr matches the | ||
// specified regular expression. | ||
Regex []string `yaml:"regex,omitempty"` | ||
} | ||
|
||
func (h IgnoreErrorHandler) HandleError(cxt *portercontext.Context, err ExitError, stdout string, stderr string) error { | ||
// We shouldn't be called when there is no error but just in case, let's check | ||
if err == nil || err.ExitCode() == 0 { | ||
return nil | ||
} | ||
|
||
if cxt.Debug { | ||
fmt.Fprintf(cxt.Err, "Evaluating mixin command error %s with the mixin's error handler\n", err.Error()) | ||
} | ||
|
||
// Check if the command should always be allowed to "pass" | ||
if h.All { | ||
if cxt.Debug { | ||
fmt.Fprintln(cxt.Err, "Ignoring mixin command error because All was specified in the mixin step definition") | ||
} | ||
return nil | ||
} | ||
|
||
// Check if the exit code was allowed | ||
exitCode := err.ExitCode() | ||
for _, code := range h.ExitCodes { | ||
if exitCode == code { | ||
if cxt.Debug { | ||
fmt.Fprintf(cxt.Err, "Ignoring mixin command error (exit code: %d) because it was included in the allowed ExitCodes list defined in the mixin step definition\n", exitCode) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
// Check if the output contains a hint that it should be allowed to pass | ||
for _, allowError := range h.Output.Contains { | ||
if strings.Contains(stderr, allowError) { | ||
if cxt.Debug { | ||
fmt.Fprintf(cxt.Err, "Ignoring mixin command error because the error contained the substring %q defined in the mixin step definition\n", allowError) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
// Check if the output matches an allowed regular expression | ||
for _, allowMatch := range h.Output.Regex { | ||
expression, regexErr := regexp.Compile(allowMatch) | ||
if regexErr != nil { | ||
fmt.Fprintf(cxt.Err, "Could not ignore failed command because the Regex specified by the mixin step definition (%q) is invalid:%s\n", allowMatch, regexErr.Error()) | ||
return err | ||
} | ||
|
||
if expression.MatchString(stderr) { | ||
if cxt.Debug { | ||
fmt.Fprintf(cxt.Err, "Ignoring mixin command error because the error matched the Regex %q defined in the mixin step definition\n", allowMatch) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
return err | ||
} |
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,93 @@ | ||
package builder | ||
|
||
import ( | ||
"testing" | ||
|
||
"get.porter.sh/porter/pkg/context" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
var _ ExitError = TestExitError{} | ||
|
||
type TestExitError struct { | ||
exitCode int | ||
} | ||
|
||
func (t TestExitError) Error() string { | ||
return "an error occurred" | ||
} | ||
|
||
func (t TestExitError) ExitCode() int { | ||
return t.exitCode | ||
} | ||
|
||
func TestIgnoreErrorHandler_HandleError(t *testing.T) { | ||
t.Run("no error", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{} | ||
err := h.HandleError(cxt.Context, nil, "", "") | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("error - passthrough", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{} | ||
origErr := &TestExitError{1} | ||
err := h.HandleError(cxt.Context, origErr, "", "") | ||
require.Same(t, origErr, err) | ||
}) | ||
|
||
t.Run("all", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{All: true} | ||
err := h.HandleError(cxt.Context, TestExitError{1}, "", "") | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("allowed exit code", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{ExitCodes: []int{2, 1, 4}} | ||
err := h.HandleError(cxt.Context, TestExitError{1}, "", "") | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("disallowed exit code", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{ExitCodes: []int{2, 1, 4}} | ||
origErr := &TestExitError{10} | ||
err := h.HandleError(cxt.Context, origErr, "", "") | ||
require.Same(t, origErr, err, "The original error should be preserved") | ||
}) | ||
|
||
t.Run("stderr contains", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{Output: IgnoreErrorWithOutput{Contains: []string{"already exists"}}} | ||
origErr := &TestExitError{10} | ||
err := h.HandleError(cxt.Context, origErr, "", "The specified thing already exists") | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("stderr does not contain", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{Output: IgnoreErrorWithOutput{Contains: []string{"already exists"}}} | ||
origErr := &TestExitError{10} | ||
err := h.HandleError(cxt.Context, origErr, "", "Something went wrong") | ||
require.Same(t, origErr, err) | ||
}) | ||
|
||
t.Run("stderr matches regex", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{Output: IgnoreErrorWithOutput{Regex: []string{"(exists|EXISTS)"}}} | ||
origErr := &TestExitError{10} | ||
err := h.HandleError(cxt.Context, origErr, "", "something EXISTS") | ||
require.NoError(t, err) | ||
}) | ||
|
||
t.Run("stderr does not match regex", func(t *testing.T) { | ||
cxt := context.NewTestContext(t) | ||
h := IgnoreErrorHandler{Output: IgnoreErrorWithOutput{Regex: []string{"(exists|EXISTS)"}}} | ||
origErr := &TestExitError{10} | ||
err := h.HandleError(cxt.Context, origErr, "", "something mumble mumble") | ||
require.Same(t, origErr, err) | ||
}) | ||
} |
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
Oops, something went wrong.