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

Implement plugin manager #37

Merged
merged 14 commits into from
May 4, 2022
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"
gokarnm marked this conversation as resolved.
Show resolved Hide resolved

// 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