Skip to content

Commit

Permalink
phase 2 plugins metadata and examples for EP
Browse files Browse the repository at this point in the history
Signed-off-by: Bryce Palmer <bpalmer@redhat.com>
  • Loading branch information
everettraven committed Jun 29, 2022
1 parent 78ad658 commit fc97428
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 24 deletions.
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

0 comments on commit fc97428

Please sign in to comment.