diff --git a/changelog.md b/changelog.md index f81be79b31..bde09e8b94 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,7 @@ ### Features +- [#3184](https://github.com/ignite/cli/pull/3184) Separate `plugins.yml` config file. - [#3038](https://github.com/ignite/cli/pull/3038) Addition of Plugin Hooks in Plugin System - [#3056](https://github.com/ignite/cli/pull/3056) Add `--genesis-config` flag option to `ignite network chain publish` - [#2892](https://github.com/ignite/cli/pull/2982/) Add `ignite scaffold react` command. diff --git a/ignite/cmd/cmd.go b/ignite/cmd/cmd.go index 46436146a7..d1eded5e8a 100644 --- a/ignite/cmd/cmd.go +++ b/ignite/cmd/cmd.go @@ -33,6 +33,7 @@ const ( flagYes = "yes" flagClearCache = "clear-cache" flagSkipProto = "skip-proto" + flagPlugins = "plugins" checkVersionTimeout = time.Millisecond * 600 cacheFileName = "ignite_cache.db" @@ -81,6 +82,7 @@ To get started, create a blockchain: c.AddCommand(NewVersion()) c.AddCommand(NewPlugin()) c.AddCommand(deprecated()...) + c.PersistentFlags().AddFlagSet(flagSetPlugins()) return c } @@ -121,7 +123,7 @@ func getHome(cmd *cobra.Command) (home string) { func flagSetConfig() *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) - fs.StringP(flagConfig, "c", "", "Ignite config file (default: ./config.yml)") + fs.StringP(flagConfig, "c", "", "path to Ignite config file (default: ./config.yml)") return fs } @@ -130,6 +132,17 @@ func getConfig(cmd *cobra.Command) (config string) { return } +func flagSetPlugins() *flag.FlagSet { + fs := flag.NewFlagSet("", flag.ContinueOnError) + fs.StringP(flagPlugins, "x", "", "path to Ignite plugins config file (default: ./plugins.yml)") + return fs +} + +func getPlugins(cmd *cobra.Command) (config string) { + config, _ = cmd.Flags().GetString(flagPlugins) + return +} + func flagSetYes() *flag.FlagSet { fs := flag.NewFlagSet("", flag.ContinueOnError) fs.BoolP(flagYes, "y", false, "answers interactive yes/no questions with yes") diff --git a/ignite/cmd/plugin.go b/ignite/cmd/plugin.go index 6dc6017499..ea2969a2b8 100644 --- a/ignite/cmd/plugin.go +++ b/ignite/cmd/plugin.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" "github.com/ignite/cli/ignite/pkg/cliui" "github.com/ignite/cli/ignite/pkg/xgit" "github.com/ignite/cli/ignite/services/plugin" @@ -26,19 +27,35 @@ const ( // LoadPlugins tries to load all the plugins found in configuration. // If no configuration found, it returns w/o error. func LoadPlugins(ctx context.Context, rootCmd *cobra.Command) error { - // NOTE(tb) Not sure if it's the right place to load this. - chain, err := newChainWithHomeFlags(rootCmd) + cfg, err := parseLocalPlugins(rootCmd) if err != nil { - // Binary is run outside of an chain app, plugins can't be loaded + // if binary is run where there is no plugins.yml, don't load return nil } - plugins, err = plugin.Load(ctx, chain) + + // TODO: parse global config + + plugins, err = plugin.Load(ctx, cfg) if err != nil { return err } return loadPlugins(rootCmd, plugins) } +func parseLocalPlugins(rootCmd *cobra.Command) (cfg *pluginsconfig.Config, err error) { + appPath := flagGetPath(rootCmd) + pluginsPath := getPlugins(rootCmd) + if pluginsPath == "" { + if pluginsPath, err = pluginsconfig.LocateDefault(appPath); err != nil { + return cfg, err + } + } + + cfg, err = pluginsconfig.ParseFile(pluginsPath) + + return cfg, err +} + func loadPlugins(rootCmd *cobra.Command, plugins []*plugin.Plugin) error { // Link plugins to related commands var loadErrors []string diff --git a/ignite/cmd/plugin_test.go b/ignite/cmd/plugin_test.go index 1c2c96ca89..d0a5daff56 100644 --- a/ignite/cmd/plugin_test.go +++ b/ignite/cmd/plugin_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/ignite/cli/ignite/config" + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" "github.com/ignite/cli/ignite/services/plugin" "github.com/ignite/cli/ignite/services/plugin/mocks" ) @@ -308,7 +308,7 @@ ignite assert := assert.New(t) pi := mocks.NewPluginInterface(t) p := &plugin.Plugin{ - Plugin: config.Plugin{ + Plugin: pluginsconfig.Plugin{ Path: "foo", With: pluginParams, }, @@ -537,7 +537,7 @@ func TestLinkPluginHooks(t *testing.T) { // assert := assert.New(t) pi := mocks.NewPluginInterface(t) p := &plugin.Plugin{ - Plugin: config.Plugin{ + Plugin: pluginsconfig.Plugin{ Path: "foo", With: pluginParams, }, diff --git a/ignite/config/chain/v1/config.go b/ignite/config/chain/v1/config.go index d5b09a73b0..6cf0fefaa9 100644 --- a/ignite/config/chain/v1/config.go +++ b/ignite/config/chain/v1/config.go @@ -23,30 +23,6 @@ type Config struct { base.Config `yaml:",inline"` Validators []Validator `yaml:"validators"` - Plugins []Plugin `yaml:"plugins,omitempty"` -} - -// Plugin keeps plugin name and location. -type Plugin struct { - // Path holds the location of the plugin. - // A path can be local, in that case it must start with a `/`. - // A remote path on the other hand, is an URL to a public remote git - // repository. For example: - // - // path: github.com/foo/bar - // - // It can contain a path inside that repository, if for instance the repo - // contains multiple plugins, For example: - // - // path: github.com/foo/bar/plugin1 - // - // It can also specify a tag or a branch, by adding a `@` and the branch/tag - // name at the end of the path. For example: - // - // path: github.com/foo/bar/plugin1@v42 - Path string `yaml:"path"` - // With holds arguments passed to the plugin interface - With map[string]string `yaml:"with"` } func (c *Config) SetDefaults() error { diff --git a/ignite/config/chain/v1/config_test.go b/ignite/config/chain/v1/config_test.go index da2992c7e9..8427e3641b 100644 --- a/ignite/config/chain/v1/config_test.go +++ b/ignite/config/chain/v1/config_test.go @@ -73,15 +73,6 @@ func TestConfigDecode(t *testing.T) { }, }, }}, - Plugins: []v1.Plugin{ - { - Path: "/path/to/plugin1", - }, - { - Path: "/path/to/plugin2", - With: map[string]string{"foo": "bar", "bar": "baz"}, - }, - }, } assert.Equal(expected, cfg) } diff --git a/ignite/config/plugins/config.go b/ignite/config/plugins/config.go new file mode 100644 index 0000000000..2bdffbd540 --- /dev/null +++ b/ignite/config/plugins/config.go @@ -0,0 +1,76 @@ +package plugins + +import ( + "io" + "os" + "path/filepath" + + "github.com/imdario/mergo" + "gopkg.in/yaml.v2" +) + +// PluginsConfigFilenames is a list of recognized names as Ignite's plugins config file. +var PluginsConfigFilenames = []string{"plugins.yml", "plugins.yaml"} + +// DefaultConfig returns a config with default values. +func DefaultConfig() *Config { + c := Config{} + return &c +} + +// LocateDefault locates the default path for the config file. +// Returns ErrConfigNotFound when no config file found. +func LocateDefault(root string) (path string, err error) { + for _, name := range PluginsConfigFilenames { + path = filepath.Join(root, name) + if _, err := os.Stat(path); err == nil { + return path, nil + } else if !os.IsNotExist(err) { + return "", err + } + } + + return "", ErrConfigNotFound +} + +type Config struct { + Plugins []Plugin `yaml:"plugins,omitempty"` +} + +// Plugin keeps plugin name and location. +type Plugin struct { + // Path holds the location of the plugin. + // A path can be local, in that case it must start with a `/`. + // A remote path on the other hand, is an URL to a public remote git + // repository. For example: + // + // path: github.com/foo/bar + // + // It can contain a path inside that repository, if for instance the repo + // contains multiple plugins, For example: + // + // path: github.com/foo/bar/plugin1 + // + // It can also specify a tag or a branch, by adding a `@` and the branch/tag + // name at the end of the path. For example: + // + // path: github.com/foo/bar/plugin1@v42 + Path string `yaml:"path"` + // With holds arguments passed to the plugin interface + With map[string]string `yaml:"with"` +} + +// Clone returns an identical copy of the instance +func (c *Config) Clone() (*Config, error) { + copy := Config{} + if err := mergo.Merge(©, c, mergo.WithAppendSlice); err != nil { + return nil, err + } + + return ©, nil +} + +// Decode decodes the config file values from YAML. +func (c *Config) Decode(r io.Reader) error { + return yaml.NewDecoder(r).Decode(c) +} diff --git a/ignite/config/plugins/config_test.go b/ignite/config/plugins/config_test.go new file mode 100644 index 0000000000..e18762c098 --- /dev/null +++ b/ignite/config/plugins/config_test.go @@ -0,0 +1,36 @@ +package plugins_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ignite/cli/ignite/config/plugins" +) + +func TestConfigDecode(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + f, err := os.Open("testdata/plugins.yml") + require.NoError(err) + defer f.Close() + var cfg plugins.Config + + err = cfg.Decode(f) + + require.NoError(err) + expected := plugins.Config{ + Plugins: []plugins.Plugin{ + { + Path: "/path/to/plugin1", + }, + { + Path: "/path/to/plugin2", + With: map[string]string{"foo": "bar", "bar": "baz"}, + }, + }, + } + assert.Equal(expected, cfg) +} diff --git a/ignite/config/plugins/errors.go b/ignite/config/plugins/errors.go new file mode 100644 index 0000000000..747d51e239 --- /dev/null +++ b/ignite/config/plugins/errors.go @@ -0,0 +1,6 @@ +package plugins + +import "errors" + +// ErrConfigNotFound indicates that the plugins.yml can't be found. +var ErrConfigNotFound = errors.New("could not locate a plugins.yml") diff --git a/ignite/config/plugins/parse.go b/ignite/config/plugins/parse.go new file mode 100644 index 0000000000..33c9940aea --- /dev/null +++ b/ignite/config/plugins/parse.go @@ -0,0 +1,33 @@ +package plugins + +import ( + "io" + "os" + + "gopkg.in/yaml.v2" +) + +// ParseFile parses a plugins config. +func ParseFile(path string) (*Config, error) { + file, err := os.Open(path) + if err != nil { + return DefaultConfig(), err + } + + defer file.Close() + + return Parse(file) +} + +// Parse reads a config file for ignite binary plugins +func Parse(configFile io.Reader) (*Config, error) { + return parse(configFile) +} + +func parse(configFile io.Reader) (*Config, error) { + var c Config + + err := yaml.NewDecoder(configFile).Decode(&c) + + return &c, err +} diff --git a/ignite/config/plugins/parse_test.go b/ignite/config/plugins/parse_test.go new file mode 100644 index 0000000000..a0f2434f74 --- /dev/null +++ b/ignite/config/plugins/parse_test.go @@ -0,0 +1,25 @@ +package plugins_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" + "github.com/ignite/cli/ignite/config/plugins/testdata" +) + +func TestParse(t *testing.T) { + // Arrange: Initialize a reader with the previous version + r := bytes.NewReader(testdata.ConfigYAML) + + // Act + cfg, err := pluginsconfig.Parse(r) + + // Assert + require.NoError(t, err) + + // Assert: Parse must return the latest version + require.Equal(t, testdata.GetConfig(t), cfg) +} diff --git a/ignite/config/plugins/testdata/plugins.yml b/ignite/config/plugins/testdata/plugins.yml new file mode 100644 index 0000000000..9e268110e2 --- /dev/null +++ b/ignite/config/plugins/testdata/plugins.yml @@ -0,0 +1,8 @@ +plugins: + - name: plugin1 + path: /path/to/plugin1 + - name: plugin2 + path: /path/to/plugin2 + with: + foo: bar + bar: baz diff --git a/ignite/config/plugins/testdata/testdata.go b/ignite/config/plugins/testdata/testdata.go new file mode 100644 index 0000000000..4762681280 --- /dev/null +++ b/ignite/config/plugins/testdata/testdata.go @@ -0,0 +1,24 @@ +package testdata + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" +) + +//go:embed plugins.yml +var ConfigYAML []byte + +func GetConfig(t *testing.T) *pluginsconfig.Config { + c := &pluginsconfig.Config{} + + err := yaml.NewDecoder(bytes.NewReader(ConfigYAML)).Decode(c) + require.NoError(t, err) + + return c +} diff --git a/ignite/config/pluginsconfig.go b/ignite/config/pluginsconfig.go deleted file mode 100644 index 6c651e2f59..0000000000 --- a/ignite/config/pluginsconfig.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -import v1 "github.com/ignite/cli/ignite/config/chain/v1" - -// Plugin defines the latest plugin config -type Plugin = v1.Plugin diff --git a/ignite/services/plugin/plugin.go b/ignite/services/plugin/plugin.go index 595b587500..638af0d6bf 100644 --- a/ignite/services/plugin/plugin.go +++ b/ignite/services/plugin/plugin.go @@ -1,5 +1,5 @@ // Package plugin implements ignite plugin management. -// A ignite plugin is a binary which communicates with the ignite binary +// An ignite plugin is a binary which communicates with the ignite binary // via RPC thanks to the github.com/hashicorp/go-plugin library. package plugin @@ -21,13 +21,13 @@ import ( "github.com/pkg/errors" "github.com/ignite/cli/ignite/config" + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" "github.com/ignite/cli/ignite/pkg/cliui" cliexec "github.com/ignite/cli/ignite/pkg/cmdrunner/exec" "github.com/ignite/cli/ignite/pkg/cmdrunner/step" "github.com/ignite/cli/ignite/pkg/env" "github.com/ignite/cli/ignite/pkg/gocmd" "github.com/ignite/cli/ignite/pkg/xfilepath" - "github.com/ignite/cli/ignite/services/chain" ) // pluginsPath holds the plugin cache directory. @@ -39,7 +39,7 @@ var pluginsPath = xfilepath.Join( // Plugin represents a ignite plugin. type Plugin struct { // Embed the plugin configuration - config.Plugin + pluginsconfig.Plugin // Interface allows to communicate with the plugin via net/rpc. Interface Interface // If any error occurred during the plugin load, it's stored here @@ -67,19 +67,17 @@ type Plugin struct { // If an error occurs during a plugin load, it's not returned but rather stored // in the Plugin.Error field. This prevents the loading of other plugins to be // interrupted. -func Load(ctx context.Context, c *chain.Chain) ([]*Plugin, error) { - conf, err := c.Config() - if err != nil { - return nil, errors.WithStack(err) - } +func Load(ctx context.Context, cfg *pluginsconfig.Config) ([]*Plugin, error) { pluginsDir, err := pluginsPath() if err != nil { return nil, errors.WithStack(err) } var plugins []*Plugin - for _, cp := range conf.Plugins { + for _, cp := range cfg.Plugins { p := newPlugin(pluginsDir, cp) p.load(ctx) + + // TODO: override global plugins with locally defined ones plugins = append(plugins, p) } return plugins, nil @@ -98,7 +96,7 @@ func Update(plugins ...*Plugin) error { } // newPlugin creates a Plugin from configuration. -func newPlugin(pluginsDir string, cp config.Plugin) *Plugin { +func newPlugin(pluginsDir string, cp pluginsconfig.Plugin) *Plugin { var ( p = &Plugin{Plugin: cp} pluginPath = cp.Path diff --git a/ignite/services/plugin/plugin_test.go b/ignite/services/plugin/plugin_test.go index 64d5c12b0f..d1c83f76cb 100644 --- a/ignite/services/plugin/plugin_test.go +++ b/ignite/services/plugin/plugin_test.go @@ -16,7 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ignite/cli/ignite/config" + pluginsconfig "github.com/ignite/cli/ignite/config/plugins" "github.com/ignite/cli/ignite/pkg/gocmd" "github.com/ignite/cli/ignite/pkg/gomodule" ) @@ -27,7 +27,7 @@ func TestNewPlugin(t *testing.T) { tests := []struct { name string - pluginCfg config.Plugin + pluginCfg pluginsconfig.Plugin expectedPlugin Plugin }{ { @@ -38,21 +38,21 @@ func TestNewPlugin(t *testing.T) { }, { name: "fail: local plugin doesnt exists", - pluginCfg: config.Plugin{Path: "/xxx/yyy/plugin"}, + pluginCfg: pluginsconfig.Plugin{Path: "/xxx/yyy/plugin"}, expectedPlugin: Plugin{ Error: errors.Errorf(`local plugin path "/xxx/yyy/plugin" not found`), }, }, { name: "fail: local plugin is not a dir", - pluginCfg: config.Plugin{Path: path.Join(wd, "testdata/fakebin")}, + pluginCfg: pluginsconfig.Plugin{Path: path.Join(wd, "testdata/fakebin")}, expectedPlugin: Plugin{ Error: errors.Errorf(fmt.Sprintf("local plugin path %q is not a dir", path.Join(wd, "testdata/fakebin"))), }, }, { name: "ok: local plugin", - pluginCfg: config.Plugin{Path: path.Join(wd, "testdata")}, + pluginCfg: pluginsconfig.Plugin{Path: path.Join(wd, "testdata")}, expectedPlugin: Plugin{ srcPath: path.Join(wd, "testdata"), binaryName: "testdata", @@ -60,21 +60,21 @@ func TestNewPlugin(t *testing.T) { }, { name: "fail: remote plugin with only domain", - pluginCfg: config.Plugin{Path: "github.com"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com"}, expectedPlugin: Plugin{ Error: errors.Errorf(`plugin path "github.com" is not a valid repository URL`), }, }, { name: "fail: remote plugin with incomplete URL", - pluginCfg: config.Plugin{Path: "github.com/ignite"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite"}, expectedPlugin: Plugin{ Error: errors.Errorf(`plugin path "github.com/ignite" is not a valid repository URL`), }, }, { name: "ok: remote plugin", - pluginCfg: config.Plugin{Path: "github.com/ignite/plugin"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/plugin"}, expectedPlugin: Plugin{ repoPath: "github.com/ignite/plugin", cloneURL: "https://github.com/ignite/plugin", @@ -86,7 +86,7 @@ func TestNewPlugin(t *testing.T) { }, { name: "ok: remote plugin with @ref", - pluginCfg: config.Plugin{Path: "github.com/ignite/plugin@develop"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/plugin@develop"}, expectedPlugin: Plugin{ repoPath: "github.com/ignite/plugin@develop", cloneURL: "https://github.com/ignite/plugin", @@ -98,7 +98,7 @@ func TestNewPlugin(t *testing.T) { }, { name: "ok: remote plugin with subpath", - pluginCfg: config.Plugin{Path: "github.com/ignite/plugin/plugin1"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/plugin/plugin1"}, expectedPlugin: Plugin{ repoPath: "github.com/ignite/plugin", cloneURL: "https://github.com/ignite/plugin", @@ -110,7 +110,7 @@ func TestNewPlugin(t *testing.T) { }, { name: "ok: remote plugin with subpath and @ref", - pluginCfg: config.Plugin{Path: "github.com/ignite/plugin/plugin1@develop"}, + pluginCfg: pluginsconfig.Plugin{Path: "github.com/ignite/plugin/plugin1@develop"}, expectedPlugin: Plugin{ repoPath: "github.com/ignite/plugin@develop", cloneURL: "https://github.com/ignite/plugin", @@ -343,7 +343,7 @@ func TestPluginClean(t *testing.T) { { name: "dont clean local plugin", plugin: &Plugin{ - Plugin: config.Plugin{Path: "/local"}, + Plugin: pluginsconfig.Plugin{Path: "/local"}, }, }, {