• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubernetes-sigs / kubebuilder / 24314119699

12 Apr 2026 07:04PM UTC coverage: 79.925% (-1.7%) from 81.659%
24314119699

Pull #5612

github

camilamacedo86
chore(helm/v2-alpha): Add advanced options with sub-charts

Generated-by: Claude
Pull Request #5612: WIP chore(helm/v2-alpha): Add advanced options with sub-charts

113 of 364 new or added lines in 7 files covered. (31.04%)

7274 of 9101 relevant lines covered (79.93%)

71.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.32
/pkg/plugins/optional/helm/v2alpha/edit.go
1
/*
2
Copyright 2025 The Kubernetes Authors.
3

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

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

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

17
package v2alpha
18

19
import (
20
        "errors"
21
        "fmt"
22
        "io"
23
        "log/slog"
24
        "os"
25
        "path/filepath"
26
        "strings"
27

28
        "github.com/spf13/pflag"
29
        "go.yaml.in/yaml/v3"
30

31
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
32
        cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
36
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
37
)
38

39
const (
40
        // DefaultManifestsFile is the default path for kustomize output manifests
41
        DefaultManifestsFile = "dist/install.yaml"
42
        // DefaultOutputDir is the default output directory for Helm charts
43
        DefaultOutputDir = "dist"
44
        // v1AlphaPluginKey is the deprecated v1-alpha plugin key
45
        v1AlphaPluginKey = "helm.kubebuilder.io/v1-alpha"
46
)
47

48
var _ plugin.EditSubcommand = &editSubcommand{}
49

50
type editSubcommand struct {
51
        config          config.Config
52
        force           bool
53
        manifestsFile   string
54
        outputDir       string
55
        crdSubchart     bool
56
        samplesSubchart bool
57
}
58

59
//nolint:lll
60
func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
1✔
61
        subcmdMeta.Description = `Generate a Helm chart from your project's kustomize output.
1✔
62

1✔
63
Parses 'make build-installer' output (dist/install.yaml) and generates chart to allow easy
1✔
64
distribution of your project. When enabled, adds Helm helpers targets to Makefile`
1✔
65

1✔
66
        subcmdMeta.Examples = fmt.Sprintf(`# Generate Helm chart from default manifests (dist/install.yaml) to default output (dist/)
1✔
67
  %[1]s edit --plugins=%[2]s
1✔
68

1✔
69
# Generate Helm chart and overwrite existing files (useful for updates)
1✔
70
  %[1]s edit --plugins=%[2]s --force
1✔
71

1✔
72
# Generate Helm chart from a custom manifests file
1✔
73
  %[1]s edit --plugins=%[2]s --manifests=path/to/custom-install.yaml
1✔
74

1✔
75
# Generate Helm chart to a custom output directory
1✔
76
  %[1]s edit --plugins=%[2]s --output-dir=charts
1✔
77

1✔
78
# Generate from custom manifests to custom output directory
1✔
79
  %[1]s edit --plugins=%[2]s --manifests=manifests/install.yaml --output-dir=helm-charts
1✔
80

1✔
81
# Generate Helm chart with CRDs in a separate sub-chart
1✔
82
  %[1]s edit --plugins=%[2]s --crd-subchart
1✔
83

1✔
84
# Generate Helm chart with CR samples in a separate sub-chart
1✔
85
  %[1]s edit --plugins=%[2]s --samples-subchart
1✔
86

1✔
87
# Generate Helm chart with both CRDs and samples in separate sub-charts
1✔
88
  %[1]s edit --plugins=%[2]s --crd-subchart --samples-subchart
1✔
89

1✔
90
# Typical workflow:
1✔
91
  make build-installer  # Generate dist/install.yaml with latest changes
1✔
92
  %[1]s edit --plugins=%[2]s  # Generate/update Helm chart in dist/chart/
1✔
93

1✔
94
**NOTE**: Chart.yaml is never overwritten (contains user-managed version info).
1✔
95
Without --force, the plugin also preserves values.yaml, NOTES.txt, _helpers.tpl, .helmignore,
1✔
96
and .github/workflows/test-chart.yml.
1✔
97
All other template files in templates/ are always regenerated to match your current
1✔
98
kustomize output. Use --force to regenerate all files except Chart.yaml.
1✔
99

1✔
100
The generated chart structure mirrors your config/ directory:
1✔
101

1✔
102
1. Default (no flags):
1✔
103
<output>/chart/
1✔
104
├── Chart.yaml
1✔
105
├── values.yaml
1✔
106
└── templates/
1✔
107
    ├── crd/             # CRDs here
1✔
108
    ├── rbac/
1✔
109
    ├── manager/
1✔
110
    └── ...              # CR samples ignored
1✔
111

1✔
112
2. With --crd-subchart:
1✔
113
<output>/chart/
1✔
114
├── Chart.yaml (with dependency on crds sub-chart)
1✔
115
├── values.yaml
1✔
116
├── crds/                # CRD sub-chart
1✔
117
│   ├── Chart.yaml
1✔
118
│   └── templates/
1✔
119
│       └── *.yaml
1✔
120
└── templates/
1✔
121
    ├── samples/         # CR samples here (if found)
1✔
122
    │   └── *.yaml       # conditional: {{ if .Values.samples.install }}
1✔
123
    ├── rbac/
1✔
124
    └── manager/
1✔
125

1✔
126
3. With --samples-subchart:
1✔
127
<output>/chart/
1✔
128
├── Chart.yaml
1✔
129
├── values.yaml
1✔
130
├── samples/             # Samples sub-chart
1✔
131
│   ├── Chart.yaml
1✔
132
│   ├── README.md
1✔
133
│   └── templates/
1✔
134
│       └── *.yaml
1✔
135
└── templates/
1✔
136
    ├── crd/             # CRDs here
1✔
137
    ├── rbac/
1✔
138
    └── manager/
1✔
139

1✔
140
4. With --crd-subchart --samples-subchart:
1✔
141
<output>/chart/
1✔
142
├── Chart.yaml (with dependency on crds sub-chart)
1✔
143
├── values.yaml
1✔
144
├── crds/                # CRD sub-chart
1✔
145
│   ├── Chart.yaml
1✔
146
│   └── templates/
1✔
147
│       └── *.yaml
1✔
148
├── samples/             # Samples sub-chart
1✔
149
│   ├── Chart.yaml
1✔
150
│   ├── README.md
1✔
151
│   └── templates/
1✔
152
│       └── *.yaml
1✔
153
└── templates/
1✔
154
    ├── rbac/
1✔
155
    └── manager/
1✔
156

1✔
157
Installation order with sub-charts:
1✔
158
  helm install my-crds ./chart/crds                    # (if --crd-subchart)
1✔
159
  helm install my-app ./chart                          # Main chart
1✔
160
  helm install my-samples ./chart/samples --wait       # (if --samples-subchart, after manager ready)
1✔
161
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
1✔
162
}
1✔
163

164
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
4✔
165
        fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
4✔
166
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
4✔
167
                "path to the YAML file containing Kubernetes manifests from kustomize output")
4✔
168
        fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
4✔
169
        fs.BoolVar(&p.crdSubchart, "crd-subchart", false,
4✔
170
                "if true, generates CRDs in a separate sub-chart for independent lifecycle management")
4✔
171
        fs.BoolVar(&p.samplesSubchart, "samples-subchart", false,
4✔
172
                "if true, generates CR samples (if any) in a separate sub-chart for independent installation")
4✔
173
}
4✔
174

175
func (p *editSubcommand) InjectConfig(c config.Config) error {
1✔
176
        p.config = c
1✔
177
        return nil
1✔
178
}
1✔
179

180
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
×
181
        // If using default manifests file, ensure it exists by running make build-installer
×
182
        if p.manifestsFile == DefaultManifestsFile {
×
183
                if err := p.ensureManifestsExist(); err != nil {
×
184
                        slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
×
185
                }
×
186
        }
187

NEW
188
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(
×
NEW
189
                p.config, p.force, p.manifestsFile, p.outputDir, p.crdSubchart, p.samplesSubchart,
×
NEW
190
        )
×
191
        scaffolder.InjectFS(fs)
×
192
        err := scaffolder.Scaffold()
×
193
        if err != nil {
×
194
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
195
        }
×
196

197
        // Remove deprecated v1-alpha plugin entry from PROJECT file
198
        // This must happen in Scaffold (before config is saved) to be persisted
199
        p.removeV1AlphaPluginEntry()
×
200

×
201
        // Save plugin config to PROJECT file
×
202
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
203
        canonicalKey := plugin.KeyFor(Plugin{})
×
204
        cfg := pluginConfig{}
×
205
        isFirstRun := false
×
206
        if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
×
207
                switch {
×
208
                case errors.As(err, &config.UnsupportedFieldError{}):
×
209
                        // Config version doesn't support plugin metadata
×
210
                        return nil
×
211
                case errors.As(err, &config.PluginKeyNotFoundError{}):
×
212
                        // This is the first time the plugin is run
×
213
                        isFirstRun = true
×
214
                        if key != canonicalKey {
×
215
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
216
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
217
                                                return nil
×
218
                                        }
×
219
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
220
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
221
                                        }
×
222
                                } else {
×
223
                                        // Found config under canonical key, not first run
×
224
                                        isFirstRun = false
×
225
                                }
×
226
                        }
227
                default:
×
228
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
229
                }
230
        }
231

232
        // Update configuration with current parameters
233
        cfg.ManifestsFile = p.manifestsFile
×
234
        cfg.OutputDir = p.outputDir
×
NEW
235
        cfg.CRDSubchart = p.crdSubchart
×
NEW
236
        cfg.SamplesSubchart = p.samplesSubchart
×
237

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

242
        // Add Helm deployment targets to Makefile only on first run
243
        if isFirstRun {
×
244
                slog.Info("adding Helm deployment targets to Makefile...")
×
245
                // Extract namespace from manifests for accurate Makefile generation
×
246
                namespace := p.extractNamespaceFromManifests()
×
247
                if err := p.addHelmMakefileTargets(namespace); err != nil {
×
248
                        slog.Warn("failed to add Helm targets to Makefile", "error", err)
×
249
                }
×
250
        }
251

252
        return nil
×
253
}
254

255
// ensureManifestsExist runs make build-installer to generate the default manifests file
256
func (p *editSubcommand) ensureManifestsExist() error {
×
257
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
258

×
259
        // Run the required make targets to generate the manifests file
×
260
        targets := []string{"manifests", "generate", "build-installer"}
×
261
        for _, target := range targets {
×
262
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
263
                        return fmt.Errorf("make %s failed: %w", target, err)
×
264
                }
×
265
        }
266

267
        // Verify the file was created
268
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
269
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
270
        }
×
271

272
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
273
        return nil
×
274
}
275

276
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
277
func (p *editSubcommand) PostScaffold() error {
2✔
278
        hasWebhooks := hasWebhooksWith(p.config)
2✔
279

2✔
280
        if hasWebhooks {
2✔
281
                workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
×
282
                if _, err := os.Stat(workflowFile); err != nil {
×
283
                        slog.Info(
×
284
                                "Workflow file not found, unable to uncomment cert-manager installation",
×
285
                                "error", err,
×
286
                                "file", workflowFile,
×
287
                        )
×
288
                        return nil
×
289
                }
×
290
                target := `
×
291
#      - name: Install cert-manager via Helm (wait for readiness)
×
292
#        run: |
×
293
#          helm repo add jetstack https://charts.jetstack.io
×
294
#          helm repo update
×
295
#          helm install cert-manager jetstack/cert-manager \
×
296
#            --namespace cert-manager \
×
297
#            --create-namespace \
×
298
#            --set crds.enabled=true \
×
299
#            --wait \
×
300
#            --timeout 300s`
×
301
                if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
×
302
                        hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
×
303
                        if !hasUncommented || errCheck != nil {
×
304
                                slog.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
×
305
                        }
×
306
                } else {
×
307
                        target = `# TODO: Uncomment if cert-manager is enabled`
×
308
                        _ = util.ReplaceInFile(workflowFile, target, "")
×
309
                }
×
310
        }
311
        return nil
2✔
312
}
313

314
// addHelmMakefileTargets appends Helm deployment targets to the Makefile if they don't already exist
315
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
316
        makefilePath := "Makefile"
3✔
317
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
318
                return fmt.Errorf("makefile not found")
1✔
319
        }
1✔
320

321
        // Get the Helm Makefile targets
322
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
2✔
323

2✔
324
        // Append the targets if they don't already exist
2✔
325
        if err := util.AppendCodeIfNotExist(makefilePath, helmTargets); err != nil {
2✔
326
                return fmt.Errorf("failed to append Helm targets to Makefile: %w", err)
×
327
        }
×
328

329
        slog.Info("added Helm deployment targets to Makefile",
2✔
330
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
2✔
331
        return nil
2✔
332
}
333

334
// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
335
// Returns projectName-system if manifests don't exist or namespace not found.
336
func (p *editSubcommand) extractNamespaceFromManifests() string {
×
337
        // Default to project-name-system pattern
×
338
        defaultNamespace := p.config.GetProjectName() + "-system"
×
339

×
340
        // If manifests file doesn't exist, use default
×
341
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
×
342
                return defaultNamespace
×
343
        }
×
344

345
        // Parse the manifests to get the namespace
346
        file, err := os.Open(p.manifestsFile)
×
347
        if err != nil {
×
348
                return defaultNamespace
×
349
        }
×
350
        defer func() {
×
351
                _ = file.Close()
×
352
        }()
×
353

354
        // Parse YAML documents looking for the manager Deployment
355
        decoder := yaml.NewDecoder(file)
×
356
        for {
×
357
                var doc map[string]any
×
358
                if err := decoder.Decode(&doc); err != nil {
×
359
                        if err == io.EOF {
×
360
                                break
×
361
                        }
362
                        continue
×
363
                }
364

365
                // Check if this is a Deployment (manager)
366
                if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
×
367
                        if metadata, ok := doc["metadata"].(map[string]any); ok {
×
368
                                // Check if it's the manager deployment
×
369
                                if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
×
370
                                        // Extract namespace from the manager Deployment
×
371
                                        if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
×
372
                                                return namespace
×
373
                                        }
×
374
                                }
375
                        }
376
                }
377
        }
378

379
        // Fallback to default if manager Deployment not found
380
        return defaultNamespace
×
381
}
382

383
// getHelmMakefileTargets returns the Helm Makefile targets as a string
384
// following the same patterns as the existing Makefile deployment section
385
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
5✔
386
        if outputDir == "" {
5✔
387
                outputDir = "dist"
×
388
        }
×
389

390
        // Use the project name as default for release name
391
        release := projectName
5✔
392

5✔
393
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
394
}
395

396
// helmMakefileTemplate returns the Helm deployment section template
397
// This follows the same pattern as the Kustomize deployment section in the Go plugin
398
const helmMakefileTemplateFormat = `
399
##@ Helm Deployment
400

401
## Helm binary to use for deploying the chart
402
HELM ?= helm
403
## Namespace to deploy the Helm release
404
HELM_NAMESPACE ?= %s
405
## Name of the Helm release
406
HELM_RELEASE ?= %s
407
## Path to the Helm chart directory
408
HELM_CHART_DIR ?= %s/chart
409
## Additional arguments to pass to helm commands
410
HELM_EXTRA_ARGS ?=
411

412
.PHONY: install-helm
413
install-helm: ## Install the latest version of Helm.
414
        @command -v $(HELM) >/dev/null 2>&1 || { \
415
                echo "Installing Helm..." && \
416
                curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
417
        }
418

419
.PHONY: helm-deploy
420
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
421
        $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
422
                --namespace $(HELM_NAMESPACE) \
423
                --create-namespace \
424
                --set manager.image.repository=$${IMG%%:*} \
425
                --set manager.image.tag=$${IMG##*:} \
426
                --wait \
427
                --timeout 5m \
428
                $(HELM_EXTRA_ARGS)
429

430
.PHONY: helm-uninstall
431
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
432
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
433

434
.PHONY: helm-status
435
helm-status: ## Show Helm release status.
436
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
437

438
.PHONY: helm-history
439
helm-history: ## Show Helm release history.
440
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
441

442
.PHONY: helm-rollback
443
helm-rollback: ## Rollback to previous Helm release.
444
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
445
`
446

447
func helmMakefileTemplate(namespace, release, outputDir string) string {
5✔
448
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
5✔
449
}
5✔
450

451
func hasWebhooksWith(c config.Config) bool {
3✔
452
        resources, err := c.GetResources()
3✔
453
        if err != nil {
3✔
454
                return false
×
455
        }
×
456

457
        for _, res := range resources {
3✔
458
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
459
                        return true
×
460
                }
×
461
        }
462

463
        return false
3✔
464
}
465

466
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
467
// This must be called from Scaffold (before config is saved) for changes to be persisted.
468
func (p *editSubcommand) removeV1AlphaPluginEntry() {
4✔
469
        // Only attempt to remove if using v3 config (which supports plugin configs)
4✔
470
        cfg, ok := p.config.(*cfgv3.Cfg)
4✔
471
        if !ok {
4✔
472
                return
×
473
        }
×
474

475
        // Check if v1-alpha plugin entry exists
476
        if cfg.Plugins == nil {
6✔
477
                return
2✔
478
        }
2✔
479

480
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
481
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
482
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
483
        }
2✔
484
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc