From 8e8cddc3550517112d3996ee59902bc8c45d6a8d Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Tue, 23 Jan 2024 12:02:01 +0100 Subject: [PATCH] Pass APIOption to Execute Also: - redirect stdout/stderr to buffer, so Execute's client can read the plugin output - unwrap rpc status error - add more tests - add ChainInfo.Home (will be used for consumer plugin) - update .gitignore in plugin template to ignore also *.ign files --- ignite/services/plugin/client_api.go | 10 + .../services/plugin/grpc/v1/client_api.pb.go | 28 +- .../services/plugin/grpc/v1/interface.pb.go | 7 +- ignite/services/plugin/grpc/v1/service.pb.go | 7 +- .../plugin/grpc/v1/service_grpc.pb.go | 1 - ignite/services/plugin/internal/execute.go | 34 ++- .../services/plugin/internal/execute_test.go | 53 +++- .../internal/testdata/execute_fail/.gitignore | 1 + .../internal/testdata/execute_fail/go.mod | 81 ++++++ .../internal/testdata/execute_fail/main.go | 44 +++ .../internal/testdata/execute_ok/.gitignore | 1 + .../internal/testdata/execute_ok/go.mod | 81 ++++++ .../internal/testdata/execute_ok/main.go | 46 +++ ignite/services/plugin/mocks/chainer.go | 267 ++++++++++++++++++ ignite/services/plugin/plugin.go | 17 +- .../services/plugin/template/.gitignore.plush | 2 +- .../services/plugin/grpc/v1/client_api.proto | 1 + 17 files changed, 639 insertions(+), 42 deletions(-) create mode 100644 ignite/services/plugin/internal/testdata/execute_fail/.gitignore create mode 100644 ignite/services/plugin/internal/testdata/execute_fail/go.mod create mode 100644 ignite/services/plugin/internal/testdata/execute_fail/main.go create mode 100644 ignite/services/plugin/internal/testdata/execute_ok/.gitignore create mode 100644 ignite/services/plugin/internal/testdata/execute_ok/go.mod create mode 100644 ignite/services/plugin/internal/testdata/execute_ok/main.go create mode 100644 ignite/services/plugin/mocks/chainer.go diff --git a/ignite/services/plugin/client_api.go b/ignite/services/plugin/client_api.go index e9d48171ef..3fbc8e6b11 100644 --- a/ignite/services/plugin/client_api.go +++ b/ignite/services/plugin/client_api.go @@ -9,6 +9,7 @@ import ( // ErrAppChainNotFound indicates that the plugin command is not running inside a blockchain app. var ErrAppChainNotFound = errors.New("blockchain app not found") +//go:generate mockery --srcpkg . --name Chainer --structname ChainerInterface --filename chainer.go --with-expecter type Chainer interface { // AppPath returns the configured App's path. AppPath() string @@ -21,6 +22,9 @@ type Chainer interface { // RPCPublicAddress returns the configured App's rpc endpoint. RPCPublicAddress() (string, error) + + // Home returns the App's home dir. + Home() (string, error) } // APIOption defines options for the client API. @@ -66,11 +70,17 @@ func (api clientAPI) GetChainInfo(context.Context) (*ChainInfo, error) { return nil, err } + home, err := chain.Home() + if err != nil { + return nil, err + } + return &ChainInfo{ ChainId: chainID, AppPath: chain.AppPath(), ConfigPath: chain.ConfigPath(), RpcAddress: rpc, + Home: home, }, nil } diff --git a/ignite/services/plugin/grpc/v1/client_api.pb.go b/ignite/services/plugin/grpc/v1/client_api.pb.go index faf4cf22fc..c721fec72a 100644 --- a/ignite/services/plugin/grpc/v1/client_api.pb.go +++ b/ignite/services/plugin/grpc/v1/client_api.pb.go @@ -1,17 +1,16 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: ignite/services/plugin/grpc/v1/client_api.proto package v1 import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( @@ -30,6 +29,7 @@ type ChainInfo struct { AppPath string `protobuf:"bytes,2,opt,name=app_path,json=appPath,proto3" json:"app_path,omitempty"` ConfigPath string `protobuf:"bytes,3,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` RpcAddress string `protobuf:"bytes,4,opt,name=rpc_address,json=rpcAddress,proto3" json:"rpc_address,omitempty"` + Home string `protobuf:"bytes,5,opt,name=home,proto3" json:"home,omitempty"` } func (x *ChainInfo) Reset() { @@ -92,6 +92,13 @@ func (x *ChainInfo) GetRpcAddress() string { return "" } +func (x *ChainInfo) GetHome() string { + if x != nil { + return x.Home + } + return "" +} + var File_ignite_services_plugin_grpc_v1_client_api_proto protoreflect.FileDescriptor var file_ignite_services_plugin_grpc_v1_client_api_proto_rawDesc = []byte{ @@ -100,7 +107,7 @@ var file_ignite_services_plugin_grpc_v1_client_api_proto_rawDesc = []byte{ 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1e, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x76, - 0x31, 0x22, 0x83, 0x01, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, + 0x31, 0x22, 0x97, 0x01, 0x0a, 0x09, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, @@ -108,11 +115,12 @@ var file_ignite_services_plugin_grpc_v1_client_api_proto_rawDesc = []byte{ 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x70, 0x63, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x70, 0x63, - 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2f, 0x63, 0x6c, 0x69, - 0x2f, 0x76, 0x32, 0x38, 0x2f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x67, 0x72, 0x70, 0x63, - 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x42, 0x3a, 0x5a, 0x38, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, + 0x2f, 0x63, 0x6c, 0x69, 0x2f, 0x76, 0x32, 0x38, 0x2f, 0x69, 0x67, 0x6e, 0x69, 0x74, 0x65, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, + 0x67, 0x72, 0x70, 0x63, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/ignite/services/plugin/grpc/v1/interface.pb.go b/ignite/services/plugin/grpc/v1/interface.pb.go index c16eef1a6a..b4ea08b1b8 100644 --- a/ignite/services/plugin/grpc/v1/interface.pb.go +++ b/ignite/services/plugin/grpc/v1/interface.pb.go @@ -1,17 +1,16 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: ignite/services/plugin/grpc/v1/interface.proto package v1 import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/ignite/services/plugin/grpc/v1/service.pb.go b/ignite/services/plugin/grpc/v1/service.pb.go index 8ed95cc549..54737533f0 100644 --- a/ignite/services/plugin/grpc/v1/service.pb.go +++ b/ignite/services/plugin/grpc/v1/service.pb.go @@ -1,17 +1,16 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 +// protoc-gen-go v1.32.0 // protoc (unknown) // source: ignite/services/plugin/grpc/v1/service.proto package v1 import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( diff --git a/ignite/services/plugin/grpc/v1/service_grpc.pb.go b/ignite/services/plugin/grpc/v1/service_grpc.pb.go index 759410d156..f369c94a1e 100644 --- a/ignite/services/plugin/grpc/v1/service_grpc.pb.go +++ b/ignite/services/plugin/grpc/v1/service_grpc.pb.go @@ -8,7 +8,6 @@ package v1 import ( context "context" - grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/ignite/services/plugin/internal/execute.go b/ignite/services/plugin/internal/execute.go index 23a5c10dab..25c48f3f02 100644 --- a/ignite/services/plugin/internal/execute.go +++ b/ignite/services/plugin/internal/execute.go @@ -1,21 +1,45 @@ package plugininternal import ( + "bytes" "context" + "errors" + "time" + + "google.golang.org/grpc/status" pluginsconfig "github.com/ignite/cli/v28/ignite/config/plugins" "github.com/ignite/cli/v28/ignite/services/plugin" ) // Execute starts and executes a plugin, then shutdowns it. -func Execute(ctx context.Context, path string, args ...string) error { - plugins, err := plugin.Load(ctx, []pluginsconfig.Plugin{{Path: path}}) +func Execute(ctx context.Context, path string, args []string, options ...plugin.APIOption) (string, error) { + var buf bytes.Buffer + plugins, err := plugin.Load( + ctx, + []pluginsconfig.Plugin{{Path: path}}, + plugin.RedirectStdout(&buf), + ) if err != nil { - return err + return "", err } defer plugins[0].KillClient() if plugins[0].Error != nil { - return plugins[0].Error + return "", plugins[0].Error + } + err = plugins[0].Interface.Execute( + ctx, + &plugin.ExecutedCommand{Args: args}, + plugin.NewClientAPI(options...), + ) + if err != nil { + // Extract the rpc status message and create a simple error from it. + // We don't want Execute to return rpc errors. + err = errors.New(status.Convert(err).Message()) } - return plugins[0].Interface.Execute(ctx, &plugin.ExecutedCommand{Args: args}, plugin.NewClientAPI()) + // NOTE(tb): This pause gives enough time for go-plugin to sync the + // output from stdout/stderr of the plugin. Without that pause, this + // output can be discarded and absent from buf. + time.Sleep(100 * time.Millisecond) + return buf.String(), err } diff --git a/ignite/services/plugin/internal/execute_test.go b/ignite/services/plugin/internal/execute_test.go index 205574c327..6f3f64565b 100644 --- a/ignite/services/plugin/internal/execute_test.go +++ b/ignite/services/plugin/internal/execute_test.go @@ -2,46 +2,69 @@ package plugininternal import ( "context" + "os" + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" "github.com/ignite/cli/v28/ignite/services/plugin" + "github.com/ignite/cli/v28/ignite/services/plugin/mocks" ) func TestPluginExecute(t *testing.T) { tests := []struct { - name string - scaffoldPlugin func(t *testing.T) string - expectedError string + name string + pluginPath string + expectedOut string + expectedError string }{ { - name: "fail: plugin doesnt exist", - scaffoldPlugin: func(t *testing.T) string { - return "/not/exists" - }, + name: "fail: plugin doesnt exist", + pluginPath: "/not/exists", expectedError: "local app path \"/not/exists\" not found: stat /not/exists: no such file or directory", }, { - name: "ok: plugin exists", - scaffoldPlugin: func(t *testing.T) string { - path, err := plugin.Scaffold(context.Background(), t.TempDir(), "foo", false) - require.NoError(t, err) - return path - }, + name: "ok: plugin execute ok ", + pluginPath: "testdata/execute_ok", + expectedOut: "ok args=[arg1 arg2] chaininfo=chain_id:\"id\" app_path:\"apppath\" config_path:\"configpath\" rpc_address:\"rpcPublicAddress\" 5:\"home\"\n", + }, + { + name: "ok: plugin execute fail ", + pluginPath: "testdata/execute_fail", + expectedError: "fail", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - path := tt.scaffoldPlugin(t) + pluginPath := tt.pluginPath + if !strings.HasPrefix(pluginPath, "/") { + // add working dir to relative paths + wd, err := os.Getwd() + require.NoError(t, err) + pluginPath = filepath.Join(wd, pluginPath) + } + chainer := mocks.NewChainerInterface(t) + chainer.EXPECT().ID().Return("id", nil).Maybe() + chainer.EXPECT().AppPath().Return("apppath").Maybe() + chainer.EXPECT().ConfigPath().Return("configpath").Maybe() + chainer.EXPECT().Home().Return("home", nil).Maybe() + chainer.EXPECT().RPCPublicAddress().Return("rpcPublicAddress", nil).Maybe() - err := Execute(context.Background(), path, "args") + out, err := Execute( + context.Background(), + pluginPath, + []string{"arg1", "arg2"}, + plugin.WithChain(chainer), + ) if tt.expectedError != "" { require.EqualError(t, err, tt.expectedError) return } require.NoError(t, err) + require.Equal(t, tt.expectedOut, out) }) } } diff --git a/ignite/services/plugin/internal/testdata/execute_fail/.gitignore b/ignite/services/plugin/internal/testdata/execute_fail/.gitignore new file mode 100644 index 0000000000..52d6a429b4 --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_fail/.gitignore @@ -0,0 +1 @@ +execute_fail* diff --git a/ignite/services/plugin/internal/testdata/execute_fail/go.mod b/ignite/services/plugin/internal/testdata/execute_fail/go.mod new file mode 100644 index 0000000000..530c15a053 --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_fail/go.mod @@ -0,0 +1,81 @@ +module execute_fail + +go 1.21.1 + +toolchain go1.21.5 + +require ( + github.com/hashicorp/go-plugin v1.5.2 + github.com/ignite/cli/v28 v28.0.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/aymanbagabas/go-osc52 v1.2.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.6.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.10.0 // indirect + github.com/gobuffalo/flect v0.3.0 // indirect + github.com/gobuffalo/genny/v2 v2.1.0 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.7 // indirect + github.com/gobuffalo/logger v1.0.7 // indirect + github.com/gobuffalo/packd v1.0.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.19 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/microcosm-cc/bluemonday v1.0.23 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.14.0 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + go.etcd.io/bbolt v1.3.8 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/term v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/ignite/services/plugin/internal/testdata/execute_fail/main.go b/ignite/services/plugin/internal/testdata/execute_fail/main.go new file mode 100644 index 0000000000..2a6eeb0030 --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_fail/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "errors" + + hplugin "github.com/hashicorp/go-plugin" + + "github.com/ignite/cli/v28/ignite/services/plugin" +) + +type app struct{} + +func (app) Manifest(ctx context.Context) (*plugin.Manifest, error) { + return &plugin.Manifest{ + Name: "execute_fail", + }, nil +} + +func (app) Execute(ctx context.Context, cmd *plugin.ExecutedCommand, api plugin.ClientAPI) error { + return errors.New("fail") +} + +func (app) ExecuteHookPre(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func (app) ExecuteHookPost(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func (app) ExecuteHookCleanUp(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func main() { + hplugin.Serve(&hplugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig(), + Plugins: map[string]hplugin.Plugin{ + "execute_fail": plugin.NewGRPC(&app{}), + }, + GRPCServer: hplugin.DefaultGRPCServer, + }) +} diff --git a/ignite/services/plugin/internal/testdata/execute_ok/.gitignore b/ignite/services/plugin/internal/testdata/execute_ok/.gitignore new file mode 100644 index 0000000000..687ce346ec --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_ok/.gitignore @@ -0,0 +1 @@ +execute_ok* diff --git a/ignite/services/plugin/internal/testdata/execute_ok/go.mod b/ignite/services/plugin/internal/testdata/execute_ok/go.mod new file mode 100644 index 0000000000..8a2a9c6766 --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_ok/go.mod @@ -0,0 +1,81 @@ +module execute_ok + +go 1.21.1 + +toolchain go1.21.5 + +require ( + github.com/hashicorp/go-plugin v1.5.2 + github.com/ignite/cli/v28 v28.0.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/aymanbagabas/go-osc52 v1.2.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/charmbracelet/lipgloss v0.6.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.10.0 // indirect + github.com/gobuffalo/flect v0.3.0 // indirect + github.com/gobuffalo/genny/v2 v2.1.0 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.7 // indirect + github.com/gobuffalo/logger v1.0.7 // indirect + github.com/gobuffalo/packd v1.0.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.19 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/microcosm-cc/bluemonday v1.0.23 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.14.0 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + go.etcd.io/bbolt v1.3.8 // indirect + golang.org/x/crypto v0.15.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.18.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/term v0.14.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/ignite/services/plugin/internal/testdata/execute_ok/main.go b/ignite/services/plugin/internal/testdata/execute_ok/main.go new file mode 100644 index 0000000000..2cdd9071b5 --- /dev/null +++ b/ignite/services/plugin/internal/testdata/execute_ok/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + + hplugin "github.com/hashicorp/go-plugin" + + "github.com/ignite/cli/v28/ignite/services/plugin" +) + +type app struct{} + +func (app) Manifest(ctx context.Context) (*plugin.Manifest, error) { + return &plugin.Manifest{ + Name: "execute_ok", + }, nil +} + +func (app) Execute(ctx context.Context, cmd *plugin.ExecutedCommand, api plugin.ClientAPI) error { + chaininfo, _ := api.GetChainInfo(ctx) + fmt.Printf("ok args=%s chaininfo=%s\n", cmd.Args, chaininfo.String()) + return nil +} + +func (app) ExecuteHookPre(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func (app) ExecuteHookPost(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func (app) ExecuteHookCleanUp(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { + return nil +} + +func main() { + hplugin.Serve(&hplugin.ServeConfig{ + HandshakeConfig: plugin.HandshakeConfig(), + Plugins: map[string]hplugin.Plugin{ + "execute_ok": plugin.NewGRPC(&app{}), + }, + GRPCServer: hplugin.DefaultGRPCServer, + }) +} diff --git a/ignite/services/plugin/mocks/chainer.go b/ignite/services/plugin/mocks/chainer.go new file mode 100644 index 0000000000..ed1e81a907 --- /dev/null +++ b/ignite/services/plugin/mocks/chainer.go @@ -0,0 +1,267 @@ +// Code generated by mockery v2.36.1. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// ChainerInterface is an autogenerated mock type for the Chainer type +type ChainerInterface struct { + mock.Mock +} + +type ChainerInterface_Expecter struct { + mock *mock.Mock +} + +func (_m *ChainerInterface) EXPECT() *ChainerInterface_Expecter { + return &ChainerInterface_Expecter{mock: &_m.Mock} +} + +// AppPath provides a mock function with given fields: +func (_m *ChainerInterface) AppPath() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ChainerInterface_AppPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppPath' +type ChainerInterface_AppPath_Call struct { + *mock.Call +} + +// AppPath is a helper method to define mock.On call +func (_e *ChainerInterface_Expecter) AppPath() *ChainerInterface_AppPath_Call { + return &ChainerInterface_AppPath_Call{Call: _e.mock.On("AppPath")} +} + +func (_c *ChainerInterface_AppPath_Call) Run(run func()) *ChainerInterface_AppPath_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ChainerInterface_AppPath_Call) Return(_a0 string) *ChainerInterface_AppPath_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ChainerInterface_AppPath_Call) RunAndReturn(run func() string) *ChainerInterface_AppPath_Call { + _c.Call.Return(run) + return _c +} + +// ConfigPath provides a mock function with given fields: +func (_m *ChainerInterface) ConfigPath() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// ChainerInterface_ConfigPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConfigPath' +type ChainerInterface_ConfigPath_Call struct { + *mock.Call +} + +// ConfigPath is a helper method to define mock.On call +func (_e *ChainerInterface_Expecter) ConfigPath() *ChainerInterface_ConfigPath_Call { + return &ChainerInterface_ConfigPath_Call{Call: _e.mock.On("ConfigPath")} +} + +func (_c *ChainerInterface_ConfigPath_Call) Run(run func()) *ChainerInterface_ConfigPath_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ChainerInterface_ConfigPath_Call) Return(_a0 string) *ChainerInterface_ConfigPath_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ChainerInterface_ConfigPath_Call) RunAndReturn(run func() string) *ChainerInterface_ConfigPath_Call { + _c.Call.Return(run) + return _c +} + +// Home provides a mock function with given fields: +func (_m *ChainerInterface) Home() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChainerInterface_Home_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Home' +type ChainerInterface_Home_Call struct { + *mock.Call +} + +// Home is a helper method to define mock.On call +func (_e *ChainerInterface_Expecter) Home() *ChainerInterface_Home_Call { + return &ChainerInterface_Home_Call{Call: _e.mock.On("Home")} +} + +func (_c *ChainerInterface_Home_Call) Run(run func()) *ChainerInterface_Home_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ChainerInterface_Home_Call) Return(_a0 string, _a1 error) *ChainerInterface_Home_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChainerInterface_Home_Call) RunAndReturn(run func() (string, error)) *ChainerInterface_Home_Call { + _c.Call.Return(run) + return _c +} + +// ID provides a mock function with given fields: +func (_m *ChainerInterface) ID() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChainerInterface_ID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ID' +type ChainerInterface_ID_Call struct { + *mock.Call +} + +// ID is a helper method to define mock.On call +func (_e *ChainerInterface_Expecter) ID() *ChainerInterface_ID_Call { + return &ChainerInterface_ID_Call{Call: _e.mock.On("ID")} +} + +func (_c *ChainerInterface_ID_Call) Run(run func()) *ChainerInterface_ID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ChainerInterface_ID_Call) Return(_a0 string, _a1 error) *ChainerInterface_ID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChainerInterface_ID_Call) RunAndReturn(run func() (string, error)) *ChainerInterface_ID_Call { + _c.Call.Return(run) + return _c +} + +// RPCPublicAddress provides a mock function with given fields: +func (_m *ChainerInterface) RPCPublicAddress() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ChainerInterface_RPCPublicAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RPCPublicAddress' +type ChainerInterface_RPCPublicAddress_Call struct { + *mock.Call +} + +// RPCPublicAddress is a helper method to define mock.On call +func (_e *ChainerInterface_Expecter) RPCPublicAddress() *ChainerInterface_RPCPublicAddress_Call { + return &ChainerInterface_RPCPublicAddress_Call{Call: _e.mock.On("RPCPublicAddress")} +} + +func (_c *ChainerInterface_RPCPublicAddress_Call) Run(run func()) *ChainerInterface_RPCPublicAddress_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ChainerInterface_RPCPublicAddress_Call) Return(_a0 string, _a1 error) *ChainerInterface_RPCPublicAddress_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ChainerInterface_RPCPublicAddress_Call) RunAndReturn(run func() (string, error)) *ChainerInterface_RPCPublicAddress_Call { + _c.Call.Return(run) + return _c +} + +// NewChainerInterface creates a new instance of ChainerInterface. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewChainerInterface(t interface { + mock.TestingT + Cleanup(func()) +}) *ChainerInterface { + mock := &ChainerInterface{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/ignite/services/plugin/plugin.go b/ignite/services/plugin/plugin.go index 059d3367fd..91415f0692 100644 --- a/ignite/services/plugin/plugin.go +++ b/ignite/services/plugin/plugin.go @@ -6,6 +6,7 @@ package plugin import ( "context" "fmt" + "io" "io/fs" "os" "os/exec" @@ -64,6 +65,9 @@ type Plugin struct { isSharedHost bool ev events.Bus + + stdout io.Writer + stderr io.Writer } // Option configures Plugin. @@ -76,6 +80,13 @@ func CollectEvents(ev events.Bus) Option { } } +func RedirectStdout(w io.Writer) Option { + return func(p *Plugin) { + p.stdout = w + p.stderr = w + } +} + // Load loads the plugins found in the chain config. // // There's 2 kinds of plugins, local or remote. @@ -117,6 +128,8 @@ func newPlugin(pluginsDir string, cp pluginsconfig.Plugin, options ...Option) *P var ( p = &Plugin{ Plugin: cp, + stdout: os.Stdout, + stderr: os.Stderr, } pluginPath = cp.Path ) @@ -259,8 +272,8 @@ func (p *Plugin) load(ctx context.Context) { HandshakeConfig: HandshakeConfig(), Plugins: pluginMap, Logger: logger, - SyncStderr: os.Stderr, - SyncStdout: os.Stdout, + SyncStderr: p.stdout, + SyncStdout: p.stderr, AllowedProtocols: []hplugin.Protocol{hplugin.ProtocolGRPC}, } diff --git a/ignite/services/plugin/template/.gitignore.plush b/ignite/services/plugin/template/.gitignore.plush index 3c921bddc4..1b8133ef7a 100644 --- a/ignite/services/plugin/template/.gitignore.plush +++ b/ignite/services/plugin/template/.gitignore.plush @@ -1 +1 @@ -<%= Name %> +<%= Name %>* diff --git a/proto/ignite/services/plugin/grpc/v1/client_api.proto b/proto/ignite/services/plugin/grpc/v1/client_api.proto index 8be0b43b18..b208aeae39 100644 --- a/proto/ignite/services/plugin/grpc/v1/client_api.proto +++ b/proto/ignite/services/plugin/grpc/v1/client_api.proto @@ -9,4 +9,5 @@ message ChainInfo { string app_path = 2; string config_path = 3; string rpc_address = 4; + string home = 5; }