Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
Signed-off-by: Pritesh Bandi <[email protected]>
  • Loading branch information
priteshbandi committed Jul 27, 2023
1 parent e82d02b commit 8a6a760
Show file tree
Hide file tree
Showing 21 changed files with 1,140 additions and 0 deletions.
168 changes: 168 additions & 0 deletions cli/cli.go
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
}
139 changes: 139 additions & 0 deletions cli/cli_test.go
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))
}
78 changes: 78 additions & 0 deletions cli/internal/mock/plugin.go
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
}
Loading

0 comments on commit 8a6a760

Please sign in to comment.