Skip to content
Closed
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
22 changes: 22 additions & 0 deletions docs/book/src/plugins/extending/external-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,28 @@ structures.
}
```

**Note:** When executing commands other than `init` (e.g., `create api`, `create webhook`, `edit`), the `PluginRequest` will also include a `config` field containing the PROJECT file configuration:

```json
{
"apiVersion": "v1alpha1",
"args": ["--group", "crew", "--version", "v1", "--kind", "Captain"],
"command": "create api",
"universe": {},
"config": {
"domain": "my.domain",
"repo": "github.com/example/my-project",
"projectName": "my-project",
"version": "3",
"layout": ["go.kubebuilder.io/v4"],
"multigroup": false,
"resources": []
}
}
```

The `config` field provides external plugins access to the PROJECT file configuration, enabling them to make informed decisions based on the project's settings. This field will be omitted from the JSON entirely (not present) during the `init` command if the PROJECT file has not been created yet. External plugins should check for the presence of this field before attempting to use it.

### PluginResponse

`PluginResponse` contains the modifications made by the plugin to the project. This data is serialized as JSON and returned to Kubebuilder through `stdout`.
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 @@ -35,6 +35,10 @@ type PluginRequest struct {
// Universe represents the modified file contents that gets updated over a series of plugin runs
// across the plugin chain. Initially, it starts out as empty.
Universe map[string]string `json:"universe"`

// 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
10 changes: 9 additions & 1 deletion pkg/plugins/external/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package external
import (
"github.com/spf13/pflag"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
Expand All @@ -32,6 +33,8 @@ const (
)

type createAPISubcommand struct {
config config.Config

Path string
Args []string
}
Expand All @@ -49,14 +52,19 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
bindExternalPluginFlags(fs, "api", p.Path, p.Args)
}

func (p *createAPISubcommand) InjectConfig(c config.Config) error {
p.config = c
return nil
}

func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
req := external.PluginRequest{
APIVersion: defaultAPIVersion,
Command: "create api",
Args: p.Args,
}

err := handlePluginResponse(fs, req, p.Path)
err := handlePluginResponse(fs, req, p.Path, p.config)
if err != nil {
return err
}
Expand Down
12 changes: 11 additions & 1 deletion pkg/plugins/external/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import (
"github.com/spf13/pflag"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
Expand All @@ -27,6 +28,8 @@
var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
config config.Config

Path string
Args []string
}
Expand All @@ -39,14 +42,21 @@
bindExternalPluginFlags(fs, "edit", p.Path, p.Args)
}

//nolint:dupl

Check failure on line 45 in pkg/plugins/external/edit.go

View workflow job for this annotation

GitHub Actions / golangci-lint

directive `//nolint:dupl` is unused for linter "dupl" (nolintlint)
func (p *editSubcommand) InjectConfig(c config.Config) error {
p.config = c
return nil
}

//nolint:dupl

Check failure on line 51 in pkg/plugins/external/edit.go

View workflow job for this annotation

GitHub Actions / golangci-lint

directive `//nolint:dupl` is unused for linter "dupl" (nolintlint)
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
req := external.PluginRequest{
APIVersion: defaultAPIVersion,
Command: "edit",
Args: p.Args,
}

err := handlePluginResponse(fs, req, p.Path)
err := handlePluginResponse(fs, req, p.Path, p.config)
if err != nil {
return err
}
Expand Down
150 changes: 150 additions & 0 deletions pkg/plugins/external/external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/spf13/afero"
"github.com/spf13/pflag"

v3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
Expand Down Expand Up @@ -75,6 +76,28 @@ func (m *mockInValidOsWdGetter) GetCurrentDir() (string, error) {
return "", fmt.Errorf("error getting current directory")
}

// mockConfigOutputGetter captures the request to verify config is passed
type mockConfigOutputGetter struct {
capturedRequest *external.PluginRequest
}

var _ ExecOutputGetter = &mockConfigOutputGetter{}

func (m *mockConfigOutputGetter) GetExecOutput(reqBytes []byte, _ string) ([]byte, error) {
// Capture the request for verification
m.capturedRequest = &external.PluginRequest{}
if err := json.Unmarshal(reqBytes, m.capturedRequest); err != nil {
return nil, fmt.Errorf("error unmarshalling request: %w", err)
}

return []byte(`{
"command": "init",
"error": false,
"error_msg": "none",
"universe": {"LICENSE": "Apache 2.0 License\n"}
}`), nil
}

type mockValidFlagOutputGetter struct{}

func (m *mockValidFlagOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) {
Expand Down Expand Up @@ -754,6 +777,133 @@ var _ = Describe("Run external plugin using Scaffold", func() {
}
})
})

Context("with config injection", func() {
const filePerm os.FileMode = 755
var (
pluginFileName string
args []string
f afero.File
fs machinery.Filesystem
mockGetter *mockConfigOutputGetter
cfg *v3.Cfg

err error
)

BeforeEach(func() {
mockGetter = &mockConfigOutputGetter{}
outputGetter = mockGetter
currentDirGetter = &mockValidOsWdGetter{}
fs = machinery.Filesystem{
FS: afero.NewMemMapFs(),
}

pluginFileName = "externalPlugin.sh"
pluginFilePath := filepath.Join("tmp", "externalPlugin", pluginFileName)

err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), filePerm)
Expect(err).ToNot(HaveOccurred())

f, err = fs.FS.Create(pluginFilePath)
Expect(err).ToNot(HaveOccurred())
Expect(f).ToNot(BeNil())

_, err = fs.FS.Stat(pluginFilePath)
Expect(err).ToNot(HaveOccurred())

args = []string{"--domain", "example.com"}

// Create a config instance
cfg = &v3.Cfg{
Version: v3.Version,
Domain: "test.domain",
Repository: "github.com/test/repo",
Name: "test-project",
}
})

It("should pass config to external plugin on init subcommand", func() {
i := initSubcommand{
Path: pluginFileName,
Args: args,
config: cfg,
}

err = i.Scaffold(fs)
Expect(err).ToNot(HaveOccurred())

// Verify that config was captured in the request
Expect(mockGetter.capturedRequest).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain"))
Expect(mockGetter.capturedRequest.Config["repo"]).To(Equal("github.com/test/repo"))
Expect(mockGetter.capturedRequest.Config["projectName"]).To(Equal("test-project"))
})

It("should pass config to external plugin on create api subcommand", func() {
c := createAPISubcommand{
Path: pluginFileName,
Args: args,
config: cfg,
}

err = c.Scaffold(fs)
Expect(err).ToNot(HaveOccurred())

// Verify that config was captured in the request
Expect(mockGetter.capturedRequest).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain"))
})

It("should pass config to external plugin on create webhook subcommand", func() {
c := createWebhookSubcommand{
Path: pluginFileName,
Args: args,
config: cfg,
}

err = c.Scaffold(fs)
Expect(err).ToNot(HaveOccurred())

// Verify that config was captured in the request
Expect(mockGetter.capturedRequest).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain"))
})

It("should pass config to external plugin on edit subcommand", func() {
e := editSubcommand{
Path: pluginFileName,
Args: args,
config: cfg,
}

err = e.Scaffold(fs)
Expect(err).ToNot(HaveOccurred())

// Verify that config was captured in the request
Expect(mockGetter.capturedRequest).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain"))
})

It("should handle nil config gracefully", func() {
i := initSubcommand{
Path: pluginFileName,
Args: args,
config: nil,
}

err = i.Scaffold(fs)
Expect(err).ToNot(HaveOccurred())

// Verify that request was made but config is nil
Expect(mockGetter.capturedRequest).ToNot(BeNil())
Expect(mockGetter.capturedRequest.Config).To(BeNil())
})
})
})

func getFlags() []external.Flag {
Expand Down
20 changes: 19 additions & 1 deletion pkg/plugins/external/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ import (

"github.com/spf13/afero"
"github.com/spf13/pflag"
"sigs.k8s.io/yaml"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
Expand Down Expand Up @@ -149,14 +151,30 @@ func getUniverseMap(fs machinery.Filesystem) (map[string]string, error) {
return universe, nil
}

func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string) error {
func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string, cfg config.Config) error {
var err error

req.Universe, err = getUniverseMap(fs)
if err != nil {
return fmt.Errorf("error getting universe map: %w", err)
}

// Marshal config to include in the request if config is provided
if cfg != nil {
var configData []byte
configData, err = cfg.MarshalYAML()
if err != nil {
return fmt.Errorf("error marshaling config: %w", err)
}

var configMap map[string]interface{}
if err = yaml.Unmarshal(configData, &configMap); err != nil {
return fmt.Errorf("error unmarshaling config to map: %w", err)
}

req.Config = configMap
}

res, err := makePluginRequest(req, path)
if err != nil {
return fmt.Errorf("error making request to external plugin: %w", err)
Expand Down
12 changes: 11 additions & 1 deletion pkg/plugins/external/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import (
"github.com/spf13/pflag"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
Expand All @@ -27,6 +28,8 @@
var _ plugin.InitSubcommand = &initSubcommand{}

type initSubcommand struct {
config config.Config

Path string
Args []string
}
Expand All @@ -39,14 +42,21 @@
bindExternalPluginFlags(fs, "init", p.Path, p.Args)
}

//nolint:dupl

Check failure on line 45 in pkg/plugins/external/init.go

View workflow job for this annotation

GitHub Actions / golangci-lint

directive `//nolint:dupl` is unused for linter "dupl" (nolintlint)
func (p *initSubcommand) InjectConfig(c config.Config) error {
p.config = c
return nil
}

//nolint:dupl
func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
req := external.PluginRequest{
APIVersion: defaultAPIVersion,
Command: "init",
Args: p.Args,
}

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