Skip to content

Commit c577244

Browse files
authored
Merge pull request #1937 from Adirio/enhance-root-cmd
✨ Build the command and use it to report user errors
2 parents 1001d36 + 84b6f53 commit c577244

File tree

4 files changed

+239
-123
lines changed

4 files changed

+239
-123
lines changed

pkg/cli/cli.go

Lines changed: 83 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package cli
1818

1919
import (
2020
"fmt"
21-
"log"
2221
"os"
2322
"strings"
2423

@@ -95,51 +94,33 @@ type cli struct { //nolint:maligned
9594
// A filtered set of plugins that should be used by command constructors.
9695
resolvedPlugins []plugin.Plugin
9796

98-
// Whether some generic help should be printed, i.e. if the binary
99-
// was invoked outside of a project with incorrect flags or -h|--help.
100-
doHelp bool
101-
10297
// Root command.
10398
cmd *cobra.Command
10499
}
105100

106101
// New creates a new cli instance.
102+
// Developer errors (e.g. not registering any plugins, extra commands with conflicting names) return an error
103+
// while user errors (e.g. errors while parsing flags, unresolvable plugins) create a command which return the error.
107104
func New(opts ...Option) (CLI, error) {
108105
// Create the CLI.
109106
c, err := newCLI(opts...)
110107
if err != nil {
111108
return nil, err
112109
}
113110

114-
// Get project version and plugin keys.
115-
if err := c.getInfo(); err != nil {
116-
return nil, err
117-
}
118-
119-
// Resolve plugins for project version and plugin keys.
120-
if err := c.resolve(); err != nil {
121-
return nil, err
111+
// Build the cmd tree.
112+
if err := c.buildCmd(); err != nil {
113+
c.cmd.RunE = errCmdFunc(err)
114+
return c, nil
122115
}
123116

124-
// Build the root command.
125-
c.cmd = c.buildRootCmd()
126-
127117
// Add extra commands injected by options.
128-
for _, cmd := range c.extraCommands {
129-
for _, subCmd := range c.cmd.Commands() {
130-
if cmd.Name() == subCmd.Name() {
131-
return nil, fmt.Errorf("command %q already exists", cmd.Name())
132-
}
133-
}
134-
c.cmd.AddCommand(cmd)
118+
if err := c.addExtraCommands(); err != nil {
119+
return nil, err
135120
}
136121

137122
// Write deprecation notices after all commands have been constructed.
138-
for _, p := range c.resolvedPlugins {
139-
if d, isDeprecated := p.(plugin.Deprecated); isDeprecated {
140-
fmt.Printf(noticeColor, fmt.Sprintf(deprecationFmt, d.DeprecationWarning()))
141-
}
142-
}
123+
c.printDeprecationWarnings()
143124

144125
return c, nil
145126
}
@@ -166,37 +147,47 @@ func newCLI(opts ...Option) (*cli, error) {
166147
}
167148

168149
// getInfoFromFlags obtains the project version and plugin keys from flags.
169-
func (c *cli) getInfoFromFlags() (string, []string) {
150+
func (c *cli) getInfoFromFlags() (string, []string, error) {
170151
// Partially parse the command line arguments
171-
fs := pflag.NewFlagSet("base", pflag.ExitOnError)
152+
fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
153+
154+
// Load the base command global flags
155+
fs.AddFlagSet(c.cmd.PersistentFlags())
172156

173157
// Omit unknown flags to avoid parsing errors
174158
fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
175159

176-
// Define the flags needed for plugin resolution
177-
var projectVersion, plugins string
178-
var help bool
179-
fs.StringVar(&projectVersion, projectVersionFlag, "", "project version")
180-
fs.StringVar(&plugins, pluginsFlag, "", "plugins to run")
181-
fs.BoolVarP(&help, "help", "h", false, "help flag")
160+
// FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default
161+
// behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command.
162+
fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName))
182163

183164
// Parse the arguments
184-
err := fs.Parse(os.Args[1:])
165+
if err := fs.Parse(os.Args[1:]); err != nil {
166+
return "", []string{}, err
167+
}
185168

186-
// User needs *generic* help if args are incorrect or --help is set and
187-
// --project-version is not set. Plugin-specific help is given if a
188-
// plugin.Context is updated, which does not require this field.
189-
c.doHelp = err != nil || help && !fs.Lookup(projectVersionFlag).Changed
169+
// Define the flags needed for plugin resolution
170+
var (
171+
projectVersion string
172+
plugins []string
173+
err error
174+
)
175+
// GetXxxxx methods will not yield errors because we know for certain these flags exist and types match.
176+
projectVersion, err = fs.GetString(projectVersionFlag)
177+
if err != nil {
178+
return "", []string{}, err
179+
}
180+
plugins, err = fs.GetStringSlice(pluginsFlag)
181+
if err != nil {
182+
return "", []string{}, err
183+
}
190184

191-
// Split the comma-separated plugins
192-
var pluginSet []string
193-
if plugins != "" {
194-
for _, p := range strings.Split(plugins, ",") {
195-
pluginSet = append(pluginSet, strings.TrimSpace(p))
196-
}
185+
// Remove leading and trailing spaces
186+
for i, key := range plugins {
187+
plugins[i] = strings.TrimSpace(key)
197188
}
198189

199-
return projectVersion, pluginSet
190+
return projectVersion, plugins, nil
200191
}
201192

202193
// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
@@ -293,14 +284,16 @@ func (c cli) resolveFlagsAndConfigFileConflicts(
293284
// getInfo obtains the project version and plugin keys resolving conflicts among flags and the project config file.
294285
func (c *cli) getInfo() error {
295286
// Get project version and plugin info from flags
296-
flagProjectVersion, flagPlugins := c.getInfoFromFlags()
287+
flagProjectVersion, flagPlugins, err := c.getInfoFromFlags()
288+
if err != nil {
289+
return err
290+
}
297291
// Get project version and plugin info from project configuration file
298292
cfgProjectVersion, cfgPlugins, _ := getInfoFromConfigFile()
299293
// We discard the error because not being able to read a project configuration file
300294
// is not fatal for some commands. The ones that require it need to check its existence.
301295

302296
// Resolve project version and plugin keys
303-
var err error
304297
c.projectVersion, c.pluginKeys, err = c.resolveFlagsAndConfigFileConflicts(
305298
flagProjectVersion, cfgProjectVersion, flagPlugins, cfgPlugins,
306299
)
@@ -399,15 +392,13 @@ func (c *cli) resolve() error {
399392
return nil
400393
}
401394

402-
// buildRootCmd returns a root command with a subcommand tree reflecting the
395+
// addSubcommands returns a root command with a subcommand tree reflecting the
403396
// current project's state.
404-
func (c cli) buildRootCmd() *cobra.Command {
405-
rootCmd := c.defaultCommand()
406-
397+
func (c *cli) addSubcommands() {
407398
// kubebuilder completion
408399
// Only add completion if requested
409400
if c.completionCommand {
410-
rootCmd.AddCommand(c.newCompletionCmd())
401+
c.cmd.AddCommand(c.newCompletionCmd())
411402
}
412403

413404
// kubebuilder create
@@ -416,76 +407,61 @@ func (c cli) buildRootCmd() *cobra.Command {
416407
createCmd.AddCommand(c.newCreateAPICmd())
417408
createCmd.AddCommand(c.newCreateWebhookCmd())
418409
if createCmd.HasSubCommands() {
419-
rootCmd.AddCommand(createCmd)
410+
c.cmd.AddCommand(createCmd)
420411
}
421412

422413
// kubebuilder edit
423-
rootCmd.AddCommand(c.newEditCmd())
414+
c.cmd.AddCommand(c.newEditCmd())
424415

425416
// kubebuilder init
426-
rootCmd.AddCommand(c.newInitCmd())
417+
c.cmd.AddCommand(c.newInitCmd())
427418

428419
// kubebuilder version
429420
// Only add version if a version string was provided
430421
if c.version != "" {
431-
rootCmd.AddCommand(c.newVersionCmd())
422+
c.cmd.AddCommand(c.newVersionCmd())
432423
}
433-
434-
return rootCmd
435424
}
436425

437-
// defaultCommand returns the root command without its subcommands.
438-
func (c cli) defaultCommand() *cobra.Command {
439-
return &cobra.Command{
440-
Use: c.commandName,
441-
Short: "Development kit for building Kubernetes extensions and tools.",
442-
Long: fmt.Sprintf(`Development kit for building Kubernetes extensions and tools.
443-
444-
Provides libraries and tools to create new projects, APIs and controllers.
445-
Includes tools for packaging artifacts into an installer container.
446-
447-
Typical project lifecycle:
448-
449-
- initialize a project:
450-
451-
%[1]s init --domain example.com --license apache2 --owner "The Kubernetes authors"
426+
// buildCmd creates the underlying cobra command and stores it internally.
427+
func (c *cli) buildCmd() error {
428+
c.cmd = c.newRootCmd()
452429

453-
- create one or more a new resource APIs and add your code to them:
454-
455-
%[1]s create api --group <group> --version <version> --kind <Kind>
456-
457-
Create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only
458-
scaffold a Controller for an existing Resource, select "n" for Resource. To only define
459-
the schema for a Resource without writing a Controller, select "n" for Controller.
460-
461-
After the scaffold is written, api will run make on the project.
462-
`,
463-
c.commandName),
464-
Example: fmt.Sprintf(`
465-
# Initialize your project
466-
%[1]s init --domain example.com --license apache2 --owner "The Kubernetes authors"
467-
468-
# Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
469-
%[1]s create api --group ship --version v1beta1 --kind Frigate
430+
// Get project version and plugin keys.
431+
if err := c.getInfo(); err != nil {
432+
return err
433+
}
470434

471-
# Edit the API Scheme
472-
nano api/v1beta1/frigate_types.go
435+
// Resolve plugins for project version and plugin keys.
436+
if err := c.resolve(); err != nil {
437+
return err
438+
}
473439

474-
# Edit the Controller
475-
nano controllers/frigate_controller.go
440+
// Add the subcommands
441+
c.addSubcommands()
476442

477-
# Install CRDs into the Kubernetes cluster using kubectl apply
478-
make install
443+
return nil
444+
}
479445

480-
# Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
481-
make run
482-
`,
483-
c.commandName),
484-
Run: func(cmd *cobra.Command, args []string) {
485-
if err := cmd.Help(); err != nil {
486-
log.Fatal(err)
446+
// addExtraCommands adds the additional commands.
447+
func (c *cli) addExtraCommands() error {
448+
for _, cmd := range c.extraCommands {
449+
for _, subCmd := range c.cmd.Commands() {
450+
if cmd.Name() == subCmd.Name() {
451+
return fmt.Errorf("command %q already exists", cmd.Name())
487452
}
488-
},
453+
}
454+
c.cmd.AddCommand(cmd)
455+
}
456+
return nil
457+
}
458+
459+
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
460+
func (c cli) printDeprecationWarnings() {
461+
for _, p := range c.resolvedPlugins {
462+
if d, isDeprecated := p.(plugin.Deprecated); isDeprecated {
463+
fmt.Printf(noticeColor, fmt.Sprintf(deprecationFmt, d.DeprecationWarning()))
464+
}
489465
}
490466
}
491467

0 commit comments

Comments
 (0)