Skip to content

✨ helm chart directory can be configured #4891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ linters:
- goconst
Copy link
Member

@camilamacedo86 camilamacedo86 Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need to add a test to ensure all helm plugin options are set.
I haven't thought of the best way yet.
However, I think we might want to add e2e tests , similar to the others https://github.com/kubernetes-sigs/kubebuilder/tree/master/test/e2e ,to mock scenarios with helm and verify that everything is as expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regarding e2e:
do you mean, my introduced changes in the e2e-tests aren't sufficient?

do you want me to introduce a new e2e-test with a custom value for the helm output dir?
In the current impl. even the default-value will get persist in the PROJECT file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bavarianbidi

I thought about this one and IMO:

1 - We need to add the new flag to the alpha generate
2 - We need to add a new mock in alpha generation that includes the Helm plugin and will utilise this option. Then, at the end, we validate that the helm chart was output in the directory path provided. That should be enough to cover both scenarios in the scope of this PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review how the alpha is generated and its associated tests. If you need a hand after that, please feel free to ping me in the Kubebuilder Slack channel. I am happy to help out.

- gocyclo
- govet
- importas
- ineffassign
- lll
- misspell
Expand All @@ -27,6 +28,10 @@ linters:
- wrapcheck
- whitespace
settings:
importas:
alias:
- pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha
alias: helmv1alpha
ginkgolinter:
forbid-focus-container: true
forbid-spec-pollution: true
Expand Down
4 changes: 2 additions & 2 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4"
grafanav1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha"
helmv1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha"
helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha"
)

func init() {
Expand Down Expand Up @@ -62,7 +62,7 @@ func Run() {
&kustomizecommonv2.Plugin{},
&deployimagev1alpha1.Plugin{},
&grafanav1alpha1.Plugin{},
&helmv1alpha1.Plugin{},
&helmv1alpha.Plugin{},
),
cli.WithPlugins(externalPlugins...),
cli.WithDefaultPlugins(cfgv3.Version, gov4Bundle),
Expand Down
9 changes: 4 additions & 5 deletions docs/book/src/plugins/available/helm-v1-alpha.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ under the [testdata][testdata] directory on the root directory of the Kubebuilde

### Basic Usage

The Helm plugin is attached to the `init` subcommand and the `edit` subcommand:
The Helm plugin is attached to the `edit` subcommand as the `helm/v1-alpha` plugin
relies on the Go project being scaffolded first.

```sh

# Initialize a new project with helm chart
kubebuilder init --plugins=helm/v1-alpha
# Initialize a new project
kubebuilder init

# Enable or Update the helm chart via the helm plugin to an existing project
# Before run the edit command, run `make manifests` to generate the manifest under `config/`
Expand Down Expand Up @@ -80,8 +81,6 @@ The Helm plugin implements the following subcommands:

- edit (`$ kubebuilder edit [OPTIONS]`)

- init (`$ kubebuilder init [OPTIONS]`)

## Affected files

The following scaffolds will be created or updated by this plugin:
Expand Down
5 changes: 5 additions & 0 deletions pkg/cli/alpha/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,10 @@ If no output directory is provided, the current working directory will be cleane
"If unset, re-scaffolding occurs in-place "+
"and will delete existing files (except .git and PROJECT).")

scaffoldCmd.Flags().StringVar(&opts.HelmDirectory, "helm-output-dir", "",
"Directory where the new project scaffold will be written. "+
"If unset, re-scaffolding occurs in-place "+
"and will delete existing files (except .git and PROJECT).")

return scaffoldCmd
}
56 changes: 45 additions & 11 deletions pkg/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ import (
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha"
hemlv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha"
grafanav1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha"
helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha"
)

// Generate store the required info for the command
type Generate struct {
InputDir string
OutputDir string
InputDir string
OutputDir string
HelmDirectory string
}

// Generate handles the migration and scaffolding process.
Expand Down Expand Up @@ -110,8 +111,8 @@ func (opts *Generate) Generate() error {
}

if hasHelmPlugin(projectConfig) {
if err = kubebuilderHelmEdit(); err != nil {
return fmt.Errorf("error editing Helm plugin: %w", err)
if err = migrateHelmPlugin(projectConfig, opts.HelmDirectory); err != nil {
return fmt.Errorf("error migrating Helm plugin: %w", err)
}
}

Expand Down Expand Up @@ -226,7 +227,7 @@ 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(v1alpha.Plugin{}), grafanaPlugin)
err := s.Config().DecodePluginConfig(plugin.KeyFor(grafanav1alpha.Plugin{}), grafanaPlugin)
if errors.As(err, &config.PluginKeyNotFoundError{}) {
log.Info("Grafana plugin not found, skipping migration")
return nil
Expand Down Expand Up @@ -481,28 +482,61 @@ func grafanaConfigMigrate(src, des string) error {

// Edits the project to include the Grafana plugin.
func kubebuilderGrafanaEdit() error {
args := []string{"edit", "--plugins", plugin.KeyFor(v1alpha.Plugin{})}
args := []string{"edit", "--plugins", plugin.KeyFor(grafanav1alpha.Plugin{})}
if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil {
return fmt.Errorf("failed to run edit subcommand for Grafana plugin: %w", err)
}
return nil
}

// Migrates the Helm plugin.
func migrateHelmPlugin(s store.Store, targetDir string) error {
var helmPlugin helmv1alpha.PluginConfig

err := s.Config().DecodePluginConfig(plugin.KeyFor(helmv1alpha.Plugin{}), &helmPlugin)
if errors.As(err, &config.PluginKeyNotFoundError{}) {
log.Info("Helm plugin not found, skipping migration")
return nil
} else if err != nil {
return fmt.Errorf("failed to decode Helm plugin config: %w", err)
}

return kubebuilderHelmEdit(helmPlugin, targetDir)
}

// Edits the project to include the Helm plugin.
func kubebuilderHelmEdit() error {
args := []string{"edit", "--plugins", plugin.KeyFor(hemlv1alpha.Plugin{})}
func kubebuilderHelmEdit(resourceData helmv1alpha.PluginConfig, targetDir string) error {
args := []string{"edit", "--plugins", plugin.KeyFor(helmv1alpha.Plugin{})}
args = append(args, getHelmOptions(resourceData, targetDir)...)
if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil {
return fmt.Errorf("failed to run edit subcommand for Helm plugin: %w", err)
}
return nil
}

// Gets the options for Helm resource.
// If the directory is not the default, it sets the directory option.
// otherwise, it returns an empty slice which then use the default value from the edit/init subcommand.
func getHelmOptions(resourceData helmv1alpha.PluginConfig, targetDir string) []string {
var args []string

if targetDir != "" {
log.Info("setting Helm chart directory")
args = append(args, fmt.Sprintf("--output-dir=%s", targetDir))
} else if resourceData.Options.Directory != helmv1alpha.HelmDefaultTargetDirectory {
log.Info("setting directory for Helm chart")
args = append(args, fmt.Sprintf("--output-dir=%s", resourceData.Options.Directory))
}

return args
}

// hasHelmPlugin checks if the Helm plugin is present by inspecting the plugin chain or configuration.
func hasHelmPlugin(cfg store.Store) bool {
var pluginConfig map[string]interface{}

// Decode the Helm plugin configuration to check if it's present
err := cfg.Config().DecodePluginConfig(plugin.KeyFor(hemlv1alpha.Plugin{}), &pluginConfig)
err := cfg.Config().DecodePluginConfig(plugin.KeyFor(helmv1alpha.Plugin{}), &pluginConfig)
if err != nil {
// If the Helm plugin is not found, return false
if errors.As(err, &config.PluginKeyNotFoundError{}) {
Expand Down
56 changes: 56 additions & 0 deletions pkg/plugin/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,60 @@ var _ = Describe("Cover plugin util helpers", func() {
Expect(lines).To(Equal([]string{"noemptylines"}))
})
})

Describe("HasFileContentWith", Ordered, func() {
const (
path = "testdata/PROJECT"
content = `# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: example.org
layout:
- go.kubebuilder.io/v4
- helm.kubebuilder.io/v1-alpha
plugins:
helm.kubebuilder.io/v1-alpha: {}
repo: github.com/example/repo
version: "3"
`
)

BeforeAll(func() {
err := os.MkdirAll("testdata", 0o755)
Expect(err).NotTo(HaveOccurred())

if _, err = os.Stat(path); os.IsNotExist(err) {
err = os.WriteFile(path, []byte(content), 0o644)
Expect(err).NotTo(HaveOccurred())
}
})

AfterAll(func() {
err := os.RemoveAll("testdata")
Expect(err).NotTo(HaveOccurred())
})

It("should return true when file contains the expected content", func() {
content := "repo: github.com/example/repo"
found, err := HasFileContentWith(path, content)
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
})

It("should return true when file contains multiline expected content", func() {
content := `plugins:
helm.kubebuilder.io/v1-alpha: {}`
found, err := HasFileContentWith(path, content)
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeTrue())
})

It("should return false when file does not contain the expected content", func() {
content := "nonExistentContent"
found, err := HasFileContentWith(path, content)
Expect(err).NotTo(HaveOccurred())
Expect(found).To(BeFalse())
})
})
})
38 changes: 0 additions & 38 deletions pkg/plugins/optional/helm/v1alpha/commons.go

This file was deleted.

28 changes: 24 additions & 4 deletions pkg/plugins/optional/helm/v1alpha/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package v1alpha

import (
"errors"
"fmt"

"github.com/spf13/pflag"
Expand All @@ -29,8 +30,9 @@ import (
var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
config config.Config
force bool
config config.Config
force bool
directory string
}

//nolint:lll
Expand Down Expand Up @@ -66,6 +68,7 @@ manifests in the chart align with the latest changes.

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
fs.StringVar(&p.directory, "output-dir", HelmDefaultTargetDirectory, "the directory where the Helm chart will be generated. Defaults to 'dist' if not specified.")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
Expand All @@ -74,13 +77,30 @@ func (p *editSubcommand) InjectConfig(c config.Config) error {
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
scaffolder := scaffolds.NewInitHelmScaffolder(p.config, p.force)
scaffolder := scaffolds.NewHelmScaffolder(p.config, p.force, p.directory)
scaffolder.InjectFS(fs)
err := scaffolder.Scaffold()
if err != nil {
return fmt.Errorf("error scaffolding Helm chart: %w", err)
}

// Track the resources following a declarative approach
return insertPluginMetaToConfig(p.config, pluginConfig{})
cfg := PluginConfig{}
if err = p.config.DecodePluginConfig(pluginKey, &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{}) {
// Fail unless the key wasn't found, which just means it is the first resource tracked
return fmt.Errorf("error decoding plugin configuration: %w", err)
}

cfg.Options = options{
Directory: p.directory,
}

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

return nil
}
Loading