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 weeks ago
parent 5c3cb20e76
commit 849730659e
No known key found for this signature in database
GPG Key ID: 819E17D2E58F89D8

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

@ -169,17 +169,26 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error {
// 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
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 eerr, ok := err.(*exec.ExitError); ok {
stderr := bytes.TrimSpace(stderrBuf.Bytes())
slog.Debug(
"plugin execution failed",
slog.String("pluginName", pluginName),
slog.String("error", err.Error()),
slog.Int("exitCode", eerr.ExitCode()),
slog.String("stderr", string(bytes.TrimSpace(eerr.Stderr))))
slog.String("stderr", string(stderr)),
)
return &InvokeExecError{
Err: fmt.Errorf("plugin %q exited with error", pluginName),
ExitCode: eerr.ExitCode(),
Stderr: stderr,
}
}

@ -16,9 +16,11 @@ limitations under the License.
package plugin
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
@ -84,3 +86,30 @@ func TestSubprocessPluginRuntime(t *testing.T) {
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{}
if errors.As(err, &execErr) {
return CommandError{
error: execErr.Err,
error: execErr,
ExitCode: execErr.ExitCode,
}
}

Loading…
Cancel
Save