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 all 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
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 {
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
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]
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
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())
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
}

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

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