Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ (phase 2 plugins): enable external plugins to specify metadata and examples #2780

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pkg/plugin/external/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugins/external/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugins/external/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
151 changes: 151 additions & 0 deletions pkg/plugins/external/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -516,3 +660,10 @@ func getFlags() []external.Flag {
},
}
}

func getMetadata() plugin.SubcommandMetadata {
return plugin.SubcommandMetadata{
Description: "Test description",
Examples: "Test examples",
}
}
89 changes: 66 additions & 23 deletions pkg/plugins/external/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions pkg/plugins/external/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugins/external/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down