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

feat: PoC for passing data to plugins #3498

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 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
33 changes: 32 additions & 1 deletion ignite/cmd/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"golang.org/x/exp/maps"

pluginsconfig "github.com/ignite/cli/ignite/config/plugins"
"github.com/ignite/cli/ignite/pkg/clictx"
"github.com/ignite/cli/ignite/pkg/cliui"
"github.com/ignite/cli/ignite/pkg/cliui/icons"
"github.com/ignite/cli/ignite/pkg/common"
"github.com/ignite/cli/ignite/pkg/cosmosanalysis"
"github.com/ignite/cli/ignite/pkg/xgit"
"github.com/ignite/cli/ignite/services/chain"
"github.com/ignite/cli/ignite/services/plugin"
)

Expand Down Expand Up @@ -263,6 +266,16 @@ func linkPluginCmds(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmds []plugi
}
}

func SetupChain(cmd *cobra.Command) (*chain.Chain, error) {
c, err := newChainWithHomeFlags(
cmd,
)
if err != nil {
return nil, err
}
return c, nil
}

func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Command) {
cmdPath := pluginCmd.PlaceCommandUnderFull()
cmd := findCommandByPath(rootCmd, cmdPath)
Expand Down Expand Up @@ -303,6 +316,24 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Co
// pluginCmd has no sub commands, so it's runnable
newCmd.RunE = func(cmd *cobra.Command, args []string) error {
return clictx.Do(cmd.Context(), func() error {
c, err := SetupChain(cmd)
if err != nil {
return err
}
manifest, err := p.Interface.Manifest()
if err != nil {
return err
}
if p.With == nil {
p.With = make(map[string]string)
}
if manifest.WithPaths {
p.With["appPath"] = c.AppPath()
}
if manifest.WithModuleAnalysis {
modules, _ := common.GetModuleList(cmd.Context(), c)
maps.Copy(p.With, modules)
}
execCmd := plugin.ExecutedCommand{
Use: cmd.Use,
Path: cmd.CommandPath(),
Expand All @@ -312,7 +343,7 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Co
}
execCmd.SetFlags(cmd)
// Call the plugin Execute
err := p.Interface.Execute(execCmd)
err = p.Interface.Execute(execCmd)
// 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 not printed in the user console.
Expand Down
255 changes: 255 additions & 0 deletions ignite/pkg/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package common
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package name should be more specific, maybe it could be named as codeanalisys, appanalisys or codeinspect. Its purpose could be to provide a simpler or unified API to inspect/analize blockchain apps using the other specific analysis libraries within pkg.

It would be ideal to have it very well documented if the point is to make it easier for plugin writers to understand how to inspect the app code.


import (
"bytes"
"context"
"encoding/json"
"os"
"path/filepath"
"strings"

"github.com/ignite/cli/ignite/pkg/cache"
"github.com/ignite/cli/ignite/pkg/cmdrunner"
"github.com/ignite/cli/ignite/pkg/cmdrunner/step"
"github.com/ignite/cli/ignite/pkg/cosmosanalysis/module"
"github.com/ignite/cli/ignite/pkg/cosmosgen"
"github.com/ignite/cli/ignite/pkg/gomodule"
"github.com/ignite/cli/ignite/pkg/xfilepath"
"github.com/ignite/cli/ignite/services/chain"
"github.com/pkg/errors"
gomod "golang.org/x/mod/module"
)

const (
defaultSDKImport = "github.com/cosmos/cosmos-sdk"
moduleCacheNamespace = "analyze.setup.module"
)

var protocGlobalInclude = xfilepath.List(
xfilepath.JoinFromHome(xfilepath.Path("local/include")),
xfilepath.JoinFromHome(xfilepath.Path(".local/include")),
)

type ModulesInPath struct {
Path string
Modules []module.Module
}
type AllModules struct {
ModulePaths []ModulesInPath
Includes []string
}
type Analyzer struct {
ctx context.Context
appPath string
protoDir string
sdkImport string
appModules []module.Module
cacheStorage cache.Storage
deps []gomod.Version
includeDirs []string
thirdModules map[string][]module.Module // app dependency-modules pair.
}

func GetModuleList(ctx context.Context, c *chain.Chain) (map[string]string, error) {

conf, err := c.Config()
if err != nil {
return nil, err
}

cacheStorage, err := cache.NewStorage(filepath.Join(c.AppPath(), "analyzer_cache.db"))
if err != nil {
return nil, err
}

if err := cosmosgen.InstallDepTools(ctx, c.AppPath()); err != nil {
return nil, err
}

var errb bytes.Buffer
if err := cmdrunner.
New(
cmdrunner.DefaultStderr(&errb),
cmdrunner.DefaultWorkdir(c.AppPath()),
).Run(ctx, step.New(step.Exec("go", "mod", "download"))); err != nil {
return nil, errors.Wrap(err, errb.String())
}

modFile, err := gomodule.ParseAt(c.AppPath())
g := &Analyzer{
ctx: ctx,
appPath: c.AppPath(),
protoDir: conf.Build.Proto.Path,
includeDirs: conf.Build.Proto.ThirdPartyPaths,
thirdModules: make(map[string][]module.Module),
cacheStorage: cacheStorage,
}
if err != nil {
return nil, err
}

g.sdkImport = defaultSDKImport

// Check if the Cosmos SDK import path points to a different path
// and if so change the default one to the new location.
for _, r := range modFile.Replace {
if r.Old.Path == defaultSDKImport {
g.sdkImport = r.New.Path
break
}
}

// Read the dependencies defined in the `go.mod` file
g.deps, err = gomodule.ResolveDependencies(modFile)
if err != nil {
return nil, err
}
includePaths, err := g.resolveInclude(c.AppPath())

// Discover any custom modules defined by the user's app
g.appModules, err = g.discoverModules(g.appPath, g.protoDir)
if err != nil {
return nil, err
}

// Go through the Go dependencies of the user's app within go.mod, some of them might be hosting Cosmos SDK modules
// that could be in use by user's blockchain.
//
// Cosmos SDK is a dependency of all blockchains, so it's absolute that we'll be discovering all modules of the
// SDK as well during this process.
//
// Even if a dependency contains some SDK modules, not all of these modules could be used by user's blockchain.
// this is fine, we can still generate TS clients for those non modules, it is up to user to use (import in typescript)
// not use generated modules.
//
// TODO: we can still implement some sort of smart filtering to detect non used modules by the user's blockchain
// at some point, it is a nice to have.
moduleCache := cache.New[ModulesInPath](g.cacheStorage, moduleCacheNamespace)
for _, dep := range g.deps {
// Try to get the cached list of modules for the current dependency package
cacheKey := cache.Key(dep.Path, dep.Version)
modulesInPath, err := moduleCache.Get(cacheKey)
if err != nil && !errors.Is(err, cache.ErrorNotFound) {
return nil, err
}

// Discover the modules of the dependency package when they are not cached
if errors.Is(err, cache.ErrorNotFound) {
// Get the absolute path to the package's directory
path, err := gomodule.LocatePath(g.ctx, g.cacheStorage, c.AppPath(), dep)
if err != nil {
return nil, err
}

// Discover any modules defined by the package
modules, err := g.discoverModules(path, "")
if err != nil {
return nil, err
}

modulesInPath = ModulesInPath{
Path: path,
Modules: modules,
}

if err := moduleCache.Put(cacheKey, modulesInPath); err != nil {
return nil, err
}
}

g.thirdModules[modulesInPath.Path] = append(g.thirdModules[modulesInPath.Path], modulesInPath.Modules...)
}

var modulelist []ModulesInPath
modulelist = append(modulelist, ModulesInPath{Path: c.AppPath(), Modules: g.appModules})
for sourcePath, modules := range g.thirdModules {
modulelist = append(modulelist, ModulesInPath{Path: sourcePath, Modules: modules})
}
var allModules = &AllModules{
ModulePaths: modulelist,
Includes: includePaths,
}
ret := make(map[string]string)
jsonm, _ := json.Marshal(allModules)
ret["ModuleAnalysis"] = string(jsonm)
return ret, nil
}

func (g *Analyzer) resolveDependencyInclude() ([]string, error) {
// Init paths with the global include paths for protoc
paths, err := protocGlobalInclude()
if err != nil {
return nil, err
}

// Relative paths to proto directories
protoDirs := append([]string{g.protoDir}, g.includeDirs...)

// Create a list of proto import paths for the dependencies.
// These paths will be available to be imported from the chain app's proto files.
for rootPath, m := range g.thirdModules {
// Skip modules without proto files
if m == nil {
continue
}

// Check each one of the possible proto directory names for the
// current module and append them only when the directory exists.
for _, d := range protoDirs {
p := filepath.Join(rootPath, d)
f, err := os.Stat(p)
if err != nil {
if os.IsNotExist(err) {
continue
}

return nil, err
}

if f.IsDir() {
paths = append(paths, p)
}
}
}

return paths, nil
}

func (g *Analyzer) resolveInclude(path string) (paths []string, err error) {
// Append chain app's proto paths
paths = append(paths, filepath.Join(path, g.protoDir))
for _, p := range g.includeDirs {
paths = append(paths, filepath.Join(path, p))
}

// Append paths for dependencies that have protocol buffer files
includePaths, err := g.resolveDependencyInclude()
if err != nil {
return nil, err
}

paths = append(paths, includePaths...)

return paths, nil
}

func (g *Analyzer) discoverModules(path, protoDir string) ([]module.Module, error) {
var filteredModules []module.Module

modules, err := module.Discover(g.ctx, g.appPath, path, protoDir)
if err != nil {
return nil, err
}

protoPath := filepath.Join(path, g.protoDir)

for _, m := range modules {
if !strings.HasPrefix(m.Pkg.Path, protoPath) {
continue
}

filteredModules = append(filteredModules, m)
}

return filteredModules, nil
}
5 changes: 5 additions & 0 deletions ignite/services/chain/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,11 @@ func (c *Chain) Home() (string, error) {
return home, nil
}

// AppPath returns the configured App's path
func (c *Chain) AppPath() string {
return c.app.Path
}

// DefaultHome returns the blockchain node's default home dir when not specified in the app.
func (c *Chain) DefaultHome() (string, error) {
// check if home is defined in config
Expand Down
3 changes: 3 additions & 0 deletions ignite/services/plugin/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ type Manifest struct {
// Hooks contains the hooks that will be attached to the existing ignite
// commands.
Hooks []Hook

WithPaths bool
WithModuleAnalysis bool
// SharedHost enables sharing a single plugin server across all running instances
// of a plugin. Useful if a plugin adds or extends long running commands
//
Expand Down
2 changes: 1 addition & 1 deletion ignite/services/plugin/template/go.mod.plush
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.19

require (
github.com/hashicorp/go-plugin v1.4.4
github.com/ignite/cli v0.25.3-0.20230104184106-15d7be221737
github.com/ignite/cli v0.26.2-0.20230511160815-7a63ab71c283
)

replace (
Expand Down
3 changes: 3 additions & 0 deletions ignite/services/plugin/template/main.go.plush
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (p) Manifest() (plugin.Manifest, error) {
*/
},
},
//
WithPaths: true,
WithModuleAnalysis: true,
// Add hooks here
Hooks: []plugin.Hook{},
SharedHost: <%= SharedHost %>,
Expand Down