diff --git a/pkg/plugin/external/types.go b/pkg/plugin/external/types.go index 242f1fda1d9..25cdd8b0735 100644 --- a/pkg/plugin/external/types.go +++ b/pkg/plugin/external/types.go @@ -16,6 +16,8 @@ limitations under the License. package external +import "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + // PluginRequest contains all information kubebuilder received from the CLI // and plugins executed before it. type PluginRequest struct { @@ -47,7 +49,7 @@ type PluginResponse struct { // Help contains the plugin specific help text that the plugin returns to Kubebuilder when it receives // `--help` flag from Kubebuilder. - Help string `json:"help"` + Metadata plugin.SubcommandMetadata `json:"metadata"` // Universe in the PluginResponse represents the updated file contents that was written by the plugin. Universe map[string]string `json:"universe"` diff --git a/pkg/plugins/external/api.go b/pkg/plugins/external/api.go index b1c05259a35..6f0d5bf5077 100644 --- a/pkg/plugins/external/api.go +++ b/pkg/plugins/external/api.go @@ -41,6 +41,10 @@ func (p *createAPISubcommand) InjectResource(*resource.Resource) error { return nil } +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + setExternalPluginMetadata("api", p.Path, subcmdMeta) +} + func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "api", p.Path, p.Args) } diff --git a/pkg/plugins/external/edit.go b/pkg/plugins/external/edit.go index 8c89c7f6d46..36ab5496e08 100644 --- a/pkg/plugins/external/edit.go +++ b/pkg/plugins/external/edit.go @@ -31,6 +31,10 @@ type editSubcommand struct { Args []string } +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + setExternalPluginMetadata("edit", p.Path, subcmdMeta) +} + func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "edit", p.Path, p.Args) } diff --git a/pkg/plugins/external/external_test.go b/pkg/plugins/external/external_test.go index 7a4932970f7..40f35055c17 100644 --- a/pkg/plugins/external/external_test.go +++ b/pkg/plugins/external/external_test.go @@ -29,6 +29,7 @@ import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugin/external" ) @@ -86,6 +87,19 @@ func (m *mockValidFlagOutputGetter) GetExecOutput(req []byte, path string) ([]by return json.Marshal(response) } +type mockValidMEOutputGetter struct{} + +func (m *mockValidMEOutputGetter) GetExecOutput(req []byte, path string) ([]byte, error) { + response := external.PluginResponse{ + Command: "metadata", + Error: false, + Universe: nil, + Metadata: getMetadata(), + } + + return json.Marshal(response) +} + const externalPlugin = "myexternalplugin.sh" const floatVal = "float" @@ -486,6 +500,136 @@ var _ = Describe("Run external plugin using Scaffold", func() { } }) }) + + // TODO(everettraven): Add tests for an external plugin setting the Metadata and Examples + Context("Successfully retrieving metadata and examples from external plugin", func() { + var ( + pluginFileName string + metadata *plugin.SubcommandMetadata + checkMetadata func() + ) + BeforeEach(func() { + outputGetter = &mockValidMEOutputGetter{} + currentDirGetter = &mockValidOsWdGetter{} + + pluginFileName = externalPlugin + metadata = &plugin.SubcommandMetadata{} + + checkMetadata = func() { + Expect(metadata.Description).Should(Equal(getMetadata().Description)) + Expect(metadata.Examples).Should(Equal(getMetadata().Examples)) + } + }) + + It("should use the external plugin's metadata and examples for `init` subcommand", func() { + sc := initSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the external plugin's metadata and examples for `create api` subcommand", func() { + sc := createAPISubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the external plugin's metadata and examples for `create webhook` subcommand", func() { + sc := createWebhookSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the external plugin's metadata and examples for `edit` subcommand", func() { + sc := editSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + }) + + Context("Failing to retrieve metadata and examples from external plugin", func() { + var ( + pluginFileName string + metadata *plugin.SubcommandMetadata + checkMetadata func() + ) + BeforeEach(func() { + outputGetter = &mockInValidOutputGetter{} + currentDirGetter = &mockValidOsWdGetter{} + + pluginFileName = externalPlugin + metadata = &plugin.SubcommandMetadata{} + + checkMetadata = func() { + Expect(metadata.Description).Should(Equal(fmt.Sprintf(defaultMetadataTemplate, "myexternalplugin"))) + Expect(metadata.Examples).Should(BeEmpty()) + } + }) + + It("should use the default metadata and examples for `init` subcommand", func() { + sc := initSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the default metadata and examples for `create api` subcommand", func() { + sc := createAPISubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the default metadata and examples for `create webhook` subcommand", func() { + sc := createWebhookSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + + It("should use the default metadata and examples for `edit` subcommand", func() { + sc := editSubcommand{ + Path: pluginFileName, + Args: nil, + } + + sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) + + checkMetadata() + }) + }) + }) func getFlags() []external.Flag { @@ -516,3 +660,10 @@ func getFlags() []external.Flag { }, } } + +func getMetadata() plugin.SubcommandMetadata { + return plugin.SubcommandMetadata{ + Description: "Test description", + Examples: "Test examples", + } +} diff --git a/pkg/plugins/external/helpers.go b/pkg/plugins/external/helpers.go index ddf5f9cdec8..7b6baa5822c 100644 --- a/pkg/plugins/external/helpers.go +++ b/pkg/plugins/external/helpers.go @@ -28,11 +28,19 @@ import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugin/external" ) var outputGetter ExecOutputGetter = &execOutputGetter{} +const defaultMetadataTemplate = ` +%s is an external plugin for scaffolding files to help with your Operator development. + +For more information on how to use this external plugin, it is recommended to +consult the external plugin's documentation. +` + // ExecOutputGetter is an interface that implements the exec output method. type ExecOutputGetter interface { GetExecOutput(req []byte, path string) ([]byte, error) @@ -70,27 +78,36 @@ func (o *osWdGetter) GetCurrentDir() (string, error) { return currentDir, nil } -func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string) error { - req.Universe = map[string]string{} - +func makePluginRequest(req external.PluginRequest, path string) (*external.PluginResponse, error) { reqBytes, err := json.Marshal(req) if err != nil { - return err + return nil, err } out, err := outputGetter.GetExecOutput(reqBytes, path) if err != nil { - return err + return nil, err } res := external.PluginResponse{} if err := json.Unmarshal(out, &res); err != nil { - return err + return nil, err } // Error if the plugin failed. if res.Error { - return fmt.Errorf(strings.Join(res.ErrorMsgs, "\n")) + return nil, fmt.Errorf(strings.Join(res.ErrorMsgs, "\n")) + } + + return &res, nil +} + +func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string) error { + req.Universe = map[string]string{} + + res, err := makePluginRequest(req, path) + if err != nil { + return fmt.Errorf("error making request to external plugin: %w", err) } currentDir, err := currentDirGetter.GetCurrentDir() @@ -123,23 +140,9 @@ func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, p func getExternalPluginFlags(req external.PluginRequest, path string) ([]external.Flag, error) { req.Universe = map[string]string{} - reqBytes, err := json.Marshal(req) + res, err := makePluginRequest(req, path) if err != nil { - return nil, err - } - - out, err := outputGetter.GetExecOutput(reqBytes, path) - if err != nil { - return nil, err - } - - res := external.PluginResponse{} - if err := json.Unmarshal(out, &res); err != nil { - return nil, err - } - - if res.Error { - return nil, fmt.Errorf(strings.Join(res.ErrorMsgs, "\n")) + return nil, fmt.Errorf("error making request to external plugin: %w", err) } return res.Flags, nil @@ -211,3 +214,43 @@ func bindExternalPluginFlags(fs *pflag.FlagSet, subcommand string, path string, bindSpecificFlags(fs, flags) } } + +// setExternalPluginMetadata is a helper function that sets the subcommand +// metadata that is used when the help text is shown for a subcommand. +// It will attempt to get the Metadata from the external plugin. If the +// external plugin returns no Metadata or an error, a default will be used. +func setExternalPluginMetadata(subcommand, path string, subcmdMeta *plugin.SubcommandMetadata) { + fileName := filepath.Base(path) + subcmdMeta.Description = fmt.Sprintf(defaultMetadataTemplate, fileName[:len(fileName)-len(filepath.Ext(fileName))]) + + res, _ := getExternalPluginMetadata(subcommand, path) + + if res != nil { + if res.Description != "" { + subcmdMeta.Description = res.Description + } + + if res.Examples != "" { + subcmdMeta.Examples = res.Examples + } + } +} + +// fetchExternalPluginMetadata performs the actual request to the +// external plugin to get the metadata. It returns the metadata +// or an error if an error occurs during the fetch process. +func getExternalPluginMetadata(subcommand, path string) (*plugin.SubcommandMetadata, error) { + req := external.PluginRequest{ + APIVersion: defaultAPIVersion, + Command: "metadata", + Args: []string{"--" + subcommand}, + Universe: map[string]string{}, + } + + res, err := makePluginRequest(req, path) + if err != nil { + return nil, fmt.Errorf("error making request to external plugin: %w", err) + } + + return &res.Metadata, nil +} diff --git a/pkg/plugins/external/init.go b/pkg/plugins/external/init.go index 1480158d536..ed5837e9411 100644 --- a/pkg/plugins/external/init.go +++ b/pkg/plugins/external/init.go @@ -31,6 +31,10 @@ type initSubcommand struct { Args []string } +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + setExternalPluginMetadata("init", p.Path, subcmdMeta) +} + func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "init", p.Path, p.Args) } diff --git a/pkg/plugins/external/webhook.go b/pkg/plugins/external/webhook.go index 253b8fd2463..a9a174020a0 100644 --- a/pkg/plugins/external/webhook.go +++ b/pkg/plugins/external/webhook.go @@ -37,6 +37,10 @@ func (p *createWebhookSubcommand) InjectResource(*resource.Resource) error { return nil } +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + setExternalPluginMetadata("webhook", p.Path, subcmdMeta) +} + func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "webhook", p.Path, p.Args) }