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

Initial commit #2

Merged
merged 9 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
162 changes: 162 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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 {
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
name string
pl plugin.Plugin
}

// New creates a new CLI using given plugin
func New(executableName string, pl plugin.Plugin) *CLI {
return &CLI{
name: executableName,
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]
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
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())
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
}

op, pluginErr := marshalResponse(ctx, resp, err)
rescueStdOut()
if pluginErr != nil {
deliverError(pluginErr.Error())
}
fmt.Println(op)
}

// 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.name, 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 interfere with notation <-> plugin communication
func deferStdout() func() {
null, _ := os.Open(os.DevNull)
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
sout := os.Stdout
serr := os.Stderr
os.Stdout = null
os.Stderr = null

return func() {
err := null.Close()
if err != nil {
return
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
}
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
}
118 changes: 118 additions & 0 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 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