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
12 changes: 9 additions & 3 deletions pkg/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,9 @@ func kubebuilderCreate(s store.Store) error {
// Migrates the Grafana plugin.
func migrateGrafanaPlugin(s store.Store, src, des string) error {
var grafanaPlugin struct{}
err := s.Config().DecodePluginConfig(plugin.KeyFor(grafanav1alpha.Plugin{}), grafanaPlugin)
// Use GetPluginKeyForConfig to support custom bundle names
key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), grafanav1alpha.Plugin{})
err := s.Config().DecodePluginConfig(key, grafanaPlugin)
if errors.As(err, &config.PluginKeyNotFoundError{}) {
slog.Info("Grafana plugin not found, skipping migration")
return nil
Expand All @@ -261,7 +263,9 @@ func migrateGrafanaPlugin(s store.Store, src, des string) error {

func migrateAutoUpdatePlugin(s store.Store) error {
var autoUpdatePlugin struct{}
err := s.Config().DecodePluginConfig(plugin.KeyFor(autoupdatev1alpha.Plugin{}), autoUpdatePlugin)
// Use GetPluginKeyForConfig to support custom bundle names
key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), autoupdatev1alpha.Plugin{})
err := s.Config().DecodePluginConfig(key, autoUpdatePlugin)
if errors.As(err, &config.PluginKeyNotFoundError{}) {
slog.Info("Auto Update plugin not found, skipping migration")
return nil
Expand All @@ -279,7 +283,9 @@ func migrateAutoUpdatePlugin(s store.Store) error {
// Migrates the Deploy Image plugin.
func migrateDeployImagePlugin(s store.Store) error {
var deployImagePlugin deployimagev1alpha1.PluginConfig
err := s.Config().DecodePluginConfig(plugin.KeyFor(deployimagev1alpha1.Plugin{}), &deployImagePlugin)
// Use GetPluginKeyForConfig to support custom bundle names
key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), deployimagev1alpha1.Plugin{})
err := s.Config().DecodePluginConfig(key, &deployImagePlugin)
if errors.As(err, &config.PluginKeyNotFoundError{}) {
slog.Info("Deploy-image plugin not found, skipping migration")
return nil
Expand Down
53 changes: 53 additions & 0 deletions pkg/plugin/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,59 @@ func SplitKey(key string) (string, string) {
return keyParts[0], keyParts[1]
}

// GetPluginKeyForConfig searches for a plugin key in the plugin chain that should be used for storing
// plugin configuration. When a plugin is wrapped in a bundle with a custom name, the bundle's key will
// be in the plugin chain instead of the plugin's own key.
//
// The function first tries to find the plugin's own key (via KeyFor). If not found, it looks for a key
// with a matching name prefix (before the domain) and version. This handles the case where a plugin like
// "deploy-image.go.kubebuilder.io/v1-alpha" is wrapped in a bundle named "deploy-image.my-domain/v1-alpha".
//
// If no match is found in the plugin chain, it falls back to using the plugin's own key.
func GetPluginKeyForConfig(pluginChain []string, p Plugin) string {
pluginKey := KeyFor(p)

// First, try exact match
for _, key := range pluginChain {
if key == pluginKey {
return pluginKey
}
}

// If no exact match, look for a key with matching base name and version
// This handles custom bundles that wrap the plugin
pluginName, _ := SplitKey(pluginKey)
pluginVersion := p.Version().String()

// Extract the base name (part before the domain)
// E.g., "deploy-image.go.kubebuilder.io" -> "deploy-image"
baseName := pluginName
if idx := strings.Index(pluginName, "."); idx != -1 {
baseName = pluginName[:idx]
}

for _, key := range pluginChain {
name, version := SplitKey(key)
if version != pluginVersion {
continue
}

// Check if this key has the same base name
// E.g., "deploy-image.my-domain" has base name "deploy-image"
keyBaseName := name
if idx := strings.Index(name, "."); idx != -1 {
keyBaseName = name[:idx]
}

if keyBaseName == baseName {
return key
}
}

// Fall back to the plugin's own key if no match found
return pluginKey
}

// Validate ensures a Plugin is valid.
func Validate(p Plugin) error {
if err := validateName(p.Name()); err != nil {
Expand Down
73 changes: 73 additions & 0 deletions pkg/plugin/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,76 @@ var _ = Describe("CommonSupportedProjectVersions", func() {
}
})
})

var _ = Describe("GetPluginKeyForConfig", func() {
It("should return the plugin's own key when it's in the plugin chain", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
"deploy-image.go.kubebuilder.io/v1-alpha",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha"))
})

It("should return the bundle key when plugin is wrapped in a bundle", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
"deploy-image.my-domain/v1-alpha",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.my-domain/v1-alpha"))
})

It("should fallback to plugin's own key when no match in chain", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha"))
})

It("should match on base name and version", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
"deploy-image.operator-sdk.io/v1-alpha",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.operator-sdk.io/v1-alpha"))
})

It("should not match if version differs", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
"deploy-image.my-domain/v2-alpha",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha"))
})

It("should not match if base name differs", func() {
plugin := mockPlugin{
name: "deploy-image.go.kubebuilder.io",
version: Version{Number: 1, Stage: stage.Alpha},
}
pluginChain := []string{
"go.kubebuilder.io/v4",
"other-plugin.my-domain/v1-alpha",
}
Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha"))
})
})
8 changes: 6 additions & 2 deletions pkg/plugins/golang/deploy-image/v1alpha1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,12 @@ func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
}

// Track the resources following a declarative approach
// Get the correct plugin key from the plugin chain - this handles the case where
// this plugin is wrapped in a bundle with a custom name (e.g., in operator-sdk)
key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})

cfg := PluginConfig{}
if err = p.config.DecodePluginConfig(pluginKey, &cfg); errors.As(err, &config.UnsupportedFieldError{}) {
if err = p.config.DecodePluginConfig(key, &cfg); errors.As(err, &config.UnsupportedFieldError{}) {
// Skip tracking as the config doesn't support per-plugin configuration
return nil
} else if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
Expand All @@ -213,7 +217,7 @@ func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
Options: configDataOptions,
})

if err = p.config.EncodePluginConfig(pluginKey, cfg); err != nil {
if err = p.config.EncodePluginConfig(key, cfg); err != nil {
return fmt.Errorf("error encoding plugin configuration: %w", err)
}

Expand Down
1 change: 0 additions & 1 deletion pkg/plugins/golang/deploy-image/v1alpha1/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const pluginName = "deploy-image." + golang.DefaultNameQualifier
var (
pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
supportedProjectVersions = []config.Version{cfgv3.Version}
pluginKey = plugin.KeyFor(Plugin{})
)

var _ plugin.CreateAPI = Plugin{}
Expand Down
83 changes: 83 additions & 0 deletions pkg/plugins/golang/deploy-image/v1alpha1/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"testing"

"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
)

// TestGetPluginKeyForConfigIntegration tests that the plugin correctly resolves
// its key based on the plugin chain, supporting custom bundle names.
func TestGetPluginKeyForConfigIntegration(t *testing.T) {
p := Plugin{}

tests := []struct {
name string
pluginChain []string
expected string
description string
}{
{
name: "exact match",
pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.go.kubebuilder.io/v1-alpha"},
expected: "deploy-image.go.kubebuilder.io/v1-alpha",
description: "When plugin is used directly, it should use its own key",
},
{
name: "bundle match with custom domain",
pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.custom-domain/v1-alpha"},
expected: "deploy-image.custom-domain/v1-alpha",
description: "When plugin is wrapped in bundle with custom domain, it should use bundle's key",
},
{
name: "bundle match with operator-sdk domain",
pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.operator-sdk.io/v1-alpha"},
expected: "deploy-image.operator-sdk.io/v1-alpha",
description: "When plugin is wrapped in operator-sdk bundle, it should use bundle's key",
},
{
name: "no match - fallback to plugin key",
pluginChain: []string{"go.kubebuilder.io/v4"},
expected: "deploy-image.go.kubebuilder.io/v1-alpha",
description: "When no matching key in chain, fallback to plugin's own key",
},
{
name: "version mismatch - fallback",
pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.custom-domain/v2-alpha"},
expected: "deploy-image.go.kubebuilder.io/v1-alpha",
description: "When version doesn't match, fallback to plugin's own key",
},
{
name: "base name mismatch - fallback",
pluginChain: []string{"go.kubebuilder.io/v4", "other-plugin.custom-domain/v1-alpha"},
expected: "deploy-image.go.kubebuilder.io/v1-alpha",
description: "When base name doesn't match, fallback to plugin's own key",
},
}

for _, tt := range tests {
// capture range variable
t.Run(tt.name, func(t *testing.T) {
result := plugin.GetPluginKeyForConfig(tt.pluginChain, p)
if result != tt.expected {
t.Errorf("%s: Expected key %q, got %q", tt.description, tt.expected, result)
}
})
}
}
2 changes: 1 addition & 1 deletion pkg/plugins/optional/autoupdate/v1alpha/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *

subcmdMeta.Examples = fmt.Sprintf(` # Edit a common project with this plugin
%[1]s edit --plugins=%[2]s
`, cliMeta.CommandName, pluginKey)
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugins/optional/autoupdate/v1alpha/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *

subcmdMeta.Examples = fmt.Sprintf(` # Initialize a common project with this plugin
%[1]s init --plugins=%[2]s
`, cliMeta.CommandName, pluginKey)
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
Expand Down
8 changes: 5 additions & 3 deletions pkg/plugins/optional/autoupdate/v1alpha/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ const pluginName = "autoupdate." + plugins.DefaultNameQualifier
var (
pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
supportedProjectVersions = []config.Version{cfgv3.Version}
pluginKey = plugin.KeyFor(Plugin{})
)

// Plugin implements the plugin.Full interface
Expand Down Expand Up @@ -81,13 +80,16 @@ func (p Plugin) DeprecationWarning() string {

// insertPluginMetaToConfig will insert the metadata to the plugin configuration
func insertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
err := target.DecodePluginConfig(pluginKey, cfg)
// Get the correct plugin key from the plugin chain
key := plugin.GetPluginKeyForConfig(target.GetPluginChain(), Plugin{})

err := target.DecodePluginConfig(key, cfg)
if !errors.As(err, &config.UnsupportedFieldError{}) {
if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
return fmt.Errorf("error decoding plugin configuration: %w", err)
}

if err = target.EncodePluginConfig(pluginKey, cfg); err != nil {
if err = target.EncodePluginConfig(key, cfg); err != nil {
return fmt.Errorf("error encoding plugin configuration: %w", err)
}
}
Expand Down
8 changes: 6 additions & 2 deletions pkg/plugins/optional/grafana/v1alpha/commons.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ import (
"fmt"

"sigs.k8s.io/kubebuilder/v4/pkg/config"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
)

// InsertPluginMetaToConfig will insert the metadata to the plugin configuration
func InsertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
err := target.DecodePluginConfig(pluginKey, cfg)
// Get the correct plugin key from the plugin chain
key := plugin.GetPluginKeyForConfig(target.GetPluginChain(), Plugin{})

err := target.DecodePluginConfig(key, cfg)
if !errors.As(err, &config.UnsupportedFieldError{}) {
if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) {
return fmt.Errorf("error decoding plugin configuration: %w", err)
}

if err = target.EncodePluginConfig(pluginKey, cfg); err != nil {
if err = target.EncodePluginConfig(key, cfg); err != nil {
return fmt.Errorf("error encoding plugin configuration: %w", err)
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugins/optional/grafana/v1alpha/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *

subcmdMeta.Examples = fmt.Sprintf(` # Edit a common project with this plugin
%[1]s edit --plugins=%[2]s
`, cliMeta.CommandName, pluginKey)
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugins/optional/grafana/v1alpha/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *

subcmdMeta.Examples = fmt.Sprintf(` # Initialize a common project with this plugin
%[1]s init --plugins=%[2]s
`, cliMeta.CommandName, pluginKey)
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
Expand Down
1 change: 0 additions & 1 deletion pkg/plugins/optional/grafana/v1alpha/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const pluginName = "grafana." + plugins.DefaultNameQualifier
var (
pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha}
supportedProjectVersions = []config.Version{cfgv3.Version}
pluginKey = plugin.KeyFor(Plugin{})
)

// Plugin implements the plugin.Full interface
Expand Down
Loading