|
| 1 | +# Extensible CLI and Scaffolding Plugins |
| 2 | + |
| 3 | +## Overview |
| 4 | +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 Kubebuilder's project versions with other custom project versions so that I can support the Kubebuilder workflow with non-Go operators (e.g. operator-sdk's Ansible and Helm-based operators). |
| 5 | + |
| 6 | +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. |
| 7 | + |
| 8 | +## Related issues and PRs |
| 9 | + |
| 10 | +* [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148) |
| 11 | +* [#1171](https://github.com/kubernetes-sigs/kubebuilder/pull/1171) |
| 12 | +* Possibly [#1218](https://github.com/kubernetes-sigs/kubebuilder/issues/1218) |
| 13 | + |
| 14 | +## Prototype implementation |
| 15 | +https://github.com/joelanford/kubebuilder-exp |
| 16 | + |
| 17 | +## Plugin interfaces |
| 18 | + |
| 19 | +### Required |
| 20 | +Each plugin would minimally be required to implement the `Plugin` interface. |
| 21 | + |
| 22 | +```go |
| 23 | +type Plugin interface { |
| 24 | + // Version is the project version that this plugin implements. |
| 25 | + // For example, Kubebuilder's Go v2 plugin implementation would return "2" |
| 26 | + Version() string |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +### Optional |
| 31 | +Next, a plugin could optionally implement further interfaces to declare its support for specific Kubebuilder subcommands. For example: |
| 32 | +* `InitPlugin` - to initialize new projects |
| 33 | +* `CreateAPIPlugin` - to create APIs (and possibly controllers) for existing projects |
| 34 | +* `CreateWebhookPlugin` - to create webhooks for existing projects |
| 35 | + |
| 36 | +Each of these interfaces would follow the same pattern (see the InitPlugin interface example below). |
| 37 | + |
| 38 | +```go |
| 39 | +type InitPlugin interface { |
| 40 | + Plugin |
| 41 | + |
| 42 | + // InitDescription returns a description of what this plugin initializes |
| 43 | + // for a new project. It is used to display help. |
| 44 | + InitDescription() string |
| 45 | + |
| 46 | + // InitHelp returns one or more examples of the command-line usage |
| 47 | + // of this plugin's project initialization support. It is used to display help. |
| 48 | + InitExample() string |
| 49 | + |
| 50 | + // BindInitFlags binds the plugin's init flags to the CLI. This allows each |
| 51 | + // plugin to define its own command line flags for the `kubebuilder init` |
| 52 | + // subcommand. |
| 53 | + BindInitFlags(fs *pflag.FlagSet) |
| 54 | + |
| 55 | + // Init initializes a project. |
| 56 | + Init() error |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +#### Deprecated Plugins |
| 61 | + |
| 62 | +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: |
| 63 | + |
| 64 | +```go |
| 65 | +// Deprecated is an interface that, if implemented, informs the CLI |
| 66 | +// that the plugin is deprecated. The CLI uses this to print deprecation |
| 67 | +// warnings when the plugin is in use. |
| 68 | +type Deprecated interface { |
| 69 | + // DeprecationWarning returns a deprecation message that callers |
| 70 | + // can use to warn users of deprecations |
| 71 | + DeprecationWarning() string |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +## CLI |
| 76 | + |
| 77 | +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. |
| 78 | + |
| 79 | +Example Kubebuilder main.go: |
| 80 | + |
| 81 | +```go |
| 82 | +func main() { |
| 83 | + c, err := cli.New( |
| 84 | + cli.WithPlugins( |
| 85 | + &golangv1.Plugin{}, |
| 86 | + &golangv2.Plugin{}, |
| 87 | + ), |
| 88 | + ) |
| 89 | + if err != nil { |
| 90 | + log.Fatal(err) |
| 91 | + } |
| 92 | + if err := c.Run(); err != nil { |
| 93 | + log.Fatal(err) |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +Example Operator SDK main.go: |
| 99 | + |
| 100 | +```go |
| 101 | +func main() { |
| 102 | + c, err := cli.New( |
| 103 | + cli.WithCommandName("operator-sdk"), |
| 104 | + cli.WithDefaultProjectVersion("2"), |
| 105 | + cli.WithExtraCommands(newCustomCobraCmd()), |
| 106 | + cli.WithPlugins( |
| 107 | + &golangv1.Plugin{}, |
| 108 | + &golangv2.Plugin{}, |
| 109 | + &helmv1.Plugin{}, |
| 110 | + &ansiblev1.Plugin{}, |
| 111 | + ), |
| 112 | + ) |
| 113 | + if err != nil { |
| 114 | + log.Fatal(err) |
| 115 | + } |
| 116 | + if err := c.Run(); err != nil { |
| 117 | + log.Fatal(err) |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +## Comments & Questions |
| 123 | + |
| 124 | +### Cobra Commands |
| 125 | +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. |
| 126 | + |
| 127 | +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. |
| 128 | + |
| 129 | +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: |
| 130 | +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. |
| 131 | +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 |
| 132 | +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. |
| 133 | + |
| 134 | +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. |
| 135 | + |
| 136 | +Are there other ideas for how to handle the following requirements? |
| 137 | +* Eliminate use of cobra in CLI interface |
| 138 | +* Allow other projects to have custom subcommands |
| 139 | +* Support cohesive help output |
| 140 | + |
| 141 | +### Other |
| 142 | +1. Should the `InitPlugin` interface methods be required of all plugins? |
| 143 | +2. Any other approaches or ideas? |
| 144 | +3. Anything I didn't cover that could use more explanation? |
0 commit comments