Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve plugin error message #371

Merged
merged 20 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion plugin/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import "errors"
var ErrNotCompliant = errors.New("plugin not compliant")

// ErrNotRegularFile is returned when the plugin file is not an regular file.
var ErrNotRegularFile = errors.New("not regular file")
var ErrNotRegularFile = errors.New("plugin executable file is not a regular file")

// PluginDowngradeError is returned when installing a plugin with version
// lower than the exisiting plugin version.
type PluginDowngradeError struct {
Msg string
}

// Error returns the error message.
func (e PluginDowngradeError) Error() string {
if e.Msg != "" {
return e.Msg
Expand All @@ -41,9 +42,39 @@ type InstallEqualVersionError struct {
Msg string
}

// Error returns the error message.
func (e InstallEqualVersionError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "installing plugin with version equal to the existing plugin version"
}

// PluginMalformedError is used when there is an issue with plugin and
// should be fixed by plugin developers.
type PluginMalformedError struct {
Msg string
InnerError error
}

// Error returns the error message.
func (e PluginMalformedError) Error() string {
if e.Msg != "" {
return e.Msg
}
return e.InnerError.Error()
}

// Unwrap returns the inner error.
func (e PluginMalformedError) Unwrap() error {
return e.InnerError
}

// PluginDirectoryWalkError is used when there is an issue with plugins directory
// and should suggest user to check the permission of plugin directory.
type PluginDirectoryWalkError error

// PluginExecutableFileError is used when there is an issue with plugin
// executable file and should suggest user to check the existence, permission
// and platform/arch compatibility of plugin.
type PluginExecutableFileError error
11 changes: 8 additions & 3 deletions plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,11 @@ func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
// List produces a list of the plugin names on the system.
func (m *CLIManager) List(ctx context.Context) ([]string, error) {
var plugins []string
fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
if err := fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
if dir == "." {
Expand All @@ -79,7 +82,9 @@ func (m *CLIManager) List(ctx context.Context) ([]string, error) {
// add plugin name
plugins = append(plugins, d.Name())
return fs.SkipDir
})
}); err != nil {
return nil, PluginDirectoryWalkError(fmt.Errorf("failed to list plugin: %w", err))
}
return plugins, nil
}

Expand Down Expand Up @@ -146,7 +151,7 @@ func (m *CLIManager) Install(ctx context.Context, installOpts CLIInstallOptions)
// validate and get new plugin metadata
newPlugin, err := NewCLIPlugin(ctx, pluginName, pluginExecutableFile)
if err != nil {
return nil, nil, fmt.Errorf("failed to create new CLI plugin: %w", err)
return nil, nil, err
}
newPluginMetadata, err := newPlugin.GetMetadata(ctx, &proto.GetMetadataRequest{})
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions plugin/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ func TestManager_Install(t *testing.T) {
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to get metadata of new plugin: executable name must be \"notation-foobar\" instead of \"notation-bar\""
expectedErrorMsg := "failed to get metadata of new plugin: plugin executable file name must be \"notation-foobar\" instead of \"notation-bar\""
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
Expand All @@ -405,7 +405,7 @@ func TestManager_Install(t *testing.T) {
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to get metadata of existing plugin: executable name must be \"notation-bar\" instead of \"notation-foo\""
expectedErrorMsg := "failed to get metadata of existing plugin: plugin executable file name must be \"notation-bar\" instead of \"notation-foo\""
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
Expand Down
46 changes: 30 additions & 16 deletions plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/log"
Expand Down Expand Up @@ -81,7 +82,7 @@ func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
if err != nil {
// Ignore any file which we cannot Stat
// (e.g. due to permissions or anything else).
return nil, err
return nil, fmt.Errorf("plugin executable file is either not found or inaccessible: %w", err)
}
if !fi.Mode().IsRegular() {
// Ignore non-regular files.
Expand All @@ -105,10 +106,13 @@ func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataReque
}
// validate metadata
if err = validate(&metadata); err != nil {
return nil, fmt.Errorf("invalid metadata: %w", err)
return nil, &PluginMalformedError{
Msg: fmt.Sprintf("metadata validation failed for plugin %s: %s", p.name, err),
InnerError: err,
}
}
if metadata.Name != p.name {
return nil, fmt.Errorf("executable name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
return nil, fmt.Errorf("plugin executable file name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
}
return &metadata, nil
}
Expand Down Expand Up @@ -171,30 +175,40 @@ func run(ctx context.Context, pluginName string, pluginPath string, req proto.Re
// serialize request
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("%s: failed to marshal request object: %w", pluginName, err)
logger.Errorf("Failed to marshal request object: %+v", req)
return fmt.Errorf("failed to marshal request object: %w", err)
}

logger.Debugf("Plugin %s request: %s", req.Command(), string(data))
// execute request
stdout, stderr, err := executor.Output(ctx, pluginPath, req.Command(), data)
if err != nil {
logger.Debugf("plugin %s execution status: %v", req.Command(), err)
logger.Debugf("Plugin %s returned error: %s", req.Command(), string(stderr))
var re proto.RequestError
jsonErr := json.Unmarshal(stderr, &re)
if jsonErr != nil {
return proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", err, string(stderr))}
logger.Errorf("plugin %s execution status: %v", req.Command(), err)

if len(stderr) == 0 {
// if stderr is empty, it is possible that the plugin is not
// running properly.
return PluginExecutableFileError(fmt.Errorf("failed to execute the %s command for plugin %s: %w", req.Command(), pluginName, err))
} else {
var re proto.RequestError
jsonErr := json.Unmarshal(stderr, &re)
if jsonErr != nil {
return &PluginMalformedError{
Msg: fmt.Sprintf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n")),
InnerError: jsonErr,
}
}
return fmt.Errorf("failed to execute the %s command for plugin %s: %w", req.Command(), pluginName, re)
}
return re
}

logger.Debugf("Plugin %s response: %s", req.Command(), string(stdout))
// deserialize response
err = json.Unmarshal(stdout, resp)
if err != nil {
return fmt.Errorf("failed to decode json response: %w", ErrNotCompliant)
if err = json.Unmarshal(stdout, resp); err != nil {
return &PluginMalformedError{
Msg: fmt.Sprintf("plugin %s response for %s command isn't compliant with Notation plugin requirement: %s", pluginName, req.Command(), string(stdout)),
InnerError: err,
}
}
return nil
}
Expand Down
40 changes: 18 additions & 22 deletions plugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"strconv"
Expand All @@ -31,14 +30,12 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin error is in invalid json format", func(t *testing.T) {
exitErr := errors.New("unknown error")
stderr := []byte("sad")
wantErr := proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
plugin := CLIPlugin{}
expectedErrMsg := "failed to execute the get-plugin-metadata command for plugin test-plugin: sad"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
if !errors.Is(err, wantErr) {
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
if err.Error() != expectedErrMsg {
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
}
})

Expand All @@ -58,14 +55,12 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin cause system error", func(t *testing.T) {
exitErr := errors.New("system error")
stderr := []byte("")
wantErr := proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
plugin := CLIPlugin{}
expectedErrMsg := "failed to execute the get-plugin-metadata command for plugin test-plugin: system error"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
if !errors.Is(err, wantErr) {
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
if err.Error() != expectedErrMsg {
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
}
})

Expand Down Expand Up @@ -199,9 +194,10 @@ func TestNewCLIPlugin_PathError(t *testing.T) {
})

t.Run("plugin is not a regular file", func(t *testing.T) {
expectedErrMsg := "plugin executable file is not a regular file"
p, err := NewCLIPlugin(ctx, "badplugin", "./testdata/plugins/badplugin/notation-badplugin")
if !errors.Is(err, ErrNotRegularFile) {
t.Errorf("NewCLIPlugin() error = %v, want %v", err, ErrNotRegularFile)
if err.Error() != expectedErrMsg {
t.Errorf("NewCLIPlugin() error = %v, want %v", err, expectedErrMsg)
}
if p != nil {
t.Errorf("NewCLIPlugin() plugin = %v, want nil", p)
Expand All @@ -218,32 +214,32 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
t.Run("command no response", func(t *testing.T) {
executor = testCommander{}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should return plugin validity error")
}
})

t.Run("invalid json", func(t *testing.T) {
executor = testCommander{stdout: []byte("content")}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should return plugin validity error")
}
})

t.Run("invalid metadata name", func(t *testing.T) {
executor = testCommander{stdout: metadataJSON(invalidMetadataName)}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), "executable name must be") {
if !strings.Contains(err.Error(), "executable file name must be") {
t.Fatal("should fail the operation.")
}
})

t.Run("invalid metadata content", func(t *testing.T) {
executor = testCommander{stdout: metadataJSON(proto.GetMetadataResponse{Name: "foo"})}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), "invalid metadata") {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should be plugin validity error.")
}
})

Expand Down
Loading