Skip to content

Commit

Permalink
Merge branch 'main' into fix/state-error
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Johnson authored Jan 9, 2023
2 parents 86082e3 + efd3bbd commit 158470c
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 132 deletions.
File renamed without changes.
43 changes: 43 additions & 0 deletions docs/docs/plugins/01-using-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
description: Using and Developing plugins
---

# Using Plugins

Ignite plugins offer a way to extend the functionality of the Ignite CLI. There
are two core concepts within plugins : `Commands` and `Hooks`. Where `Commands`
extend the cli's functionality, and `Hooks` extend existing command
functionality.

Plugins are registered in an Ignite scaffolded Blockchain project through the
`plugins.yml`, or globally through `$HOME/.ignite/plugins/plugins.yml`.

To use a plugin within your project, execute the following command inside the
project directory:

```sh
ignite plugin add github.com/project/cli-plugin
```

The plugin will be available only when running `ignite` inside the project
directory.

To use a plugin globally on the other hand, execute the following command:

```sh
ignite plugin add -g github.com/project/cli-plugin
```

The command will compile the plugin and make it immediately available to the
`ignite` command lists.

## Listing installed plugins

When in an ignite scaffolded blockchain you can use the command `ignite plugin
list` to list all plugins and there statuses.

## Updating plugins

When a plugin in a remote repository releases updates, running `ignite plugin
update <path/to/plugin>` will update a specific plugin declared in your
project's `config.yml`.
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,7 @@
description: Using and Developing plugins
---

# Developing plugins

## Using Plugins

Ignite plugins offer a way to extend the functionality of the Ignite CLI. There
are two core concepts within plugins : `Commands` and `Hooks`. Where `Commands`
extend the cli's functionality, and `Hooks` extend existing command
functionality.

Plugins are registered in an Ignite scaffolded Blockchain project through the
`config.yml`.

### Adding plugins to a project

Plugins are registered per project, using the `config.yml` file. To use a plugin
within your project, add a `plugins` section with the following:

```yaml title=config.yml
plugins:
- path: github.com/project/cli-plugin
```
Now the next time the `ignite` command is run under your project, the declared
plugin will be fetched, compiled and ran. This will result in more available
commands, and/or hooks attached to existing commands.

### Listing installed plugins

When in an ignite scaffolded blockchain you can use the command `ignite plugin
list` to list all plugins and there statuses.

### Updating plugins

When a plugin in a remote repository releases updates, running `ignite plugin
update <path/to/plugin>` will update a specific plugin declared in your
project's `config.yml`.

## Developing Plugins
# Developing Plugins

It's easy to create a plugin and use it immediately in your project. First
choose a directory outside your project and run :
Expand All @@ -49,32 +12,28 @@ $ ignite plugin scaffold my-plugin
```

This will create a new directory `my-plugin` that contains the plugin's code,
and will output some instructions about how to declare your plugin in your
project. Indeed it's possible to declare a local directory in your project's
`config.yml`, which has several benefits:
and will output some instructions about how to use your plugin with the
`ignite` command. Indeed, a plugin path can be a local directory, which has
several benefits:

- you don't need to use a git repository during the development of your plugin.
- the plugin is recompiled each time you run the `ignite` binary in your
project, if the source files are older than the plugin binary.

Thus, the plugin development workflow is as simple as :

1. scaffold a plugin
2. declare it in the `config.yml` of a chain (which can be a fresh new chain
created via `ignite scaffold chain my-chain`)
1. scaffold a plugin with `ignite plugin scaffold my-plugin`
2. add it to your config via `ignite plugin add -g /path/to/my-plugin`
3. update plugin code
4. run `ignite my-command` binary in your chain to compile and run the plugin,
where `my-command` is a command added by your plugin, or an existing `ignite`
command with hooks added by your plugin.
4. run `ignite my-plugin` binary to compile and run the plugin.
5. go back to 3.

Once your plugin is ready, you can publish it to a git repository, and the
community can use it by declaring this git repository path in their chain's
`config.yml`.
community can use it by calling `ignite plugin add github.com/foo/my-plugin`.

Now let's detail how to update your plugin's code.

### The plugin interface
## The plugin interface

The `ignite` plugin system uses `github.com/hashicorp/go-plugin` under the hood,
which implies to implement a predefined interface:
Expand Down Expand Up @@ -115,7 +74,7 @@ The code scaffolded already implements this interface, you just need to update
the methods' body.


### Defining plugin's manifest
## Defining plugin's manifest

Here is the `Manifest` struct :

Expand Down Expand Up @@ -160,7 +119,7 @@ A plugin may also share a host process by setting `SharedHost` to `true`.
Commands executed from the same plugin context interact with the same plugin server.
Allowing all executing commands to share the same server instance, giving shared execution context.

### Adding new command
## Adding new command

Plugin commands are custom commands added to the ignite cli by a registered
plugin. Commands can be of any path not defined already by ignite. All plugin
Expand Down Expand Up @@ -212,10 +171,9 @@ func (p) Execute(cmd plugin.ExecutedCommand) error {
}
```

Then, run `ignite scaffold oracle` in a chain where the plugin is registered to
compile and execute the plugin.
Then, run `ignite scaffold oracle` to execute the plugin.

### Adding hooks
## Adding hooks

Plugin `Hooks` allow existing ignite commands to be extended with new
functionality. Hooks are useful when you want to streamline functionality
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/plugins/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"label": "Plugins",
"position": 7,
"link": null
}
89 changes: 18 additions & 71 deletions ignite/cmd/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
)

const (
igniteCmdPrefix = "ignite "
flagPluginsGlobal = "global"
)

Expand Down Expand Up @@ -163,7 +162,7 @@ func linkPluginHooks(rootCmd *cobra.Command, p *plugin.Plugin, hooks []plugin.Ho
}

func linkPluginHook(rootCmd *cobra.Command, p *plugin.Plugin, hook plugin.Hook) {
cmdPath := hook.PlaceHookOn
cmdPath := hook.PlaceHookOnFull()
cmd := findCommandByPath(rootCmd, cmdPath)
if cmd == nil {
p.Error = errors.Errorf("unable to find commandPath %q for plugin hook %q", cmdPath, hook.Name)
Expand Down Expand Up @@ -265,7 +264,7 @@ func linkPluginCmds(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmds []plugi
}

func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Command) {
cmdPath := pluginCmd.PlaceCommandUnder
cmdPath := pluginCmd.PlaceCommandUnderFull()
cmd := findCommandByPath(rootCmd, cmdPath)
if cmd == nil {
p.Error = errors.Errorf("unable to find commandPath %q for plugin %q", cmdPath, p.Path)
Expand Down Expand Up @@ -333,13 +332,6 @@ func linkPluginCmd(rootCmd *cobra.Command, p *plugin.Plugin, pluginCmd plugin.Co
}

func findCommandByPath(cmd *cobra.Command, cmdPath string) *cobra.Command {
if !strings.HasPrefix(cmdPath, "ignite") {
// cmdPath must start with `ignite ` before comparison with
// cmd.CommandPath()
cmdPath = igniteCmdPrefix + cmdPath
}
cmdPath = strings.TrimSpace(cmdPath)

if cmd.CommandPath() == cmdPath {
return cmd
}
Expand Down Expand Up @@ -579,9 +571,13 @@ func NewPluginScaffold() *cobra.Command {
⭐️ Successfully created a new plugin '%[1]s'.
👉 update plugin code at '%[2]s/main.go'
👉 test plugin integration by adding the following lines in a chain config.yaml:
plugins:
- path: %[2]s
👉 test plugin integration by adding the plugin to a chain's config:
ignite plugin add %[2]s
Or to the global config:
ignite plugin add -g %[2]s
👉 once the plugin is pushed to a repository, replace the local path by the repository path.
`
Expand All @@ -606,12 +602,15 @@ func NewPluginDescribe() *cobra.Command {
if err != nil {
return fmt.Errorf("error while loading plugin manifest: %w", err)
}

if err := printPluginCommands(manifest.Commands, s); err != nil {
return err
s.Printf("Plugin '%s':\n", args[0])
s.Printf("%s %d Command(s):\n", icons.Command, len(manifest.Commands))
for i, c := range manifest.Commands {
cmdPath := fmt.Sprintf("%s %s", c.PlaceCommandUnderFull(), c.Use)
s.Printf("\t%d) '%s'\n", i+1, cmdPath)
}
if err := printPluginHooks(manifest.Hooks, s); err != nil {
return err
s.Printf("%s %d Hook(s):\n", icons.Hook, len(manifest.Hooks))
for i, h := range manifest.Hooks {
s.Printf("\t%d) '%s' on command '%s'\n", i+1, h.Name, h.PlaceHookOnFull())
}
break
}
Expand All @@ -637,8 +636,7 @@ func printPlugins(session *cliui.Session) error {
hookCount = len(manifest.Hooks)
cmdCount = len(manifest.Commands)
)

return fmt.Sprintf("%s Loaded: 🪝%d 💻%d", icons.OK, hookCount, cmdCount)
return fmt.Sprintf("%s Loaded: %s %d %s%d ", icons.OK, icons.Command, cmdCount, icons.Hook, hookCount)
}
installedStatus = func(p *plugin.Plugin) string {
if p.IsGlobal() {
Expand All @@ -656,57 +654,6 @@ func printPlugins(session *cliui.Session) error {
return nil
}

func printPluginCommands(cmds []plugin.Command, session *cliui.Session) error {
var entries [][]string
// Processes command graph
traverse := func(cmd plugin.Command) {
// cmdPair is a Wrapper struct to create parent child relationship for sub commands without a `place command under`
type cmdPair struct {
cmd plugin.Command
parent plugin.Command
}

queue := make([]cmdPair, 0)
queue = append(queue, cmdPair{cmd: cmd, parent: plugin.Command{}})

for len(queue) > 0 {
c := queue[0]
queue = queue[1:]
if c.cmd.PlaceCommandUnder != "" {
entries = append(entries, []string{c.cmd.Use, c.cmd.PlaceCommandUnder})
} else {
entries = append(entries, []string{c.cmd.Use, c.parent.Use})
}

for _, sc := range c.cmd.Commands {
queue = append(queue, cmdPair{cmd: sc, parent: c.cmd})
}
}
}

for _, c := range cmds {
traverse(c)
}

if err := session.PrintTable([]string{"command use", "under"}, entries...); err != nil {
return fmt.Errorf("error while printing plugin commands: %w", err)
}
return nil
}

func printPluginHooks(hooks []plugin.Hook, session *cliui.Session) error {
var entries [][]string

for _, h := range hooks {
entries = append(entries, []string{h.Name, h.PlaceHookOn})
}

if err := session.PrintTable([]string{"hook name", "on"}, entries...); err != nil {
return fmt.Errorf("error while printing plugin hooks: %w", err)
}
return nil
}

func flagSetPluginsGlobal() *flag.FlagSet {
fs := flag.NewFlagSet("", flag.ContinueOnError)
fs.BoolP(flagPluginsGlobal, "g", false, "use global plugins configuration"+
Expand Down
7 changes: 4 additions & 3 deletions ignite/cmd/plugin_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ func TestEnsureDefaultPlugins(t *testing.T) {
expectAddedInCommand bool
}{
{
name: "empty config",
name: "should add because absent from config",
cfg: &pluginsconfig.Config{},
expectAddedInCommand: true,
},
{
name: "config with default plugin",
name: "should not add because already present in config",
cfg: &pluginsconfig.Config{
Plugins: []pluginsconfig.Plugin{{
Path: "github.com/ignite/cli-plugin-network@v42",
}},
},
expectAddedInCommand: false,
},
}
for _, tt := range tests {
Expand All @@ -35,7 +36,7 @@ func TestEnsureDefaultPlugins(t *testing.T) {

ensureDefaultPlugins(cmd, tt.cfg)

expectedCmd := findCommandByPath(cmd, "network")
expectedCmd := findCommandByPath(cmd, "ignite network")
if tt.expectAddedInCommand {
assert.NotNil(t, expectedCmd)
} else {
Expand Down
8 changes: 5 additions & 3 deletions ignite/pkg/cliui/icons/icon.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
)

var (
Earth = "🌍"
CD = "💿"
User = "👤"
Earth = "🌍"
CD = "💿"
User = "👤"
Command = "❯⎯"
Hook = "🪝"

// OK is an OK mark.
OK = colors.SprintFunc(colors.Green)("✔")
Expand Down
20 changes: 20 additions & 0 deletions ignite/services/plugin/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,20 @@ type Command struct {
Commands []Command
}

// PlaceCommandUnderFull returns a normalized p.PlaceCommandUnder, by adding
// the `ignite ` prefix if not present.
func (c Command) PlaceCommandUnderFull() string {
return commandFull(c.PlaceCommandUnder)
}

func commandFull(cmdPath string) string {
const rootCmdName = "ignite"
if !strings.HasPrefix(cmdPath, rootCmdName) {
cmdPath = rootCmdName + " " + cmdPath
}
return strings.TrimSpace(cmdPath)
}

// ToCobraCommand turns Command into a cobra.Command so it can be added to a
// parent command.
func (c Command) ToCobraCommand() (*cobra.Command, error) {
Expand Down Expand Up @@ -147,6 +161,12 @@ type Hook struct {
PlaceHookOn string
}

// PlaceHookOnFull returns a normalized p.PlaceCommandUnder, by adding the
// `ignite ` prefix if not present.
func (h Hook) PlaceHookOnFull() string {
return commandFull(h.PlaceHookOn)
}

// ExecutedCommand represents a plugin command under execution.
type ExecutedCommand struct {
// Use is copied from Command.Use
Expand Down

0 comments on commit 158470c

Please sign in to comment.