-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Pritesh Bandi <[email protected]>
- Loading branch information
1 parent
e82d02b
commit 8a6a760
Showing
21 changed files
with
1,140 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Package cli provides boilerplate code required to generate plugin executable. | ||
// At high level it performs following tasks | ||
// 1. Validate command arguments | ||
// 2. Read and unmarshal input | ||
// 3. Execute relevant plugin functions | ||
// 4. marshals output | ||
|
||
package cli | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"reflect" | ||
|
||
"github.com/notaryproject/notation-plugin-framework-go/internal/slices" | ||
"github.com/notaryproject/notation-plugin-framework-go/log" | ||
"github.com/notaryproject/notation-plugin-framework-go/plugin" | ||
) | ||
|
||
type Cli struct { | ||
syntax string | ||
pl plugin.Plugin | ||
} | ||
|
||
// New creates a new Cli using given plugin | ||
func New(pl plugin.Plugin) *Cli { | ||
return &Cli{pl: pl} | ||
} | ||
|
||
// Execute is main controller that reads/validates commands, parses input, executes relevant plugin functions | ||
// and returns corresponding output. | ||
func (c Cli) Execute(ctx context.Context, args []string) { | ||
c.validateArgs(ctx, args) | ||
|
||
rescueStdOut := deferStdout() | ||
command := args[1] | ||
var resp any | ||
var err error | ||
logger := log.GetLogger(ctx) | ||
switch plugin.Command(command) { | ||
case plugin.CommandGetMetadata: | ||
var request plugin.GetMetadataRequest | ||
err = unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
logger.Debugf("executing %s plugin's GetMetadata function", reflect.TypeOf(c.pl)) | ||
resp, err = c.pl.GetMetadata(ctx, &request) | ||
} | ||
case plugin.CommandGenerateEnvelope: | ||
var request plugin.GenerateEnvelopeRequest | ||
err = unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
logger.Debugf("executing %s plugin's GenerateEnvelope function", reflect.TypeOf(c.pl)) | ||
resp, err = c.pl.GenerateEnvelope(ctx, &request) | ||
} | ||
case plugin.CommandVerifySignature: | ||
var request plugin.VerifySignatureRequest | ||
err = unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
logger.Debugf("executing %s plugin's VerifySignature function", reflect.TypeOf(c.pl)) | ||
resp, err = c.pl.VerifySignature(ctx, &request) | ||
} | ||
case plugin.CommandDescribeKey: | ||
var request plugin.DescribeKeyRequest | ||
err = unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
logger.Debugf("executing %s plugin's DescribeKey function", reflect.TypeOf(c.pl)) | ||
resp, err = c.pl.DescribeKey(ctx, &request) | ||
} | ||
case plugin.CommandGenerateSignature: | ||
var request plugin.VerifySignatureRequest | ||
err = unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
logger.Debugf("executing %s plugin's VerifySignature function", reflect.TypeOf(c.pl)) | ||
resp, err = c.pl.VerifySignature(ctx, &request) | ||
} | ||
case plugin.Version: | ||
rescueStdOut() | ||
c.printVersion(ctx) | ||
default: | ||
// should never happen | ||
rescueStdOut() | ||
deliverError(plugin.NewGenericError("something went wrong").Error()) | ||
} | ||
|
||
op, pluginErr := marshalResponse(ctx, resp, err) | ||
rescueStdOut() | ||
if pluginErr != nil { | ||
deliverError(pluginErr.Error()) | ||
} | ||
fmt.Println(op) | ||
} | ||
|
||
// printHelp prints help text for executable | ||
func (c Cli) printHelp(ctx context.Context) { | ||
md := getMetadata(ctx, c.pl) | ||
args := getValidArgsString(md) | ||
|
||
fmt.Printf("%s - %s\n Usage:%s %s", md.Name, md.Description, c.syntax, args) | ||
} | ||
|
||
// printVersion prints version of executable | ||
func (c Cli) printVersion(ctx context.Context) { | ||
md := getMetadata(ctx, c.pl) | ||
|
||
fmt.Printf("%s - %s\nVersion: %s", md.Name, md.Description, md.Version) | ||
os.Exit(0) | ||
} | ||
|
||
// validateArgs validate commands/arguments passed to executable. | ||
func (c Cli) validateArgs(ctx context.Context, args []string) { | ||
md := getMetadata(ctx, c.pl) | ||
if !(len(args) == 2 && slices.Contains(getValidArgs(md), args[1])) { | ||
deliverError(fmt.Sprintf("Invalid command, valid choices are %s %s", c.syntax, getValidArgsString(md))) | ||
} | ||
} | ||
|
||
// deferStdout is used to make sure that nothing get emitted to stdout and stderr until intentionally rescued. | ||
// This is required to make sure that the plugin or its dependency doesn't interferes with notation <-> plugin communication | ||
func deferStdout() func() { | ||
null, _ := os.Open(os.DevNull) | ||
sout := os.Stdout | ||
serr := os.Stderr | ||
os.Stdout = null | ||
os.Stderr = null | ||
|
||
return func() { | ||
err := null.Close() | ||
if err != nil { | ||
return | ||
} | ||
os.Stdout = sout | ||
os.Stderr = serr | ||
} | ||
} | ||
|
||
// unmarshalRequest reads input from std.in and unmarshal it into given request struct | ||
func unmarshalRequest(ctx context.Context, request any) error { | ||
if err := json.NewDecoder(os.Stdin).Decode(request); err != nil { | ||
logger := log.GetLogger(ctx) | ||
logger.Errorf("%s unmarshalling error :%v", reflect.TypeOf(request), err) | ||
return plugin.NewJSONParsingError(plugin.ErrorMsgMalformedInput) | ||
} | ||
return nil | ||
} | ||
|
||
// marshalResponse marshals the given response struct into json | ||
func marshalResponse(ctx context.Context, response any, err error) (string, *plugin.Error) { | ||
logger := log.GetLogger(ctx) | ||
if err != nil { | ||
logger.Errorf("%s error :%v", reflect.TypeOf(response), err) | ||
if plgErr, ok := err.(*plugin.Error); ok { | ||
return "", plgErr | ||
} | ||
return "", plugin.NewGenericError(err.Error()) | ||
} | ||
|
||
logger.Debug("marshalling response") | ||
jsonResponse, err := json.Marshal(response) | ||
if err != nil { | ||
logger.Errorf("%s marshalling error: %v", reflect.TypeOf(response), err) | ||
return "", plugin.NewGenericErrorf(plugin.ErrorMsgMalformedOutputFmt, err.Error()) | ||
} | ||
|
||
logger.Debugf("%s response :%s", reflect.TypeOf(response), jsonResponse) | ||
return string(jsonResponse), nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package cli | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"os" | ||
"reflect" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/notaryproject/notation-plugin-framework-go/plugin" | ||
) | ||
|
||
func TestMarshalResponse(t *testing.T) { | ||
ctx := context.Background() | ||
res := plugin.GenerateEnvelopeResponse{ | ||
SignatureEnvelope: []byte("envelope"), | ||
Annotations: map[string]string{"key": "value"}, | ||
SignatureEnvelopeType: "envelopeType", | ||
} | ||
op, err := marshalResponse(ctx, res, nil) | ||
if err != nil { | ||
t.Errorf("Error found in marshalResponse: %v", err) | ||
} | ||
|
||
expected := "{\"signatureEnvelope\":\"ZW52ZWxvcGU=\",\"signatureEnvelopeType\":\"envelopeType\",\"annotations\":{\"key\":\"value\"}}" | ||
if !strings.EqualFold("{\"signatureEnvelope\":\"ZW52ZWxvcGU=\",\"signatureEnvelopeType\":\"envelopeType\",\"annotations\":{\"key\":\"value\"}}", op) { | ||
t.Errorf("Not equal: \n expected: %s\n actual : %s", expected, op) | ||
} | ||
} | ||
|
||
func TestMarshalResponseError(t *testing.T) { | ||
ctx := context.Background() | ||
|
||
_, err := marshalResponse(ctx, nil, fmt.Errorf("expected error thrown")) | ||
assertErr(t, err, plugin.CodeGeneric) | ||
|
||
_, err = marshalResponse(ctx, nil, plugin.NewValidationError("expected validation error thrown")) | ||
assertErr(t, err, plugin.CodeValidation) | ||
|
||
_, err = marshalResponse(ctx, make(chan int), nil) | ||
assertErr(t, err, plugin.CodeGeneric) | ||
} | ||
|
||
func TestUnmarshalRequest(t *testing.T) { | ||
ctx := context.Background() | ||
content := "{\"contractVersion\":\"1.0\",\"keyId\":\"someKeyId\",\"pluginConfig\":{\"pc1\":\"pk1\"}}" | ||
closer := setupReader(content) | ||
defer closer() | ||
|
||
var request plugin.DescribeKeyRequest | ||
if err := unmarshalRequest(ctx, &request); err != nil { | ||
t.Errorf("unmarshalRequest() failed with error: %v", err) | ||
} | ||
|
||
if request.ContractVersion != "1.0" || request.KeyID != "someKeyId" || request.PluginConfig["pc1"] != "pk1" { | ||
t.Errorf("unmarshalRequest() returned incorrect struct") | ||
} | ||
} | ||
|
||
func TestUnmarshalRequestError(t *testing.T) { | ||
ctx := context.Background() | ||
closer := setupReader("InvalidJson") | ||
defer closer() | ||
|
||
var request plugin.DescribeKeyRequest | ||
err := unmarshalRequest(ctx, &request) | ||
if err == nil { | ||
t.Fatalf("unmarshalRequest() expected error but not found") | ||
} | ||
|
||
plgErr, ok := err.(*plugin.Error) | ||
if !ok { | ||
t.Fatalf("unmarshalRequest() expected error of type plugin.Error but found %s", reflect.TypeOf(err)) | ||
} | ||
|
||
expectedErrStr := "{\"errorCode\":\"VALIDATION_ERROR\",\"errorMessage\":\"Input is not a valid JSON\"}" | ||
if plgErr.Error() != expectedErrStr { | ||
t.Fatalf("unmarshalRequest() expected error string to be %s but found %s", expectedErrStr, plgErr.Error()) | ||
} | ||
|
||
} | ||
|
||
func setupReader(content string) func() { | ||
tmpfile, err := os.CreateTemp("", "example") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
if _, err := tmpfile.Write([]byte(content)); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
if _, err := tmpfile.Seek(0, 0); err != nil { | ||
log.Fatal(err) | ||
} | ||
|
||
oldStdin := os.Stdin | ||
os.Stdin = tmpfile | ||
return func() { | ||
os.Remove(tmpfile.Name()) | ||
os.Stdin = oldStdin // Restore original Stdin | ||
|
||
tmpfile.Close() | ||
} | ||
} | ||
|
||
//func TestUnmarshalRequest(t *testing.T) { | ||
// ctx := context.Background() | ||
// req := plugin.GenerateSignatureRequest{ | ||
// ContractVersion: "1.0", | ||
// KeyID: "keyId", | ||
// KeySpec: plugin.KeySpecEC384, | ||
// Hash: plugin.HashAlgorithmSHA384, | ||
// Payload: []byte("payload"), | ||
// } | ||
// | ||
// err := unmarshalRequest(ctx, req) | ||
// if err != nil { | ||
// t.Errorf("Error found in marshalResponse: %v", err) | ||
// } | ||
// | ||
// expected := "{\"signatureEnvelope\":\"ZW52ZWxvcGU=\",\"signatureEnvelopeType\":\"envelopeType\",\"annotations\":{\"key\":\"value\"}}" | ||
// if !strings.EqualFold("{\"signatureEnvelope\":\"ZW52ZWxvcGU=\",\"signatureEnvelopeType\":\"envelopeType\",\"annotations\":{\"key\":\"value\"}}", op) { | ||
// t.Errorf("Not equal: \n expected: %s\n actual : %s", expected, op) | ||
// } | ||
//} | ||
|
||
func assertErr(t *testing.T, err error, code plugin.Code) { | ||
if plgErr, ok := err.(*plugin.Error); ok { | ||
if reflect.DeepEqual(code, plgErr.ErrCode) { | ||
return | ||
} | ||
t.Errorf("mismatch in error code: \n expected: %s\n actual : %s", code, plgErr.ErrCode) | ||
} | ||
|
||
t.Errorf("expected error of type PluginError but found %s", reflect.TypeOf(err)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Package mock contains various mock structures required for testing. | ||
package mock | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/notaryproject/notation-plugin-framework-go/plugin" | ||
) | ||
|
||
// Mock plugin used only for testing. | ||
type mockPlugin struct { | ||
fail bool | ||
} | ||
|
||
func NewPlugin(failPlugin bool) *mockPlugin { | ||
return &mockPlugin{fail: failPlugin} | ||
} | ||
|
||
func (p *mockPlugin) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) { | ||
return nil, plugin.NewUnsupportedError("DescribeKey operation is not implemented by example plugin") | ||
} | ||
|
||
func (p *mockPlugin) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) { | ||
return nil, plugin.NewUnsupportedError("GenerateSignature operation is not implemented by example plugin") | ||
} | ||
|
||
func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) { | ||
if p.fail { | ||
return nil, fmt.Errorf("expected error") | ||
} | ||
return &plugin.GenerateEnvelopeResponse{ | ||
SignatureEnvelope: []byte(""), | ||
SignatureEnvelopeType: "application/jose+json", | ||
Annotations: map[string]string{"manifestAnntnKey1": "value1"}, | ||
}, nil | ||
} | ||
|
||
func (p *mockPlugin) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) { | ||
if p.fail { | ||
return nil, fmt.Errorf("expected error") | ||
} | ||
upAttrs := req.Signature.UnprocessedAttributes | ||
pAttrs := make([]interface{}, len(upAttrs)) | ||
for i := range upAttrs { | ||
pAttrs[i] = upAttrs[i] | ||
} | ||
|
||
return &plugin.VerifySignatureResponse{ | ||
ProcessedAttributes: pAttrs, | ||
VerificationResults: map[plugin.Capability]*plugin.VerificationResult{ | ||
plugin.CapabilityTrustedIdentityVerifier: { | ||
Success: true, | ||
Reason: "Valid trusted Identity", | ||
}, | ||
plugin.CapabilityRevocationCheckVerifier: { | ||
Success: true, | ||
Reason: "Not revoked", | ||
}, | ||
}, | ||
}, nil | ||
} | ||
|
||
func (p *mockPlugin) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) { | ||
if p.fail { | ||
return nil, fmt.Errorf("expected error") | ||
} | ||
return &plugin.GetMetadataResponse{ | ||
Name: "Example Plugin", | ||
Description: "This is an description of example plugin. 🍺", | ||
URL: "https://example.com/notation/plugin", | ||
Version: "1.0.0", | ||
Capabilities: []plugin.Capability{ | ||
plugin.CapabilityEnvelopeGenerator, | ||
plugin.CapabilityTrustedIdentityVerifier, | ||
plugin.CapabilityRevocationCheckVerifier}, | ||
}, nil | ||
} |
Oops, something went wrong.