Skip to content

Commit

Permalink
Initial commit - minimal implementation
Browse files Browse the repository at this point in the history
Initial commit - minimal implementation
  • Loading branch information
priteshbandi authored Dec 22, 2023
2 parents 7b5a8a6 + 3e2defd commit c8c68d2
Show file tree
Hide file tree
Showing 21 changed files with 1,110 additions and 0 deletions.
217 changes: 217 additions & 0 deletions cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// 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 {
name string
pl plugin.Plugin
logger log.Logger
}

// New creates a new CLI using given plugin
func New(executableName string, pl plugin.Plugin) *CLI {
return NewWithLogger(executableName, pl, &discardLogger{})
}

// NewWithLogger creates a new CLI using given plugin and logger
func NewWithLogger(executableName string, pl plugin.Plugin, l log.Logger) *CLI {
return &CLI{
name: executableName,
pl: pl,
logger: l,
}
}

// 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
switch plugin.Command(command) {
case plugin.CommandGetMetadata:
var request plugin.GetMetadataRequest
err = c.unmarshalRequest(&request)
if err == nil {
c.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 = c.unmarshalRequest(&request)
if err == nil {
c.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 = c.unmarshalRequest(&request)
if err == nil {
c.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 = c.unmarshalRequest(&request)
if err == nil {
c.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 = c.unmarshalRequest(&request)
if err == nil {
c.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 := c.marshalResponse(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 := c.getMetadata(ctx, c.pl)

fmt.Printf("%s - %s\nVersion: %s \n", 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 := c.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)))
}
}

// unmarshalRequest reads input from std.in and unmarshal it into given request struct
func (c *CLI) unmarshalRequest(request any) error {
if err := json.NewDecoder(os.Stdin).Decode(request); err != nil {
c.logger.Errorf("%s unmarshalling error :%v", reflect.TypeOf(request), err)
return plugin.NewJSONParsingError(plugin.ErrorMsgMalformedInput)
}
return nil
}

func (c *CLI) getMetadata(ctx context.Context, p plugin.Plugin) *plugin.GetMetadataResponse {
md, err := p.GetMetadata(ctx, &plugin.GetMetadataRequest{})
if err != nil {
c.logger.Errorf("GetMetadataRequest error :%v", err)
deliverError("Error: Failed to get plugin metadata.")
}
return md
}

// marshalResponse marshals the given response struct into json
func (c *CLI) marshalResponse(response any, err error) (string, *plugin.Error) {
if err != nil {
c.logger.Errorf("%s error: %v", reflect.TypeOf(response), err)
if plgErr, ok := err.(*plugin.Error); ok {
return "", plgErr
}
return "", plugin.NewGenericError(err.Error())
}

c.logger.Debug("marshalling response")
jsonResponse, err := json.Marshal(response)
if err != nil {
c.logger.Errorf("%s marshalling error: %v", reflect.TypeOf(response), err)
return "", plugin.NewGenericErrorf(plugin.ErrorMsgMalformedOutputFmt, err.Error())
}

c.logger.Debugf("%s response: %s", reflect.TypeOf(response), jsonResponse)
return string(jsonResponse), nil
}

// 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() {
// Ignoring error because we don't want plugin to fail if `os.DevNull` is misconfigured.
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
}
}

// discardLogger implements Logger but logs nothing. It is used when user
// disenabled logging option in notation, i.e. loggerKey is not in the context.
type discardLogger struct{}

func (dl *discardLogger) Debug(_ ...interface{}) {
}

func (dl *discardLogger) Debugf(_ string, _ ...interface{}) {
}

func (dl *discardLogger) Debugln(_ ...interface{}) {
}

func (dl *discardLogger) Info(_ ...interface{}) {
}

func (dl *discardLogger) Infof(_ string, _ ...interface{}) {
}

func (dl *discardLogger) Infoln(_ ...interface{}) {
}

func (dl *discardLogger) Warn(_ ...interface{}) {
}

func (dl *discardLogger) Warnf(_ string, _ ...interface{}) {
}

func (dl *discardLogger) Warnln(_ ...interface{}) {
}

func (dl *discardLogger) Error(_ ...interface{}) {
}

func (dl *discardLogger) Errorf(_ string, _ ...interface{}) {
}

func (dl *discardLogger) Errorln(_ ...interface{}) {
}
165 changes: 165 additions & 0 deletions cli/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package cli

import (
"context"
"fmt"
"log"
"os"
"os/exec"
"reflect"
"strings"
"testing"

"github.com/notaryproject/notation-plugin-framework-go/cli/internal/mock"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)

var cli = New("mockCli", &mockPlugin{})

func TestMarshalResponse(t *testing.T) {
res := plugin.GenerateEnvelopeResponse{
SignatureEnvelope: []byte("envelope"),
Annotations: map[string]string{"key": "value"},
SignatureEnvelopeType: "envelopeType",
}
op, err := cli.marshalResponse(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) {

_, err := cli.marshalResponse(nil, fmt.Errorf("expected error thrown"))
assertErr(t, err, plugin.CodeGeneric)

_, err = cli.marshalResponse(nil, plugin.NewValidationError("expected validation error thrown"))
assertErr(t, err, plugin.CodeValidation)

_, err = cli.marshalResponse(make(chan int), nil)
assertErr(t, err, plugin.CodeGeneric)
}

func TestUnmarshalRequest(t *testing.T) {
content := "{\"contractVersion\":\"1.0\",\"keyId\":\"someKeyId\",\"pluginConfig\":{\"pc1\":\"pk1\"}}"
closer := setupReader(content)
defer closer()

var request plugin.DescribeKeyRequest
if err := cli.unmarshalRequest(&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) {
closer := setupReader("InvalidJson")
defer closer()

var request plugin.DescribeKeyRequest
err := cli.unmarshalRequest(&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 TestGetMetadata(t *testing.T) {
pl := mock.NewPlugin(false)
ctx := context.Background()

cli.getMetadata(ctx, pl)
}

func TestGetMetadataError(t *testing.T) {
if os.Getenv("TEST_OS_EXIT") == "1" {
pl := mock.NewPlugin(true)
ctx := context.Background()

cli.getMetadata(ctx, pl)
return
}
cmd := exec.Command(os.Args[0], "-test.run=TestGetMetadataError")
cmd.Env = append(os.Environ(), "TEST_OS_EXIT=1")
err := cmd.Run()
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}

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))
}

type mockPlugin struct {
}

func (p *mockPlugin) DescribeKey(_ context.Context, _ *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
return nil, plugin.NewUnsupportedError("DescribeKey operation is not implemented by example plugin")
}

func (p *mockPlugin) GenerateSignature(_ context.Context, _ *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
return nil, plugin.NewUnsupportedError("GenerateSignature operation is not implemented by example plugin")
}

func (p *mockPlugin) GenerateEnvelope(_ context.Context, _ *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
return nil, plugin.NewUnsupportedError("GenerateEnvelope operation is not implemented by example plugin")
}

func (p *mockPlugin) VerifySignature(_ context.Context, _ *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
return nil, plugin.NewUnsupportedError("VerifySignature operation is not implemented by example plugin")

}

func (p *mockPlugin) GetMetadata(_ context.Context, _ *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
return nil, plugin.NewUnsupportedError("GetMetadata operation is not implemented by mock plugin")
}
Loading

0 comments on commit c8c68d2

Please sign in to comment.