diff --git a/.golangci.yml b/.golangci.yml index 00ef274e974..54834f66f10 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -13,6 +13,7 @@ linters: - goconst - gocyclo - govet + - importas - ineffassign - lll - misspell @@ -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 diff --git a/cmd/cmd.go b/cmd/cmd.go index 276c98791d4..3866929bd98 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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() { @@ -62,7 +62,7 @@ func Run() { &kustomizecommonv2.Plugin{}, &deployimagev1alpha1.Plugin{}, &grafanav1alpha1.Plugin{}, - &helmv1alpha1.Plugin{}, + &helmv1alpha.Plugin{}, ), cli.WithPlugins(externalPlugins...), cli.WithDefaultPlugins(cfgv3.Version, gov4Bundle), diff --git a/docs/book/src/plugins/available/helm-v1-alpha.md b/docs/book/src/plugins/available/helm-v1-alpha.md index 4bd25126073..72a5772b394 100644 --- a/docs/book/src/plugins/available/helm-v1-alpha.md +++ b/docs/book/src/plugins/available/helm-v1-alpha.md @@ -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/` @@ -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: diff --git a/pkg/cli/alpha/command.go b/pkg/cli/alpha/command.go index 55d7266cf4b..95032cf89f8 100644 --- a/pkg/cli/alpha/command.go +++ b/pkg/cli/alpha/command.go @@ -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 } diff --git a/pkg/cli/alpha/internal/generate.go b/pkg/cli/alpha/internal/generate.go index e71bf93eab0..bff67f065b4 100644 --- a/pkg/cli/alpha/internal/generate.go +++ b/pkg/cli/alpha/internal/generate.go @@ -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. @@ -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) } } @@ -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 @@ -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{}) { diff --git a/pkg/plugin/util/util_test.go b/pkg/plugin/util/util_test.go index d487fa22367..937af1a752f 100644 --- a/pkg/plugin/util/util_test.go +++ b/pkg/plugin/util/util_test.go @@ -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()) + }) + }) }) diff --git a/pkg/plugins/optional/helm/v1alpha/commons.go b/pkg/plugins/optional/helm/v1alpha/commons.go deleted file mode 100644 index 02370c48dae..00000000000 --- a/pkg/plugins/optional/helm/v1alpha/commons.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2024 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 v1alpha - -import ( - "errors" - "fmt" - - "sigs.k8s.io/kubebuilder/v4/pkg/config" -) - -func insertPluginMetaToConfig(target config.Config, cfg pluginConfig) error { - err := target.DecodePluginConfig(pluginKey, 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 { - return fmt.Errorf("error encoding plugin config: %w", err) - } - } - - return nil -} diff --git a/pkg/plugins/optional/helm/v1alpha/edit.go b/pkg/plugins/optional/helm/v1alpha/edit.go index f790c14d753..af0db4360d5 100644 --- a/pkg/plugins/optional/helm/v1alpha/edit.go +++ b/pkg/plugins/optional/helm/v1alpha/edit.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha import ( + "errors" "fmt" "github.com/spf13/pflag" @@ -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 @@ -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 { @@ -74,7 +77,7 @@ 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 { @@ -82,5 +85,22 @@ func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error { } // 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 } diff --git a/pkg/plugins/optional/helm/v1alpha/init.go b/pkg/plugins/optional/helm/v1alpha/init.go deleted file mode 100644 index c09d0051334..00000000000 --- a/pkg/plugins/optional/helm/v1alpha/init.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2024 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 v1alpha - -import ( - "fmt" - - "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/plugins/optional/helm/v1alpha/scaffolds" -) - -var _ plugin.InitSubcommand = &initSubcommand{} - -type initSubcommand struct { - config config.Config -} - -func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { - subcmdMeta.Description = `Initialize a helm chart to distribute the project under dist/ -` - subcmdMeta.Examples = fmt.Sprintf(`# Initialize a helm chart to distribute the project under dist/ - %[1]s init --plugins=%[2]s - -**IMPORTANT** You must use %[1]s edit --plugins=%[2]s to update the chart when changes are made. -`, cliMeta.CommandName, plugin.KeyFor(Plugin{})) -} - -func (p *initSubcommand) InjectConfig(c config.Config) error { - p.config = c - return nil -} - -func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { - scaffolder := scaffolds.NewInitHelmScaffolder(p.config, false) - 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{}) -} diff --git a/pkg/plugins/optional/helm/v1alpha/plugin.go b/pkg/plugins/optional/helm/v1alpha/plugin.go index 41d97de288d..a907cec9a7c 100644 --- a/pkg/plugins/optional/helm/v1alpha/plugin.go +++ b/pkg/plugins/optional/helm/v1alpha/plugin.go @@ -24,7 +24,15 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/plugins" ) -const pluginName = "helm." + plugins.DefaultNameQualifier +const ( + pluginName = "helm." + plugins.DefaultNameQualifier + + // HelmDefaultTargetDirectory is the default directory where the Helm chart will be scaffolded + // This is used in the init subcommand to scaffold the Helm chart. + // It is also used in the edit subcommand to add or edit a Helm chart. + // It is set to "dist" to match the default target directory for other plugins. + HelmDefaultTargetDirectory = "dist" +) var ( pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha} @@ -34,16 +42,20 @@ var ( // Plugin implements the plugin.Full interface type Plugin struct { - initSubcommand editSubcommand } -var ( - _ plugin.Init = Plugin{} - _ plugin.Edit = Plugin{} -) +var _ plugin.Edit = Plugin{} -type pluginConfig struct{} +type PluginConfig struct { + // Options contains the options for the Helm plugin + Options options `json:"options,omitempty"` +} + +type options struct { + // Directory is the directory where the Helm chart will be scaffolded + Directory string `json:"directory,omitempty"` +} // Name returns the name of the plugin func (Plugin) Name() string { return pluginName } @@ -54,9 +66,6 @@ func (Plugin) Version() plugin.Version { return pluginVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } -// GetInitSubcommand will return the subcommand which is responsible for initializing and scaffolding helm manifests -func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand } - // GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a helm chart func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand } diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/init.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go similarity index 92% rename from pkg/plugins/optional/helm/v1alpha/scaffolds/init.go rename to pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go index 9fa00e64406..7ae3a83e301 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/init.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go @@ -42,31 +42,34 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github" ) -var _ plugins.Scaffolder = &initScaffolder{} +var _ plugins.Scaffolder = &editScaffolder{} -type initScaffolder struct { +type editScaffolder struct { config config.Config fs machinery.Filesystem force bool + + directory string } -// NewInitHelmScaffolder returns a new Scaffolder for HelmPlugin -func NewInitHelmScaffolder(cfg config.Config, force bool) plugins.Scaffolder { - return &initScaffolder{ - config: cfg, - force: force, +// NewHelmScaffolder returns a new Scaffolder for HelmPlugin +func NewHelmScaffolder(cfg config.Config, force bool, dir string) plugins.Scaffolder { + return &editScaffolder{ + config: cfg, + force: force, + directory: dir, } } // InjectFS implements cmdutil.Scaffolder -func (s *initScaffolder) InjectFS(fs machinery.Filesystem) { +func (s *editScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold scaffolds the Helm chart with the necessary files. -func (s *initScaffolder) Scaffold() error { +func (s *editScaffolder) Scaffold() error { log.Println("Generating Helm Chart to distribute project") imagesEnvVars := s.getDeployImagesEnvVars() @@ -83,23 +86,40 @@ func (s *initScaffolder) Scaffold() error { hasWebhooks := hasWebhooksWith(s.config) || (len(mutatingWebhooks) > 0 && len(validatingWebhooks) > 0) buildScaffold := []machinery.Builder{ - &github.HelmChartCI{}, - &templates.HelmChart{}, + &github.HelmChartCI{ + Directory: s.directory, + }, + &templates.HelmChart{ + Directory: s.directory, + }, &templates.HelmValues{ HasWebhooks: hasWebhooks, DeployImages: imagesEnvVars, Force: s.force, + Directory: s.directory, + }, + &templates.HelmIgnore{ + Directory: s.directory, + }, + &charttemplates.HelmHelpers{ + Directory: s.directory, }, - &templates.HelmIgnore{}, - &charttemplates.HelmHelpers{}, &manager.Deployment{ Force: s.force, DeployImages: len(imagesEnvVars) > 0, HasWebhooks: hasWebhooks, + Directory: s.directory, + }, + &templatescertmanager.Certificate{ + HasWebhooks: hasWebhooks, + Directory: s.directory, + }, + &templatesmetrics.Service{ + Directory: s.directory, + }, + &prometheus.Monitor{ + Directory: s.directory, }, - &templatescertmanager.Certificate{HasWebhooks: hasWebhooks}, - &templatesmetrics.Service{}, - &prometheus.Monitor{}, } if len(mutatingWebhooks) > 0 || len(validatingWebhooks) > 0 { @@ -107,13 +127,16 @@ func (s *initScaffolder) Scaffold() error { &templateswebhooks.Template{ MutatingWebhooks: mutatingWebhooks, ValidatingWebhooks: validatingWebhooks, + Directory: s.directory, }, ) } if hasWebhooks { buildScaffold = append(buildScaffold, - &templateswebhooks.Service{}, + &templateswebhooks.Service{ + Directory: s.directory, + }, ) } @@ -122,9 +145,9 @@ func (s *initScaffolder) Scaffold() error { } // Copy relevant files from config/ to dist/chart/templates/ - err = s.copyConfigFiles() + err = s.copyConfigFiles(s.directory) if err != nil { - return fmt.Errorf("failed to copy manifests from config to dist/chart/templates/: %w", err) + return fmt.Errorf("failed to copy manifests from config %s/chart/templates/: %w", s.directory, err) } return nil @@ -132,7 +155,7 @@ func (s *initScaffolder) Scaffold() error { // getDeployImagesEnvVars will return the values to append the envvars for projects // which has the APIs scaffolded with DeployImage plugin -func (s *initScaffolder) getDeployImagesEnvVars() map[string]string { +func (s *editScaffolder) getDeployImagesEnvVars() map[string]string { deployImages := make(map[string]string) pluginConfig := struct { @@ -157,7 +180,7 @@ func (s *initScaffolder) getDeployImagesEnvVars() map[string]string { // extractWebhooksFromGeneratedFiles parses the files generated by controller-gen under // config/webhooks and created Mutating and Validating helper structures to // generate the webhook manifest for the helm-chart -func (s *initScaffolder) extractWebhooksFromGeneratedFiles() (mutatingWebhooks []templateswebhooks.DataWebhook, +func (s *editScaffolder) extractWebhooksFromGeneratedFiles() (mutatingWebhooks []templateswebhooks.DataWebhook, validatingWebhooks []templateswebhooks.DataWebhook, err error, ) { manifestFile := "config/webhook/manifests.yaml" @@ -227,15 +250,15 @@ func (s *initScaffolder) extractWebhooksFromGeneratedFiles() (mutatingWebhooks [ } // Helper function to copy files from config/ to dist/chart/templates/ -func (s *initScaffolder) copyConfigFiles() error { +func (s *editScaffolder) copyConfigFiles(targetDirectory string) error { configDirs := []struct { SrcDir string DestDir string SubDir string }{ - {"config/rbac", "dist/chart/templates/rbac", "rbac"}, - {"config/crd/bases", "dist/chart/templates/crd", "crd"}, - {"config/network-policy", "dist/chart/templates/network-policy", "networkPolicy"}, + {"config/rbac", targetDirectory + "/chart/templates/rbac", "rbac"}, + {"config/crd/bases", targetDirectory + "/chart/templates/crd", "crd"}, + {"config/network-policy", targetDirectory + "/chart/templates/network-policy", "networkPolicy"}, } for _, dir := range configDirs { diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go index 01a90f4f8cb..d5cb12f0854 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go @@ -30,12 +30,14 @@ type Certificate struct { // HasWebhooks is true when webhooks were found in the config HasWebhooks bool + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *Certificate) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "certmanager", "certificate.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "certmanager", "certificate.yaml") } f.TemplateBody = certificateTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go index 42d6c9f245f..26df268d5a0 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go @@ -28,12 +28,15 @@ var _ machinery.Template = &HelmHelpers{} type HelmHelpers struct { machinery.TemplateMixin machinery.ProjectNameMixin + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *HelmHelpers) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "_helpers.tpl") + f.Path = filepath.Join(f.Directory, "chart", "templates", "_helpers.tpl") } f.TemplateBody = helmHelpersTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go index e6b2cdf36ba..439a6cadcb0 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go @@ -35,12 +35,14 @@ type Deployment struct { Force bool // HasWebhooks is true when webhooks were found in the config HasWebhooks bool + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *Deployment) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "manager", "manager.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "manager", "manager.yaml") } f.TemplateBody = managerDeploymentTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go index 06edd0d0757..849b1ba873f 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go @@ -27,12 +27,15 @@ var _ machinery.Template = &Service{} type Service struct { machinery.TemplateMixin machinery.ProjectNameMixin + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *Service) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "metrics", "metrics-service.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "metrics", "metrics-service.yaml") } f.TemplateBody = metricsServiceTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go index eb157cad611..971ff928b85 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go @@ -27,12 +27,15 @@ var _ machinery.Template = &Monitor{} type Monitor struct { machinery.TemplateMixin machinery.ProjectNameMixin + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *Monitor) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "prometheus", "monitor.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "prometheus", "monitor.yaml") } f.TemplateBody = monitorTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go index f29dac307f1..01794b00a54 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go @@ -30,12 +30,15 @@ type Service struct { // Force if true allows overwriting the scaffolded file Force bool + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets the default template configuration func (f *Service) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "webhook", "service.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "webhook", "service.yaml") } f.TemplateBody = webhookServiceTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go index b8b5b0d5214..3fd5a400404 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go @@ -30,12 +30,15 @@ type Template struct { MutatingWebhooks []DataWebhook ValidatingWebhooks []DataWebhook + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults sets default configuration for the webhook template func (f *Template) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "templates", "webhook", "webhooks.yaml") + f.Path = filepath.Join(f.Directory, "chart", "templates", "webhook", "webhooks.yaml") } f.TemplateBody = webhookTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go index aebc7a142cd..96e0feb71d9 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go @@ -28,12 +28,15 @@ var _ machinery.Template = &HelmChart{} type HelmChart struct { machinery.TemplateMixin machinery.ProjectNameMixin + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults implements machinery.Template func (f *HelmChart) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "Chart.yaml") + f.Path = filepath.Join(f.Directory, "chart", "Chart.yaml") } f.TemplateBody = helmChartTemplate diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go index ef9eddee6ed..a678e75990f 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go @@ -28,6 +28,9 @@ var _ machinery.Template = &HelmChartCI{} type HelmChartCI struct { machinery.TemplateMixin machinery.ProjectNameMixin + + // Directory is the directory where the Helm chart CI workflow will be scaffolded + Directory string } // SetTemplateDefaults implements machinery.Template @@ -90,7 +93,7 @@ jobs: - name: Lint Helm Chart run: | - helm lint ./dist/chart + helm lint ./{{ .Directory }}/chart # TODO: Uncomment if cert-manager is enabled # - name: Install cert-manager via Helm @@ -124,7 +127,7 @@ jobs: - name: Install Helm chart for project run: | - helm install my-release ./dist/chart --create-namespace --namespace {{ .ProjectName }}-system + helm install my-release ./{{ .Directory }}/chart --create-namespace --namespace {{ .ProjectName }}-system - name: Check Helm release status run: | diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go index 5aef329f3e7..5d5a55fe252 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go @@ -27,12 +27,15 @@ var _ machinery.Template = &HelmIgnore{} // HelmIgnore scaffolds a file that defines the .helmignore for Helm packaging type HelmIgnore struct { machinery.TemplateMixin + + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults implements machinery.Template func (f *HelmIgnore) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", ".helmignore") + f.Path = filepath.Join(f.Directory, "chart", ".helmignore") } f.TemplateBody = helmIgnoreTemplate @@ -66,5 +69,5 @@ const helmIgnoreTemplate = `# Patterns to ignore when building Helm packages. .vscode/ # Helm chart artifacts -dist/chart/*.tgz +{{ .Directory }}/chart/*.tgz ` diff --git a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go index 86942894636..5eaafaee1bf 100644 --- a/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go +++ b/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go @@ -34,12 +34,14 @@ type HelmValues struct { Force bool // HasWebhooks is true when webhooks were found in the config HasWebhooks bool + // Directory is the directory where the Helm chart will be scaffolded + Directory string } // SetTemplateDefaults implements machinery.Template func (f *HelmValues) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("dist", "chart", "values.yaml") + f.Path = filepath.Join(f.Directory, "chart", "values.yaml") } f.TemplateBody = helmValuesTemplate diff --git a/test/e2e/alphagenerate/generate_test.go b/test/e2e/alphagenerate/generate_test.go index 67e209fdd37..95e571d730a 100644 --- a/test/e2e/alphagenerate/generate_test.go +++ b/test/e2e/alphagenerate/generate_test.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/machinery" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" + helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" . "github.com/onsi/ginkgo/v2" @@ -62,10 +63,10 @@ var _ = Describe("kubebuilder", func() { Expect(err).NotTo(HaveOccurred(), "Failed to create project") }) - AfterEach(func() { - By("destroying directory") - kbc.Destroy() - }) + // AfterEach(func() { + // By("destroying directory") + // kbc.Destroy() + // }) It("should regenerate the project in current directory with success", func() { generateProject(kbc) @@ -76,20 +77,20 @@ var _ = Describe("kubebuilder", func() { It("should regenerate project with grafana plugin with success", func() { generateProjectWithGrafanaPlugin(kbc) - regenerateProjectWith(kbc, projectOutputDir) + regenerateProjectWith(kbc, projectOutputDir, "") validateGrafanaPlugin(projectFilePath) }) It("should regenerate project with DeployImage plugin with success", func() { generateProjectWithDeployImagePlugin(kbc) - regenerateProjectWith(kbc, projectOutputDir) + regenerateProjectWith(kbc, projectOutputDir, "") validateDeployImagePlugin(projectFilePath) }) It("should regenerate project with helm plugin with success", func() { - generateProjectWithHelmPlugin(kbc) - regenerateProjectWith(kbc, projectOutputDir) - validateHelmPlugin(projectFilePath) + generateProjectWithHelmPlugin(kbc, "helm") + regenerateProjectWith(kbc, projectOutputDir, "helm") + validateHelmPlugin(projectFilePath, "helm") }) }) }) @@ -191,11 +192,20 @@ func regenerateProject(kbc *utils.TestContext) { Expect(err).NotTo(HaveOccurred(), "Failed to regenerate project") } -func regenerateProjectWith(kbc *utils.TestContext, projectOutputDir string) { +func regenerateProjectWith(kbc *utils.TestContext, projectOutputDir, helmOutputDir string) { By("regenerating the project") - err := kbc.Regenerate( + + regenerateOptions := []string{ fmt.Sprintf("--input-dir=%s", kbc.Dir), fmt.Sprintf("--output-dir=%s", projectOutputDir), + } + + if helmOutputDir != "" { + regenerateOptions = append(regenerateOptions, "--helm-output-dir", helmOutputDir) + } + + err := kbc.Regenerate( + regenerateOptions..., ) Expect(err).NotTo(HaveOccurred(), "Failed to regenerate project") } @@ -206,9 +216,21 @@ func generateProjectWithGrafanaPlugin(kbc *utils.TestContext) { Expect(err).NotTo(HaveOccurred(), "Failed to edit project to enable Grafana Plugin") } -func generateProjectWithHelmPlugin(kbc *utils.TestContext) { +func generateProjectWithHelmPlugin(kbc *utils.TestContext, directory string) { By("editing project to enable Helm plugin") - err := kbc.Edit("--plugins", "helm.kubebuilder.io/v1-alpha") + editOptions := []string{ + "--plugins", "helm.kubebuilder.io/v1-alpha", + } + + if directory != "" { + editOptions = append(editOptions, "--output-dir", directory) + } else { + directory = "dist" + } + + err := kbc.Edit( + editOptions..., + ) Expect(err).NotTo(HaveOccurred(), "Failed to edit project to enable Helm Plugin") } @@ -355,12 +377,12 @@ func validateDeployImagePlugin(projectFile string) { Expect(options.RunAsUser).To(Equal("1001"), "Expected runAsUser to match") } -func validateHelmPlugin(projectFile string) { +func validateHelmPlugin(projectFile string, directory string) { projectConfig := getConfigFromProjectFile(projectFile) By("checking the Helm plugin in the PROJECT file") - var helmPluginConfig map[string]interface{} + var helmPluginConfig helmv1alpha.PluginConfig err := projectConfig.DecodePluginConfig("helm.kubebuilder.io/v1-alpha", &helmPluginConfig) Expect(err).NotTo(HaveOccurred(), "Failed to decode Helm plugin configuration") - Expect(helmPluginConfig).NotTo(BeNil(), "Expected Helm plugin configuration to be present in the PROJECT file") + Expect(helmPluginConfig.Options.Directory).To(Equal("helm"), "Expected Helm plugin configuration to be present in the PROJECT file") } diff --git a/test/e2e/alphagenerate/generate_v4_multigroup_test.go b/test/e2e/alphagenerate/generate_v4_multigroup_test.go index 477953b8478..c588825089d 100644 --- a/test/e2e/alphagenerate/generate_v4_multigroup_test.go +++ b/test/e2e/alphagenerate/generate_v4_multigroup_test.go @@ -62,7 +62,7 @@ var _ = Describe("kubebuilder", func() { }) It("should regenerate the project in project-v4-multigroup directory with success", func() { - regenerateProjectWith(kbc, projectOutputDir) + regenerateProjectWith(kbc, projectOutputDir, "") By("checking that the project file was generated in the current directory") validateV4MultigroupProjectFile(kbc, projectFilePath) }) diff --git a/test/e2e/alphagenerate/generate_v4_test.go b/test/e2e/alphagenerate/generate_v4_test.go index 38a899acd15..11081f2aaaf 100644 --- a/test/e2e/alphagenerate/generate_v4_test.go +++ b/test/e2e/alphagenerate/generate_v4_test.go @@ -65,7 +65,7 @@ var _ = Describe("kubebuilder", func() { }) func regenerateAndValidate(kbc *utils.TestContext, projectOutputDir, projectFilePath string) { - regenerateProjectWith(kbc, projectOutputDir) + regenerateProjectWith(kbc, projectOutputDir, "") By("checking that the project file was generated in the current directory") validateV4ProjectFile(kbc, projectFilePath) } diff --git a/test/e2e/alphagenerate/generate_v4_with_plugins_test.go b/test/e2e/alphagenerate/generate_v4_with_plugins_test.go index 3ca96ae3a0f..178285713ac 100644 --- a/test/e2e/alphagenerate/generate_v4_with_plugins_test.go +++ b/test/e2e/alphagenerate/generate_v4_with_plugins_test.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" + helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" . "github.com/onsi/ginkgo/v2" @@ -60,15 +61,15 @@ var _ = Describe("kubebuilder", func() { }) It("should regenerate the project in project-v4-with-plugins directory with success", func() { - regenerateProjectWith(kbc, projectOutputDir) + regenerateProjectWith(kbc, projectOutputDir, "helm") By("checking that the project file was generated in the current directory") - validateV4WithPluginsProjectFile(kbc, projectFilePath) + validateV4WithPluginsProjectFile(kbc, projectFilePath, "helm") }) }) }) // Validate the PROJECT file for basic content and additional resources -func validateV4WithPluginsProjectFile(kbc *utils.TestContext, projectFile string) { +func validateV4WithPluginsProjectFile(kbc *utils.TestContext, projectFile string, helmOutputDir string) { projectConfig := getConfigFromProjectFile(projectFile) By("checking the layout in the PROJECT file") @@ -182,16 +183,16 @@ func validateV4WithPluginsProjectFile(kbc *utils.TestContext, projectFile string By("decoding the grafana plugin configuration") var grafanaConfig v1alpha1.PluginConfig err = projectConfig.DecodePluginConfig("grafana.kubebuilder.io/v1-alpha", &grafanaConfig) - Expect(err).NotTo(HaveOccurred(), "Failed to decode DeployImage plugin configuration") + Expect(err).NotTo(HaveOccurred(), "Failed to decode Grafana plugin configuration") // Validate the resource configuration Expect(grafanaConfig.Resources).To(BeEmpty(), "Expected zero resource for the Grafana plugin") By("decoding the helm plugin configuration") - var helmConfig v1alpha1.PluginConfig - err = projectConfig.DecodePluginConfig("grafana.kubebuilder.io/v1-alpha", &helmConfig) + var helmConfig helmv1alpha.PluginConfig + err = projectConfig.DecodePluginConfig("helm.kubebuilder.io/v1-alpha", &helmConfig) Expect(err).NotTo(HaveOccurred(), "Failed to decode Helm plugin configuration") // Validate the resource configuration - Expect(helmConfig.Resources).To(BeEmpty(), "Expected zero resource for the Helm plugin") + Expect(helmConfig.Options.Directory).To(Equal(helmOutputDir), "Expected default directory to be 'dist'") } diff --git a/test/e2e/helm/e2e_suite_test.go b/test/e2e/helm/e2e_suite_test.go new file mode 100644 index 00000000000..a1f038da53d --- /dev/null +++ b/test/e2e/helm/e2e_suite_test.go @@ -0,0 +1,29 @@ +/* +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 helm + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Run e2e tests using the Ginkgo runner. +func TestE2E(t *testing.T) { + RegisterFailHandler(Fail) + _, _ = fmt.Fprintf(GinkgoWriter, "Starting helm plugin kubebuilder suite\n") + RunSpecs(t, "Kubebuilder helm plugin e2e suite") +} diff --git a/test/e2e/helm/generate_test.go b/test/e2e/helm/generate_test.go new file mode 100644 index 00000000000..b7a8f37fe37 --- /dev/null +++ b/test/e2e/helm/generate_test.go @@ -0,0 +1,183 @@ +/* +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 helm + +import ( + "path/filepath" + + pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" + + . "github.com/onsi/ginkgo/v2" + + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" +) + +var _ = Describe("kubebuilder", func() { + Context("plugin helm/v1-alpha", func() { + var kbc *utils.TestContext + + BeforeEach(func() { + var err error + kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on") + Expect(err).NotTo(HaveOccurred()) + Expect(kbc.Prepare()).To(Succeed()) + }) + + AfterEach(func() { + kbc.Destroy() + }) + + It("should extend a runnable project with helm plugin", func() { + initTheProject(kbc) + generateProject(kbc, "") + }) + + It("should extend a runnable project with helm plugin and webhooks", func() { + initTheProject(kbc) + generateProject(kbc, "") + extendProjectWithWebhooks(kbc) + + By("re-edit the project after creating webhooks") + err := kbc.Edit( + "--plugins", "helm.kubebuilder.io/v1-alpha", "--force", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to edit the project") + + fileContainsExpr, err := pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, "dist", "chart", "values.yaml"), + `webhook: + enable: true`) + Expect(err).NotTo(HaveOccurred(), "Failed to read values.yaml file") + Expect(fileContainsExpr).To(BeTrue(), "Failed to get enabled webhook value from values.yaml file") + }) + + // Without --force, the webhooks should not be enabled in the values.yaml file. + It("should extend a runnable project with helm plugin but not running with --force", func() { + initTheProject(kbc) + generateProject(kbc, "") + extendProjectWithWebhooks(kbc) + + By("re-edit the project after creating webhooks without --force") + err := kbc.Edit( + "--plugins", "helm.kubebuilder.io/v1-alpha", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to edit the project") + + fileContainsExpr, err := pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, "dist", "chart", "values.yaml"), + `webhook: + enable: true`) + Expect(err).NotTo(HaveOccurred(), "Failed to read values.yaml file") + Expect(fileContainsExpr).To(BeFalse(), "Failed to get enabled webhook value from values.yaml file") + }) + + It("should extend a runnable project with helm plugin and a custom helm directory", func() { + initTheProject(kbc) + generateProject(kbc, "helm-charts") + }) + }) +}) + +// generateProject implements a helm/v1(-alpha) plugin project defined by a TestContext. +func generateProject(kbc *utils.TestContext, directory string) { + var err error + + editOptions := []string{ + "--plugins", "helm.kubebuilder.io/v1-alpha", + } + + if directory != "" { + editOptions = append(editOptions, "--output-dir", directory) + } else { + directory = "dist" + } + + By("editing a project") + err = kbc.Edit( + editOptions..., + ) + Expect(err).NotTo(HaveOccurred(), "Failed to edit the project") + + fileContainsExpr, err := pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, "PROJECT"), + `helm.kubebuilder.io/v1-alpha: + options: + directory: `+directory) + Expect(err).NotTo(HaveOccurred(), "Failed to read PROJECT file") + Expect(fileContainsExpr).To(BeTrue(), "Failed to find helm plugin in PROJECT file") + + fileContainsExpr, err = pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, "PROJECT"), + "projectName: e2e-"+kbc.TestSuffix) + Expect(err).NotTo(HaveOccurred(), "Failed to read PROJECT file") + Expect(fileContainsExpr).To(BeTrue(), "Failed to find projectName in PROJECT file") + + fileContainsExpr, err = pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, directory, "chart", "Chart.yaml"), + "name: e2e-"+kbc.TestSuffix) + Expect(err).NotTo(HaveOccurred(), "Failed to read Chart.yaml file") + Expect(fileContainsExpr).To(BeTrue(), "Failed to find name in Chart.yaml file") + + fileContainsExpr, err = pluginutil.HasFileContentWith( + filepath.Join(kbc.Dir, directory, "chart", "templates", "manager", "manager.yaml"), + `metadata: + name: e2e-`+kbc.TestSuffix) + Expect(err).NotTo(HaveOccurred(), "Failed to read manager.yaml file") + Expect(fileContainsExpr).To(BeTrue(), "Failed to find name in helm template manager.yaml file") +} + +// extendProjectWithWebhooks is creating API and scaffolding webhooks in the project +func extendProjectWithWebhooks(kbc *utils.TestContext) { + By("creating API definition") + err := kbc.CreateAPI( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--namespaced", + "--resource", + "--controller", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to create API") + + By("scaffolding mutating and validating webhooks") + err = kbc.CreateWebhook( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--defaulting", + "--programmatic-validation", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to scaffolding mutating webhook") + + By("run make manifests") + Expect(kbc.Make("manifests")).To(Succeed()) +} + +// initTheProject initializes a project with the go/v4 plugin and sets the domain. +func initTheProject(kbc *utils.TestContext) { + By("initializing a project") + err := kbc.Init( + "--plugins", "go/v4", + "--project-version", "3", + "--domain", kbc.Domain, + ) + Expect(err).NotTo(HaveOccurred(), "Failed to initialize project") +} diff --git a/test/e2e/setup.sh b/test/e2e/setup.sh index b023a791cda..58040791cd1 100755 --- a/test/e2e/setup.sh +++ b/test/e2e/setup.sh @@ -65,6 +65,7 @@ function test_cluster { kind load docker-image --name $KIND_CLUSTER busybox:1.36.1 go test $(dirname "$0")/grafana $flags -timeout 30m + go test $(dirname "$0")/helm $flags -timeout 30m go test $(dirname "$0")/deployimage $flags -timeout 30m go test $(dirname "$0")/v4 $flags -timeout 30m go test $(dirname "$0")/alphagenerate $flags -timeout 30m diff --git a/testdata/project-v4-multigroup/go.mod b/testdata/project-v4-multigroup/go.mod index 8ca19c87533..f1d2972edc9 100644 --- a/testdata/project-v4-multigroup/go.mod +++ b/testdata/project-v4-multigroup/go.mod @@ -3,7 +3,7 @@ module sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup go 1.24.0 require ( - github.com/cert-manager/cert-manager v1.18.1 + github.com/cert-manager/cert-manager v1.18.2 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 k8s.io/api v0.33.0 diff --git a/testdata/project-v4-with-plugins/PROJECT b/testdata/project-v4-with-plugins/PROJECT index dc0cdc93ce6..61cfe596c20 100644 --- a/testdata/project-v4-with-plugins/PROJECT +++ b/testdata/project-v4-with-plugins/PROJECT @@ -25,7 +25,9 @@ plugins: image: busybox:1.36.1 version: v1alpha1 grafana.kubebuilder.io/v1-alpha: {} - helm.kubebuilder.io/v1-alpha: {} + helm.kubebuilder.io/v1-alpha: + options: + directory: dist projectName: project-v4-with-plugins repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins resources: diff --git a/testdata/project-v4/go.mod b/testdata/project-v4/go.mod index faa842214b5..ef3693e5798 100644 --- a/testdata/project-v4/go.mod +++ b/testdata/project-v4/go.mod @@ -3,7 +3,7 @@ module sigs.k8s.io/kubebuilder/testdata/project-v4 go 1.24.0 require ( - github.com/cert-manager/cert-manager v1.18.1 + github.com/cert-manager/cert-manager v1.18.2 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 k8s.io/api v0.33.0