Skip to content

Commit

Permalink
Implement plugin manager (#37)
Browse files Browse the repository at this point in the history
* implement plugin manager
* ignore symlinks
* fix Manager.Command
* export constants
* improve error message
* move manager to its own package
* change metadata error messages
* improve the plugin manager interface
* improve Manager.Run
* pr feedback
* remove Command.Capability() and Command.NewResponse()
* remove command validation
* ignore symlinked plugin directories

Signed-off-by: qmuntal <qmuntaldiaz@microsoft.com>
  • Loading branch information
qmuntal authored May 4, 2022
1 parent b1d0c27 commit 182873b
Show file tree
Hide file tree
Showing 9 changed files with 1,069 additions and 0 deletions.
74 changes: 74 additions & 0 deletions plugin/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package plugin

import (
"encoding/json"
"errors"
"fmt"
)

type ErrorCode string

const (
// Any of the required request fields was empty,
// or a value was malformed/invalid.
ErrorCodeValidation ErrorCode = "VALIDATION_ERROR"

// The contract version used in the request is unsupported.
ErrorCodeUnsupportedContractVersion ErrorCode = "UNSUPPORTED_CONTRACT_VERSION"

// Authentication/authorization error to use given key.
ErrorCodeAccessDenied ErrorCode = "ACCESS_DENIED"

// The operation to generate signature timed out
// and can be retried by Notation.
ErrorCodeTimeout ErrorCode = "TIMEOUT"

// The operation to generate signature was throttles
// and can be retried by Notation.
ErrorCodeThrottled ErrorCode = "THROTTLED"

// Any general error that does not fall into any categories.
ErrorCodeGeneric ErrorCode = "ERROR"
)

type jsonErr struct {
Code ErrorCode `json:"errorCode"`
Message string `json:"errorMessage,omitempty"`
Metadata map[string]string `json:"errorMetadata,omitempty"`
}

// RequestError is the common error response for any request.
type RequestError struct {
Code ErrorCode
Err error
Metadata map[string]string
}

func (e RequestError) Error() string {
return fmt.Sprintf("%s: %v", e.Code, e.Err)
}

func (e RequestError) Unwrap() error {
return e.Err
}

func (e RequestError) MarshalJSON() ([]byte, error) {
var msg string
if e.Err != nil {
msg = e.Err.Error()
}
return json.Marshal(jsonErr{e.Code, msg, e.Metadata})
}

func (e *RequestError) UnmarshalJSON(data []byte) error {
var tmp jsonErr
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil {
return errors.New("incomplete json")
}
*e = RequestError{tmp.Code, errors.New(tmp.Message), tmp.Metadata}
return nil
}
90 changes: 90 additions & 0 deletions plugin/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package plugin

import (
"encoding/json"
"errors"
"reflect"
"testing"
)

func TestRequestError_Error(t *testing.T) {
err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")}
want := string(ErrorCodeAccessDenied) + ": an error"
if got := err.Error(); got != want {
t.Errorf("RequestError.Error() = %v, want %v", got, want)
}
}

func TestRequestError_Unwrap(t *testing.T) {
want := errors.New("an error")
got := RequestError{Code: ErrorCodeAccessDenied, Err: want}.Unwrap()
if got != want {
t.Errorf("RequestError.Unwrap() = %v, want %v", got, want)
}
}

func TestRequestError_MarshalJSON(t *testing.T) {
tests := []struct {
name string
e RequestError
want []byte
}{
{"empty", RequestError{}, []byte("{\"errorCode\":\"\"}")},
{"with code", RequestError{Code: ErrorCodeAccessDenied}, []byte("{\"errorCode\":\"ACCESS_DENIED\"}")},
{"with message", RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("failed")}, []byte("{\"errorCode\":\"ACCESS_DENIED\",\"errorMessage\":\"failed\"}")},
{
"with metadata",
RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("failed"), Metadata: map[string]string{"a": "b"}},
[]byte("{\"errorCode\":\"ACCESS_DENIED\",\"errorMessage\":\"failed\",\"errorMetadata\":{\"a\":\"b\"}}"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.e.MarshalJSON()
if err != nil {
t.Fatalf("RequestError.MarshalJSON() error = %v, wantErr false", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("RequestError.MarshalJSON() = %s, want %s", got, tt.want)
}
if tt.e.Code == "" {
return
}
var got1 RequestError
err = json.Unmarshal(got, &got1)
if err != nil {
t.Fatalf("RequestError.UnmarshalJSON() error = %v, wantErr false", err)
}
if got1.Code != tt.e.Code || !reflect.DeepEqual(got1.Metadata, tt.e.Metadata) {
t.Fatalf("RequestError.UnmarshalJSON() = %s, want %s", got1, tt.e)
}
})
}
}

func TestRequestError_UnmarshalJSON(t *testing.T) {
type args struct {
data []byte
}
tests := []struct {
name string
args args
want RequestError
wantErr bool
}{
{"invalid", args{[]byte("")}, RequestError{}, true},
{"empty", args{[]byte("{}")}, RequestError{}, true},
{"with code", args{[]byte("{\"errorCode\":\"ACCESS_DENIED\"}")}, RequestError{Code: ErrorCodeAccessDenied}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var e RequestError
if err := e.UnmarshalJSON(tt.args.data); (err != nil) != tt.wantErr {
t.Errorf("RequestError.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr && (e.Code != tt.want.Code || !reflect.DeepEqual(e.Metadata, tt.want.Metadata)) {
t.Fatalf("RequestError.UnmarshalJSON() = %s, want %s", e, tt.want)
}
})
}
}
78 changes: 78 additions & 0 deletions plugin/manager/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package manager

import (
"context"
"io"
"os"
"os/exec"
"path/filepath"
"reflect"
"testing"

"github.com/notaryproject/notation-go/plugin"
)

func preparePlugin(t *testing.T) string {
root := t.TempDir()
src, err := os.Open("./testdata/main.go")
if err != nil {
t.Fatal(err)
}
defer src.Close()

dst, err := os.Create(filepath.Join(root, "main.go"))
if err != nil {
t.Fatal(err)
}
defer dst.Close()
_, err = io.Copy(dst, src)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(root, "go.mod"), []byte("module main"), 0666)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir(filepath.Join(root, "foo"), 0755)
if err != nil {
t.Fatal(err)
}
out := filepath.Join(root, "foo", plugin.Prefix+"foo")
out = addExeSuffix(out)
cmd := exec.Command("go", "build", "-o", out)
cmd.Dir = root
err = cmd.Run()
if err != nil {
t.Fatal(err)
}
return root
}

func TestIntegration(t *testing.T) {
if _, err := exec.LookPath("go"); err != nil {
t.Skip()
}
root := preparePlugin(t)
mgr := &Manager{rootedFS{os.DirFS(root), root}, execCommander{}}
p, err := mgr.Get(context.Background(), "foo")
if err != nil {
t.Fatal(err)
}
if p.Err != nil {
t.Fatal(p.Err)
}
list, err := mgr.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(list) != 1 {
t.Fatalf("Manager.List() len got %d, want 1", len(list))
}
if !reflect.DeepEqual(list[0].Metadata, p.Metadata) {
t.Errorf("Manager.List() got %v, want %v", list[0], p)
}
_, err = mgr.Run(context.Background(), "foo", plugin.CommandGetMetadata, nil)
if err != nil {
t.Fatal(err)
}
}
Loading

0 comments on commit 182873b

Please sign in to comment.