sigs.k8s.io/kubebuilder/v3@v3.14.0/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md (about) 1 | Authors | Creation Date | Status | Extra | 2 |---------------|---------------|-------------|-----------------------------------------------------------------| 3 | @adirio | Mar 9, 2021 | Implemented | [Plugins doc](https://book.kubebuilder.io/plugins/plugins.html) | 4 5 # Extensible CLI and Scaffolding Plugins - Phase 1.5 6 7 Continuation of [Extensible CLI and Scaffolding Plugins](./extensible-cli-and-scaffolding-plugins-phase-1.md). 8 9 ## Goal 10 11 The goal of this phase is to achieve one of the goals proposed for Phase 2: chaining plugins. 12 Phase 2 includes several other challenging goals, but being able to chain plugins will be beneficial 13 for third-party developers that are using kubebuilder as a library. 14 15 The other main goal of phase 2, discovering and using external plugins, is out of the scope of this phase, 16 and will be tackled when phase 2 is implemented. 17 18 ## Table of contents 19 - [Goal](#goal) 20 - [Motivation](#motivation) 21 - [Proposal](#proposal) 22 - [Implementation](#implementation) 23 24 ## Motivation 25 26 There are several cases of plugins that want to maintain most of the go plugin functionality and add 27 certain features on top of it, both inside and outside kubebuilder repository: 28 - [Addon pattern](../plugins/addon) 29 - [Operator SDK](https://github.com/operator-framework/operator-sdk/tree/master/internal/plugins/golang) 30 31 This behavior fits perfectly under Phase 1.5, where plugins could be chained. However, as this feature is 32 not available, the adopted temporal solution is to wrap the base go plugin and perform additional actions 33 after its `Run` method has been executed. This solution faces several issues: 34 35 - Wrapper plugins are unable to access the data of the wrapped plugins, as they weren't designed for this 36 purpose, and therefore, most of its internal data is non-exported. An example of this inaccessible data 37 would be the `Resource` objects created inside the `create api` and `create webhook` commands. 38 - Wrapper plugins are dependent on their wrapped plugins, and therefore can't be used for other plugins. 39 - Under the hood, subcommands implement a second hidden interface: `RunOptions`, which further accentuates 40 these issues. 41 42 Plugin chaining solves the aforementioned problems but the current plugin API, and more specifically the 43 `Subcommand` interface, does not support plugin chaining. 44 45 - The `RunOptions` interface implemented under the hood is not part of the plugin API, and therefore 46 the cli is not able to run post-scaffold logic (implemented in `RunOptions.PostScaffold` method) after 47 all the plugins have scaffolded their part. 48 - `Resource`-related commands can't bind flags like `--group`, `--version` or `--kind` in each plugin, 49 it must be created outside the plugins and then injected into them similar to the approach followed 50 currently for `Config` objects. 51 52 ## Proposal 53 54 Design a Plugin API that combines the current [`Subcommand`](../pkg/plugin/interfaces.go) and 55 [`RunOptions`](../pkg/plugins/internal/cmdutil/cmdutil.go) interfaces and enables plugin-chaining. 56 The new `Subcommand` hooks can be split in two different categories: 57 - Initialization hooks 58 - Execution hooks 59 60 Initialization hooks are run during the dynamic creation of the CLI, which means that they are able to 61 modify the CLI, e.g. providing descriptions and examples for subcommands or binding flags. 62 Execution hooks are run after the CLI is created, and therefore cannot modify the CLI. On the other hand, 63 as they are run during the CLI execution, they have access to user-provided flag values, project configuration, 64 the new API resource or the filesystem abstraction, as opposed to the initialization hooks. 65 66 Additionally, some of these hooks may be optional, in which case a non-implemented hook will be skipped 67 when it should be called and consider it succeeded. This also allows to create some hooks specific for 68 a certain subcommand call (e.g.: `Resource`-related hooks for the `edit` subcommand are not needed). 69 70 Different ordering guarantees can be considered: 71 - Hook order guarantee: a hook for a plugin will be called after its previous hooks succeeded. 72 - Steps order guarantee: hooks will be called when all plugins have finished the previous hook. 73 - Plugin order guarantee: same hook for each plugin will be called in the order specified 74 by the plugin position at the plugin chain. 75 76 All of the hooks will offer plugin order guarantee, as they all modify/update some item so the order 77 of plugins is important. Execution hooks need to guarantee step order, as the items that are being modified 78 in each step (config, resource, and filesystem) are also needed in the following steps. This is not true for 79 initialization hooks that modify items (metadata and flagset) that are only used in their own methods, 80 so they only need to guarantee hook order. 81 82 Execution hooks will be able to return an error. A specific error can be returned to specify that 83 no further hooks of this plugin should be called, but that the scaffold process should be continued. 84 This enables plugins to exit early, e.g., a plugin that scaffolds some files only for cluster-scoped 85 resources can detect if the resource is cluster-scoped at one of the first execution steps, and 86 therefore, use this error to tell the CLI that no further execution step should be called for itself. 87 88 ### Initialization hooks 89 90 #### Update metadata 91 This hook will be used for two purposes. It provides CLI-related metadata to the Subcommand (e.g., 92 command name) and update the subcommands metadata such as the description or examples. 93 94 - Required/optional 95 - [ ] Required 96 - [x] Optional 97 - Subcommands 98 - [x] Init 99 - [x] Edit 100 - [x] Create API 101 - [x] Create webhook 102 103 #### Bind flags 104 This hook will allow subcommands to define specific flags. 105 106 - Required/optional 107 - [ ] Required 108 - [x] Optional 109 - Subcommands 110 - [x] Init 111 - [x] Edit 112 - [x] Create API 113 - [x] Create webhook 114 115 ### Execution methods 116 117 #### Inject configuration 118 This hook will be used to inject the `Config` object that the plugin can modify at will. 119 The CLI will create/load/save this configuration object. 120 121 - Required/optional 122 - [ ] Required 123 - [x] Optional 124 - Subcommands 125 - [x] Init 126 - [x] Edit 127 - [x] Create API 128 - [x] Create webhook 129 130 #### Inject resource 131 This hook will be used to inject the `Resource` object created by the CLI. 132 133 - Required/optional 134 - [x] Required 135 - [ ] Optional 136 - Subcommands 137 - [ ] Init 138 - [ ] Edit 139 - [x] Create API 140 - [x] Create webhook 141 142 #### Pre-scaffold 143 This hook will be used to take actions before the main scaffolding is performed, e.g. validations. 144 145 NOTE: a filesystem abstraction will be passed to this hook, but it should not be used for scaffolding. 146 147 - Required/optional 148 - [ ] Required 149 - [x] Optional 150 - Subcommands 151 - [x] Init 152 - [x] Edit 153 - [x] Create API 154 - [x] Create webhook 155 156 #### Scaffold 157 This hook will be used to perform the main scaffolding. 158 159 NOTE: a filesystem abstraction will be passed to this hook that must be used for scaffolding. 160 161 - Required/optional 162 - [x] Required 163 - [ ] Optional 164 - Subcommands 165 - [x] Init 166 - [x] Edit 167 - [x] Create API 168 - [x] Create webhook 169 170 #### Post-scaffold 171 This hook will be used to take actions after the main scaffolding is performed, e.g. cleanup. 172 173 NOTE: a filesystem abstraction will **NOT** be passed to this hook, as post-scaffold task do not require it. 174 In case some post-scaffold task requires a filesystem abstraction, it could be added. 175 176 NOTE 2: the project configuration is saved by the CLI before calling this hook, so changes done to the 177 configuration at this hook will not be persisted. 178 179 - Required/optional 180 - [ ] Required 181 - [x] Optional 182 - Subcommands 183 - [x] Init 184 - [x] Edit 185 - [x] Create API 186 - [x] Create webhook 187 188 ### Override plugins for single subcommand calls 189 190 Defining plugins at initialization and using them for every command call will solve most of the cases. 191 However, there are some cases where a plugin may be wanted just for a certain subcommand call. For 192 example, a project with multiple controllers may want to follow the declarative pattern in only one of 193 their controllers. The other case is also relevant, a project where most of the controllers follow the 194 declarative pattern may need a single controller not to follow it. 195 196 In order to achieve this, the `--plugins` flag will be allowed in every command call, overriding the 197 value used in its corresponging project initialization call. 198 199 ### Plugin chain persistence 200 201 Currently, the project configuration v3 offers two mechanisms for storing plugin-related information. 202 203 - A layout field (`string`) that is used for plugin resolution on initialized projects. 204 - A plugin field (`map[string]interface{}`) that is used for plugin configuration raw storage. 205 206 Plugin resolution uses the `layout` field to resolve plugins. In this phase, it has to store a plugin 207 chain and not a single plugin. As this value is stored as a string, comma-separated representation can 208 be used to represent a chain of plugins instead. 209 210 NOTE: commas are not allowed in the plugin key. 211 212 While the `plugin` field may seem like a better fit to store the plugin chain, as it can already 213 contain multiple values, there are several issues with this alternative approach: 214 - A map does not provide any order guarantee, and the plugin chain order is relevant. 215 - Some plugins do not store plugin-specific configuration information, e.g. the `go`-plugins. So 216 the absence of a plugin key doesn't mean that the plugin is not part of the plugin chain. 217 - The desire of running a different set of plugins for a single subcommand call has already been 218 mentioned. Some of these out-of-chain plugins may need to store plugin-specific configuration, 219 so the presence of a plugin doesn't mean that is part of the plugin chain. 220 221 The next project configuration version could consider this new requirements to define the 222 names/types of these two fields. 223 224 ### Plugin bundle 225 226 As a side-effect of plugin chaining, the user experience may suffer if they need to provide 227 several plugin keys for the `--plugins` flag. Additionally, this would also mean a user-facing 228 important breaking change. 229 230 In order to solve this issue, a plugin bundle concept will be introduced. A plugin bundle 231 behaves as a plugin: 232 - It has a name: provided at creation. 233 - It has a version: provided at creation. 234 - It has a list of supported project versions: computed from the common supported project 235 versions of all the plugins in the bundled. 236 237 Instead of implementing the optional getter methods that return a subcommand, it offers a way 238 to retrieve the list of bundled plugins. This process will be done after plugin resolution. 239 240 This way, CLIs will be able to define bundles, which will be used in the user-facing API and 241 the plugin resolution process, but later they will be treated as separate plugins offering 242 the maintainability and separation of concerns advantages that smaller plugins have in 243 comparison with bigger monolithic plugins. 244 245 ## Implementation 246 247 The following types are used as input/output values of the described hooks: 248 ```go 249 // CLIMetadata is the runtime meta-data of the CLI 250 type CLIMetadata struct { 251 // CommandName is the root command name. 252 CommandName string 253 } 254 255 // SubcommandMetadata is the runtime meta-data for a subcommand 256 type SubcommandMetadata struct { 257 // Description is a description of what this subcommand does. It is used to display help. 258 Description string 259 // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. 260 Examples string 261 } 262 263 type ExitError struct { 264 Plugin string 265 Reason string 266 } 267 268 func (e ExitError) Error() string { 269 return fmt.Sprintf("plugin %s exit early: %s", e.Plugin, e.Reason) 270 } 271 ``` 272 273 The described hooks are implemented through the use of the following interfaces. 274 ```go 275 type RequiresCLIMetadata interface { 276 InjectCLIMetadata(CLIMetadata) 277 } 278 279 type UpdatesSubcommandMetadata interface { 280 UpdateSubcommandMetadata(*SubcommandMetadata) 281 } 282 283 type HasFlags interface { 284 BindFlags(*pflag.FlagSet) 285 } 286 287 type RequiresConfig interface { 288 InjectConfig(config.Config) error 289 } 290 291 type RequiresResource interface { 292 InjectResource(*resource.Resource) error 293 } 294 295 type HasPreScaffold interface { 296 PreScaffold(machinery.Filesystem) error 297 } 298 299 type Scaffolder interface { 300 Scaffold(machinery.Filesystem) error 301 } 302 303 type HasPostScaffold interface { 304 PostScaffold() error 305 } 306 ``` 307 308 Additional interfaces define the required method for each type of plugin: 309 ```go 310 // InitSubcommand is the specific interface for subcommands returned by init plugins. 311 type InitSubcommand interface { 312 Scaffolder 313 } 314 315 // EditSubcommand is the specific interface for subcommands returned by edit plugins. 316 type EditSubcommand interface { 317 Scaffolder 318 } 319 320 // CreateAPISubcommand is the specific interface for subcommands returned by create API plugins. 321 type CreateAPISubcommand interface { 322 RequiresResource 323 Scaffolder 324 } 325 326 // CreateWebhookSubcommand is the specific interface for subcommands returned by create webhook plugins. 327 type CreateWebhookSubcommand interface { 328 RequiresResource 329 Scaffolder 330 } 331 ``` 332 333 An additional interface defines the bundle method to return the wrapped plugins: 334 ```go 335 type Bundle interface { 336 Plugin 337 Plugins() []Plugin 338 } 339 ```