From d17eb4e7742655ae95d142c655acf83c76168ce9 Mon Sep 17 00:00:00 2001 From: Adrian Orive Date: Thu, 4 Feb 2021 15:44:56 +0100 Subject: [PATCH] Implement the plugin phase 1.5 API Signed-off-by: Adrian Orive --- pkg/cli/api.go | 81 +++--- pkg/cli/cli.go | 9 +- pkg/cli/cmd_helpers.go | 220 +++++++++++++-- pkg/cli/edit.go | 81 +++--- pkg/cli/init.go | 158 ++++++----- pkg/cli/resource.go | 93 +++++++ pkg/cli/resource_test.go | 209 ++++++++++++++ pkg/cli/webhook.go | 81 +++--- .../v2/suite_test.go => plugin/errors.go} | 19 +- pkg/plugin/metadata.go | 31 +++ pkg/plugin/{interfaces.go => plugin.go} | 50 ---- pkg/plugin/subcommand.go | 94 +++++++ pkg/plugin/version_test.go | 21 +- pkg/plugins/golang/options.go | 101 ++----- pkg/plugins/golang/options_test.go | 262 +++++------------- pkg/plugins/golang/v2/api.go | 61 ++-- pkg/plugins/golang/v2/edit.go | 42 ++- pkg/plugins/golang/v2/init.go | 84 +++--- pkg/plugins/golang/v2/options.go | 189 ------------- pkg/plugins/golang/v2/options_test.go | 237 ---------------- pkg/plugins/golang/v2/webhook.go | 62 ++--- pkg/plugins/golang/v3/api.go | 71 +++-- pkg/plugins/golang/v3/edit.go | 42 ++- pkg/plugins/golang/v3/init.go | 92 +++--- pkg/plugins/golang/v3/webhook.go | 55 ++-- pkg/plugins/internal/cmdutil/cmdutil.go | 40 --- 26 files changed, 1149 insertions(+), 1336 deletions(-) create mode 100644 pkg/cli/resource.go create mode 100644 pkg/cli/resource_test.go rename pkg/{plugins/golang/v2/suite_test.go => plugin/errors.go} (64%) create mode 100644 pkg/plugin/metadata.go rename pkg/plugin/{interfaces.go => plugin.go} (58%) create mode 100644 pkg/plugin/subcommand.go delete mode 100644 pkg/plugins/golang/v2/options.go delete mode 100644 pkg/plugins/golang/v2/options_test.go diff --git a/pkg/cli/api.go b/pkg/cli/api.go index c8d0fa29e08..3f8bc2a033f 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -14,77 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const apiErrorMsg = "failed to create API" + func (c cli) newCreateAPICmd() *cobra.Command { - ctx := c.newAPIContext() cmd := &cobra.Command{ - Use: "api", - Short: "Scaffold a Kubernetes API", - Long: ctx.Description, - Example: ctx.Examples, + Use: "api", + Short: "Scaffold a Kubernetes API", + Long: `Scaffold a Kubernetes API. +`, RunE: errCmdFunc( fmt.Errorf("api subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateAPI(ctx, cmd) - return cmd -} - -func (c cli) newAPIContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a Kubernetes API. -`, - } -} - -// nolint:dupl -func (c cli) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createAPIPlugin plugin.CreateAPI - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateAPI) - if isValid { - if createAPIPlugin != nil { - err := fmt.Errorf("duplicate API creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createAPIPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createAPIPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateAPI. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateAPI) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateAPI).GetCreateAPISubcommand() + }, + ) - if createAPIPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide an API creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"API creation"}) + return cmd } - subcommand := createAPIPlugin.GetCreateAPISubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, apiErrorMsg) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index ecd2ef32f45..eeda6863869 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -38,8 +38,6 @@ const ( projectVersionFlag = "project-version" pluginsFlag = "plugins" - - noPluginError = "invalid config file please verify that the version and layout fields are set and valid" ) // equalStringSlice checks if two string slices are equal. @@ -480,6 +478,13 @@ func (c cli) printDeprecationWarnings() { } } +// metadata returns CLI's metadata. +func (c cli) metadata() plugin.CLIMetadata { + return plugin.CLIMetadata{ + CommandName: c.commandName, + } +} + // Run implements CLI.Run. func (c cli) Run() error { return c.cmd.Execute() diff --git a/pkg/cli/cmd_helpers.go b/pkg/cli/cmd_helpers.go index 15ee41751aa..5e6532ad949 100644 --- a/pkg/cli/cmd_helpers.go +++ b/pkg/cli/cmd_helpers.go @@ -17,6 +17,7 @@ limitations under the License. package cli import ( + "errors" "fmt" "os" @@ -24,9 +25,29 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v3/pkg/config/store" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +// noResolvedPluginError is returned by subcommands that require a plugin when none was resolved. +type noResolvedPluginError struct{} + +// Error implements error interface. +func (e noResolvedPluginError) Error() string { + return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file" +} + +// noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found. +type noAvailablePluginError struct { + subcommand string +} + +// Error implements error interface. +func (e noAvailablePluginError) Error() string { + return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand) +} + // cmdErr updates a cobra command to output error information when executed // or used with the help flag. func cmdErr(cmd *cobra.Command, err error) { @@ -34,12 +55,6 @@ func cmdErr(cmd *cobra.Command, err error) { cmd.RunE = errCmdFunc(err) } -// cmdErrNoHelp calls cmdErr(cmd, err) then turns cmd's usage off. -func cmdErrNoHelp(cmd *cobra.Command, err error) { - cmdErr(cmd, err) - cmd.SilenceUsage = true -} - // errCmdFunc returns a cobra RunE function that returns the provided error func errCmdFunc(err error) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { @@ -47,39 +62,208 @@ func errCmdFunc(err error) func(*cobra.Command, []string) error { } } -// preRunECmdFunc returns a cobra PreRunE function that loads the configuration file -// and injects it into the subcommand -func preRunECmdFunc(subcmd plugin.Subcommand, cfg store.Store, msg string) func(*cobra.Command, []string) error { +// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins. +func (c cli) filterSubcommands( + filter func(plugin.Plugin) bool, + extract func(plugin.Plugin) plugin.Subcommand, +) ([]string, []plugin.Subcommand) { + pluginKeys := make([]string, 0, len(c.resolvedPlugins)) + subcommands := make([]plugin.Subcommand, 0, len(c.resolvedPlugins)) + for _, p := range c.resolvedPlugins { + if filter(p) { + pluginKeys = append(pluginKeys, plugin.KeyFor(p)) + subcommands = append(subcommands, extract(p)) + } + } + return pluginKeys, subcommands +} + +// initializationMethods +func (c cli) initializationMethods(cmd *cobra.Command, subcommands []plugin.Subcommand) *ResourceOptions { + // Update metadata method. + meta := plugin.SubcommandMetadata{ + Description: cmd.Long, + Examples: cmd.Example, + } + for _, subcommand := range subcommands { + if subcmd, updatesMetadata := subcommand.(plugin.UpdatesMetadata); updatesMetadata { + subcmd.UpdateMetadata(c.metadata(), &meta) + } + } + cmd.Long = meta.Description + cmd.Example = meta.Examples + + // Before binding specific plugin flags, bind common ones + requiresResource := false + for _, subcommand := range subcommands { + if _, requiresResource = subcommand.(plugin.RequiresResource); requiresResource { + break + } + } + var options *ResourceOptions + if requiresResource { + options = bindResourceFlags(cmd.Flags()) + } + + // Bind flags method. + for _, subcommand := range subcommands { + if subcmd, hasFlags := subcommand.(plugin.HasFlags); hasFlags { + subcmd.BindFlags(cmd.Flags()) + } + } + + return options +} + +// executionMethodsFuncs returns cobra RunE functions for PreRunE, RunE, PostRunE cobra hooks. +func (c cli) executionMethodsFuncs( + pluginKeys []string, + subcommands []plugin.Subcommand, + options *ResourceOptions, + msg string, +) ( + func(*cobra.Command, []string) error, + func(*cobra.Command, []string) error, + func(*cobra.Command, []string) error, +) { + cfg := yamlstore.New(c.fs) + return executionMethodsPreRunEFunc(pluginKeys, subcommands, cfg, options, c.fs, msg), + executionMethodsRunEFunc(pluginKeys, subcommands, c.fs, msg), + executionMethodsPostRunEFunc(pluginKeys, subcommands, cfg, msg) +} + +// executionMethodsPreRunEFunc returns a cobra RunE function that loads the configuration +// and executes inject config, inject resource and pre-scaffold methods. +func executionMethodsPreRunEFunc( + pluginKeys []string, + subcommands []plugin.Subcommand, + cfg store.Store, + options *ResourceOptions, + fs afero.Fs, + msg string, +) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - err := cfg.Load() - if os.IsNotExist(err) { + if err := cfg.Load(); os.IsNotExist(err) { return fmt.Errorf("%s: unable to find configuration file, project must be initialized", msg) } else if err != nil { return fmt.Errorf("%s: unable to load configuration file: %w", msg, err) } - subcmd.InjectConfig(cfg.Config()) + var res *resource.Resource + if options != nil { + options.Domain = cfg.Config().GetDomain() // TODO: offer a flag instead of hard-coding project-wide domain + if err := options.Validate(); err != nil { + return fmt.Errorf("%s: unable to create resource: %w", msg, err) + } + res = options.NewResource(cfg.Config()) + if err := res.Validate(); err != nil { + return fmt.Errorf("%s: created invalid resource: %w", msg, err) + } + } + + // Inject config method. + for i, subcommand := range subcommands { + if subcmd, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { + if err := subcmd.InjectConfig(cfg.Config()); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the configuration to %q: %w", msg, pluginKeys[i], err) + } + } + } + } + + // Inject resource method. + for i, subcommand := range subcommands { + if subcmd, requiresResource := subcommand.(plugin.RequiresResource); requiresResource { + if err := subcmd.InjectResource(res); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the resource to %q: %w", msg, pluginKeys[i], err) + } + } + } + } + + // Pre-scaffold method. + for i, subcommand := range subcommands { + if subcmd, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { + if err := subcmd.PreScaffold(fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run pre-scaffold tasks of %q: %w", msg, pluginKeys[i], err) + } + } + } + } + return nil } } -// runECmdFunc returns a cobra RunE function that runs subcommand -func runECmdFunc(fs afero.Fs, subcommand plugin.Subcommand, msg string) func(*cobra.Command, []string) error { +// executionMethodsRunEFunc returns a cobra RunE function that executes the scaffold method. +func executionMethodsRunEFunc( + pluginKeys []string, + subcommands []plugin.Subcommand, + fs afero.Fs, + msg string, +) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - if err := subcommand.Run(fs); err != nil { - return fmt.Errorf("%s: %v", msg, err) + // Scaffold method. + for i, subcommand := range subcommands { + if err := subcommand.Scaffold(fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to scaffold with %q: %v", msg, pluginKeys[i], err) + } + } } + return nil } } -// postRunECmdFunc returns a cobra PostRunE function that saves the configuration file -func postRunECmdFunc(cfg store.Store, msg string) func(*cobra.Command, []string) error { +// executionMethodsPostRunEFunc returns a cobra RunE function that executes the post-scaffold method +// and saves the configuration. +func executionMethodsPostRunEFunc( + pluginKeys []string, + subcommands []plugin.Subcommand, + cfg store.Store, + msg string, +) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { + // Post-scaffold method. + for i, subcommand := range subcommands { + if subcmd, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold { + if err := subcmd.PostScaffold(); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run post-scaffold tasks of %q: %w", msg, pluginKeys[i], err) + } + } + } + + } err := cfg.Save() if err != nil { return fmt.Errorf("%s: unable to save configuration file: %w", msg, err) } + return nil } } diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index 202ca8a7022..108f7fe5a3b 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -14,77 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const editErrorMsg = "failed to edit project" + func (c cli) newEditCmd() *cobra.Command { - ctx := c.newEditContext() cmd := &cobra.Command{ - Use: "edit", - Short: "This command will edit the project configuration", - Long: ctx.Description, - Example: ctx.Examples, + Use: "edit", + Short: "This command will edit the project configuration", + Long: `Edit the project configuration. +`, RunE: errCmdFunc( fmt.Errorf("project must be initialized"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindEdit(ctx, cmd) - return cmd -} - -func (c cli) newEditContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Edit the project configuration. -`, - } -} - -func (c cli) bindEdit(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var editPlugin plugin.Edit - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Edit) - if isValid { - if editPlugin != nil { - err := fmt.Errorf( - "duplicate edit project plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(editPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - editPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Edit. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Edit) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Edit).GetEditSubcommand() + }, + ) - if editPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project edit plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"edit project"}) + return cmd } - subcommand := editPlugin.GetEditSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, editErrorMsg) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to edit project with %q", plugin.KeyFor(editPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 4f4463b519d..f7f38ce8564 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -32,13 +32,17 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const initErrorMsg = "failed to initialize project" + func (c cli) newInitCmd() *cobra.Command { - ctx := c.newInitContext() cmd := &cobra.Command{ - Use: "init", - Short: "Initialize a new project", - Long: ctx.Description, - Example: ctx.Examples, + Use: "init", + Short: "Initialize a new project", + Long: `Initialize a new project. + +For further help about a specific project version, set --project-version. +`, + Example: c.getInitHelpExamples(), Run: func(cmd *cobra.Command, args []string) {}, } @@ -53,20 +57,90 @@ func (c cli) newInitCmd() *cobra.Command { fmt.Sprintf("Available plugins: (%s)", strings.Join(c.getAvailablePlugins(), ", "))) } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindInit(ctx, cmd) - return cmd -} + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. + if len(c.resolvedPlugins) == 0 { + cmdErr(cmd, noResolvedPluginError{}) + return cmd + } -func (c cli) newInitContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Initialize a new project. + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Init. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Init) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Init).GetInitSubcommand() + }, + ) + + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"project initialization"}) + return cmd + } -For further help about a specific project version, set --project-version. -`, - Examples: c.getInitHelpExamples(), + // Initialization methods. + _ = c.initializationMethods(cmd, subcommands) + + // Execution methods. + cfg := yamlstore.New(c.fs) + cmd.PreRunE = func(*cobra.Command, []string) error { + // Check if a config is initialized. + if err := cfg.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("%s: already initialized", initErrorMsg) + } + + err := cfg.New(c.projectVersion) + if err != nil { + return fmt.Errorf("%s: error initializing project configuration: %w", initErrorMsg, err) + } + + resolvedPluginKeys := make([]string, 0, len(c.resolvedPlugins)) + for _, p := range c.resolvedPlugins { + resolvedPluginKeys = append(resolvedPluginKeys, plugin.KeyFor(p)) + } + _ = cfg.Config().SetLayout(strings.Join(resolvedPluginKeys, ",")) + + // Inject config method. + for i, subcommand := range subcommands { + if subcmd, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { + if err := subcmd.InjectConfig(cfg.Config()); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the configuration to %q: %w", + initErrorMsg, pluginKeys[i], err) + } + } + } + } + + // Pre-scaffold method. + for i, subcommand := range subcommands { + if subcmd, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { + if err := subcmd.PreScaffold(c.fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + subcommands = append(subcommands[:i], subcommands[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run pre-scaffold tasks of %q: %w", + initErrorMsg, pluginKeys[i], err) + } + } + } + } + + return nil } + cmd.RunE = executionMethodsRunEFunc(pluginKeys, subcommands, c.fs, initErrorMsg) + cmd.PostRunE = executionMethodsPostRunEFunc(pluginKeys, subcommands, cfg, initErrorMsg) + + return cmd } func (c cli) getInitHelpExamples() string { @@ -109,55 +183,3 @@ func (c cli) getAvailablePlugins() (pluginKeys []string) { sort.Strings(pluginKeys) return pluginKeys } - -func (c cli) bindInit(ctx plugin.Context, cmd *cobra.Command) { - if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf("no resolved plugins, please specify plugins with --%s or/and --%s flags", - projectVersionFlag, pluginsFlag)) - return - } - - var initPlugin plugin.Init - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Init) - if isValid { - if initPlugin != nil { - err := fmt.Errorf("duplicate initialization plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(initPlugin), plugin.KeyFor(p)) - cmdErrNoHelp(cmd, err) - return - } - initPlugin = tmpPlugin - } - } - - if initPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project init plugin: %v", c.pluginKeys)) - return - } - - subcommand := initPlugin.GetInitSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to initialize project with %q", plugin.KeyFor(initPlugin)) - cmd.PreRunE = func(*cobra.Command, []string) error { - // Check if a config is initialized. - if err := cfg.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("%s: already initialized", msg) - } - - err := cfg.New(c.projectVersion) - if err != nil { - return fmt.Errorf("%s: error initializing project configuration: %w", msg, err) - } - - subcommand.InjectConfig(cfg.Config()) - return nil - } - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) -} diff --git a/pkg/cli/resource.go b/pkg/cli/resource.go new file mode 100644 index 00000000000..bb7c04f6616 --- /dev/null +++ b/pkg/cli/resource.go @@ -0,0 +1,93 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cli + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + groupPresent = "group flag present but empty" + versionPresent = "version flag present but empty" + kindPresent = "kind flag present but empty" + groupRequired = "group cannot be empty if the domain is empty" + versionRequired = "version cannot be empty" + kindRequired = "kind cannot be empty" +) + +// ResourceOptions contains the information required to build a new resource.Resource. +type ResourceOptions struct { + resource.GVK +} + +func bindResourceFlags(fs *pflag.FlagSet) *ResourceOptions { + options := &ResourceOptions{} + + fs.StringVar(&options.Group, "group", "", "resource Group") + fs.StringVar(&options.Version, "version", "", "resource Version") + fs.StringVar(&options.Kind, "kind", "", "resource Kind") + + return options +} + +// Validate verifies that all the fields have valid values. +func (opts ResourceOptions) Validate() error { + // Check that the required flags did not get a flag as their value. + // We can safely look for a '-' as the first char as none of the fields accepts it. + // NOTE: We must do this for all the required flags first or we may output the wrong + // error as flags may seem to be missing because Cobra assigned them to another flag. + if strings.HasPrefix(opts.Group, "-") { + return fmt.Errorf(groupPresent) + } + if strings.HasPrefix(opts.Version, "-") { + return fmt.Errorf(versionPresent) + } + if strings.HasPrefix(opts.Kind, "-") { + return fmt.Errorf(kindPresent) + } + + // Now we can check that all the required flags are not empty. + if len(opts.Group) == 0 && len(opts.Domain) == 0 { + return fmt.Errorf(groupRequired) + } + if len(opts.Version) == 0 { + return fmt.Errorf(versionRequired) + } + if len(opts.Kind) == 0 { + return fmt.Errorf(kindRequired) + } + + return nil +} + +// NewResource creates a new resource from the options +func (opts ResourceOptions) NewResource(c config.Config) *resource.Resource { + return &resource.Resource{ + GVK: opts.GVK, + Plural: resource.RegularPlural(opts.Kind), + Path: resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()), + API: &resource.API{}, + Controller: false, + Webhooks: &resource.Webhooks{}, + } +} diff --git a/pkg/cli/resource_test.go b/pkg/cli/resource_test.go new file mode 100644 index 00000000000..e3766730527 --- /dev/null +++ b/pkg/cli/resource_test.go @@ -0,0 +1,209 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cli + +import ( + "path" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("ResourceOptions", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" + ) + + var ( + fullGVK = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } + noDomainGVK = resource.GVK{ + Group: group, + Version: version, + Kind: kind, + } + noGroupGVK = resource.GVK{ + Domain: domain, + Version: version, + Kind: kind, + } + ) + + Context("Validate", func() { + DescribeTable("should succeed for valid options", + func(options ResourceOptions) { Expect(options.Validate()).To(Succeed()) }, + Entry("full GVK", ResourceOptions{GVK: fullGVK}), + Entry("missing domain", ResourceOptions{GVK: noDomainGVK}), + Entry("missing group", ResourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should fail for invalid options", + func(options ResourceOptions) { Expect(options.Validate()).NotTo(Succeed()) }, + Entry("group flag captured another flag", ResourceOptions{GVK: resource.GVK{Group: "--version"}}), + Entry("version flag captured another flag", ResourceOptions{GVK: resource.GVK{Version: "--kind"}}), + Entry("kind flag captured another flag", ResourceOptions{GVK: resource.GVK{Kind: "--group"}}), + Entry("missing group and domain", ResourceOptions{GVK: resource.GVK{Version: version, Kind: kind}}), + Entry("missing version", ResourceOptions{GVK: resource.GVK{Group: group, Domain: domain, Kind: kind}}), + Entry("missing kind", ResourceOptions{GVK: resource.GVK{Group: group, Domain: domain, Version: version}}), + ) + }) + + Context("NewResource", func() { + var cfg config.Config + + BeforeEach(func() { + cfg = cfgv3.New() + _ = cfg.SetRepository("test") + }) + + DescribeTable("should succeed if the Resource is valid", + func(options ResourceOptions) { + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.API).NotTo(BeNil()) + Expect(resource.API.CRDVersion).To(Equal("")) + Expect(resource.API.Namespaced).To(BeFalse()) + Expect(resource.Controller).To(BeFalse()) + Expect(resource.Webhooks).NotTo(BeNil()) + Expect(resource.Webhooks.WebhookVersion).To(Equal("")) + Expect(resource.Webhooks.Defaulting).To(BeFalse()) + Expect(resource.Webhooks.Validation).To(BeFalse()) + Expect(resource.Webhooks.Conversion).To(BeFalse()) + } + }, + Entry("full GVK", ResourceOptions{GVK: fullGVK}), + Entry("missing domain", ResourceOptions{GVK: noDomainGVK}), + Entry("missing group", ResourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should default the Plural by pluralizing the Kind", + func(kind, plural string) { + options := ResourceOptions{GVK: resource.GVK{Group: group, Version: version, Kind: kind}} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.Plural).To(Equal(plural)) + } + }, + Entry("for `FirstMate`", "FirstMate", "firstmates"), + Entry("for `Fish`", "Fish", "fish"), + Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), + ) + + DescribeTable("should allow hyphens and dots in group names", + func(group, safeGroup string) { + options := ResourceOptions{GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + if multiGroup { + Expect(resource.Path).To(Equal( + path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) + } else { + Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + } + Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) + Expect(resource.PackageName()).To(Equal(safeGroup)) + Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) + } + }, + Entry("for hyphen-containing group", "my-project", "myproject"), + Entry("for dot-containing group", "my.project", "myproject"), + ) + + It("should not append '.' if provided an empty domain", func() { + options := ResourceOptions{GVK: noDomainGVK} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.QualifiedGroup()).To(Equal(options.Group)) + } + }) + + It("should use domain if the group is empty", func() { + safeDomain := "testio" + + options := ResourceOptions{GVK: noGroupGVK} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.Group).To(Equal("")) + if multiGroup { + Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "apis", options.Version))) + } else { + Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + } + Expect(resource.QualifiedGroup()).To(Equal(options.Domain)) + Expect(resource.PackageName()).To(Equal(safeDomain)) + Expect(resource.ImportAlias()).To(Equal(safeDomain + options.Version)) + } + }) + }) +}) diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index 443edbf3585..e06430730d3 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -14,77 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const webhookErrorMsg = "failed to create webhook" + func (c cli) newCreateWebhookCmd() *cobra.Command { - ctx := c.newWebhookContext() cmd := &cobra.Command{ - Use: "webhook", - Short: "Scaffold a webhook for an API resource", - Long: ctx.Description, - Example: ctx.Examples, + Use: "webhook", + Short: "Scaffold a webhook for an API resource", + Long: `Scaffold a webhook for an API resource. +`, RunE: errCmdFunc( fmt.Errorf("webhook subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateWebhook(ctx, cmd) - return cmd -} - -func (c cli) newWebhookContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a webhook for an API resource. -`, - } -} - -// nolint:dupl -func (c cli) bindCreateWebhook(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createWebhookPlugin plugin.CreateWebhook - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateWebhook) - if isValid { - if createWebhookPlugin != nil { - err := fmt.Errorf("duplicate webhook creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createWebhookPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createWebhookPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateWebhook. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateWebhook) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateWebhook).GetCreateWebhookSubcommand() + }, + ) - if createWebhookPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a webhook creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"webhook creation"}) + return cmd } - subcommand := createWebhookPlugin.GetCreateWebhookSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, webhookErrorMsg) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to create webhook with %q", plugin.KeyFor(createWebhookPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/plugins/golang/v2/suite_test.go b/pkg/plugin/errors.go similarity index 64% rename from pkg/plugins/golang/v2/suite_test.go rename to pkg/plugin/errors.go index de06fb4cf53..fe89538d0f2 100644 --- a/pkg/plugins/golang/v2/suite_test.go +++ b/pkg/plugin/errors.go @@ -14,16 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package plugin import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "fmt" ) -func TestGoPluginV2(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Go Plugin v2 Suite") +// ExitError is a typed error that is returned by a plugin when no further steps should be executed for itself. +type ExitError struct { + Plugin string + Reason string +} + +// Error implements error +func (e ExitError) Error() string { + return fmt.Sprintf("plugin %s exit early: %s", e.Plugin, e.Reason) } diff --git a/pkg/plugin/metadata.go b/pkg/plugin/metadata.go new file mode 100644 index 00000000000..5a83d8bdf2c --- /dev/null +++ b/pkg/plugin/metadata.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +// CLIMetadata is the runtime meta-data of the CLI +type CLIMetadata struct { + // CommandName is the root command name. + CommandName string +} + +// SubcommandMetadata is the runtime meta-data for a subcommand +type SubcommandMetadata struct { + // Description is a description of what this command does. It is used to display help. + Description string + // Examples are one or more examples of the command-line usage of this command. It is used to display help. + Examples string +} diff --git a/pkg/plugin/interfaces.go b/pkg/plugin/plugin.go similarity index 58% rename from pkg/plugin/interfaces.go rename to pkg/plugin/plugin.go index 6957c104c1e..d8a04ee8f02 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/plugin.go @@ -17,9 +17,6 @@ limitations under the License. package plugin import ( - "github.com/spf13/afero" - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" ) @@ -44,33 +41,6 @@ type Deprecated interface { DeprecationWarning() string } -// Subcommand is an interface that defines the common base for subcommands returned by plugins -type Subcommand interface { - // UpdateContext updates a Context with subcommand-specific help text, like description and examples. It also serves - // to pass context from the CLI to the subcommand, such as the command name. - // Can be a no-op if default help text is desired. - UpdateContext(*Context) - // BindFlags binds the subcommand's flags to the CLI. This allows each subcommand to define its own - // command line flags. - // NOTE(Adirio): by the time we bind flags, the config hasn't been injected, trying to use it panics - BindFlags(*pflag.FlagSet) - // InjectConfig passes a config to a plugin. The plugin may modify the config. - // Initializing, loading, and saving the config is managed by the cli package. - InjectConfig(config.Config) - // Run runs the subcommand. - Run(fs afero.Fs) error -} - -// Context is the runtime context for a subcommand. -type Context struct { - // CommandName sets the command name for a subcommand. - CommandName string - // Description is a description of what this subcommand does. It is used to display help. - Description string - // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. - Examples string -} - // Init is an interface for plugins that provide an `init` subcommand type Init interface { Plugin @@ -78,11 +48,6 @@ type Init interface { GetInitSubcommand() InitSubcommand } -// InitSubcommand is an interface that represents an `init` subcommand -type InitSubcommand interface { - Subcommand -} - // CreateAPI is an interface for plugins that provide a `create api` subcommand type CreateAPI interface { Plugin @@ -90,11 +55,6 @@ type CreateAPI interface { GetCreateAPISubcommand() CreateAPISubcommand } -// CreateAPISubcommand is an interface that represents a `create api` subcommand -type CreateAPISubcommand interface { - Subcommand -} - // CreateWebhook is an interface for plugins that provide a `create webhook` subcommand type CreateWebhook interface { Plugin @@ -102,11 +62,6 @@ type CreateWebhook interface { GetCreateWebhookSubcommand() CreateWebhookSubcommand } -// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand -type CreateWebhookSubcommand interface { - Subcommand -} - // Edit is an interface for plugins that provide a `edit` subcommand type Edit interface { Plugin @@ -114,11 +69,6 @@ type Edit interface { GetEditSubcommand() EditSubcommand } -// EditSubcommand is an interface that represents an `edit` subcommand -type EditSubcommand interface { - Subcommand -} - // Full is an interface for plugins that provide `init`, `create api`, `create webhook` and `edit` subcommands type Full interface { Init diff --git a/pkg/plugin/subcommand.go b/pkg/plugin/subcommand.go new file mode 100644 index 00000000000..483c018d3e0 --- /dev/null +++ b/pkg/plugin/subcommand.go @@ -0,0 +1,94 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "github.com/spf13/afero" + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// UpdatesMetadata is an interface that implements the optional metadata update method. +type UpdatesMetadata interface { + // UpdateMetadata updates the subcommand metadata. + UpdateMetadata(CLIMetadata, *SubcommandMetadata) +} + +// HasFlags is an interface that implements the optional bind flags method. +type HasFlags interface { + // BindFlags binds flags to the CLI subcommand. + BindFlags(*pflag.FlagSet) +} + +// RequiresConfig is an interface that implements the optional inject config method. +type RequiresConfig interface { + // InjectConfig injects the configuration to a subcommand. + InjectConfig(config.Config) error +} + +// RequiresResource is an interface that implements the required inject resource method. +type RequiresResource interface { + // InjectResource injects the resource model to a subcommand. + InjectResource(*resource.Resource) error +} + +// HasPreScaffold is an interface that implements the optional pre-scaffold method. +type HasPreScaffold interface { + // PreScaffold executes tasks before the main scaffolding. + PreScaffold(afero.Fs) error +} + +// Scaffolder is an interface that implements the required scaffold method. +type Scaffolder interface { + // Scaffold implements the main scaffolding. + Scaffold(afero.Fs) error +} + +// HasPostScaffold is an interface that implements the optional post-scaffold method. +type HasPostScaffold interface { + // PostScaffold executes tasks after the main scaffolding. + PostScaffold() error +} + +// Subcommand is a base interface for all subcommands. +type Subcommand interface { + Scaffolder +} + +// InitSubcommand is an interface that represents an `init` subcommand. +type InitSubcommand interface { + Subcommand +} + +// CreateAPISubcommand is an interface that represents a `create api` subcommand. +type CreateAPISubcommand interface { + Subcommand + RequiresResource +} + +// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand. +type CreateWebhookSubcommand interface { + Subcommand + RequiresResource +} + +// EditSubcommand is an interface that represents an `edit` subcommand. +type EditSubcommand interface { + Subcommand +} diff --git a/pkg/plugin/version_test.go b/pkg/plugin/version_test.go index f1512dbf7d8..00f72ee58b7 100644 --- a/pkg/plugin/version_test.go +++ b/pkg/plugin/version_test.go @@ -20,7 +20,7 @@ import ( "sort" "testing" - g "github.com/onsi/ginkgo" // An alias is required because Context is defined elsewhere in this package. + . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" @@ -28,12 +28,12 @@ import ( ) func TestPlugin(t *testing.T) { - RegisterFailHandler(g.Fail) - g.RunSpecs(t, "Plugin Suite") + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Suite") } -var _ = g.Describe("Version", func() { - g.Context("Parse", func() { +var _ = Describe("Version", func() { + Context("Parse", func() { DescribeTable("should be correctly parsed for valid version strings", func(str string, number int, s stage.Stage) { var v Version @@ -72,7 +72,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("String", func() { + Context("String", func() { DescribeTable("should return the correct string value", func(version Version, str string) { Expect(version.String()).To(Equal(str)) }, Entry("for version 0", Version{Number: 0}, "v0"), @@ -94,7 +94,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Validate", func() { + Context("Validate", func() { DescribeTable("should validate valid versions", func(version Version) { Expect(version.Validate()).To(Succeed()) }, Entry("for version 0", Version{Number: 0}), @@ -125,7 +125,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Compare", func() { + Context("Compare", func() { // Test Compare() by sorting a list. var ( versions = []Version{ @@ -155,7 +155,7 @@ var _ = g.Describe("Version", func() { } ) - g.It("sorts a valid list of versions correctly", func() { + It("sorts a valid list of versions correctly", func() { sort.Slice(versions, func(i int, j int) bool { return versions[i].Compare(versions[j]) == -1 }) @@ -164,7 +164,7 @@ var _ = g.Describe("Version", func() { }) - g.Context("IsStable", func() { + Context("IsStable", func() { DescribeTable("should return true for stable versions", func(version Version) { Expect(version.IsStable()).To(BeTrue()) }, Entry("for version 1", Version{Number: 1}), @@ -189,4 +189,5 @@ var _ = g.Describe("Version", func() { Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) }) + }) diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index ab2a2e0c96b..f5b91a6c2e5 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -17,23 +17,12 @@ limitations under the License. package golang import ( - "fmt" "path" - "strings" - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty if the domain is empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - var ( coreGroups = map[string]string{ "admission": "k8s.io", @@ -64,17 +53,7 @@ var ( // Options contains the information required to build a new resource.Resource. type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - // Plural is the resource's kind plural form. - // Optional Plural string // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. @@ -95,57 +74,13 @@ type Options struct { // Validate verifies that all the fields have valid values func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 && len(opts.Domain) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - return nil } -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - // NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Path: resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()), - Controller: opts.DoController, - } - +func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { if opts.Plural != "" { res.Plural = opts.Plural - } else { - // If not provided, compute a plural for Kind - res.Plural = resource.RegularPlural(opts.Kind) } if opts.DoAPI { @@ -153,21 +88,23 @@ func (opts Options) NewResource(c newconfig.Config) resource.Resource { CRDVersion: opts.CRDVersion, Namespaced: opts.Namespaced, } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} + } + + if opts.DoController { + res.Controller = true } if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, + res.Webhooks.WebhookVersion = opts.WebhookVersion + if opts.DoDefaulting { + res.Webhooks.Defaulting = true + } + if opts.DoValidation { + res.Webhooks.Validation = true + } + if opts.DoConversion { + res.Webhooks.Conversion = true } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} } // domain and path may need to be changed in case we are referring to a builtin core resource: @@ -177,13 +114,11 @@ func (opts Options) NewResource(c newconfig.Config) resource.Resource { // - In any other case, default to => project resource // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath if !opts.DoAPI { - if !c.HasResource(opts.GVK()) { - if domain, found := coreGroups[opts.Group]; found { + if !c.HasResource(res.GVK) { + if domain, found := coreGroups[res.Group]; found { res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) + res.Path = path.Join("k8s.io", "api", res.Group, res.Version) } } } - - return res } diff --git a/pkg/plugins/golang/options_test.go b/pkg/plugins/golang/options_test.go index 983313fb111..618e08815aa 100644 --- a/pkg/plugins/golang/options_test.go +++ b/pkg/plugins/golang/options_test.go @@ -25,37 +25,42 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) var _ = Describe("Options", func() { Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - ) + // There is no validation so it should succeed for any Options, if some + // validation is added add the corresponding tests. + It("should always succeed", func() { + Expect(Options{}.Validate()).To(Succeed()) + }) + }) - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group and domain", Options{Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), + Context("UpdateResource", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" ) - }) + var ( + gvk = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } - Context("NewResource", func() { - var cfg config.Config + cfg config.Config + ) BeforeEach(func() { cfg = cfgv3.New() _ = cfg.SetRepository("test") }) - DescribeTable("should succeed if the Resource is valid", + DescribeTable("should succeed", func(options Options) { Expect(options.Validate()).To(Succeed()) @@ -64,159 +69,46 @@ var _ = Describe("Options", func() { Expect(cfg.SetMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + res := resource.Resource{ + GVK: gvk, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, } - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) - } - }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - WebhookVersion: "v1", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - }), - ) - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + Expect(res.GVK.IsEqualTo(gvk)).To(BeTrue()) - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) + if options.Plural != "" { + Expect(res.Plural).To(Equal(options.Plural)) } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) + Expect(res.API).NotTo(BeNil()) + if options.DoAPI { + Expect(res.API.CRDVersion).To(Equal(options.CRDVersion)) + Expect(res.API.Namespaced).To(Equal(options.Namespaced)) } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), - ) - - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + Expect(res.Controller).To(Equal(options.DoController)) + Expect(res.Webhooks).NotTo(BeNil()) + if options.DoDefaulting || options.DoValidation || options.DoConversion { + Expect(res.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) + Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) + Expect(res.Webhooks.Validation).To(Equal(options.DoValidation)) + Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion)) } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) } }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), + Entry("when updating nothing", Options{}), + Entry("when updating the plural", Options{Plural: "mates"}), + Entry("when updating the API", Options{DoAPI: true, CRDVersion: "v1", Namespaced: true}), + Entry("when updating the Controller", Options{DoController: true}), + Entry("when updating Webhooks", + Options{WebhookVersion: "v1", DoDefaulting: true, DoValidation: true, DoConversion: true}), ) - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) - DescribeTable("should use core apis", func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } + options := Options{} Expect(options.Validate()).To(Succeed()) for _, multiGroup := range []bool{false, true} { @@ -224,44 +116,28 @@ var _ = Describe("Options", func() { Expect(cfg.SetMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API.CRDVersion).To(Equal("")) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) + res := resource.Resource{ + GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + } + + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + + Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version))) + Expect(res.HasAPI()).To(BeFalse()) + Expect(res.QualifiedGroup()).To(Equal(qualified)) } }, Entry("for `apps`", "apps", "apps"), Entry("for `authentication`", "authentication", "authentication.k8s.io"), ) - - It("should use domain if the group is empty", func() { - safeDomain := "testio" - - options := Options{ - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal("")) - if multiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "apis", options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Domain)) - Expect(resource.PackageName()).To(Equal(safeDomain)) - Expect(resource.ImportAlias()).To(Equal(safeDomain + options.Version)) - } - }) }) }) diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index 997c9af174f..0517ce31ffd 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -30,21 +30,23 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config // pattern indicates that we should use a plugin to build according to a pattern pattern string - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -57,13 +59,8 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only scaffold a Controller for an existing Resource, select "n" for Resource. To only define @@ -71,7 +68,7 @@ the schema for a Resource without writing a Controller, select "n" for Controlle After the scaffold is written, api will run make on the project. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate %s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme @@ -88,8 +85,7 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run - `, - ctx.CommandName) + `, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { @@ -103,16 +99,11 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - p.options = &Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") - // p.options.Plural can be set to specify an irregular plural form + p.options = &goPlugin.Options{CRDVersion: "v1beta1"} fs.BoolVar(&p.options.DoAPI, "resource", true, "if set, generate the resource without prompting the user") p.resourceFlag = fs.Lookup("resource") - p.options.CRDVersion = "v1beta1" fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") fs.BoolVar(&p.options.DoController, "controller", true, @@ -120,13 +111,19 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c - p.options.Domain = c.GetDomain() + return nil } -func (p *createAPISubcommand) Run(fs afero.Fs) error { +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") + } + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { @@ -138,17 +135,12 @@ func (p *createAPISubcommand) Run(fs afero.Fs) error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} - -func (p *createAPISubcommand) Validate() error { if err := p.options.Validate(); err != nil { return err } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } @@ -170,7 +162,7 @@ func (p *createAPISubcommand) Validate() error { return nil } -func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { // Load the requested plugins plugins := make([]model.Plugin, 0) switch strings.ToLower(p.pattern) { @@ -179,10 +171,12 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { case "addon": plugins = append(plugins, &addon.Plugin{}) default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) + return fmt.Errorf("unknown pattern %q", p.pattern) } - return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force, plugins) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *createAPISubcommand) PostScaffold() error { @@ -201,8 +195,9 @@ func (p *createAPISubcommand) PostScaffold() error { return fmt.Errorf("unknown pattern %q", p.pattern) } - if p.runMake { // TODO: check if API was scaffolded + if p.runMake && p.resource.HasAPI() { return util.RunCmd("Running make", "make", "generate") } + return nil } diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go index 505abc2497b..988588082bc 100644 --- a/pkg/plugins/golang/v2/edit.go +++ b/pkg/plugins/golang/v2/edit.go @@ -25,51 +25,41 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %s edit --multigroup +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(`# Enable the multigroup layout + %[1]s edit --multigroup # Disable the multigroup layout - %s edit --multigroup=false - `, ctx.CommandName, ctx.CommandName) + %[1]s edit --multigroup=false + `, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run(fs afero.Fs) error { - return cmdutil.Run(p, fs) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index 62fe6f97326..0bec8b162b0 100644 --- a/pkg/plugins/golang/v2/init.go +++ b/pkg/plugins/golang/v2/init.go @@ -30,10 +30,11 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config @@ -54,30 +55,24 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. + subcmdMeta.Description = `Initialize a new project including vendor/ directory and Go package directories. Writes the following files: - a boilerplate license file - a PROJECT file with the domain and repo - a Makefile to build the project - a go.mod with project dependencies -- a Kustomization.yaml for customizating manifests +- a Kustomization.yaml for customizing manifests - a Patch file for customizing image for manager manifests - a Patch file for enabling prometheus metrics - a main.go to run ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, - ctx.CommandName) - - p.commandName = ctx.CommandName + subcmdMeta.Examples = fmt.Sprintf(`# Scaffold a project using the apache2 license with %[2]q as owners + %[1]s init --project-version=2 --domain example.org --license apache2 --owner %[2]q +`, cliMeta.CommandName, "The Kubernetes authors") } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -99,25 +94,23 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.name, "project-name", "", "name of this project") } -func (p *initSubcommand) InjectConfig(c config.Config) { - // v2+ project configs get a 'layout' value. - if c.GetVersion().Compare(cfgv2.Version) > 0 { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) - } - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run(fs afero.Fs) error { - return cmdutil.Run(p, fs) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := util.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := util.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath + } + if err := p.config.SetRepository(p.repo); err != nil { + return err } if p.config.GetVersion().Compare(cfgv2.Version) > 0 { @@ -133,34 +126,29 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } - } - - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := util.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if err := p.config.SetProjectName(p.name); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if p.config.GetVersion().Compare(cfgv2.Version) > 0 { - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(afero.Fs) error { + // Requires go1.11+ + if !p.skipGoVersionCheck { + if err := util.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + return nil +} + +func (p *initSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *initSubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v2/options.go b/pkg/plugins/golang/v2/options.go deleted file mode 100644 index 4b3613fe295..00000000000 --- a/pkg/plugins/golang/v2/options.go +++ /dev/null @@ -1,189 +0,0 @@ -/* -Copyright 2021 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v2 - -import ( - "fmt" - "path" - "strings" - - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - -var ( - coreGroups = map[string]string{ - "admission": "k8s.io", - "admissionregistration": "k8s.io", - "apps": "", - "auditregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "authorization": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "coordination": "k8s.io", - "core": "", - "events": "k8s.io", - "extensions": "", - "imagepolicy": "k8s.io", - "networking": "k8s.io", - "node": "k8s.io", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "scheduling": "k8s.io", - "setting": "k8s.io", - "storage": "k8s.io", - } -) - -// Options contains the information required to build a new resource.Resource. -type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - - // Plural is the resource's kind plural form. - // Optional - Plural string - - // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. - CRDVersion string - // WebhookVersion is the {Validating,Mutating}WebhookConfiguration API version that will be used for the resource. - WebhookVersion string - - // Namespaced is true if the resource should be namespaced. - Namespaced bool - - // Flags that define which parts should be scaffolded - DoAPI bool - DoController bool - DoDefaulting bool - DoValidation bool - DoConversion bool -} - -// Validate verifies that all the fields have valid values -func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - return nil -} - -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - -// NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Path: resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()), - Controller: opts.DoController, - } - - if opts.Plural != "" { - res.Plural = opts.Plural - } else { - // If not provided, compute a plural for for Kind - res.Plural = resource.RegularPlural(opts.Kind) - } - - if opts.DoAPI { - res.API = &resource.API{ - CRDVersion: opts.CRDVersion, - Namespaced: opts.Namespaced, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} - } - - if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} - } - - // domain and path may need to be changed in case we are referring to a builtin core resource: - // - Check if we are scaffolding the resource now => project resource - // - Check if we already scaffolded the resource => project resource - // - Check if the resource group is a well-known core group => builtin core resource - // - In any other case, default to => project resource - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - if !opts.DoAPI { - if !c.HasResource(opts.GVK()) { - if domain, found := coreGroups[opts.Group]; found { - res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) - } - } - } - - return res -} diff --git a/pkg/plugins/golang/v2/options_test.go b/pkg/plugins/golang/v2/options_test.go deleted file mode 100644 index e4997e684a7..00000000000 --- a/pkg/plugins/golang/v2/options_test.go +++ /dev/null @@ -1,237 +0,0 @@ -/* -Copyright 2021 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package v2 - -import ( - "path" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" -) - -var _ = Describe("Options", func() { - Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - ) - - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), - ) - }) - - Context("NewResource", func() { - var cfg config.Config - - BeforeEach(func() { - cfg = cfgv2.New() - _ = cfg.SetRepository("test") - }) - - DescribeTable("should succeed if the Resource is valid", - func(options Options) { - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) - } - }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1beta1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - WebhookVersion: "v1beta1", - }), - ) - - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), - ) - - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), - ) - - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) - - DescribeTable("should use core apis", - func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API.CRDVersion).To(Equal("")) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) - } - }, - Entry("for `apps`", "apps", "apps"), - Entry("for `authentication`", "authentication", "authentication.k8s.io"), - ) - }) -}) diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index 0ce7176f8ab..cebc22e28fa 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -25,49 +25,42 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { config config.Config // For help text. commandName string - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, validating and (or) conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 # and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation # Create conversion webhook for CRD of group shio, version v1beta1 and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, - ctx.CommandName, ctx.CommandName) - - p.commandName = ctx.CommandName + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion +`, cliMeta.CommandName) } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { - p.options = &Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + p.options = &goPlugin.Options{WebhookVersion: "v1beta1"} + fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form") - p.options.WebhookVersion = "v1beta1" fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, @@ -76,24 +69,25 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "if set, scaffold the conversion webhook") } -func (p *createWebhookSubcommand) InjectConfig(c config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c - p.options.Domain = c.GetDomain() + return nil } -func (p *createWebhookSubcommand) Run(fs afero.Fs) error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res - return cmdutil.Run(p, fs) -} + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") + } -func (p *createWebhookSubcommand) Validate() error { if err := p.options.Validate(); err != nil { return err } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } @@ -111,10 +105,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewWebhookScaffolder(p.config, p.resource), nil -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index 48525c68699..b959e322128 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -32,7 +32,6 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) @@ -50,6 +49,8 @@ const ( // DefaultMainPath is default file path of main.go const DefaultMainPath = "main.go" +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config @@ -58,7 +59,7 @@ type createAPISubcommand struct { options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -71,13 +72,8 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only scaffold a Controller for an existing Resource, select "n" for Resource. To only define @@ -85,7 +81,7 @@ the schema for a Resource without writing a Controller, select "n" for Controlle After the scaffold is written, api will run make on the project. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate %s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme @@ -102,8 +98,7 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run - `, - ctx.CommandName) + `, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { @@ -119,9 +114,7 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { "attempt to create resource even if it already exists") p.options = &goPlugin.Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") fs.BoolVar(&p.options.DoAPI, "resource", true, @@ -136,16 +129,18 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c - // TODO: offer a flag instead of hard-coding the project-wide domain - p.options.Domain = c.GetDomain() + return nil } -func (p *createAPISubcommand) Run(fs afero.Fs) error { +// TODO: inject the resource instead of creating it here +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + // TODO: re-evaluate whether y/n input still makes sense. We should probably always - // scaffold the resource and controller. + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") @@ -156,30 +151,20 @@ func (p *createAPISubcommand) Run(fs afero.Fs) error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} - -func (p *createAPISubcommand) Validate() error { if err := p.options.Validate(); err != nil { return err } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } - // check if main.go is present in the root directory - if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { - return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) - } - // In case we want to scaffold a resource API we need to do some checks if p.resource.HasAPI() { // Check that resource doesn't exist or flag force was set - if res, err := p.config.GetResource(p.resource.GVK); err == nil && res.HasAPI() && !p.force { + if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force { return errors.New("API resource already exists") } @@ -199,7 +184,16 @@ func (p *createAPISubcommand) Validate() error { return nil } -func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { +func (p *createAPISubcommand) PreScaffold(afero.Fs) error { + // check if main.go is present in the root directory + if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { + return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) + } + + return nil +} + +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { // Load the requested plugins plugins := make([]model.Plugin, 0) switch strings.ToLower(p.pattern) { @@ -208,10 +202,12 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { case "addon": plugins = append(plugins, &addon.Plugin{}) default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) + return fmt.Errorf("unknown pattern %q", p.pattern) } - return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force, plugins) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *createAPISubcommand) PostScaffold() error { @@ -231,8 +227,9 @@ func (p *createAPISubcommand) PostScaffold() error { return fmt.Errorf("unknown pattern %q", p.pattern) } - if p.runMake { // TODO: check if API was scaffolded + if p.runMake && p.resource.HasAPI() { return util.RunCmd("Running make", "make", "generate") } + return nil } diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go index 99aa054e568..cdec7bdc75f 100644 --- a/pkg/plugins/golang/v3/edit.go +++ b/pkg/plugins/golang/v3/edit.go @@ -25,51 +25,41 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %s edit --multigroup +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(`# Enable the multigroup layout + %[1]s edit --multigroup # Disable the multigroup layout - %s edit --multigroup=false - `, ctx.CommandName, ctx.CommandName) + %[1]s edit --multigroup=false + `, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run(fs afero.Fs) error { - return cmdutil.Run(p, fs) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index 739d2ad9330..59e33be0eaf 100644 --- a/pkg/plugins/golang/v3/init.go +++ b/pkg/plugins/golang/v3/init.go @@ -29,10 +29,11 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config // For help text. @@ -53,30 +54,24 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. + subcmdMeta.Description = `Initialize a new project including vendor/ directory and Go package directories. Writes the following files: - a boilerplate license file - a PROJECT file with the domain and repo - a Makefile to build the project - a go.mod with project dependencies -- a Kustomization.yaml for customizating manifests +- a Kustomization.yaml for customizing manifests - a Patch file for customizing image for manager manifests - a Patch file for enabling prometheus metrics - a main.go to run ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, - ctx.CommandName) - - p.commandName = ctx.CommandName + subcmdMeta.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with %[2]q as owners + %[1]s init --project-version=3-alpha --domain example.org --license apache2 --owner %[2]q +`, cliMeta.CommandName, "The Kubernetes authors") } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -100,26 +95,22 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { "create a versioned ComponentConfig file, may be 'true' or 'false'") } -func (p *initSubcommand) InjectConfig(c config.Config) { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run(fs afero.Fs) error { - return cmdutil.Run(p, fs) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := util.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := util.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath } - - // Check if the current directory has not files or directories which does not allow to init the project - if err := checkDir(); err != nil { + if err := p.config.SetRepository(p.repo); err != nil { return err } @@ -135,36 +126,39 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } + if err := p.config.SetProjectName(p.name); err != nil { + return err + } - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := util.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if p.componentConfig { + if err := p.config.SetComponentConfig(); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err - } - if p.componentConfig { - if err := p.config.SetComponentConfig(); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(afero.Fs) error { + // Requires go1.11+ + if !p.skipGoVersionCheck { + if err := util.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + // Check if the current directory has not files or directories which does not allow to init the project + if err := checkDir(); err != nil { + return err + } + + return nil +} + +func (p *initSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *initSubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 931d2eca51b..2c03fe92b65 100644 --- a/pkg/plugins/golang/v3/webhook.go +++ b/pkg/plugins/golang/v3/webhook.go @@ -28,12 +28,13 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) // defaultWebhookVersion is the default mutating/validating webhook config API version to scaffold. const defaultWebhookVersion = "v1" +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { config config.Config // For help text. @@ -41,38 +42,30 @@ type createWebhookSubcommand struct { options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // force indicates that the resource should be created even if it already exists force bool } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, validating and (or) conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 # and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation # Create conversion webhook for CRD of group ship, version v1beta1 and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, - ctx.CommandName, ctx.CommandName) - - p.commandName = ctx.CommandName + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion +`, cliMeta.CommandName) } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &goPlugin.Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion, @@ -88,25 +81,21 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "attempt to create resource even if it already exists") } -func (p *createWebhookSubcommand) InjectConfig(c config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c - // TODO: offer a flag instead of hard-coding the project-wide domain - p.options.Domain = c.GetDomain() + return nil } -func (p *createWebhookSubcommand) Run(fs afero.Fs) error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res - return cmdutil.Run(p, fs) -} - -func (p *createWebhookSubcommand) Validate() error { if err := p.options.Validate(); err != nil { return err } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } @@ -131,10 +120,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewWebhookScaffolder(p.config, p.resource, p.force), nil -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/internal/cmdutil/cmdutil.go b/pkg/plugins/internal/cmdutil/cmdutil.go index 8c591cedc98..e7538e6e79f 100644 --- a/pkg/plugins/internal/cmdutil/cmdutil.go +++ b/pkg/plugins/internal/cmdutil/cmdutil.go @@ -26,43 +26,3 @@ type Scaffolder interface { // Scaffold performs the scaffolding Scaffold() error } - -// RunOptions represent the types used to implement the different commands -type RunOptions interface { - // - Step 1: verify that the command can be run (e.g., go version, project version, arguments, ...). - Validate() error - // - Step 2: create the Scaffolder instance. - GetScaffolder() (Scaffolder, error) - // - Step 3: inject the filesystem into the Scaffolder instance. Doesn't need any method. - // - Step 4: call the Scaffold method of the Scaffolder instance. Doesn't need any method. - // - Step 5: finish the command execution. - PostScaffold() error -} - -// Run executes a command -func Run(options RunOptions, fs afero.Fs) error { - // Step 1: validate - if err := options.Validate(); err != nil { - return err - } - - // Step 2: get scaffolder - scaffolder, err := options.GetScaffolder() - if err != nil { - return err - } - // Step 3: inject filesystem - scaffolder.InjectFS(fs) - // Step 4: scaffold - if scaffolder != nil { - if err := scaffolder.Scaffold(); err != nil { - return err - } - } - // Step 5: finish - if err := options.PostScaffold(); err != nil { - return err - } - - return nil -}