From 86ebb5582fbc4d20de34b39b1714bd80c5ca57a9 Mon Sep 17 00:00:00 2001 From: Ehsan-saradar Date: Tue, 30 Jan 2024 01:27:38 +0330 Subject: [PATCH] feat: Improve app scaffolding (#3839) * Add new structure for app scaffolding * Fix some minor issues * Fix integration test plush * Add go mod tidy and fmt to scaffolding apps * Add changelog * Fix tests * Fix integration test package name * New app.ignite.yml format * Add AppYML * Add go.work.sum to app scaffold .gitignore * Upgrade app scaffold go.mod * Rename AppYML to AppsConfig * Add additional test for app scaffolding * Fix changelog * Improve app testing * Fix app_test.go * Move to cobra independent arch for app scaffolding * Fix plugin test * Fix changelog * Increase app scaffold integration test time * Update ignite/services/plugin/template/cmd/hello.go.plush Co-authored-by: Julien Robert * Update ignite/services/plugin/template/integration/app_test.go.plush Co-authored-by: Danny * Update ignite/services/plugin/template/cmd/cmd.go.plush Co-authored-by: Danny * Update ignite/services/plugin/template/app.ignite.yml.plush Co-authored-by: Danny * Update ignite/services/plugin/template/.gitignore.plush Co-authored-by: Danny --------- Co-authored-by: Danilo Pantani Co-authored-by: Julien Robert Co-authored-by: Danny --- changelog.md | 1 + ignite/pkg/gocmd/gocmd.go | 12 ++ ignite/services/plugin/apps_config.go | 14 +++ ignite/services/plugin/plugin_test.go | 2 +- ignite/services/plugin/scaffold.go | 17 +++ ignite/services/plugin/scaffold_test.go | 21 ++++ .../services/plugin/template/.gitignore.plush | 22 ++++ .../plugin/template/app.ignite.yml.plush | 6 + .../services/plugin/template/cmd/cmd.go.plush | 19 ++++ .../plugin/template/cmd/hello.go.plush | 14 +++ ignite/services/plugin/template/go.mod.plush | 1 + .../template/integration/app_test.go.plush | 70 ++++++++++++ ignite/services/plugin/template/main.go.plush | 105 +++--------------- 13 files changed, 215 insertions(+), 89 deletions(-) create mode 100644 ignite/services/plugin/apps_config.go create mode 100644 ignite/services/plugin/template/app.ignite.yml.plush create mode 100644 ignite/services/plugin/template/cmd/cmd.go.plush create mode 100644 ignite/services/plugin/template/cmd/hello.go.plush create mode 100644 ignite/services/plugin/template/integration/app_test.go.plush diff --git a/changelog.md b/changelog.md index 09b45bd136..8b7cc0e520 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,7 @@ ### Features +- [#3839](https://github.com/ignite/cli/pull/3839) New structure for app scaffolding - [#3835](https://github.com/ignite/cli/pull/3835) Add `--minimal` flag to `scaffold chain` to scaffold a chain with the least amount of sdk modules ### Changes diff --git a/ignite/pkg/gocmd/gocmd.go b/ignite/pkg/gocmd/gocmd.go index 13d9560803..ef4ee23e40 100644 --- a/ignite/pkg/gocmd/gocmd.go +++ b/ignite/pkg/gocmd/gocmd.go @@ -45,6 +45,9 @@ const ( // CommandList represents go "list" command. CommandList = "list" + // CommandTest represents go "test" command. + CommandTest = "test" + // EnvGOARCH represents GOARCH variable. EnvGOARCH = "GOARCH" // EnvGOMOD represents GOMOD variable. @@ -199,6 +202,15 @@ func List(ctx context.Context, path string, flags []string, options ...exec.Opti return strings.Fields(b.String()), nil } +func Test(ctx context.Context, path string, flags []string, options ...exec.Option) error { + command := []string{ + Name(), + CommandTest, + } + command = append(command, flags...) + return exec.Exec(ctx, command, append(options, exec.StepOption(step.Workdir(path)))...) +} + // Ldflags returns a combined ldflags set from flags. func Ldflags(flags ...string) string { return strings.Join(flags, " ") diff --git a/ignite/services/plugin/apps_config.go b/ignite/services/plugin/apps_config.go new file mode 100644 index 0000000000..7333867af1 --- /dev/null +++ b/ignite/services/plugin/apps_config.go @@ -0,0 +1,14 @@ +package plugin + +// AppsConfig is the structure of app.ignite.yml file. +type AppsConfig struct { + Version uint `yaml:"version"` + Apps map[string]AppInfo `yaml:"apps"` +} + +// AppInfo is the structure of app info in app.ignite.yml file which only holds +// the description and the relative path of the app. +type AppInfo struct { + Description string `yaml:"description"` + Path string `yaml:"path"` +} diff --git a/ignite/services/plugin/plugin_test.go b/ignite/services/plugin/plugin_test.go index 7dc6b9efdd..c0a4750f7d 100644 --- a/ignite/services/plugin/plugin_test.go +++ b/ignite/services/plugin/plugin_test.go @@ -387,7 +387,7 @@ func TestPluginLoad(t *testing.T) { manifest, err := p.Interface.Manifest(ctx) require.NoError(err) assert.Equal(p.name, manifest.Name) - assert.NoError(p.Interface.Execute(ctx, &ExecutedCommand{}, clientAPI)) + assert.NoError(p.Interface.Execute(ctx, &ExecutedCommand{OsArgs: []string{"ignite", p.name, "hello"}}, clientAPI)) assert.NoError(p.Interface.ExecuteHookPre(ctx, &ExecutedHook{}, clientAPI)) assert.NoError(p.Interface.ExecuteHookPost(ctx, &ExecutedHook{}, clientAPI)) assert.NoError(p.Interface.ExecuteHookCleanUp(ctx, &ExecutedHook{}, clientAPI)) diff --git a/ignite/services/plugin/scaffold.go b/ignite/services/plugin/scaffold.go index cd82fe45ae..e3500d0597 100644 --- a/ignite/services/plugin/scaffold.go +++ b/ignite/services/plugin/scaffold.go @@ -6,11 +6,15 @@ import ( "os" "path" "path/filepath" + "strings" "github.com/gobuffalo/genny/v2" "github.com/gobuffalo/plush/v4" + "golang.org/x/text/cases" + "golang.org/x/text/language" "github.com/ignite/cli/v28/ignite/pkg/errors" + "github.com/ignite/cli/v28/ignite/pkg/gocmd" "github.com/ignite/cli/v28/ignite/pkg/xgenny" ) @@ -21,6 +25,7 @@ var fsPluginSource embed.FS func Scaffold(ctx context.Context, dir, moduleName string, sharedHost bool) (string, error) { var ( name = filepath.Base(moduleName) + title = toTitle(name) finalDir = path.Join(dir, name) g = genny.New() template = xgenny.NewEmbedWalker( @@ -42,6 +47,7 @@ func Scaffold(ctx context.Context, dir, moduleName string, sharedHost bool) (str pctx := plush.NewContextWithContext(ctx) pctx.Set("ModuleName", moduleName) pctx.Set("Name", name) + pctx.Set("Title", title) pctx.Set("SharedHost", sharedHost) g.Transformer(xgenny.Transformer(pctx)) @@ -55,5 +61,16 @@ func Scaffold(ctx context.Context, dir, moduleName string, sharedHost bool) (str return "", errors.WithStack(err) } + if err := gocmd.ModTidy(ctx, finalDir); err != nil { + return "", errors.WithStack(err) + } + if err := gocmd.Fmt(ctx, finalDir); err != nil { + return "", errors.WithStack(err) + } + return finalDir, nil } + +func toTitle(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(cases.Title(language.English).String(s), "_", ""), "-", "") +} diff --git a/ignite/services/plugin/scaffold_test.go b/ignite/services/plugin/scaffold_test.go index a6b66a9db1..cf79655f99 100644 --- a/ignite/services/plugin/scaffold_test.go +++ b/ignite/services/plugin/scaffold_test.go @@ -2,10 +2,13 @@ package plugin import ( "context" + "os" "path/filepath" "testing" + "github.com/ignite/cli/v28/ignite/pkg/gocmd" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" ) func TestScaffold(t *testing.T) { @@ -21,4 +24,22 @@ func TestScaffold(t *testing.T) { require.DirExists(t, path) require.FileExists(t, filepath.Join(path, "go.mod")) require.FileExists(t, filepath.Join(path, "main.go")) + + // app.ignite.yml check + appYML, err := os.ReadFile(filepath.Join(path, "app.ignite.yml")) + require.NoError(t, err) + var config AppsConfig + err = yaml.Unmarshal(appYML, &config) + require.NoError(t, err) + require.EqualValues(t, 1, config.Version) + require.Len(t, config.Apps, 1) + + // Integration test check + err = gocmd.Test(ctx, filepath.Join(path, "integration"), []string{ + "-timeout", + "5m", + "-run", + "^TestBar$", + }) + require.NoError(t, err) } diff --git a/ignite/services/plugin/template/.gitignore.plush b/ignite/services/plugin/template/.gitignore.plush index 1b8133ef7a..1c428025a7 100644 --- a/ignite/services/plugin/template/.gitignore.plush +++ b/ignite/services/plugin/template/.gitignore.plush @@ -1 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.ign + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# App <%= Name %>* diff --git a/ignite/services/plugin/template/app.ignite.yml.plush b/ignite/services/plugin/template/app.ignite.yml.plush new file mode 100644 index 0000000000..d6156a0b80 --- /dev/null +++ b/ignite/services/plugin/template/app.ignite.yml.plush @@ -0,0 +1,6 @@ +version: 1 +apps: + <%= Name %>: + description: <%= Name %> is an awesome Ignite application! + path: ./ + \ No newline at end of file diff --git a/ignite/services/plugin/template/cmd/cmd.go.plush b/ignite/services/plugin/template/cmd/cmd.go.plush new file mode 100644 index 0000000000..d7d5cc17de --- /dev/null +++ b/ignite/services/plugin/template/cmd/cmd.go.plush @@ -0,0 +1,19 @@ +package cmd + +import "github.com/ignite/cli/v28/ignite/services/plugin" + +// GetCommands returns the list of <%= Name %> app commands. +func GetCommands() []*plugin.Command { + return []*plugin.Command{ + { + Use: "<%= Name %> [command]", + Short: "<%= Name %> is an awesome Ignite application!", + Commands: []*plugin.Command{ + { + Use: "hello", + Short: "Say hello to the world of ignite!", + }, + }, + }, + } +} diff --git a/ignite/services/plugin/template/cmd/hello.go.plush b/ignite/services/plugin/template/cmd/hello.go.plush new file mode 100644 index 0000000000..81c8116afc --- /dev/null +++ b/ignite/services/plugin/template/cmd/hello.go.plush @@ -0,0 +1,14 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/ignite/cli/v28/ignite/services/plugin" +) + +// ExecuteHello executes the hello subcommand. +func ExecuteHello(ctx context.Context, cmd *plugin.ExecutedCommand) error { + fmt.Println("Hello, world!") + return nil +} diff --git a/ignite/services/plugin/template/go.mod.plush b/ignite/services/plugin/template/go.mod.plush index 71ae3ecdf8..1ab1c690fa 100644 --- a/ignite/services/plugin/template/go.mod.plush +++ b/ignite/services/plugin/template/go.mod.plush @@ -5,4 +5,5 @@ go 1.21 require ( github.com/hashicorp/go-plugin v1.5.0 github.com/ignite/cli/v28 v28.0.0 + github.com/stretchr/testify v1.8.4 ) diff --git a/ignite/services/plugin/template/integration/app_test.go.plush b/ignite/services/plugin/template/integration/app_test.go.plush new file mode 100644 index 0000000000..0551c112c3 --- /dev/null +++ b/ignite/services/plugin/template/integration/app_test.go.plush @@ -0,0 +1,70 @@ +package integration_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + pluginsconfig "github.com/ignite/cli/v28/ignite/config/plugins" + "github.com/ignite/cli/v28/ignite/pkg/cmdrunner/step" + "github.com/ignite/cli/v28/ignite/services/plugin" + envtest "github.com/ignite/cli/v28/integration" +) + +func Test<%= Title %>(t *testing.T) { + var ( + require = require.New(t) + env = envtest.New(t) + app = env.Scaffold("github.com/test/test") + ) + + dir, err := os.Getwd() + require.NoError(err) + pluginPath := filepath.Join(filepath.Dir(filepath.Dir(dir)), "<%= Name %>") + + env.Must(env.Exec("install <%= Name %> app locally", + step.NewSteps(step.New( + step.Exec(envtest.IgniteApp, "app", "install", pluginPath), + step.Workdir(app.SourcePath()), + )), + )) + + // One local plugin expected + assertLocalPlugins(t, app, []pluginsconfig.Plugin{ + { + Path: pluginPath, + }, + }) + assertGlobalPlugins(t, app, nil) + + buf := &bytes.Buffer{} + env.Must(env.Exec("run <%= Name %>", + step.NewSteps(step.New( + step.Exec( + envtest.IgniteApp, + "<%= Name %>", + "hello", + ), + step.Workdir(app.SourcePath()), + step.Stdout(buf), + )), + )) + require.Equal("Hello, world!\n", buf.String()) +} + +func assertLocalPlugins(t *testing.T, app envtest.App, expectedPlugins []pluginsconfig.Plugin) { + cfg, err := pluginsconfig.ParseDir(app.SourcePath()) + require.NoError(t, err) + require.ElementsMatch(t, expectedPlugins, cfg.Apps, "unexpected local apps") +} + +func assertGlobalPlugins(t *testing.T, app envtest.App, expectedPlugins []pluginsconfig.Plugin) { + cfgPath, err := plugin.PluginsPath() + require.NoError(t, err) + cfg, err := pluginsconfig.ParseDir(cfgPath) + require.NoError(t, err) + require.ElementsMatch(t, expectedPlugins, cfg.Apps, "unexpected global apps") +} diff --git a/ignite/services/plugin/template/main.go.plush b/ignite/services/plugin/template/main.go.plush index 56d8b447c0..1224628072 100644 --- a/ignite/services/plugin/template/main.go.plush +++ b/ignite/services/plugin/template/main.go.plush @@ -3,118 +3,47 @@ package main import ( "context" "fmt" - "path/filepath" hplugin "github.com/hashicorp/go-plugin" - "github.com/ignite/cli/v28/ignite/services/chain" "github.com/ignite/cli/v28/ignite/services/plugin" + "<%= ModuleName %>/cmd" ) type app struct{} -func (app) Manifest(ctx context.Context) (*plugin.Manifest, error) { +func (app) Manifest(_ context.Context) (*plugin.Manifest, error) { return &plugin.Manifest{ - Name: "<%= Name %>", - // TODO: Add commands here - Commands: []*plugin.Command{ - // Example of a command - { - Use: "<%= Name %>", - Short: "Explain what the command is doing...", - Long: "Long description goes here...", - Flags: []*plugin.Flag{ - {Name: "my-flag", Type: plugin.FlagTypeString, Usage: "my flag description"}, - }, - PlaceCommandUnder: "ignite", - // Examples of adding subcommands: - // Commands: []*plugin.Command{ - // {Use: "add"}, - // {Use: "list"}, - // {Use: "delete"}, - // }, - }, - }, - // TODO: Add hooks here - Hooks: []*plugin.Hook{}, - SharedHost: <%= SharedHost %>, + Name: "<%= Name %>",<%= if (SharedHost) { %> + SharedHost: true,<% } %> + Commands: cmd.GetCommands(), }, nil } -func (app) Execute(ctx context.Context, cmd *plugin.ExecutedCommand, api plugin.ClientAPI) error { - // TODO: write command execution here - fmt.Printf("Hello I'm the example-plugin plugin\n") - fmt.Printf("My executed command: %q\n", cmd.Path) - fmt.Printf("My args: %v\n", cmd.Args) - - flags, err := cmd.NewFlags() - if err != nil { - return err - } - - myFlag, _ := flags.GetString("my-flag") - fmt.Printf("My flags: my-flag=%q\n", myFlag) - fmt.Printf("My config parameters: %v\n", cmd.With) - - // This is how the plugin can access the chain: - // c, err := getChain(cmd) - // if err != nil { - // return err - // } - - // According to the number of declared commands, you may need a switch: - /* - switch cmd.Use { - case "add": - fmt.Println("Adding stuff...") - case "list": - fmt.Println("Listing stuff...") - case "delete": - fmt.Println("Deleting stuff...") - } - */ +func (app) Execute(ctx context.Context, c *plugin.ExecutedCommand, _ plugin.ClientAPI) error { + // Remove the first two elements "ignite" and "<%= Name %>" from OsArgs. + args := c.OsArgs[2:] - // ClientAPI call example - fmt.Println(api.GetChainInfo(ctx)) - - return nil + switch args[0] { + case "hello": + return cmd.ExecuteHello(ctx, c) + default: + return fmt.Errorf("unknown command: %s", c.Path) + } } -func (app) ExecuteHookPre(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { - fmt.Printf("Executing hook pre %q\n", h.Hook.GetName()) +func (app) ExecuteHookPre(_ context.Context, _ *plugin.ExecutedHook, _ plugin.ClientAPI) error { return nil } -func (app) ExecuteHookPost(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { - fmt.Printf("Executing hook post %q\n", h.Hook.GetName()) +func (app) ExecuteHookPost(_ context.Context, _ *plugin.ExecutedHook, _ plugin.ClientAPI) error { return nil } -func (app) ExecuteHookCleanUp(ctx context.Context, h *plugin.ExecutedHook, api plugin.ClientAPI) error { - fmt.Printf("Executing hook cleanup %q\n", h.Hook.GetName()) +func (app) ExecuteHookCleanUp(_ context.Context, _ *plugin.ExecutedHook, _ plugin.ClientAPI) error { return nil } -func getChain(cmd *plugin.ExecutedCommand, chainOption ...chain.Option) (*chain.Chain, error) { - flags, err := cmd.NewFlags() - if err != nil { - return nil, err - } - - var ( - home, _ = flags.GetString("home") - path, _ = flags.GetString("path") - ) - if home != "" { - chainOption = append(chainOption, chain.HomePath(home)) - } - absPath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - return chain.New(absPath, chainOption...) -} - func main() { hplugin.Serve(&hplugin.ServeConfig{ HandshakeConfig: plugin.HandshakeConfig(),