Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

✨ Allow Downstream Root Commands to add custom descriptions #1830

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import (
)

func main() {
rootCmdCfg := cli.RootCommandConfig{
CommandName: "kubebuilder",
}
c, err := cli.New(
cli.WithCommandName("kubebuilder"),
cli.WithRootCommandConfig(rootCmdCfg),
cli.WithVersion(versionString()),
cli.WithDefaultProjectVersion(config.Version3Alpha),
cli.WithPlugins(
Expand Down
5 changes: 4 additions & 1 deletion docs/book/src/reference/cli-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,11 @@ import (
// Go plugin version unless '--plugins' or 'layout' specify
// one of the other plugins.
func main() {
rootCmdCfg := cli.RootCommandConfig{
CommandName: "controller-builder",
}
c, err := cli.New(
cli.WithCommandName("controller-builder"),
cli.WithRootCommandConfig(rootCmdCfg),
cli.WithDefaultProjectVersion("3"),
cli.WithExtraCommands(newCustomCobraCmd()),
cli.WithPlugins(
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c cli) newCreateAPICmd() *cobra.Command {

func (c cli) newAPIContext() plugin.Context {
return plugin.Context{
CommandName: c.commandName,
CommandName: c.cmdCfg.CommandName,
Description: `Scaffold a Kubernetes API.
`,
}
Expand Down
141 changes: 81 additions & 60 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,49 @@ const (
pluginsFlag = "plugins"

noPluginError = "invalid config file please verify that the version and layout fields are set and valid"

defaultCommandName = "kubebuilder"
defaultShortDescription = "Development kit for building Kubernetes extensions and tools."
defaultLongDescription = `Development kit for building Kubernetes extensions and tools.

Provides libraries and tools to create new projects, APIs and controllers.
Includes tools for packaging artifacts into an installer container.

Typical project lifecycle:

- initialize a project:

placeHolder init --domain example.com --license apache2 --owner "The Kubernetes authors"

- create one or more a new resource APIs and add your code to them:

placeHolder create api --group <group> --version <version> --kind <Kind>

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
the schema for a Resource without writing a Controller, select "n" for Controller.

After the scaffold is written, api will run make on the project.
`
defaultExample = `
# Initialize your project
placeHolder init --domain example.com --license apache2 --owner "The Kubernetes authors"

# Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
placeHolder create api --group ship --version v1beta1 --kind Frigate

# Edit the API Scheme
nano api/v1beta1/frigate_types.go

# Edit the Controller
nano controllers/frigate_controller.go

# Install CRDs into the Kubernetes cluster using kubectl apply
make install

# Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
make run
`
)

// equalStringSlice checks if two string slices are equal.
Expand All @@ -65,13 +108,21 @@ type CLI interface {
Run() error
}

//RootCommandConfig returns the configuration for root cobra.Command
type RootCommandConfig struct {
CommandName string
Short string
Long string
Example string
}

// cli defines the command line structure and interfaces that are used to
// scaffold kubebuilder project files.
type cli struct { //nolint:maligned
/* Fields set by Option */

// Root command name. It is injected downstream to provide correct help, usage, examples and errors.
commandName string
//Root command config. It is injected downstream to provide correct help, usage, examples and errors.
cmdCfg RootCommandConfig
// CLI version string.
version string
// Default project version in case none is provided and a config file can't be found.
Expand Down Expand Up @@ -147,9 +198,16 @@ func New(opts ...Option) (CLI, error) {
// newCLI creates a default cli instance and applies the provided options.
// It is as a separate function for test purposes.
func newCLI(opts ...Option) (*cli, error) {
//Get default command descriptions
shortDescription, longDescription, example := buildDefaultDescriptors(defaultCommandName)
// Default cli options.
c := &cli{
anmol372 marked this conversation as resolved.
Show resolved Hide resolved
commandName: "kubebuilder",
cmdCfg: RootCommandConfig{
CommandName: defaultCommandName,
Short: shortDescription,
Long: longDescription,
Example: example,
},
defaultProjectVersion: internalconfig.DefaultVersion,
defaultPlugins: make(map[string][]string),
plugins: make(map[string]plugin.Plugin),
Expand All @@ -165,6 +223,15 @@ func newCLI(opts ...Option) (*cli, error) {
return c, nil
}

// buildDefaultDescriptors replaces placeholder in default descriptors
func buildDefaultDescriptors(commandName string) (string, string, string) {
replacer := strings.NewReplacer("placeHolder", commandName)
short := replacer.Replace(defaultShortDescription)
long := replacer.Replace(defaultLongDescription)
example := replacer.Replace(defaultExample)
return short, long, example
}

// getInfoFromFlags obtains the project version and plugin keys from flags.
func (c *cli) getInfoFromFlags() (string, []string) {
// Partially parse the command line arguments
Expand Down Expand Up @@ -402,8 +469,17 @@ func (c *cli) resolve() error {
// buildRootCmd returns a root command with a subcommand tree reflecting the
// current project's state.
func (c cli) buildRootCmd() *cobra.Command {
rootCmd := c.defaultCommand()

rootCmd := &cobra.Command{
Use: c.cmdCfg.CommandName,
Short: c.cmdCfg.Short,
Long: c.cmdCfg.Long,
Example: c.cmdCfg.Example,
Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Help(); err != nil {
log.Fatal(err)
}
},
}
// kubebuilder completion
// Only add completion if requested
if c.completionCommand {
Expand Down Expand Up @@ -434,61 +510,6 @@ func (c cli) buildRootCmd() *cobra.Command {
return rootCmd
}

// defaultCommand returns the root command without its subcommands.
func (c cli) defaultCommand() *cobra.Command {
return &cobra.Command{
Use: c.commandName,
Short: "Development kit for building Kubernetes extensions and tools.",
Long: fmt.Sprintf(`Development kit for building Kubernetes extensions and tools.

Provides libraries and tools to create new projects, APIs and controllers.
Includes tools for packaging artifacts into an installer container.

Typical project lifecycle:

- initialize a project:

%[1]s init --domain example.com --license apache2 --owner "The Kubernetes authors"

- create one or more a new resource APIs and add your code to them:

%[1]s create api --group <group> --version <version> --kind <Kind>

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
the schema for a Resource without writing a Controller, select "n" for Controller.

After the scaffold is written, api will run make on the project.
`,
c.commandName),
Example: fmt.Sprintf(`
# Initialize your project
%[1]s init --domain example.com --license apache2 --owner "The Kubernetes authors"

# Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
%[1]s create api --group ship --version v1beta1 --kind Frigate

# Edit the API Scheme
nano api/v1beta1/frigate_types.go

# Edit the Controller
nano controllers/frigate_controller.go

# Install CRDs into the Kubernetes cluster using kubectl apply
make install

# Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
make run
`,
c.commandName),
Run: func(cmd *cobra.Command, args []string) {
if err := cmd.Help(); err != nil {
log.Fatal(err)
}
},
}
}

// Run implements CLI.Run.
func (c cli) Run() error {
return c.cmd.Execute()
Expand Down
1 change: 0 additions & 1 deletion pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,6 @@ var _ = Describe("CLI", func() {
Expect(err).To(HaveOccurred())
})
})

When("an invalid plugin key is set", func() {
It("should fail", func() {
c = &cli{
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Linux:
$ %[1]s completion bash > /etc/bash_completion.d/%[1]s
MacOS:
$ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s
`, c.commandName),
`, c.cmdCfg.CommandName),
RunE: func(cmd *cobra.Command, cmdArgs []string) error {
return cmd.Root().GenBashCompletion(os.Stdout)
},
Expand All @@ -54,7 +54,7 @@ $ echo "autoload -U compinit; compinit" >> ~/.zshrc
$ %[1]s completion zsh > "${fpath[1]}/_%[1]s"

# You will need to start a new shell for this setup to take effect.
`, c.commandName),
`, c.cmdCfg.CommandName),
RunE: func(cmd *cobra.Command, cmdArgs []string) error {
return cmd.Root().GenZshCompletion(os.Stdout)
},
Expand Down Expand Up @@ -98,7 +98,7 @@ func (c cli) newCompletionCmd() *cobra.Command {
Long: fmt.Sprintf(`Output shell completion code for the specified shell.
The shell code must be evaluated to provide interactive completion of %[1]s commands.
Detailed instructions on how to do this for each shell are provided in their own commands.
`, c.commandName),
`, c.cmdCfg.CommandName),
}
cmd.AddCommand(c.newBashCmd())
cmd.AddCommand(c.newZshCmd())
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c cli) newEditCmd() *cobra.Command {

func (c cli) newEditContext() plugin.Context {
return plugin.Context{
CommandName: c.commandName,
CommandName: c.cmdCfg.CommandName,
Description: `Edit the project configuration.
`,
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (c cli) newInitCmd() *cobra.Command {

func (c cli) newInitContext() plugin.Context {
return plugin.Context{
CommandName: c.commandName,
CommandName: c.cmdCfg.CommandName,
Description: `Initialize a new project.

For further help about a specific project version, set --project-version.
Expand All @@ -79,7 +79,7 @@ func (c cli) getInitHelpExamples() string {
%[1]s init --project-version=%[2]s -h

`,
c.commandName, version)
c.cmdCfg.CommandName, version)
sb.WriteString(rendered)
}
return strings.TrimSuffix(sb.String(), "\n\n")
Expand Down
24 changes: 21 additions & 3 deletions pkg/cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,28 @@ import (
// Option is a function that can configure the cli
type Option func(*cli) error

// WithCommandName is an Option that sets the cli's root command name.
func WithCommandName(name string) Option {
// WithRootCommandConfig configures a cli's root command.
func WithRootCommandConfig(cfg RootCommandConfig) Option {
return func(c *cli) error {
c.commandName = name
if cfg.CommandName != "" {
c.cmdCfg.CommandName = cfg.CommandName
}
shortDescription, longDescription, example := buildDefaultDescriptors(c.cmdCfg.CommandName)
if cfg.Short != "" {
c.cmdCfg.Short = cfg.Short
} else {
c.cmdCfg.Short = shortDescription
}
if cfg.Long != "" {
c.cmdCfg.Long = cfg.Long
} else {
c.cmdCfg.Long = longDescription
}
if cfg.Example != "" {
c.cmdCfg.Example = cfg.Example
} else {
c.cmdCfg.Example = example
}
return nil
}
}
Expand Down
43 changes: 38 additions & 5 deletions pkg/cli/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,46 @@ var _ = Describe("CLI options", func() {
np4 = newMockPlugin(pluginName, pluginVersion, "a")
)

Context("WithCommandName", func() {
It("should use provided command name", func() {
commandName := "other-command"
c, err = newCLI(WithCommandName(commandName))
Context("WithRootCommandConfig", func() {
It("should use the default command name and descriptions", func() {
c, err = newCLI()
shortDescription, longDescription, example := buildDefaultDescriptors(defaultCommandName)
Expect(err).NotTo(HaveOccurred())
Expect(c).NotTo(BeNil())
Expect(c.cmdCfg.CommandName).To(Equal(defaultCommandName))
Expect(c.cmdCfg.Short).To(Equal(shortDescription))
Expect(c.cmdCfg.Long).To(Equal(longDescription))
Expect(c.cmdCfg.Example).To(Equal(example))
})

It("should use the provided command name and default descriptions", func() {
cfg := RootCommandConfig{
CommandName: "other-command",
}
c, err = newCLI(WithRootCommandConfig(cfg))
shortDescription, longDescription, example := buildDefaultDescriptors(cfg.CommandName)
Expect(err).NotTo(HaveOccurred())
Expect(c).NotTo(BeNil())
Expect(c.cmdCfg.CommandName).To(Equal(cfg.CommandName))
Expect(c.cmdCfg.Short).To(Equal(shortDescription))
Expect(c.cmdCfg.Long).To(Equal(longDescription))
Expect(c.cmdCfg.Example).To(Equal(example))
})

It("should use the provided command name and descriptions", func() {
cfg := RootCommandConfig{
CommandName: "other-command",
Short: "Short Description",
Long: "Longer Description of the command",
Example: "Example usage of the command",
}
c, err = newCLI(WithRootCommandConfig(cfg))
Expect(err).NotTo(HaveOccurred())
Expect(c).NotTo(BeNil())
Expect(c.commandName).To(Equal(commandName))
Expect(c.cmdCfg.CommandName).To(Equal(cfg.CommandName))
Expect(c.cmdCfg.Short).To(Equal(cfg.Short))
Expect(c.cmdCfg.Long).To(Equal(cfg.Long))
Expect(c.cmdCfg.Example).To(Equal(cfg.Example))
})
})

Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import (
func (c cli) newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: fmt.Sprintf("Print the %s version", c.commandName),
Long: fmt.Sprintf("Print the %s version", c.commandName),
Example: fmt.Sprintf("%s version", c.commandName),
Short: fmt.Sprintf("Print the %s version", c.cmdCfg.CommandName),
Long: fmt.Sprintf("Print the %s version", c.cmdCfg.CommandName),
Example: fmt.Sprintf("%s version", c.cmdCfg.CommandName),
RunE: func(_ *cobra.Command, _ []string) error {
fmt.Println(c.version)
return nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c cli) newCreateWebhookCmd() *cobra.Command {

func (c cli) newWebhookContext() plugin.Context {
return plugin.Context{
CommandName: c.commandName,
CommandName: c.cmdCfg.CommandName,
Description: `Scaffold a webhook for an API resource.
`,
}
Expand Down