sigs.k8s.io/kubebuilder/v3@v3.14.0/designs/extensible-cli-and-scaffolding-plugins-phase-1.md (about)

     1  
     2  | Authors       | Creation Date | Status      | Extra                                                           |
     3  |---------------|---------------|-------------|-----------------------------------------------------------------|
     4  | @estroz,@joelanford | Dec 10, 2019  | Implemented | [Plugins doc](https://book.kubebuilder.io/plugins/plugins.html) |
     5  
     6  # Extensible CLI and Scaffolding Plugins
     7  
     8  ## Overview
     9  
    10  I would like for Kubebuilder to become more extensible, such that it could be imported and used as a library in other projects. Specifically, I'm looking for a way to use Kubebuilder's existing CLI and scaffolding for Go projects, but to also be able to augment the Kubebuilder project structure with other custom project types so that I can support the Kubebuilder workflow with non-Go operators (e.g. operator-sdk's Ansible and Helm-based operators).
    11  
    12  The idea is for Kubebuilder to define one or more plugin interfaces that can be used to drive what the `init`, `create api` and `create webhooks` subcommands do and to add a new `cli` package that other projects can use to integrate out-of-tree plugins with the Kubebuilder CLI in their own projects.
    13  
    14  ## Related issues and PRs
    15  
    16  * [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148)
    17  * [#1171](https://github.com/kubernetes-sigs/kubebuilder/pull/1171)
    18  * Possibly [#1218](https://github.com/kubernetes-sigs/kubebuilder/issues/1218)
    19  
    20  ## Prototype implementation
    21  
    22  Barebones plugin refactor: https://github.com/joelanford/kubebuilder-exp
    23  Kubebuilder feature branch: https://github.com/kubernetes-sigs/kubebuilder/tree/feature/plugins-part-2-electric-boogaloo
    24  
    25  ## Plugin interfaces
    26  
    27  ### Required
    28  
    29  Each plugin would minimally be required to implement the `Plugin` interface.
    30  
    31  ```go
    32  type Plugin interface {
    33      // Version returns the plugin's semantic version, ex. "v1.2.3".
    34      //
    35      // Note: this version is different from config version.
    36      Version() string
    37      // Name returns a DNS1123 label string defining the plugin type.
    38      // For example, Kubebuilder's main plugin would return "go".
    39      //
    40      // Plugin names can be fully-qualified, and non-fully-qualified names are
    41      // prepended to ".kubebuilder.io" to prevent conflicts.
    42      Name() string
    43      // SupportedProjectVersions lists all project configuration versions this
    44      // plugin supports, ex. []string{"2", "3"}. The returned slice cannot be empty.
    45      SupportedProjectVersions() []string
    46  }
    47  ```
    48  
    49  #### Plugin naming
    50  
    51  Plugin names (returned by `Name()`) must be DNS1123 labels. The returned name
    52  may be fully qualified (fq), ex. `go.kubebuilder.io`, or not but internally will
    53  always be fq by either appending `.kubebuilder.io` to the name or using an
    54  existing qualifier defined by the plugin. FQ names prevent conflicts between
    55  plugin names; the plugin runner will ask the user to add a name qualifier to
    56  a conflicting plugin.
    57  
    58  ### Optional
    59  
    60  Next, a plugin could optionally implement further interfaces to declare its support for specific Kubebuilder subcommands. For example:
    61  * `InitPlugin` - to initialize new projects
    62  * `CreateAPIPlugin` - to create APIs (and possibly controllers) for existing projects
    63  * `CreateWebhookPlugin` - to create webhooks for existing projects
    64  
    65  Each of these interfaces would follow the same pattern (see the `InitPlugin` interface example below).
    66  
    67  ```go
    68  type InitPluginGetter interface {
    69      Plugin
    70      // GetInitPlugin returns the underlying InitPlugin interface.
    71      GetInitPlugin() InitPlugin
    72  }
    73  
    74  type InitPlugin interface {
    75      GenericSubcommand
    76  }
    77  ```
    78  
    79  Each specialized plugin interface can leverage a generic subcommand interface, which prevents duplication of methods while permitting type checking and interface flexibility. A plugin context can be used to preserve default help text in case a plugin does not implement its own.
    80  
    81  ```go
    82  type GenericSubcommand interface {
    83      // UpdateContext updates a PluginContext with command-specific help text, like description and examples.
    84      // Can be a no-op if default help text is desired.
    85      UpdateContext(*PluginContext)
    86      // BindFlags binds the plugin's flags to the CLI. This allows each plugin to define its own
    87      // command line flags for the kubebuilder subcommand.
    88      BindFlags(*pflag.FlagSet)
    89      // Run runs the subcommand.
    90      Run() error
    91      // InjectConfig passes a config to a plugin. The plugin may modify the
    92      // config. Initializing, loading, and saving the config is managed by the
    93      // cli package.
    94      InjectConfig(*config.Config)
    95  }
    96  
    97  type PluginContext struct {
    98      // Description is a description of what this subcommand does. It is used to display help.
    99      Description string
   100      // Examples are one or more examples of the command-line usage
   101      // of this plugin's project subcommand support. It is used to display help.
   102      Examples string
   103  }
   104  ```
   105  
   106  #### Deprecated Plugins
   107  
   108  To generically support deprecated project versions, we could also add a `Deprecated` interface that the CLI could use to decide when to print deprecation warnings:
   109  
   110  ```go
   111  // Deprecated is an interface that, if implemented, informs the CLI
   112  // that the plugin is deprecated.  The CLI uses this to print deprecation
   113  // warnings when the plugin is in use.
   114  type Deprecated interface {
   115      // DeprecationWarning returns a deprecation message that callers
   116      // can use to warn users of deprecations
   117      DeprecationWarning() string
   118  }
   119  ```
   120  
   121  ## Configuration
   122  
   123  ### Config version `3-alpha`
   124  
   125  Any changes that break `PROJECT` file backwards-compatibility require a version
   126  bump. This new version will be `3-alpha`, which will eventually be bumped to
   127  `3` once the below config changes have stabilized.
   128  
   129  ### Project file plugin `layout`
   130  
   131  The `PROJECT` file will specify what base plugin generated the project under
   132  a `layout` key. `layout` will have the format: `Plugin.Name() + "/" + Plugin.Version()`.
   133  `version` and `layout` have versions with different meanings: `version` is the
   134  project config version, while `layout`'s version is the plugin semantic version.
   135  The value in `version` will determine that in `layout` by a plugin's supported
   136  project versions (via `SupportedProjectVersions()`).
   137  
   138  Example `PROJECT` file:
   139  
   140  ```yaml
   141  version: "3-alpha"
   142  layout: go/v1.0.0
   143  domain: testproject.org
   144  repo: github.com/test-inc/testproject
   145  resources:
   146  - group: crew
   147    kind: Captain
   148    version: v1
   149  ```
   150  
   151  ## CLI
   152  
   153  To make the above plugin system extensible and usable by other projects, we could add a new CLI package that Kubebuilder (and other projects) could use as their entrypoint.
   154  
   155  Example Kubebuilder main.go:
   156  
   157  ```go
   158  func main() {
   159  	c, err := cli.New(
   160  		cli.WithPlugins(
   161  			&golangv1.Plugin{},
   162  			&golangv2.Plugin{},
   163  		),
   164  	)
   165  	if err != nil {
   166  		log.Fatal(err)
   167  	}
   168  	if err := c.Run(); err != nil {
   169  		log.Fatal(err)
   170  	}
   171  }
   172  ```
   173  
   174  Example Operator SDK main.go:
   175  
   176  ```go
   177  func main() {
   178  	c, err := cli.New(
   179  		cli.WithCommandName("operator-sdk"),
   180  		cli.WithDefaultProjectVersion("2"),
   181  		cli.WithExtraCommands(newCustomCobraCmd()),
   182  		cli.WithPlugins(
   183  			&golangv1.Plugin{},
   184  			&golangv2.Plugin{},
   185  			&helmv1.Plugin{},
   186  			&ansiblev1.Plugin{},
   187  		),
   188  	)
   189  	if err != nil {
   190  		log.Fatal(err)
   191  	}
   192  	if err := c.Run(); err != nil {
   193  		log.Fatal(err)
   194  	}
   195  }
   196  ```
   197  
   198  ## Comments & Questions
   199  
   200  ### Cobra Commands
   201  
   202  **RESOLUTION:** `cobra` will be used directly in Phase 1 since it is a widely used, feature-rich CLI package. This, however unlikely, may change in future phases.
   203  
   204  As discussed earlier as part of [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148), one goal is to eliminate the use of `cobra.Command` in the exported API of Kubebuilder since that is considered an internal implementation detail.
   205  
   206  However, at some point, projects that make use of this extensibility will likely want to integrate their own subcommands. In this proposal, `cli.WithExtraCommands()` _DOES_ expose `cobra.Command` to allow callers to pass their own subcommands to the CLI.
   207  
   208  In [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148), callers would use Kubebuilder's cobra commands to build their CLI. Here, control of the CLI is retained by Kubebuilder, and callers pass their subcommands to Kubebuilder. This has several benefits:
   209  1. Kubebuilder's CLI subcommands are never exposed except via the explicit plugin interface. This allows the Kubebuilder project to re-implement its subcommand internals without worrying about backwards compatibility of consumers of Kubebuilder's CLI.
   210  2. If desired, Kubebuilder could ensure that extra subcommands do not overwrite/reuse the existing Kubebuilder subcommand names. For example, only Kubebuilder gets to define the `init` subcommand
   211  3. The overall binary's help handling is self-contained in Kubebuilder's CLI. Callers don't have to figure out how to have a cohesive help output between the Kubebuilder CLI and their own custom subcommands.
   212  
   213  With all of that said, even this exposure of `cobra.Command` could be problematic. If Kubebuilder decides in the future to transition to a different CLI framework (or to roll its own) it has to either continue maintaining support for these extra cobra commands passed into it, or it was to break the CLI API.
   214  
   215  Are there other ideas for how to handle the following requirements?
   216  * Eliminate use of cobra in CLI interface
   217  * Allow other projects to have custom subcommands
   218  * Support cohesive help output
   219  
   220  ### Other
   221  1. ~Should the `InitPlugin` interface methods be required of all plugins?~ No
   222  2. ~Any other approaches or ideas?~
   223  3. ~Anything I didn't cover that could use more explanation?~