fix(plugin): capture stderr during invocation

Capture process stderr in plugin invocations via executeCmd.

Closes #31843

Signed-off-by: Maksym Kondratenko <m.kondratenko.ua@gmail.com>
pull/32207/head
Maksym Kondratenko 4 months ago committed by Maksym Kondratenko
parent bfa3b6c325
commit f25950f069

@ -15,15 +15,21 @@ limitations under the License.
package plugin package plugin
import "strconv"
// InvokeExecError is returned when a plugin invocation returns a non-zero status/exit code // InvokeExecError is returned when a plugin invocation returns a non-zero status/exit code
// - subprocess plugin: child process exit code // - subprocess plugin: child process exit code
// - extism plugin: wasm function return code // - extism plugin: wasm function return code
type InvokeExecError struct { type InvokeExecError struct {
ExitCode int // Exit code from plugin code execution ExitCode int // Exit code from plugin code execution
Err error // Underlying error Err error // Underlying error
Stderr []byte // Captured stderr output
} }
// Error implements the error interface // Error implements the error interface
func (e *InvokeExecError) Error() string { func (e *InvokeExecError) Error() string {
if len(e.Stderr) > 0 {
return e.Err.Error() + ": " + strconv.Quote(string(e.Stderr))
}
return e.Err.Error() return e.Err.Error()
} }

@ -166,17 +166,26 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
// right now we implement status and error return in 3 slightly different ways in this file // right now we implement status and error return in 3 slightly different ways in this file
// then replace the other three with a call to this func // then replace the other three with a call to this func
func executeCmd(prog *exec.Cmd, pluginName string) error { func executeCmd(prog *exec.Cmd, pluginName string) error {
var stderrBuf bytes.Buffer
if prog.Stderr != nil {
prog.Stderr = io.MultiWriter(prog.Stderr, &stderrBuf)
} else {
prog.Stderr = &stderrBuf
}
if err := prog.Run(); err != nil { if err := prog.Run(); err != nil {
if eerr, ok := errors.AsType[*exec.ExitError](err); ok { if eerr, ok := errors.AsType[*exec.ExitError](err); ok {
stderr := bytes.TrimSpace(stderrBuf.Bytes())
slog.Debug( slog.Debug(
"plugin execution failed", "plugin execution failed",
slog.String("pluginName", pluginName), slog.String("pluginName", pluginName),
slog.String("error", err.Error()), slog.String("error", err.Error()),
slog.Int("exitCode", eerr.ExitCode()), slog.Int("exitCode", eerr.ExitCode()),
slog.String("stderr", string(bytes.TrimSpace(eerr.Stderr)))) slog.String("stderr", string(stderr)),
)
return &InvokeExecError{ return &InvokeExecError{
Err: fmt.Errorf("plugin %q exited with error", pluginName), Err: fmt.Errorf("plugin %q exited with error", pluginName),
ExitCode: eerr.ExitCode(), ExitCode: eerr.ExitCode(),
Stderr: stderr,
} }
} }

@ -16,8 +16,10 @@ limitations under the License.
package plugin package plugin
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
@ -82,3 +84,30 @@ func TestSubprocessPluginRuntime(t *testing.T) {
assert.Nil(t, output) assert.Nil(t, output)
} }
func TestExecuteCmdCapturesStderr(t *testing.T) {
cmd := exec.Command("sh", "-c", `echo "plugin error" >&2; exit 1`)
err := executeCmd(cmd, "test-plugin")
require.Error(t, err)
var ieerr *InvokeExecError
require.ErrorAs(t, err, &ieerr)
assert.Equal(t, 1, ieerr.ExitCode)
assert.Equal(t, []byte("plugin error"), ieerr.Stderr)
assert.Equal(t, `plugin "test-plugin" exited with error: "plugin error"`, ieerr.Error())
}
func TestExecuteCmdTeesStderr(t *testing.T) {
var existing bytes.Buffer
cmd := exec.Command("sh", "-c", `echo "plugin error" >&2; exit 1`)
cmd.Stderr = &existing
err := executeCmd(cmd, "test-plugin")
require.Error(t, err)
var ieerr *InvokeExecError
require.ErrorAs(t, err, &ieerr)
assert.Equal(t, 1, ieerr.ExitCode)
assert.Equal(t, []byte("plugin error"), ieerr.Stderr)
assert.Equal(t, "plugin error\n", existing.String())
}

@ -124,7 +124,7 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) {
execErr := &plugin.InvokeExecError{} execErr := &plugin.InvokeExecError{}
if errors.As(err, &execErr) { if errors.As(err, &execErr) {
return CommandError{ return CommandError{
error: execErr.Err, error: execErr,
ExitCode: execErr.ExitCode, ExitCode: execErr.ExitCode,
} }
} }

Loading…
Cancel
Save