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  ```