Skip to content

Commit 620c3bf

Browse files
Copilotcamilamacedo86
authored andcommitted
(feat): Add PluginConfig to the external plugins API:
1 parent c580bca commit 620c3bf

File tree

10 files changed

+522
-19
lines changed

10 files changed

+522
-19
lines changed

docs/book/src/plugins/extending/external-plugins.md

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,19 @@ standard I/O. Any language can be used to create the plugin, as long
2626
as it follows the [PluginRequest][code-plugin-external] and [PluginResponse][code-plugin-external]
2727
structures.
2828

29-
### PluginRequest
30-
3129
`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`.
3230

31+
**Fields:**
32+
- `apiVersion`: Version of the PluginRequest schema.
33+
- `args`: Command-line arguments passed to the plugin.
34+
- `command`: The subcommand being executed (e.g., `init`, `create api`, `create webhook`, `edit`).
35+
- `universe`: Map of file paths to contents, updated across the plugin chain.
36+
- `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`).
37+
- `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.
38+
39+
40+
**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.
41+
3342
**Example `PluginRequest` (triggered by `kubebuilder init --plugins go/v4,sampleexternalplugin/v1 --domain my.domain`):**
3443

3544
```json
@@ -42,12 +51,25 @@ structures.
4251
}
4352
```
4453

45-
**Fields:**
46-
- `apiVersion`: Version of the PluginRequest schema.
47-
- `args`: Command-line arguments passed to the plugin.
48-
- `command`: The subcommand being executed (e.g., `init`, `create api`, `create webhook`, `edit`).
49-
- `universe`: Map of file paths to contents, updated across the plugin chain.
50-
- `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.
54+
**Example `PluginRequest` for `create api` (includes `config`):**
55+
```json
56+
{
57+
"apiVersion": "v1alpha1",
58+
"args": ["--group", "crew", "--version", "v1", "--kind", "Captain"],
59+
"command": "create api",
60+
"universe": {},
61+
"pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"],
62+
"config": {
63+
"domain": "my.domain",
64+
"repo": "github.com/example/my-project",
65+
"projectName": "my-project",
66+
"version": "3",
67+
"layout": ["go.kubebuilder.io/v4"],
68+
"multigroup": false,
69+
"resources": []
70+
}
71+
}
72+
```
5173

5274
### PluginResponse
5375

pkg/cli/cli_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,18 @@ func hasSubCommand(cmd *cobra.Command, name string) bool {
7777
return false
7878
}
7979

80+
type pluginChainCapturingSubcommand struct {
81+
pluginChain []string
82+
}
83+
84+
func (s *pluginChainCapturingSubcommand) Scaffold(machinery.Filesystem) error {
85+
return nil
86+
}
87+
88+
func (s *pluginChainCapturingSubcommand) SetPluginChain(chain []string) {
89+
s.pluginChain = append([]string(nil), chain...)
90+
}
91+
8092
var _ = Describe("CLI", func() {
8193
var (
8294
c *CLI
@@ -431,6 +443,42 @@ plugins:
431443
})
432444
})
433445

446+
Context("applySubcommandHooks", func() {
447+
var (
448+
cmd *cobra.Command
449+
sub1, sub2 *pluginChainCapturingSubcommand
450+
tuples []keySubcommandTuple
451+
chainKeys []string
452+
)
453+
454+
BeforeEach(func() {
455+
cmd = &cobra.Command{}
456+
sub1 = &pluginChainCapturingSubcommand{}
457+
sub2 = &pluginChainCapturingSubcommand{}
458+
tuples = []keySubcommandTuple{
459+
{key: "alpha.kubebuilder.io/v1", subcommand: sub1},
460+
{key: "beta.kubebuilder.io/v1", subcommand: sub2},
461+
}
462+
chainKeys = []string{"alpha.kubebuilder.io/v1", "beta.kubebuilder.io/v1"}
463+
})
464+
465+
It("sets the plugin chain on subcommands", func() {
466+
c.applySubcommandHooks(cmd, tuples, "test", false)
467+
468+
Expect(sub1.pluginChain).To(Equal(chainKeys))
469+
Expect(sub2.pluginChain).To(Equal(chainKeys))
470+
})
471+
472+
It("sets the plugin chain when creating a new configuration", func() {
473+
c.resolvedPlugins = makeMockPluginsFor(projectVersion, chainKeys...)
474+
475+
c.applySubcommandHooks(cmd, tuples, "test", true)
476+
477+
Expect(sub1.pluginChain).To(Equal(chainKeys))
478+
Expect(sub2.pluginChain).To(Equal(chainKeys))
479+
})
480+
})
481+
434482
Context("New", func() {
435483
var c *CLI
436484
var err error

pkg/cli/cmd_helpers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ type keySubcommandTuple struct {
7272
skip bool
7373
}
7474

75+
type pluginChainSetter interface {
76+
SetPluginChain([]string)
77+
}
78+
7579
// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins.
7680
func (c *CLI) filterSubcommands(
7781
filter func(plugin.Plugin) bool,
@@ -107,6 +111,16 @@ func (c *CLI) applySubcommandHooks(
107111
errorMessage string,
108112
createConfig bool,
109113
) {
114+
commandPluginChain := make([]string, len(subcommands))
115+
for i, tuple := range subcommands {
116+
commandPluginChain[i] = tuple.key
117+
}
118+
for _, tuple := range subcommands {
119+
if setter, ok := tuple.subcommand.(pluginChainSetter); ok {
120+
setter.SetPluginChain(commandPluginChain)
121+
}
122+
}
123+
110124
// In case we create a new project configuration we need to compute the plugin chain.
111125
pluginChain := make([]string, 0, len(c.resolvedPlugins))
112126
if createConfig {

pkg/plugin/external/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type PluginRequest struct {
4040
// This allows external plugins to know which other plugins are in use.
4141
// Format: ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"]
4242
PluginChain []string `json:"pluginChain,omitempty"`
43+
44+
// Config contains the PROJECT file config. This field may be empty if the
45+
// project is being initialized and the PROJECT file has not been created yet.
46+
Config map[string]interface{} `json:"config,omitempty"`
4347
}
4448

4549
// PluginResponse is returned to kubebuilder by the plugin and contains all files

pkg/plugins/external/api.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,33 @@ type createAPISubcommand struct {
3636
Path string
3737
Args []string
3838
pluginChain []string
39+
config config.Config
3940
}
4041

41-
// InjectConfig injects the project configuration to access plugin chain information
42+
// InjectConfig injects the project configuration so external plugins can read the PROJECT file.
4243
func (p *createAPISubcommand) InjectConfig(c config.Config) error {
43-
p.pluginChain = c.GetPluginChain()
44+
p.config = c
45+
46+
if c == nil {
47+
return nil
48+
}
49+
50+
if chain := c.GetPluginChain(); len(chain) > 0 {
51+
p.pluginChain = append([]string(nil), chain...)
52+
}
53+
4454
return nil
4555
}
4656

57+
func (p *createAPISubcommand) SetPluginChain(chain []string) {
58+
if len(chain) == 0 {
59+
p.pluginChain = nil
60+
return
61+
}
62+
63+
p.pluginChain = append([]string(nil), chain...)
64+
}
65+
4766
func (p *createAPISubcommand) InjectResource(*resource.Resource) error {
4867
// Do nothing since resource flags are passed to the external plugin directly.
4968
return nil
@@ -65,7 +84,7 @@ func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
6584
PluginChain: p.pluginChain,
6685
}
6786

68-
err := handlePluginResponse(fs, req, p.Path)
87+
err := handlePluginResponse(fs, req, p.Path, p.config)
6988
if err != nil {
7089
return err
7190
}

pkg/plugins/external/edit.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,33 @@ type editSubcommand struct {
3232
Path string
3333
Args []string
3434
pluginChain []string
35+
config config.Config
3536
}
3637

3738
// InjectConfig injects the project configuration to access plugin chain information
3839
func (p *editSubcommand) InjectConfig(c config.Config) error {
39-
p.pluginChain = c.GetPluginChain()
40+
p.config = c
41+
42+
if c == nil {
43+
return nil
44+
}
45+
46+
if chain := c.GetPluginChain(); len(chain) > 0 {
47+
p.pluginChain = append([]string(nil), chain...)
48+
}
49+
4050
return nil
4151
}
4252

53+
func (p *editSubcommand) SetPluginChain(chain []string) {
54+
if len(chain) == 0 {
55+
p.pluginChain = nil
56+
return
57+
}
58+
59+
p.pluginChain = append([]string(nil), chain...)
60+
}
61+
4362
func (p *editSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
4463
setExternalPluginMetadata("edit", p.Path, subcmdMeta)
4564
}
@@ -56,7 +75,7 @@ func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
5675
PluginChain: p.pluginChain,
5776
}
5877

59-
err := handlePluginResponse(fs, req, p.Path)
78+
err := handlePluginResponse(fs, req, p.Path, p.config)
6079
if err != nil {
6180
return err
6281
}

0 commit comments

Comments
 (0)