-
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.
Initial commit - minimal implementation
Initial commit - minimal implementation
- Loading branch information
Showing
21 changed files
with
1,110 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,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{}) { | ||
} |
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,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") | ||
} |
Oops, something went wrong.