|
| 1 | +# Extensible CLI and Scaffolding Plugins - Phase 1.5 |
| 2 | + |
| 3 | +Continuation of [Extensible CLI and Scaffolding Plugins](./extensible-cli-and-scaffolding-plugins-phase-1.md). |
| 4 | + |
| 5 | +## Goal |
| 6 | + |
| 7 | +The goal of this phase is to achieve one of the goals proposed for Phase 2: chaining plugins. |
| 8 | +Phase 2 includes several other challenging goals, but being able to chain plugins will be beneficial |
| 9 | +for third-party developers that are using kubebuilder as a library. |
| 10 | + |
| 11 | +## Table of contents |
| 12 | +- [Goal](#goal) |
| 13 | +- [Motivation](#motivation) |
| 14 | +- [Proposal](#proposal) |
| 15 | +- [Implementation](#implementation) |
| 16 | + |
| 17 | +## Motivation |
| 18 | + |
| 19 | +There are several cases of plugins that want to maintain most of the go plugin functionality and add |
| 20 | +certain features on top of it, both inside and outside kubebuilder repository: |
| 21 | +- [Addon pattern](../plugins/addon) |
| 22 | +- [Operator SDK](https://github.com/operator-framework/operator-sdk/tree/master/internal/plugins/golang) |
| 23 | + |
| 24 | +This behavior fits perfectly under Phase 1.5, where plugins could be chained. However, as this feature is |
| 25 | +not available, the adopted temporal solution is to wrap the base go plugin and perform additional actions |
| 26 | +after its `Run` method has been executed. This solution faces several issues: |
| 27 | + |
| 28 | +- Wrapper plugins are unable to access the data of the wrapped plugins, as they weren't designed for this |
| 29 | + purpose, and therefore, most of its internal data is non-exported. An example of this inaccessible data |
| 30 | + would be the `Resource` objects created inside the `create api` and `create webhook` commands. |
| 31 | +- Wrapper plugins are dependent on their wrapped plugins, and therefore can't be used for other plugins. |
| 32 | +- Under the hood, subcommands implement a second hidden interface: `RunOptions`, which further accentuates |
| 33 | + these issues. |
| 34 | + |
| 35 | +Plugin chaining solves the aforementioned problems but the current plugin API, and more specifically the |
| 36 | +`Subcommand` interface, does not support plugin chaining. |
| 37 | + |
| 38 | +- The `RunOptions` interface implemented under the hood is not part of the plugin API, and therefore |
| 39 | + the cli is not able to run post-scaffold logic (implemented in `RunOptions.PostScaffold` method) after |
| 40 | + all the plugins have scaffolded their part. |
| 41 | +- `Resource`-related commands can't bind flags like `--group`, `--version` or `--kind` in each plugin, |
| 42 | + it must be created outside the plugins and then injected into them similar to the approach followed |
| 43 | + currently for `Config` objects. |
| 44 | + |
| 45 | +## Proposal |
| 46 | + |
| 47 | +Design a Plugin API that combines the current [`Subcommand`](../pkg/plugin/interfaces.go) and |
| 48 | +[`RunOptions`](../pkg/plugins/internal/cmdutil/cmdutil.go) interfaces and enables plugin-chaining. |
| 49 | +The new `Subcommand` methods can be split in two different categories: |
| 50 | +- Initialization methods |
| 51 | +- Execution methods |
| 52 | + |
| 53 | +Additionally, some of these methods may be optional, in which case a non-implemented method will be skipped |
| 54 | +when it should be called and consider it succeeded. This also allows to create some methods specific for |
| 55 | +a certain subcommand call (e.g.: `Resource`-related methods for the `edit` subcommand are not needed). |
| 56 | + |
| 57 | +Different ordering guarantees can be considered: |
| 58 | +- Method order guarantee: a method for a plugin will be called after its previous methods succeeded. |
| 59 | +- Steps order guarantee: methods will be called when all plugins have finished the previous method. |
| 60 | +- Plugin order guarantee: same method for each plugin will be called in the order specified |
| 61 | + by the plugin position at the plugin chain. |
| 62 | + |
| 63 | +All of the methods will offer plugin order guarantee, as they all modify/update some item so the order |
| 64 | +of plugins is important. Execution methods need to guarantee step order, as the items that are being modified |
| 65 | +in each step (config, resource, and filesystem) are also needed in the following steps. This is not true for |
| 66 | +initialization methods that modify items (metadata and flagset) that are only used in their own methods, |
| 67 | +so they only need to guarantee method order. |
| 68 | + |
| 69 | +Execution methods will be able to return an error. A specific error can be returned to specify that |
| 70 | +no further methods of this plugin should be called, but that the scaffold process should be continued. |
| 71 | +This enables plugins to exit early, e.g., a plugin that scaffolds some files only for cluster-scoped |
| 72 | +resources can detect if the resource is cluster-scoped at one of the first execution steps, and |
| 73 | +therefore, use this error to tell the CLI that no further execution step should be called for itself. |
| 74 | + |
| 75 | +### Initialization methods |
| 76 | + |
| 77 | +#### Update metadata |
| 78 | +This method will be used for two purposes. It provides CLI-related metadata to the Subcommand (e.g., |
| 79 | +command name) and update the subcommands metadata such as the description or examples. |
| 80 | + |
| 81 | +- Required/optional |
| 82 | + - [ ] Required |
| 83 | + - [x] Optional |
| 84 | +- Subcommands |
| 85 | + - [x] Init |
| 86 | + - [x] Edit |
| 87 | + - [x] Create API |
| 88 | + - [x] Create webhook |
| 89 | + |
| 90 | +#### Bind flags |
| 91 | +This method will allow subcommands to define specific flags. |
| 92 | + |
| 93 | +- Required/optional |
| 94 | + - [ ] Required |
| 95 | + - [x] Optional |
| 96 | +- Subcommands |
| 97 | + - [x] Init |
| 98 | + - [x] Edit |
| 99 | + - [x] Create API |
| 100 | + - [x] Create webhook |
| 101 | + |
| 102 | +### Execution methods |
| 103 | + |
| 104 | +#### Inject configuration |
| 105 | +This method will be used to inject the `Config` object that the plugin can modify at will. |
| 106 | +The CLI will create/load/save this configuration object. |
| 107 | + |
| 108 | +- Required/optional |
| 109 | + - [ ] Required |
| 110 | + - [x] Optional |
| 111 | +- Subcommands |
| 112 | + - [x] Init |
| 113 | + - [x] Edit |
| 114 | + - [x] Create API |
| 115 | + - [x] Create webhook |
| 116 | + |
| 117 | +#### Inject resource |
| 118 | +This method will be used to inject the `Resource` object. |
| 119 | + |
| 120 | +- Required/optional |
| 121 | + - [x] Required |
| 122 | + - [ ] Optional |
| 123 | +- Subcommands |
| 124 | + - [ ] Init |
| 125 | + - [ ] Edit |
| 126 | + - [x] Create API |
| 127 | + - [x] Create webhook |
| 128 | + |
| 129 | +#### Pre-scaffold |
| 130 | +This method will be used to take actions before the main scaffolding is performed, e.g. validations. |
| 131 | + |
| 132 | +NOTE: a filesystem abstraction will be passed to this method that must be used for scaffolding. |
| 133 | + |
| 134 | +- Required/optional |
| 135 | + - [ ] Required |
| 136 | + - [x] Optional |
| 137 | +- Subcommands |
| 138 | + - [x] Init |
| 139 | + - [x] Edit |
| 140 | + - [x] Create API |
| 141 | + - [x] Create webhook |
| 142 | + |
| 143 | +#### Scaffold |
| 144 | +This method will be used to perform the main scaffolding. |
| 145 | + |
| 146 | +NOTE: a filesystem abstraction will be passed to this method that must be used for scaffolding. |
| 147 | + |
| 148 | +- Required/optional |
| 149 | + - [x] Required |
| 150 | + - [ ] Optional |
| 151 | +- Subcommands |
| 152 | + - [x] Init |
| 153 | + - [x] Edit |
| 154 | + - [x] Create API |
| 155 | + - [x] Create webhook |
| 156 | + |
| 157 | +#### Post-scaffold |
| 158 | +This method will be used to take actions after the main scaffolding is performed, e.g. cleanup. |
| 159 | + |
| 160 | +NOTE: a filesystem abstraction will **NOT** be passed to this method, as post-scaffold task do not require it. |
| 161 | +In case some post-scaffold task requires a filesystem abstraction, it could be added. |
| 162 | + |
| 163 | +- Required/optional |
| 164 | + - [ ] Required |
| 165 | + - [x] Optional |
| 166 | +- Subcommands |
| 167 | + - [x] Init |
| 168 | + - [x] Edit |
| 169 | + - [x] Create API |
| 170 | + - [x] Create webhook |
| 171 | + |
| 172 | +## Implementation |
| 173 | + |
| 174 | +The following types are used as input/output values of the described methods: |
| 175 | +```go |
| 176 | +// CLIMetadata is the runtime meta-data of the CLI |
| 177 | +type CLIMetadata struct { |
| 178 | + // CommandName is the root command name. |
| 179 | + CommandName string |
| 180 | +} |
| 181 | + |
| 182 | +// SubcommandMetadata is the runtime meta-data for a subcommand |
| 183 | +type SubcommandMetadata struct { |
| 184 | + // Description is a description of what this subcommand does. It is used to display help. |
| 185 | + Description string |
| 186 | + // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. |
| 187 | + Examples string |
| 188 | +} |
| 189 | + |
| 190 | +type ExitError struct { |
| 191 | + Plugin string |
| 192 | + Reason string |
| 193 | +} |
| 194 | + |
| 195 | +func (e ExitError) Error() string { |
| 196 | + return fmt.Sprintf("plugin %s exit early: %s", e.Plugin, e.Reason) |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +The described methods are implemented through the use of the following interfaces. |
| 201 | +```go |
| 202 | +type RequiresCLIMetadata interface { |
| 203 | + InjectCLIMetadata(CLIMetadata) |
| 204 | +} |
| 205 | + |
| 206 | +type UpdatesSubcommandMetadata interface { |
| 207 | + UpdateSubcommandMetadata(*SubcommandMetadata) |
| 208 | +} |
| 209 | + |
| 210 | +type HasFlags interface { |
| 211 | + BindFlags(*pflag.FlagSet) |
| 212 | +} |
| 213 | + |
| 214 | +type RequiresConfig interface { |
| 215 | + InjectConfig(config.Config) error |
| 216 | +} |
| 217 | + |
| 218 | +type RequiresResource interface { |
| 219 | + InjectResource(*resource.Resource) error |
| 220 | +} |
| 221 | + |
| 222 | +type HasPreScaffold interface { |
| 223 | + PreScaffold(afero.Fs) error |
| 224 | +} |
| 225 | + |
| 226 | +type Scaffolder interface { |
| 227 | + Scaffold(afero.Fs) error |
| 228 | +} |
| 229 | + |
| 230 | +type HasPostScaffold interface { |
| 231 | + PostScaffold() error |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +Additional interfaces define the required method for each type of plugin: |
| 236 | +```go |
| 237 | +// InitSubcommand is the specific interface for subcommands returned by init plugins. |
| 238 | +type InitSubcommand interface { |
| 239 | + Scaffolder |
| 240 | +} |
| 241 | + |
| 242 | +// EditSubcommand is the specific interface for subcommands returned by edit plugins. |
| 243 | +type EditSubcommand interface { |
| 244 | + Scaffolder |
| 245 | +} |
| 246 | + |
| 247 | +// CreateAPISubcommand is the specific interface for subcommands returned by create API plugins. |
| 248 | +type CreateAPISubcommand interface { |
| 249 | + RequiresResource |
| 250 | + Scaffolder |
| 251 | +} |
| 252 | + |
| 253 | +// CreateWebhookSubcommand is the specific interface for subcommands returned by create webhook plugins. |
| 254 | +type CreateWebhookSubcommand interface { |
| 255 | + RequiresResource |
| 256 | + Scaffolder |
| 257 | +} |
| 258 | +``` |
0 commit comments