From e8556170cf4d1786e8eb27b142a4d7060f140a83 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Mon, 20 Jun 2022 12:14:04 -0500 Subject: [PATCH 1/4] wip Signed-off-by: Carolyn Van Slyck --- cmd/porter/aliases.go | 74 +++++-------- cmd/porter/bundle.go | 5 +- cmd/porter/completion.go | 8 +- cmd/porter/completion_test.go | 3 +- cmd/porter/credentials.go | 9 +- cmd/porter/docs.go | 7 +- cmd/porter/installations.go | 5 +- cmd/porter/logs.go | 5 +- cmd/porter/main.go | 159 ++------------------------- cmd/porter/mixins.go | 10 +- cmd/porter/outputs.go | 5 +- cmd/porter/parameters.go | 9 +- cmd/porter/plugins.go | 7 +- cmd/porter/run.go | 6 +- cmd/porter/schema.go | 6 +- cmd/porter/storage.go | 12 +- cmd/porter/version.go | 8 +- docs/content/cli/storage.md | 1 - pkg/cli/app.go | 20 ++++ pkg/cli/cobra.go | 116 +++++++++++++++++++ pkg/cli/main_helpers.go | 109 ++++++++++++++++++ pkg/plugins/pluginbuilder/cmd.go | 90 +++++++++++++++ pkg/plugins/pluginbuilder/config.go | 26 +++++ pkg/plugins/pluginbuilder/doc.go | 2 + pkg/plugins/pluginbuilder/plugin.go | 91 +++++++++++++++ pkg/plugins/pluginbuilder/run.go | 82 ++++++++++++++ pkg/plugins/pluginbuilder/version.go | 42 +++++++ pkg/plugins/plugins.go | 26 ++++- pkg/porter/porter.go | 7 ++ 29 files changed, 699 insertions(+), 251 deletions(-) create mode 100644 pkg/cli/app.go create mode 100644 pkg/cli/cobra.go create mode 100644 pkg/cli/main_helpers.go create mode 100644 pkg/plugins/pluginbuilder/cmd.go create mode 100644 pkg/plugins/pluginbuilder/config.go create mode 100644 pkg/plugins/pluginbuilder/doc.go create mode 100644 pkg/plugins/pluginbuilder/plugin.go create mode 100644 pkg/plugins/pluginbuilder/run.go create mode 100644 pkg/plugins/pluginbuilder/version.go diff --git a/cmd/porter/aliases.go b/cmd/porter/aliases.go index 5db92e3a7..392344b58 100644 --- a/cmd/porter/aliases.go +++ b/cmd/porter/aliases.go @@ -3,6 +3,7 @@ package main import ( "strings" + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -30,126 +31,110 @@ func buildAliasCommands(p *porter.Porter) []*cobra.Command { func buildCreateAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleCreateCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle create", "porter create", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") return cmd } func buildBuildAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleBuildCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle build", "porter build", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") return cmd } func buildLintAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleLintCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle lint", "porter lint", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildInstallAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleInstallCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle install", "porter install", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildUpgradeAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleUpgradeCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle upgrade", "porter upgrade", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildInvokeAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleInvokeCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle invoke", "porter invoke", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildUninstallAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleUninstallCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle uninstall", "porter uninstall", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildPublishAlias(p *porter.Porter) *cobra.Command { cmd := buildBundlePublishCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle publish", "porter publish", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildShowAlias(p *porter.Porter) *cobra.Command { cmd := buildInstallationShowCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter installation show", "porter show", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildListAlias(p *porter.Porter) *cobra.Command { cmd := buildInstallationsListCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter installations list", "porter list", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildArchiveAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleArchiveCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle archive", "porter archive", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildExplainAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleExplainCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle explain", "porter explain", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildCopyAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleCopyCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle copy", "porter copy", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } func buildInspectAlias(p *porter.Porter) *cobra.Command { cmd := buildBundleInspectCommand(p) cmd.Example = strings.Replace(cmd.Example, "porter bundle inspect", "porter inspect", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } @@ -158,8 +143,7 @@ func buildLogsAlias(p *porter.Porter) *cobra.Command { cmd.Use = "logs" cmd.Aliases = []string{"log"} cmd.Example = strings.Replace(cmd.Example, "porter installation logs show", "porter logs", -1) - cmd.Annotations = map[string]string{ - "group": "alias", - } + cli.SetCommandGroup(cmd, "alias") + return cmd } diff --git a/cmd/porter/bundle.go b/cmd/porter/bundle.go index f2cbf0e65..94be2f190 100644 --- a/cmd/porter/bundle.go +++ b/cmd/porter/bundle.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -15,9 +16,7 @@ func buildBundleCommands(p *porter.Porter) *cobra.Command { Short: "Bundle commands", Long: "Commands for working with bundles. These all have shortcuts so that you can call these commands without the bundle resource prefix. For example, porter bundle install is available as porter install as well.", } - cmd.Annotations = map[string]string{ - "group": "resource", - } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildBundleCreateCommand(p)) cmd.AddCommand(buildBundleBuildCommand(p)) diff --git a/cmd/porter/completion.go b/cmd/porter/completion.go index c5a43938c..f962eefd4 100644 --- a/cmd/porter/completion.go +++ b/cmd/porter/completion.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -29,9 +30,8 @@ For additional details see: https://porter.sh/install#command-completion`, } }, } - cmd.Annotations = map[string]string{ - "group": "meta", - skipConfig: "", - } + cli.SetCommandGroup(cmd, "meta") + cli.SkipConfigForCommand(cmd) + return cmd } diff --git a/cmd/porter/completion_test.go b/cmd/porter/completion_test.go index 276b02246..293858e17 100644 --- a/cmd/porter/completion_test.go +++ b/cmd/porter/completion_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,6 +30,6 @@ func TestCompletion(t *testing.T) { func TestCompletion_SkipConfig(t *testing.T) { p := porter.NewTestPorter(t) cmd := buildCompletionCommand(p.Porter) - shouldSkip := shouldSkipConfig(cmd) + shouldSkip := cli.ShouldSkipConfig(cmd) require.True(t, shouldSkip, "expected that we skip loading configuration for the completion command") } diff --git a/cmd/porter/credentials.go b/cmd/porter/credentials.go index 30d878411..9bf1e09e8 100644 --- a/cmd/porter/credentials.go +++ b/cmd/porter/credentials.go @@ -1,17 +1,18 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) func buildCredentialsCommands(p *porter.Porter) *cobra.Command { cmd := &cobra.Command{ - Use: "credentials", - Aliases: []string{"credential", "cred", "creds"}, - Annotations: map[string]string{"group": "resource"}, - Short: "Credentials commands", + Use: "credentials", + Aliases: []string{"credential", "cred", "creds"}, + Short: "Credentials commands", } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildCredentialsApplyCommand(p)) cmd.AddCommand(buildCredentialsEditCommand(p)) diff --git a/cmd/porter/docs.go b/cmd/porter/docs.go index 8573e1397..b31b101ba 100644 --- a/cmd/porter/docs.go +++ b/cmd/porter/docs.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/docs" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" @@ -23,10 +24,8 @@ func buildDocsCommand(p *porter.Porter) *cobra.Command { }, } - cmd.Annotations = map[string]string{ - "group": "meta", - skipConfig: "", - } + cli.SkipConfigForCommand(cmd) + cli.SetCommandGroup(cmd, "meta") flags := cmd.Flags() flags.StringVarP(&opts.Destination, "dest", "d", docs.DefaultDestination, diff --git a/cmd/porter/installations.go b/cmd/porter/installations.go index 04abb8b4a..31f9bd093 100644 --- a/cmd/porter/installations.go +++ b/cmd/porter/installations.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -12,9 +13,7 @@ func buildInstallationCommands(p *porter.Porter) *cobra.Command { Short: "Installation commands", Long: "Commands for working with installations of a bundle", } - cmd.Annotations = map[string]string{ - "group": "resource", - } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildInstallationsListCommand(p)) cmd.AddCommand(buildInstallationShowCommand(p)) diff --git a/cmd/porter/logs.go b/cmd/porter/logs.go index b82d979a7..505760acc 100644 --- a/cmd/porter/logs.go +++ b/cmd/porter/logs.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -12,9 +13,7 @@ func buildInstallationLogCommands(p *porter.Porter) *cobra.Command { Short: "Installation Logs commands", Long: "Commands for working with installation logs", } - cmd.Annotations = map[string]string{ - "group": "resource", - } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildInstallationLogShowCommand(p)) diff --git a/cmd/porter/main.go b/cmd/porter/main.go index 65c79381b..82a2cffec 100644 --- a/cmd/porter/main.go +++ b/cmd/porter/main.go @@ -3,17 +3,11 @@ package main import ( "context" _ "embed" - "fmt" - "os" - "os/signal" - "runtime/debug" - "strings" "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" "github.com/spf13/pflag" - "go.opentelemetry.io/otel/attribute" ) var includeDocsCommand = false @@ -21,143 +15,12 @@ var includeDocsCommand = false //go:embed helptext/usage.txt var usageText string -const ( - // Indicates that config should not be loaded for this command. - // This is used for commands like help and version which should never - // fail, even with porter is misconfigured. - skipConfig string = "skipConfig" - - // exitCodeSuccess indicates the program ran successfully - exitCodeSuccess = 0 - - // exitCodeErr indicates the program encountered an error - exitCodeErr = 1 - - // exitCodeInterrupt indicates the program was cancelled - exitCodeInterrupt = 2 -) - func main() { - run := func() int { - p := porter.New() - ctx, cancel := handleInterrupt(context.Background(), p) - defer cancel() - - rootCmd := buildRootCommandFrom(p) - - // Trace the command that called porter, e.g. porter installation show - cmd, commandName, formattedCommand := getCalledCommand(rootCmd) - - // When running an internal plugin, switch how we log to be compatible - // with the hashicorp go-plugin framework - if commandName == "porter plugins run" { - p.IsInternalPlugin = true - if len(os.Args) > 3 { - p.InternalPluginKey = os.Args[3] - } - } - - // Only run init logic that could fail for commands that - // really need it, skip it for commands that should NEVER - // fail. - if !shouldSkipConfig(cmd) { - if err := p.Connect(ctx); err != nil { - fmt.Fprintln(os.Stderr, err.Error()) - os.Exit(exitCodeErr) - } - } - - ctx, log := p.StartRootSpan(ctx, commandName, attribute.String("command", formattedCommand)) - defer func() { - // Capture panics and trace them - if panicErr := recover(); panicErr != nil { - log.Error(fmt.Errorf("%s", panicErr), - attribute.Bool("panic", true), - attribute.String("stackTrace", string(debug.Stack()))) - log.EndSpan() - p.Close() - os.Exit(exitCodeErr) - } else { - log.Close() - p.Close() - } - }() - - if err := rootCmd.ExecuteContext(ctx); err != nil { - // Ideally we log all errors in the span that generated it, - // but as a failsafe, always log the error at the root span as well - log.Error(err) - return exitCodeErr - } - return exitCodeSuccess - } - - // Wrapping the main run logic in a function because os.Exit will not - // execute defer statements - os.Exit(run()) -} - -// Try to exit gracefully when the interrupt signal is sent (CTRL+C) -// Thanks to Mat Ryer, https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html -func handleInterrupt(ctx context.Context, p *porter.Porter) (context.Context, func()) { - ctx, cancel := context.WithCancel(ctx) - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt) - - go func() { - select { - case <-signalChan: // first signal, cancel context - fmt.Println("cancel requested", p.InternalPluginKey) - cancel() - case <-ctx.Done(): - } - <-signalChan // second signal, hard exit - fmt.Println("hard interrupt received, bye!") - os.Exit(exitCodeInterrupt) - }() - - return ctx, func() { - signal.Stop(signalChan) - cancel() - } -} - -func shouldSkipConfig(cmd *cobra.Command) bool { - if cmd.Name() == "help" { - return true - } - - _, skip := cmd.Annotations[skipConfig] - return skip -} - -// Returns the porter command called, e.g. porter installation list -// and also the fully formatted command as passed with arguments/flags. -func getCalledCommand(cmd *cobra.Command) (*cobra.Command, string, string) { - // Ask cobra what sub-command was called, and walk up the tree to get the full command called. - var cmdChain []string - calledCommand, _, err := cmd.Find(os.Args[1:]) - if err != nil { - cmdChain = append(cmdChain, "porter") - } else { - cmd := calledCommand - for cmd != nil { - cmdChain = append(cmdChain, cmd.Name()) - cmd = cmd.Parent() - } - } - // reverse the command from [list installations porter] to porter installation list - var calledCommandBuilder strings.Builder - for i := len(cmdChain); i > 0; i-- { - calledCommandBuilder.WriteString(cmdChain[i-1]) - calledCommandBuilder.WriteString(" ") - } - calledCommandStr := calledCommandBuilder.String()[0 : calledCommandBuilder.Len()-1] + ctx := context.Background() + app := porter.New() + rootCmd := buildRootCommandFrom(app) - // Also figure out the full command called, with args/flags. - formattedCommand := fmt.Sprintf("porter %s", strings.Join(os.Args[1:], " ")) - - return calledCommand, calledCommandStr, formattedCommand + cli.Main(ctx, rootCmd, app) } func buildRootCommand() *cobra.Command { @@ -184,7 +47,7 @@ Try our QuickStart https://porter.sh/quickstart to learn how to use Porter. p.Out = cmd.OutOrStdout() p.Err = cmd.OutOrStderr() - if shouldSkipConfig(cmd) { + if cli.ShouldSkipConfig(cmd) { return nil } @@ -212,9 +75,7 @@ Try our QuickStart https://porter.sh/quickstart to learn how to use Porter. SilenceErrors: true, // Errors are printed by main } - cmd.Annotations = map[string]string{ - skipConfig: "", - } + cli.SkipConfigForCommand(cmd) // These flags are available for every command globalFlags := cmd.PersistentFlags() @@ -264,7 +125,11 @@ func ShouldShowGroupCommands(cmd *cobra.Command, group string) bool { } func ShouldShowGroupCommand(cmd *cobra.Command, group string) bool { - return cmd.Annotations["group"] == group + if cmd.Annotations[cli.AnnotationGroup] == group { + return true + } + + return false } func ShouldShowUngroupedCommands(cmd *cobra.Command) bool { @@ -281,7 +146,7 @@ func ShouldShowUngroupedCommand(cmd *cobra.Command) bool { return false } - _, hasGroup := cmd.Annotations["group"] + _, hasGroup := cmd.Annotations[cli.AnnotationGroup] return !hasGroup } diff --git a/cmd/porter/mixins.go b/cmd/porter/mixins.go index bd9bbe08e..28cf93ca5 100644 --- a/cmd/porter/mixins.go +++ b/cmd/porter/mixins.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/mixin" "get.porter.sh/porter/pkg/pkgmgmt" "get.porter.sh/porter/pkg/pkgmgmt/feed" @@ -13,11 +14,10 @@ func buildMixinCommands(p *porter.Porter) *cobra.Command { Use: "mixins", Aliases: []string{"mixin"}, Short: "Mixin commands. Mixins assist with authoring bundles.", - Annotations: map[string]string{ - "group": "resource", - }, } + cli.SetCommandGroup(cmd, "resource") + cmd.AddCommand(buildMixinsListCommand(p)) cmd.AddCommand(buildMixinsSearchCommand(p)) cmd.AddCommand(BuildMixinInstallCommand(p)) @@ -132,10 +132,8 @@ func buildMixinsFeedCommand(p *porter.Porter) *cobra.Command { Use: "feed", Aliases: []string{"feeds"}, Short: "Feed commands", - Annotations: map[string]string{ - "group": "resource", - }, } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(BuildMixinFeedGenerateCommand(p)) cmd.AddCommand(BuildMixinFeedTemplateCommand(p)) diff --git a/cmd/porter/outputs.go b/cmd/porter/outputs.go index 162c872bc..84acb7538 100644 --- a/cmd/porter/outputs.go +++ b/cmd/porter/outputs.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -10,10 +11,8 @@ func buildInstallationOutputsCommands(p *porter.Porter) *cobra.Command { Use: "output", Aliases: []string{"outputs"}, Short: "Output commands", - Annotations: map[string]string{ - "group": "resource", - }, } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildBundleOutputShowCommand(p)) cmd.AddCommand(buildBundleOutputListCommand(p)) diff --git a/cmd/porter/parameters.go b/cmd/porter/parameters.go index c6da70ffd..1d984d6bf 100644 --- a/cmd/porter/parameters.go +++ b/cmd/porter/parameters.go @@ -1,17 +1,18 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) func buildParametersCommands(p *porter.Porter) *cobra.Command { cmd := &cobra.Command{ - Use: "parameters", - Aliases: []string{"parameter", "param", "params"}, - Annotations: map[string]string{"group": "resource"}, - Short: "Parameter set commands", + Use: "parameters", + Aliases: []string{"parameter", "param", "params"}, + Short: "Parameter set commands", } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildParametersApplyCommand(p)) cmd.AddCommand(buildParametersEditCommand(p)) diff --git a/cmd/porter/plugins.go b/cmd/porter/plugins.go index 75995f5a6..aad17d44b 100644 --- a/cmd/porter/plugins.go +++ b/cmd/porter/plugins.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/pkgmgmt" "get.porter.sh/porter/pkg/plugins" "get.porter.sh/porter/pkg/porter" @@ -12,10 +13,8 @@ func buildPluginsCommands(p *porter.Porter) *cobra.Command { Use: "plugins", Aliases: []string{"plugin"}, Short: "Plugin commands. Plugins enable Porter to work on different cloud providers and systems.", - Annotations: map[string]string{ - "group": "resource", - }, } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildPluginsListCommand(p)) cmd.AddCommand(buildPluginSearchCommand(p)) @@ -161,5 +160,7 @@ func buildPluginRunCommand(p *porter.Porter) *cobra.Command { Hidden: true, // This should ALWAYS be hidden, it is not a user-facing command } + cli.MarkCommandAsPlugin(cmd) + return cmd } diff --git a/cmd/porter/run.go b/cmd/porter/run.go index a16e73727..f1c976a47 100644 --- a/cmd/porter/run.go +++ b/cmd/porter/run.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -19,13 +20,10 @@ func buildRunCommand(p *porter.Porter) *cobra.Command { }, Hidden: true, // Hide runtime commands from the helptext } + cli.SetCommandGroup(cmd, "runtime") cmd.Flags().StringVarP(&opts.File, "file", "f", "porter.yaml", "The porter configuration file (Defaults to porter.yaml)") cmd.Flags().StringVar(&opts.Action, "action", "", "The bundle action to execute (Defaults to CNAB_ACTION)") - cmd.Annotations = map[string]string{ - "group": "runtime", - } - return cmd } diff --git a/cmd/porter/schema.go b/cmd/porter/schema.go index c00150a16..6dbebf4a0 100644 --- a/cmd/porter/schema.go +++ b/cmd/porter/schema.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) @@ -13,8 +14,7 @@ func buildSchemaCommand(p *porter.Porter) *cobra.Command { return p.PrintManifestSchema(cmd.Context()) }, } - cmd.Annotations = map[string]string{ - "group": "meta", - } + cli.SetCommandGroup(cmd, "meta") + return cmd } diff --git a/cmd/porter/storage.go b/cmd/porter/storage.go index 079486d97..02047474a 100644 --- a/cmd/porter/storage.go +++ b/cmd/porter/storage.go @@ -1,25 +1,23 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "github.com/spf13/cobra" ) func buildStorageCommand(p *porter.Porter) *cobra.Command { - cmd := cobra.Command{ + cmd := &cobra.Command{ Use: "storage", Short: "Manage data stored by Porter", - Long: `Manage the data stored by Porter, such as credentials and installation data. -`, - Annotations: map[string]string{ - "group": "resource", - }, + Long: `Manage the data stored by Porter, such as credentials and installation data.`, } + cli.SetCommandGroup(cmd, "resource") cmd.AddCommand(buildStorageMigrateCommand(p)) cmd.AddCommand(buildStorageFixPermissionsCommand(p)) - return &cmd + return cmd } func buildStorageMigrateCommand(p *porter.Porter) *cobra.Command { diff --git a/cmd/porter/version.go b/cmd/porter/version.go index 641f26339..6149cecc6 100644 --- a/cmd/porter/version.go +++ b/cmd/porter/version.go @@ -1,6 +1,7 @@ package main import ( + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/porter" "get.porter.sh/porter/pkg/porter/version" "github.com/spf13/cobra" @@ -18,10 +19,9 @@ func buildVersionCommand(p *porter.Porter) *cobra.Command { return p.PrintVersion(cmd.Context(), opts) }, } - cmd.Annotations = map[string]string{ - "group": "meta", - skipConfig: "", - } + + cli.SkipConfigForCommand(cmd) + cli.SetCommandGroup(cmd, "meta") f := cmd.Flags() f.StringVarP(&opts.RawFormat, "output", "o", string(version.DefaultVersionFormat), diff --git a/docs/content/cli/storage.md b/docs/content/cli/storage.md index 703a5486d..67b58a869 100644 --- a/docs/content/cli/storage.md +++ b/docs/content/cli/storage.md @@ -11,7 +11,6 @@ Manage data stored by Porter Manage the data stored by Porter, such as credentials and installation data. - ### Options ``` diff --git a/pkg/cli/app.go b/pkg/cli/app.go new file mode 100644 index 000000000..b52ce354c --- /dev/null +++ b/pkg/cli/app.go @@ -0,0 +1,20 @@ +package cli + +import ( + "context" + + "get.porter.sh/porter/pkg/config" +) + +// PorterApp represents the application that goes with a cobra command. +type PorterApp interface { + // Connect performs any startup initialization required by the application. + Connect(ctx context.Context) error + + // Close releases any resources held by your application. + // Any errors returned are logged. + Close() error + + // GetConfig returns the Porter configuration for your application. + GetConfig() *config.Config +} diff --git a/pkg/cli/cobra.go b/pkg/cli/cobra.go new file mode 100644 index 000000000..110d58dbf --- /dev/null +++ b/pkg/cli/cobra.go @@ -0,0 +1,116 @@ +package cli + +import ( + "fmt" + "os" + "strings" + + "get.porter.sh/porter/pkg/config" + "github.com/spf13/cobra" +) + +// GetCalledCommand returns metadata about the command that was called. +func GetCalledCommand(cmd *cobra.Command) CalledCommand { + var result CalledCommand + + // Determine what sub-command was called, such as porter installations list + calledCommand, _, err := cmd.Find(os.Args[1:]) + if err != nil { + result.CommandPath = cmd.Name() + } else { + result.CommandPath = calledCommand.CommandPath() + } + + // Also figure out the full command called, with args/flags. + result.FormattedCommand = fmt.Sprintf("%s %s", cmd.Name(), strings.Join(os.Args[1:], " ")) + + // Detect if this command runs a plugin + if IsPluginCommand(calledCommand) { + result.IsPlugin = true + // Get the first positional argument of the command called, such as plugin run PLUGIN_KEY + firstArgIndex := strings.Count(calledCommand.CommandPath(), " ") + 1 + if len(os.Args) > firstArgIndex { + result.PluginKey = os.Args[firstArgIndex] + } + } + + return result +} + +type CalledCommand struct { + // CommandPath of the command that was called, such as "porter installation show". The + // arguments and flags are not included so that it can be used to tell which + // command was called. + CommandPath string + + // Cmd is the resolved cobra command. + Cmd *cobra.Command + + // FormattedCommand is the fully-formatted command that was called, including arguments and flags. + // This is useful for logging the requested command. + FormattedCommand string + + // SkipConfig indicates that the command should not load the Porter configuration. + // This is reserved for commands that should never fail, like help or version. + SkipConfig bool + + // IsPlugin indicates if the command is running a plugin. + IsPlugin bool + + // PluginKey is the key of the plugin that was requested by the command. + PluginKey string +} + +// ConfigureCommand applies configuration from the command to the Porter configuration. +func ConfigureCommand(rootCmd *cobra.Command, config *config.Config) CalledCommand { + // Trace the command that called porter, e.g. porter installation show + called := GetCalledCommand(rootCmd) + + // When running an internal plugin, switch how we log to be compatible + // with the hashicorp go-plugin framework + config.IsInternalPlugin = called.IsPlugin + config.InternalPluginKey = called.PluginKey + + return called +} + +// SkipConfigForCommand sets the command to skip loading Porter's configuration. +// This is useful for commands that should NEVER fail, like help or version. +func SkipConfigForCommand(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string, 1) + } + cmd.Annotations[AnnotationSkipConfig] = "" +} + +// ShouldSkipConfig returns if the command has opted out of loading Porter's configuration. +func ShouldSkipConfig(cmd *cobra.Command) bool { + if cmd.Name() == "help" { + return true + } + + _, skip := cmd.Annotations[AnnotationSkipConfig] + return skip +} + +// MarkCommandAsPlugin indicates that the command runs a plugin. +func MarkCommandAsPlugin(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string, 1) + } + cmd.Annotations[AnnotationIsPluginCommand] = "" +} + +// IsPluginCommand returns if the command runs a plugin. +func IsPluginCommand(cmd *cobra.Command) bool { + _, isPlugin := cmd.Annotations[AnnotationIsPluginCommand] + return isPlugin +} + +// SetCommandGroup indicates how the command should be grouped in the help text. +func SetCommandGroup(cmd *cobra.Command, group string) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string, 1) + } + cmd.Annotations[AnnotationGroup] = group +} diff --git a/pkg/cli/main_helpers.go b/pkg/cli/main_helpers.go new file mode 100644 index 000000000..ac879ab1b --- /dev/null +++ b/pkg/cli/main_helpers.go @@ -0,0 +1,109 @@ +package cli + +import ( + "context" + "fmt" + "os" + "os/signal" + "runtime/debug" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "go.opentelemetry.io/otel/attribute" +) + +const ( + // AnnotationSkipConfig indicates that config should not be loaded for this command. + // This is used for commands like help and version which should never + // fail, even with porter is mis-configured. + AnnotationSkipConfig string = "skipConfig" + + // AnnotationIsPluginCommand indicates that a command is running a plugin. + AnnotationIsPluginCommand string = "isPlugin" + + // AnnotationGroup specifies how the command should be grouped in the help text. + AnnotationGroup string = "group" + + // ExitCodeSuccess indicates the program ran successfully. + ExitCodeSuccess = 0 + + // ExitCodeErr indicates the program encountered an error. + ExitCodeErr = 1 + + // ExitCodeInterrupt indicates the program was cancelled. + ExitCodeInterrupt = 2 +) + +// Main implements your Porter application's entrypoint. +func Main(ctx context.Context, rootCmd *cobra.Command, app PorterApp) { + ctx, cancel := HandleInterrupt(ctx) + defer cancel() + + // Wrapping the main run logic in a function because os.Exit will not + // execute defer statements + os.Exit(RunCommand(ctx, rootCmd, app)) +} + +// RunCommand configures and runs the specified command against your app. +func RunCommand(ctx context.Context, rootCmd *cobra.Command, app PorterApp) int { + config := app.GetConfig() + + // Configure Porter based on the command called + calledCmd := ConfigureCommand(rootCmd, config) + + // Prepare the app to run only if the called command requires configuration/setup + if !calledCmd.SkipConfig { + if err := app.Connect(ctx); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(ExitCodeErr) + } + } + + // Trace the command + ctx, log := config.Context.StartRootSpan(ctx, calledCmd.CommandPath, attribute.String("command", calledCmd.FormattedCommand)) + defer func() { + // Capture panics and trace them + if panicErr := recover(); panicErr != nil { + log.Error(errors.New(fmt.Sprintf("%s", panicErr)), + attribute.Bool("panic", true), + attribute.String("stackTrace", string(debug.Stack()))) + log.EndSpan() + app.Close() + os.Exit(ExitCodeErr) + } else { + log.Close() + app.Close() + } + }() + + if err := rootCmd.ExecuteContext(ctx); err != nil { + // Ideally we log all errors in the span that generated it, + // but as a failsafe, always log the error at the root span as well + log.Error(err) + return 1 + } + return 0 +} + +// HandleInterrupt tries to exit gracefully when the interrupt signal is sent (CTRL+C) +// Thanks to Mat Ryer, https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html +func HandleInterrupt(ctx context.Context) (context.Context, func()) { + ctx, cancel := context.WithCancel(ctx) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + + go func() { + select { + case <-signalChan: // first signal, cancel context + cancel() + case <-ctx.Done(): + } + <-signalChan // second signal, hard exit + os.Exit(ExitCodeInterrupt) + }() + + return ctx, func() { + signal.Stop(signalChan) + cancel() + } +} diff --git a/pkg/plugins/pluginbuilder/cmd.go b/pkg/plugins/pluginbuilder/cmd.go new file mode 100644 index 000000000..1ca52bbaa --- /dev/null +++ b/pkg/plugins/pluginbuilder/cmd.go @@ -0,0 +1,90 @@ +package pluginbuilder + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + + "get.porter.sh/porter/pkg/cli" + "get.porter.sh/porter/pkg/porter/version" + "github.com/spf13/cobra" +) + +func main() { + opts := PluginOptions{ + Name: "myplugin", + RegisteredPlugins: nil, + Version: "v1", + Commit: "abc123", + } + ctx := context.Background() + app := NewPlugin(opts) + rootCmd := buildPluginCommand(app) + cli.Main(ctx, rootCmd, app) +} + +func buildPluginCommand(p *PorterPlugin) *cobra.Command { + p.porterConfig.In = getInput() + + cmd := &cobra.Command{ + Use: p.Name(), + Short: fmt.Sprintf("%s plugin for porter", p.Name()), + } + + cmd.AddCommand(buildVersionCommand(p)) + cmd.AddCommand(buildRunCommand(p)) + + return cmd +} + +func buildRunCommand(p *PorterPlugin) *cobra.Command { + opts := RunOptions{} + cmd := &cobra.Command{ + Use: "run PLUGIN_KEY", + Short: "Run the plugin and listen for client connections", + Long: `Run the specified PLUGIN_KEY and listen for client connections. + +PLUGIN_KEY should be the fully-qualified 3-part key for the requested plugin which follows the format: "INTERFACE.NAME.IMPLEMENTATION". +For example, "storage.porter.mongodb" or "secrets.hashicorp.vault". +`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := opts.ApplyArgs(args); err != nil { + return err + } + return p.Run(cmd.Context(), opts) + }, + } + + return cmd +} + +func buildVersionCommand(p *PorterPlugin) *cobra.Command { + opts := version.Options{} + + cmd := &cobra.Command{ + Use: "version", + Short: "Print the plugin version", + RunE: func(cmd *cobra.Command, args []string) error { + return p.PrintVersion(cmd.Context(), opts) + }, + } + + f := cmd.Flags() + f.StringVarP(&opts.RawFormat, "output", "o", string(version.DefaultVersionFormat), + "Specify an output format. Allowed values: json, plaintext") + + return cmd +} + +// getInput attempts to use os.Stdin for standard input +// otherwise it returns an empty buffer +func getInput() io.Reader { + s, _ := os.Stdin.Stat() + if (s.Mode() & os.ModeCharDevice) == 0 { + return os.Stdin + } + + return &bytes.Buffer{} +} diff --git a/pkg/plugins/pluginbuilder/config.go b/pkg/plugins/pluginbuilder/config.go new file mode 100644 index 000000000..9e63c3365 --- /dev/null +++ b/pkg/plugins/pluginbuilder/config.go @@ -0,0 +1,26 @@ +package pluginbuilder + +import ( + "encoding/json" + "fmt" + "io" +) + +// LoadConfig reads the plugin's configuration from stdin. +func (p *PorterPlugin) loadConfig() error { + // Use the configuration data structure provided by the plugin, if specified. + if p.opts.DefaultConfig != nil { + p.pluginConfig = p.opts.DefaultConfig + } else { + p.pluginConfig = make(map[string]interface{}) + } + + if err := json.NewDecoder(p.porterConfig.In).Decode(&p.pluginConfig); err != nil { + if err == io.EOF { + // No plugin pluginConfig was specified + return nil + } + return fmt.Errorf("error unmarshaling the plugins configuration data from stdin into %T: %w", p.pluginConfig, err) + } + return nil +} diff --git a/pkg/plugins/pluginbuilder/doc.go b/pkg/plugins/pluginbuilder/doc.go new file mode 100644 index 000000000..9f5f09136 --- /dev/null +++ b/pkg/plugins/pluginbuilder/doc.go @@ -0,0 +1,2 @@ +// Package pluginbuilder is a reusable library for building your own plugin for Porter. +package pluginbuilder diff --git a/pkg/plugins/pluginbuilder/plugin.go b/pkg/plugins/pluginbuilder/plugin.go new file mode 100644 index 000000000..067340d29 --- /dev/null +++ b/pkg/plugins/pluginbuilder/plugin.go @@ -0,0 +1,91 @@ +package pluginbuilder + +import ( + "context" + "strings" + + "get.porter.sh/porter/pkg/cli" + "get.porter.sh/porter/pkg/config" + "get.porter.sh/porter/pkg/plugins" +) + +var _ cli.PorterApp = &PorterPlugin{} + +// PorterPlugin is a reusable helper that provides most of the basic plugin +// functionality for a Porter plugin. +type PorterPlugin struct { + // Opts customizes the default implementation of plugin. + opts PluginOptions + + // porterConfig provides Porter's configuration. This is helpful for retrieving + // the PORTER_HOME directory, and using the portercontext.Context field to make + // the plugin more easily testable. + porterConfig *config.Config + + // pluginConfig holds any plugin-specific configuration defined in the Porter + // configuration file. + pluginConfig interface{} +} + +// Connect prepares the plugin to run. +func (p *PorterPlugin) Connect(ctx context.Context) error { + // Load the Porter configuration + return p.porterConfig.Load(ctx, nil) +} + +// Close the plugin and release any resources held. +func (p *PorterPlugin) Close() error { + // Release resources held by the Porter configuration + return p.porterConfig.Close() +} + +// GetConfig returns the plugin's Porter configuration. +func (p *PorterPlugin) GetConfig() *config.Config { + return p.porterConfig +} + +// Name returns the plugin name. +func (p *PorterPlugin) Name() string { + return p.opts.Name +} + +// PluginOptions customizes the default plugin implementation of your custom +// plugin. +type PluginOptions struct { + // Name of the plugin. + Name string + + // DefaultConfig contains the default configuration data structure into which + // the plugin's configuration will be placed when the plugin is run. + // Defaults to a map[string]interface{}. + DefaultConfig interface{} + + // RegisteredPlugins is a lookup from a fully-qualified 3-part plugin key + // to the information necessary to run the plugin. + RegisteredPlugins map[string]plugins.PluginRegistration + + // Version is the semantic version for this build of the plugin. + Version string + + // Commit is the git commit hash for this build of the plugin. + Commit string +} + +// NewPlugin creates a PorterPlugin and customizes the implementation using the +// specified PluginOptions. +func NewPlugin(opts PluginOptions) *PorterPlugin { + return &PorterPlugin{ + opts: opts, + porterConfig: config.New(), + } +} + +// ListSupportedKeys prints a human-readable list of the plugin keys supported by +// the plugin. +func (opts *PluginOptions) ListSupportedKeys() string { + keys := make([]string, 0, len(opts.RegisteredPlugins)) + for key := range opts.RegisteredPlugins { + keys = append(keys, key) + } + return strings.Join(keys, ", ") +} diff --git a/pkg/plugins/pluginbuilder/run.go b/pkg/plugins/pluginbuilder/run.go new file mode 100644 index 000000000..2dc2c6fb6 --- /dev/null +++ b/pkg/plugins/pluginbuilder/run.go @@ -0,0 +1,82 @@ +package pluginbuilder + +import ( + "context" + "errors" + "fmt" + + "get.porter.sh/porter/pkg/plugins" + "get.porter.sh/porter/pkg/tracing" +) + +// RunOptions are the arguments passed to the run command. +type RunOptions struct { + // Key is the fully-qualified 3-part plugin key. + Key string +} + +// ApplyArgs applies the arguments from the command-line to the run command options. +func (o *RunOptions) ApplyArgs(args []string) error { + if len(args) == 0 { + return errors.New("the positional argument PLUGIN_KEY was not specified") + } + if len(args) > 1 { + return errors.New("multiple positional arguments were specified but only one, PLUGIN_KEY, is expected") + } + + o.Key = args[0] + return nil +} + +// Run executes the plugin. +func (p PorterPlugin) Run(ctx context.Context, opts RunOptions) error { + err := p.validateRunOptions(opts) + if err != nil { + return err + } + + // Read the plugin configuration from the porter pluginConfig file from stdin + if err := p.loadConfig(); err != nil { + return err + } + + // Create an instance of the plugin + selectedPlugin := p.opts.RegisteredPlugins[opts.Key] + impl, err := selectedPlugin.Create(p.porterConfig, p.pluginConfig) + if err != nil { + return fmt.Errorf("could not create an instance of the requested internal plugin %s: %w", opts.Key, err) + } + + // Clean up after the plugin when it is done + defer func() { + if panicErr := recover(); err != nil { + err = fmt.Errorf("%v", panicErr) + } + + if closer, ok := impl.(plugins.PluginCloser); ok { + if err = closer.Close(ctx); err != nil { + log := tracing.LoggerFromContext(ctx) + log.Error(fmt.Errorf("error stopping the %s plugin: %w", opts.Key, err)) + } + } + }() + + // Run the plugin + plugins.Serve(p.porterConfig.Context, selectedPlugin.Interface, impl, selectedPlugin.ProtocolVersion) + + // Return the error that may have been set during recover above + return err +} + +// validateRunOptions validates the arguments and flags passed to the run command. +func (p PorterPlugin) validateRunOptions(opts RunOptions) error { + if opts.Key == "" { + return fmt.Errorf("no plugin key was specified") + } + + if _, ok := p.opts.RegisteredPlugins[opts.Key]; !ok { + return fmt.Errorf("unsupported plugin key specified: %s: the plugin supports the following keys: %s", opts.Key, p.opts.ListSupportedKeys()) + } + + return nil +} diff --git a/pkg/plugins/pluginbuilder/version.go b/pkg/plugins/pluginbuilder/version.go new file mode 100644 index 000000000..854826ff1 --- /dev/null +++ b/pkg/plugins/pluginbuilder/version.go @@ -0,0 +1,42 @@ +package pluginbuilder + +import ( + "context" + "fmt" + "strings" + + "get.porter.sh/porter/pkg/pkgmgmt" + "get.porter.sh/porter/pkg/plugins" + "get.porter.sh/porter/pkg/porter/version" +) + +// PrintVersion introspects the configured plugin and returns metadata about the +// plugin. This is the plugin's implementation for the porter plugins list +// command. +func (p *PorterPlugin) PrintVersion(ctx context.Context, opts version.Options) error { + metadata := plugins.Metadata{ + Metadata: pkgmgmt.Metadata{ + Name: p.Name(), + VersionInfo: pkgmgmt.VersionInfo{ + Version: p.opts.Version, + Commit: p.opts.Commit, + Author: "Porter Authors", + }, + }, + Implementations: make([]plugins.Implementation, 0, len(p.opts.RegisteredPlugins)), + } + + for key := range p.opts.RegisteredPlugins { + parts := strings.Split(key, ".") + if len(parts) != 3 { + return fmt.Errorf("the plugin is configured with an invalid set of plugin implementations: plugin keys should have 3 parts but got %s", key) + } + pluginInterface := parts[0] + pluginImplementation := parts[2] + metadata.Implementations = append(metadata.Implementations, plugins.Implementation{ + Type: pluginInterface, + Name: pluginImplementation, + }) + } + return version.PrintVersion(p.porterConfig.Context, opts, metadata) +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 153c9c999..eaa504ba3 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -1,10 +1,11 @@ package plugins import ( - "errors" + "context" "fmt" "strings" + "get.porter.sh/porter/pkg/config" "github.com/hashicorp/go-plugin" ) @@ -42,7 +43,7 @@ func ParsePluginKey(value string) (PluginKey, error) { key.Binary = parts[1] key.Implementation = parts[2] default: - return PluginKey{}, errors.New("invalid plugin key '%s', allowed format is [INTERFACE].BINARY.IMPLEMENTATION") + return PluginKey{}, fmt.Errorf("invalid plugin key '%s', allowed format is [INTERFACE].BINARY.IMPLEMENTATION", value) } if key.Binary == "porter" { @@ -51,3 +52,24 @@ func ParsePluginKey(value string) (PluginKey, error) { return key, nil } + +// PluginRegistration is the info needed to automatically handle +// running a plugin when requested. +type PluginRegistration struct { + // Interface that the plugin implements, such as storage or secrets. + Interface string + + // ProtocolVersion is the version of the plugin protocol that the plugin supports. + ProtocolVersion int + + // Create is the handler called to make an instance of the plugin. + Create func(c *config.Config, pluginCfg interface{}) (plugin.Plugin, error) +} + +// PluginCloser is the interface that plugins should implement when they need to +// clean up resources when Porter is done with the plugin. +type PluginCloser interface { + // Close requests that the plugin clean up long-held resources. + // A context is passed so that the plugin can still output log/trace data. + Close(ctx context.Context) error +} diff --git a/pkg/porter/porter.go b/pkg/porter/porter.go index af524dced..657aaea88 100644 --- a/pkg/porter/porter.go +++ b/pkg/porter/porter.go @@ -9,6 +9,7 @@ import ( "get.porter.sh/porter/pkg/build" "get.porter.sh/porter/pkg/build/buildkit" "get.porter.sh/porter/pkg/cache" + "get.porter.sh/porter/pkg/cli" cnabtooci "get.porter.sh/porter/pkg/cnab/cnab-to-oci" cnabprovider "get.porter.sh/porter/pkg/cnab/provider" "get.porter.sh/porter/pkg/config" @@ -24,6 +25,8 @@ import ( "github.com/hashicorp/go-multierror" ) +var _ cli.PorterApp = &Porter{} + // Porter is the logic behind the porter client. type Porter struct { *config.Config @@ -80,6 +83,10 @@ func NewFor(c *config.Config, store storage.Store, secretStorage secrets.Store) } } +func (p *Porter) GetConfig() *config.Config { + return p.Config +} + // Connect initializes Porter for use and must be called before other Porter methods. // It is the responsibility of the caller to also call Close when done with Porter. func (p *Porter) Connect(ctx context.Context) error { From bf45602df7843b5eede61c96d3f3706a8478960b Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Thu, 30 Jun 2022 21:59:42 -0500 Subject: [PATCH 2/4] Fix setting the plugin version/commit/author metadata Signed-off-by: Carolyn Van Slyck --- pkg/plugins/pluginbuilder/cmd.go | 7 +++---- pkg/plugins/pluginbuilder/plugin.go | 11 +++++------ pkg/plugins/pluginbuilder/version.go | 16 +++++++++++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pkg/plugins/pluginbuilder/cmd.go b/pkg/plugins/pluginbuilder/cmd.go index 1ca52bbaa..94db69f0e 100644 --- a/pkg/plugins/pluginbuilder/cmd.go +++ b/pkg/plugins/pluginbuilder/cmd.go @@ -16,16 +16,15 @@ func main() { opts := PluginOptions{ Name: "myplugin", RegisteredPlugins: nil, - Version: "v1", - Commit: "abc123", } ctx := context.Background() app := NewPlugin(opts) - rootCmd := buildPluginCommand(app) + rootCmd := BuildPluginCommand(app) cli.Main(ctx, rootCmd, app) } -func buildPluginCommand(p *PorterPlugin) *cobra.Command { +// BuildPluginCommand creates the cobra.Command +func BuildPluginCommand(p *PorterPlugin) *cobra.Command { p.porterConfig.In = getInput() cmd := &cobra.Command{ diff --git a/pkg/plugins/pluginbuilder/plugin.go b/pkg/plugins/pluginbuilder/plugin.go index 067340d29..c12e7f403 100644 --- a/pkg/plugins/pluginbuilder/plugin.go +++ b/pkg/plugins/pluginbuilder/plugin.go @@ -53,8 +53,13 @@ func (p *PorterPlugin) Name() string { // plugin. type PluginOptions struct { // Name of the plugin. + // This must match the plugin binary name. Name string + // Author of the plugin. + // This is displayed in the output of porter plugins list. + Author string + // DefaultConfig contains the default configuration data structure into which // the plugin's configuration will be placed when the plugin is run. // Defaults to a map[string]interface{}. @@ -63,12 +68,6 @@ type PluginOptions struct { // RegisteredPlugins is a lookup from a fully-qualified 3-part plugin key // to the information necessary to run the plugin. RegisteredPlugins map[string]plugins.PluginRegistration - - // Version is the semantic version for this build of the plugin. - Version string - - // Commit is the git commit hash for this build of the plugin. - Commit string } // NewPlugin creates a PorterPlugin and customizes the implementation using the diff --git a/pkg/plugins/pluginbuilder/version.go b/pkg/plugins/pluginbuilder/version.go index 854826ff1..4f54eb192 100644 --- a/pkg/plugins/pluginbuilder/version.go +++ b/pkg/plugins/pluginbuilder/version.go @@ -10,6 +10,16 @@ import ( "get.porter.sh/porter/pkg/porter/version" ) +var ( + // Version of your plugin + // This is set by ldflags when you compile the go binary for your plugin + Version string + + // Commit hash of your plugin + // This is set by ldflags when you compile the go binary for your plugin + Commit string +) + // PrintVersion introspects the configured plugin and returns metadata about the // plugin. This is the plugin's implementation for the porter plugins list // command. @@ -18,9 +28,9 @@ func (p *PorterPlugin) PrintVersion(ctx context.Context, opts version.Options) e Metadata: pkgmgmt.Metadata{ Name: p.Name(), VersionInfo: pkgmgmt.VersionInfo{ - Version: p.opts.Version, - Commit: p.opts.Commit, - Author: "Porter Authors", + Version: Version, + Commit: Commit, + Author: p.opts.Author, }, }, Implementations: make([]plugins.Implementation, 0, len(p.opts.RegisteredPlugins)), From d4149220aa211867f9dd63bff650a5b36ea6cb50 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Thu, 30 Jun 2022 23:05:09 -0500 Subject: [PATCH 3/4] wip Signed-off-by: Carolyn Van Slyck --- go.mod | 2 +- pkg/cli/main_helpers.go | 3 +-- pkg/plugins/pluginbuilder/plugin.go | 33 ++++++++++++++++++++++++++++ pkg/plugins/pluginbuilder/version.go | 9 +++++++- pkg/plugins/plugins.go | 5 ++++- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b27a7e0bd..3f1391517 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/mitchellh/mapstructure v1.4.2 github.com/mmcdole/gofeed v1.0.0-beta2 github.com/moby/buildkit v0.10.0 + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 github.com/olekukonko/tablewriter v0.0.4 github.com/opencontainers/go-digest v1.0.0 github.com/osteele/liquid v1.3.0 @@ -161,7 +162,6 @@ require ( github.com/moby/sys/mount v0.3.0 // indirect github.com/moby/sys/mountinfo v0.6.0 // indirect github.com/moby/sys/signal v0.6.0 // indirect - github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect diff --git a/pkg/cli/main_helpers.go b/pkg/cli/main_helpers.go index ac879ab1b..db8d0d878 100644 --- a/pkg/cli/main_helpers.go +++ b/pkg/cli/main_helpers.go @@ -7,7 +7,6 @@ import ( "os/signal" "runtime/debug" - "github.com/pkg/errors" "github.com/spf13/cobra" "go.opentelemetry.io/otel/attribute" ) @@ -64,7 +63,7 @@ func RunCommand(ctx context.Context, rootCmd *cobra.Command, app PorterApp) int defer func() { // Capture panics and trace them if panicErr := recover(); panicErr != nil { - log.Error(errors.New(fmt.Sprintf("%s", panicErr)), + log.Error(fmt.Errorf("%s", panicErr), attribute.Bool("panic", true), attribute.String("stackTrace", string(debug.Stack()))) log.EndSpan() diff --git a/pkg/plugins/pluginbuilder/plugin.go b/pkg/plugins/pluginbuilder/plugin.go index c12e7f403..9c0112393 100644 --- a/pkg/plugins/pluginbuilder/plugin.go +++ b/pkg/plugins/pluginbuilder/plugin.go @@ -2,8 +2,13 @@ package pluginbuilder import ( "context" + "fmt" "strings" + storageplugins "get.porter.sh/porter/pkg/storage/plugins" + + secretsplugins "get.porter.sh/porter/pkg/secrets/plugins" + "get.porter.sh/porter/pkg/cli" "get.porter.sh/porter/pkg/config" "get.porter.sh/porter/pkg/plugins" @@ -88,3 +93,31 @@ func (opts *PluginOptions) ListSupportedKeys() string { } return strings.Join(keys, ", ") } + +// RegisterSecretsPlugin adds a secrets plugin implementation to the list of available plugins. +func (opts *PluginOptions) RegisterSecretsPlugin(implementationName string, handler plugins.CreatePluginHandler) { + if opts.RegisteredPlugins == nil { + opts.RegisteredPlugins = make(map[string]plugins.PluginRegistration, 1) + } + + key := fmt.Sprintf("%s.%s.%s", secretsplugins.PluginInterface, opts.Name, implementationName) + opts.RegisteredPlugins[key] = plugins.PluginRegistration{ + Interface: secretsplugins.PluginInterface, + ProtocolVersion: secretsplugins.PluginProtocolVersion, + Create: handler, + } +} + +// RegisterStoragePlugin adds a storage plugin implementation to the list of available plugins. +func (opts *PluginOptions) RegisterStoragePlugin(implementationName string, handler plugins.CreatePluginHandler) { + if opts.RegisteredPlugins == nil { + opts.RegisteredPlugins = make(map[string]plugins.PluginRegistration, 1) + } + + key := fmt.Sprintf("%s.%s.%s", storageplugins.PluginInterface, opts.Name, implementationName) + opts.RegisteredPlugins[key] = plugins.PluginRegistration{ + Interface: storageplugins.PluginInterface, + ProtocolVersion: storageplugins.PluginProtocolVersion, + Create: handler, + } +} diff --git a/pkg/plugins/pluginbuilder/version.go b/pkg/plugins/pluginbuilder/version.go index 4f54eb192..0ed068e4f 100644 --- a/pkg/plugins/pluginbuilder/version.go +++ b/pkg/plugins/pluginbuilder/version.go @@ -24,6 +24,10 @@ var ( // plugin. This is the plugin's implementation for the porter plugins list // command. func (p *PorterPlugin) PrintVersion(ctx context.Context, opts version.Options) error { + if err := opts.Validate(); err != nil { + return err + } + metadata := plugins.Metadata{ Metadata: pkgmgmt.Metadata{ Name: p.Name(), @@ -39,8 +43,11 @@ func (p *PorterPlugin) PrintVersion(ctx context.Context, opts version.Options) e for key := range p.opts.RegisteredPlugins { parts := strings.Split(key, ".") if len(parts) != 3 { - return fmt.Errorf("the plugin is configured with an invalid set of plugin implementations: plugin keys should have 3 parts but got %s", key) + // Keep going so that we can at least print the version + fmt.Fprintf(p.porterConfig.Err, "WARNING: the plugin is configured with an invalid set of plugin implementations: plugin keys should have 3 parts but got %s\n", key) + continue } + pluginInterface := parts[0] pluginImplementation := parts[2] metadata.Implementations = append(metadata.Implementations, plugins.Implementation{ diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index eaa504ba3..e2e38f0d9 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -53,6 +53,9 @@ func ParsePluginKey(value string) (PluginKey, error) { return key, nil } +// CreatePluginHandler creates an instance of a plugin given its configuration. +type CreatePluginHandler func(porterCfg *config.Config, pluginCfg interface{}) (plugin.Plugin, error) + // PluginRegistration is the info needed to automatically handle // running a plugin when requested. type PluginRegistration struct { @@ -63,7 +66,7 @@ type PluginRegistration struct { ProtocolVersion int // Create is the handler called to make an instance of the plugin. - Create func(c *config.Config, pluginCfg interface{}) (plugin.Plugin, error) + Create CreatePluginHandler } // PluginCloser is the interface that plugins should implement when they need to From e4bb80d89a7bb15aa144c642c93a3d058c81f644 Mon Sep 17 00:00:00 2001 From: Carolyn Van Slyck Date: Sun, 3 Jul 2022 20:25:41 -0500 Subject: [PATCH 4/4] Export helper method for mongo plugin implementors Signed-off-by: Carolyn Van Slyck --- pkg/storage/plugins/mongodb/mongodb.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/storage/plugins/mongodb/mongodb.go b/pkg/storage/plugins/mongodb/mongodb.go index 5f0700fbe..3fd4323fb 100644 --- a/pkg/storage/plugins/mongodb/mongodb.go +++ b/pkg/storage/plugins/mongodb/mongodb.go @@ -189,7 +189,7 @@ func (s *Store) Find(ctx context.Context, opts plugins.FindOptions) ([]bson.Raw, } c := s.getCollection(opts.Collection) - findOpts, err := s.buildFindOptions(opts) + findOpts, err := BuildFindOptions(opts) if err != nil { return nil, span.Error(err) } @@ -209,7 +209,8 @@ func (s *Store) Find(ctx context.Context, opts plugins.FindOptions) ([]bson.Raw, return results, span.Error(err) } -func (s *Store) buildFindOptions(opts plugins.FindOptions) (*options.FindOptions, error) { +// BuildFindOptions converts from Porter's FindOptions structure to the FindOptions for mongodb. +func BuildFindOptions(opts plugins.FindOptions) (*options.FindOptions, error) { query := options.Find() if opts.Select != nil {