Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions docs/book/src/plugins/extending/external-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ standard I/O. Any language can be used to create the plugin, as long
as it follows the [PluginRequest][code-plugin-external] and [PluginResponse][code-plugin-external]
structures.

### PluginRequest

`PluginRequest` contains the data collected from the CLI and any previously executed plugins. Kubebuilder sends this data as a JSON object to the external plugin via `stdin`.

**Fields:**
- `apiVersion`: Version of the PluginRequest schema.
- `args`: Command-line arguments passed to the plugin.
- `command`: The subcommand being executed (e.g., `init`, `create api`, `create webhook`, `edit`).
- `universe`: Map of file paths to contents, updated across the plugin chain.
- `pluginChain` (optional): Array of plugin keys in the order they were executed. External plugins can inspect this to tailor behavior based on other plugins that ran (for example, `go.kubebuilder.io/v4` or `kustomize.common.kubebuilder.io/v2`).
- `config` (optional): Serialized PROJECT file configuration for the current project. Use it to inspect metadata, existing resources, or plugin-specific settings. Kubebuilder omits this field before the PROJECT file exists—typically during the first `init`—so plugins should check for its presence.


**Note:** Whenever Kubebuilder has a PROJECT file available (for example during `create api`, `create webhook`, `edit`, or a subsequent `init` run), `PluginRequest` includes the `config` field. During the very first `init` run the field is omitted because the PROJECT file does not exist yet.

**Example `PluginRequest` (triggered by `kubebuilder init --plugins go/v4,sampleexternalplugin/v1 --domain my.domain`):**

```json
Expand All @@ -42,12 +51,25 @@ structures.
}
```

**Fields:**
- `apiVersion`: Version of the PluginRequest schema.
- `args`: Command-line arguments passed to the plugin.
- `command`: The subcommand being executed (e.g., `init`, `create api`, `create webhook`, `edit`).
- `universe`: Map of file paths to contents, updated across the plugin chain.
- `pluginChain` (optional): Array of plugin keys in the chain. This allows external plugins to determine which other plugins are being used. For example, a plugin can check if `go.kubebuilder.io/v4` or `go.kubebuilder.io/v3` is in the chain to adjust its scaffolding accordingly.
**Example `PluginRequest` for `create api` (includes `config`):**
```json
{
"apiVersion": "v1alpha1",
"args": ["--group", "crew", "--version", "v1", "--kind", "Captain"],
"command": "create api",
"universe": {},
"pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"],
"config": {
"domain": "my.domain",
"repo": "github.com/example/my-project",
"projectName": "my-project",
"version": "3",
"layout": ["go.kubebuilder.io/v4"],
"multigroup": false,
"resources": []
}
}
```

### PluginResponse

Expand Down
48 changes: 48 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ func hasSubCommand(cmd *cobra.Command, name string) bool {
return false
}

type pluginChainCapturingSubcommand struct {
pluginChain []string
}

func (s *pluginChainCapturingSubcommand) Scaffold(machinery.Filesystem) error {
return nil
}

func (s *pluginChainCapturingSubcommand) SetPluginChain(chain []string) {
s.pluginChain = append([]string(nil), chain...)
}

var _ = Describe("CLI", func() {
var (
c *CLI
Expand Down Expand Up @@ -431,6 +443,42 @@ plugins:
})
})

Context("applySubcommandHooks", func() {
var (
cmd *cobra.Command
sub1, sub2 *pluginChainCapturingSubcommand
tuples []keySubcommandTuple
chainKeys []string
)

BeforeEach(func() {
cmd = &cobra.Command{}
sub1 = &pluginChainCapturingSubcommand{}
sub2 = &pluginChainCapturingSubcommand{}
tuples = []keySubcommandTuple{
{key: "alpha.kubebuilder.io/v1", subcommand: sub1},
{key: "beta.kubebuilder.io/v1", subcommand: sub2},
}
chainKeys = []string{"alpha.kubebuilder.io/v1", "beta.kubebuilder.io/v1"}
})

It("sets the plugin chain on subcommands", func() {
c.applySubcommandHooks(cmd, tuples, "test", false)

Expect(sub1.pluginChain).To(Equal(chainKeys))
Expect(sub2.pluginChain).To(Equal(chainKeys))
})

It("sets the plugin chain when creating a new configuration", func() {
c.resolvedPlugins = makeMockPluginsFor(projectVersion, chainKeys...)

c.applySubcommandHooks(cmd, tuples, "test", true)

Expect(sub1.pluginChain).To(Equal(chainKeys))
Expect(sub2.pluginChain).To(Equal(chainKeys))
})
})

Context("New", func() {
var c *CLI
var err error
Expand Down
14 changes: 14 additions & 0 deletions pkg/cli/cmd_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ type keySubcommandTuple struct {
skip bool
}

type pluginChainSetter interface {
SetPluginChain([]string)
}

// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins.
func (c *CLI) filterSubcommands(
filter func(plugin.Plugin) bool,
Expand Down Expand Up @@ -107,6 +111,16 @@ func (c *CLI) applySubcommandHooks(
errorMessage string,
createConfig bool,
) {
commandPluginChain := make([]string, len(subcommands))
for i, tuple := range subcommands {
commandPluginChain[i] = tuple.key
}
for _, tuple := range subcommands {
if setter, ok := tuple.subcommand.(pluginChainSetter); ok {
setter.SetPluginChain(commandPluginChain)
}
}

// In case we create a new project configuration we need to compute the plugin chain.
pluginChain := make([]string, 0, len(c.resolvedPlugins))
if createConfig {
Expand Down
4 changes: 4 additions & 0 deletions pkg/plugin/external/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type PluginRequest struct {
// This allows external plugins to know which other plugins are in use.
// Format: ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"]
PluginChain []string `json:"pluginChain,omitempty"`

// Config contains the PROJECT file config. This field may be empty if the
// project is being initialized and the PROJECT file has not been created yet.
Config map[string]interface{} `json:"config,omitempty"`
}

// PluginResponse is returned to kubebuilder by the plugin and contains all files
Expand Down
25 changes: 22 additions & 3 deletions pkg/plugins/external/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,33 @@ type createAPISubcommand struct {
Path string
Args []string
pluginChain []string
config config.Config
}

// InjectConfig injects the project configuration to access plugin chain information
// InjectConfig injects the project configuration so external plugins can read the PROJECT file.
func (p *createAPISubcommand) InjectConfig(c config.Config) error {
p.pluginChain = c.GetPluginChain()
p.config = c

if c == nil {
return nil
}

if chain := c.GetPluginChain(); len(chain) > 0 {
p.pluginChain = append([]string(nil), chain...)
}

return nil
}

func (p *createAPISubcommand) SetPluginChain(chain []string) {
if len(chain) == 0 {
p.pluginChain = nil
return
}

p.pluginChain = append([]string(nil), chain...)
}

func (p *createAPISubcommand) InjectResource(*resource.Resource) error {
// Do nothing since resource flags are passed to the external plugin directly.
return nil
Expand All @@ -65,7 +84,7 @@ func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
PluginChain: p.pluginChain,
}

err := handlePluginResponse(fs, req, p.Path)
err := handlePluginResponse(fs, req, p.Path, p.config)
if err != nil {
return err
}
Expand Down
23 changes: 21 additions & 2 deletions pkg/plugins/external/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,33 @@ type editSubcommand struct {
Path string
Args []string
pluginChain []string
config config.Config
}

// InjectConfig injects the project configuration to access plugin chain information
func (p *editSubcommand) InjectConfig(c config.Config) error {
p.pluginChain = c.GetPluginChain()
p.config = c

if c == nil {
return nil
}

if chain := c.GetPluginChain(); len(chain) > 0 {
p.pluginChain = append([]string(nil), chain...)
}

return nil
}

func (p *editSubcommand) SetPluginChain(chain []string) {
if len(chain) == 0 {
p.pluginChain = nil
return
}

p.pluginChain = append([]string(nil), chain...)
}

func (p *editSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
setExternalPluginMetadata("edit", p.Path, subcmdMeta)
}
Expand All @@ -56,7 +75,7 @@ func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
PluginChain: p.pluginChain,
}

err := handlePluginResponse(fs, req, p.Path)
err := handlePluginResponse(fs, req, p.Path, p.config)
if err != nil {
return err
}
Expand Down
Loading