Skip to content

Commit 29479a4

Browse files
authored
Merge pull request #5161 from kubernetes-sigs/copilot/add-plugins-api-support
✨ (External Plugins API) Add PluginChain field to external plugin API
2 parents e9ee396 + 9a4b848 commit 29479a4

File tree

7 files changed

+175
-22
lines changed

7 files changed

+175
-22
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,25 @@ structures.
3030

3131
`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`.
3232

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

3535
```json
3636
{
3737
"apiVersion": "v1alpha1",
3838
"args": ["--domain", "my.domain"],
3939
"command": "init",
40-
"universe": {}
40+
"universe": {},
41+
"pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"]
4142
}
4243
```
4344

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.
51+
4452
### PluginResponse
4553

4654
`PluginResponse` contains the modifications made by the plugin to the project. This data is serialized as JSON and returned to Kubebuilder through `stdout`.

pkg/plugin/external/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ type PluginRequest struct {
3535
// Universe represents the modified file contents that gets updated over a series of plugin runs
3636
// across the plugin chain. Initially, it starts out as empty.
3737
Universe map[string]string `json:"universe"`
38+
39+
// PluginChain contains the full plugin chain being used for this project.
40+
// This allows external plugins to know which other plugins are in use.
41+
// Format: ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"]
42+
PluginChain []string `json:"pluginChain,omitempty"`
3843
}
3944

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

pkg/plugins/external/api.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package external
1919
import (
2020
"github.com/spf13/pflag"
2121

22+
"sigs.k8s.io/kubebuilder/v4/pkg/config"
2223
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
2324
"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
2425
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
@@ -32,8 +33,15 @@ const (
3233
)
3334

3435
type createAPISubcommand struct {
35-
Path string
36-
Args []string
36+
Path string
37+
Args []string
38+
pluginChain []string
39+
}
40+
41+
// InjectConfig injects the project configuration to access plugin chain information
42+
func (p *createAPISubcommand) InjectConfig(c config.Config) error {
43+
p.pluginChain = c.GetPluginChain()
44+
return nil
3745
}
3846

3947
func (p *createAPISubcommand) InjectResource(*resource.Resource) error {
@@ -51,9 +59,10 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
5159

5260
func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
5361
req := external.PluginRequest{
54-
APIVersion: defaultAPIVersion,
55-
Command: "create api",
56-
Args: p.Args,
62+
APIVersion: defaultAPIVersion,
63+
Command: "create api",
64+
Args: p.Args,
65+
PluginChain: p.pluginChain,
5766
}
5867

5968
err := handlePluginResponse(fs, req, p.Path)

pkg/plugins/external/edit.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package external
1919
import (
2020
"github.com/spf13/pflag"
2121

22+
"sigs.k8s.io/kubebuilder/v4/pkg/config"
2223
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
2324
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
2425
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
@@ -27,8 +28,15 @@ import (
2728
var _ plugin.EditSubcommand = &editSubcommand{}
2829

2930
type editSubcommand struct {
30-
Path string
31-
Args []string
31+
Path string
32+
Args []string
33+
pluginChain []string
34+
}
35+
36+
// InjectConfig injects the project configuration to access plugin chain information
37+
func (p *editSubcommand) InjectConfig(c config.Config) error {
38+
p.pluginChain = c.GetPluginChain()
39+
return nil
3240
}
3341

3442
func (p *editSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
@@ -41,9 +49,10 @@ func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
4149

4250
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
4351
req := external.PluginRequest{
44-
APIVersion: defaultAPIVersion,
45-
Command: "edit",
46-
Args: p.Args,
52+
APIVersion: defaultAPIVersion,
53+
Command: "edit",
54+
Args: p.Args,
55+
PluginChain: p.pluginChain,
4756
}
4857

4958
err := handlePluginResponse(fs, req, p.Path)

pkg/plugins/external/external_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ func (m *mockInValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, err
5959
return nil, fmt.Errorf("error getting exec command output")
6060
}
6161

62+
type mockPluginChainCaptureGetter struct {
63+
capturedChain *[]string
64+
}
65+
66+
var _ ExecOutputGetter = &mockPluginChainCaptureGetter{}
67+
68+
func (m *mockPluginChainCaptureGetter) GetExecOutput(request []byte, _ string) ([]byte, error) {
69+
// Parse the request to capture the plugin chain
70+
var req external.PluginRequest
71+
if err := json.Unmarshal(request, &req); err != nil {
72+
return nil, fmt.Errorf("error unmarshalling request: %w", err)
73+
}
74+
75+
// Capture the plugin chain
76+
*m.capturedChain = req.PluginChain
77+
78+
// Return a valid response
79+
return []byte(`{
80+
"command": "init",
81+
"error": false,
82+
"error_msg": "none",
83+
"universe": {"LICENSE": "Apache 2.0 License\n"}
84+
}`), nil
85+
}
86+
6287
type mockValidOsWdGetter struct{}
6388

6489
var _ OsWdGetter = &mockValidOsWdGetter{}
@@ -754,6 +779,85 @@ var _ = Describe("Run external plugin using Scaffold", func() {
754779
}
755780
})
756781
})
782+
783+
Context("PluginChain is passed to external plugin", func() {
784+
var (
785+
pluginChainCaptured []string
786+
mockOutputGetter *mockPluginChainCaptureGetter
787+
)
788+
789+
BeforeEach(func() {
790+
mockOutputGetter = &mockPluginChainCaptureGetter{
791+
capturedChain: &pluginChainCaptured,
792+
}
793+
outputGetter = mockOutputGetter
794+
currentDirGetter = &mockValidOsWdGetter{}
795+
})
796+
797+
It("should pass plugin chain to init subcommand", func() {
798+
fs := machinery.Filesystem{
799+
FS: afero.NewMemMapFs(),
800+
}
801+
802+
i := initSubcommand{
803+
Path: "test.sh",
804+
Args: []string{"--domain", "example.com"},
805+
pluginChain: []string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"},
806+
}
807+
808+
err := i.Scaffold(fs)
809+
Expect(err).ToNot(HaveOccurred())
810+
Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"}))
811+
})
812+
813+
It("should pass plugin chain to create api subcommand", func() {
814+
fs := machinery.Filesystem{
815+
FS: afero.NewMemMapFs(),
816+
}
817+
818+
c := createAPISubcommand{
819+
Path: "test.sh",
820+
Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"},
821+
pluginChain: []string{"go.kubebuilder.io/v4"},
822+
}
823+
824+
err := c.Scaffold(fs)
825+
Expect(err).ToNot(HaveOccurred())
826+
Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4"}))
827+
})
828+
829+
It("should pass plugin chain to create webhook subcommand", func() {
830+
fs := machinery.Filesystem{
831+
FS: afero.NewMemMapFs(),
832+
}
833+
834+
w := createWebhookSubcommand{
835+
Path: "test.sh",
836+
Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"},
837+
pluginChain: []string{"go.kubebuilder.io/v3"},
838+
}
839+
840+
err := w.Scaffold(fs)
841+
Expect(err).ToNot(HaveOccurred())
842+
Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v3"}))
843+
})
844+
845+
It("should pass plugin chain to edit subcommand", func() {
846+
fs := machinery.Filesystem{
847+
FS: afero.NewMemMapFs(),
848+
}
849+
850+
e := editSubcommand{
851+
Path: "test.sh",
852+
Args: []string{"--multigroup"},
853+
pluginChain: []string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"},
854+
}
855+
856+
err := e.Scaffold(fs)
857+
Expect(err).ToNot(HaveOccurred())
858+
Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"}))
859+
})
860+
})
757861
})
758862

759863
func getFlags() []external.Flag {

pkg/plugins/external/init.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package external
1919
import (
2020
"github.com/spf13/pflag"
2121

22+
"sigs.k8s.io/kubebuilder/v4/pkg/config"
2223
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
2324
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
2425
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
@@ -27,8 +28,15 @@ import (
2728
var _ plugin.InitSubcommand = &initSubcommand{}
2829

2930
type initSubcommand struct {
30-
Path string
31-
Args []string
31+
Path string
32+
Args []string
33+
pluginChain []string
34+
}
35+
36+
// InjectConfig injects the project configuration to access plugin chain information
37+
func (p *initSubcommand) InjectConfig(c config.Config) error {
38+
p.pluginChain = c.GetPluginChain()
39+
return nil
3240
}
3341

3442
func (p *initSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
@@ -41,9 +49,10 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
4149

4250
func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
4351
req := external.PluginRequest{
44-
APIVersion: defaultAPIVersion,
45-
Command: "init",
46-
Args: p.Args,
52+
APIVersion: defaultAPIVersion,
53+
Command: "init",
54+
Args: p.Args,
55+
PluginChain: p.pluginChain,
4756
}
4857

4958
err := handlePluginResponse(fs, req, p.Path)

pkg/plugins/external/webhook.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package external
1919
import (
2020
"github.com/spf13/pflag"
2121

22+
"sigs.k8s.io/kubebuilder/v4/pkg/config"
2223
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
2324
"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
2425
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
@@ -28,8 +29,15 @@ import (
2829
var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{}
2930

3031
type createWebhookSubcommand struct {
31-
Path string
32-
Args []string
32+
Path string
33+
Args []string
34+
pluginChain []string
35+
}
36+
37+
// InjectConfig injects the project configuration to access plugin chain information
38+
func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
39+
p.pluginChain = c.GetPluginChain()
40+
return nil
3341
}
3442

3543
func (p *createWebhookSubcommand) InjectResource(*resource.Resource) error {
@@ -47,9 +55,10 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
4755

4856
func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error {
4957
req := external.PluginRequest{
50-
APIVersion: defaultAPIVersion,
51-
Command: "create webhook",
52-
Args: p.Args,
58+
APIVersion: defaultAPIVersion,
59+
Command: "create webhook",
60+
Args: p.Args,
61+
PluginChain: p.pluginChain,
5362
}
5463

5564
err := handlePluginResponse(fs, req, p.Path)

0 commit comments

Comments
 (0)