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

kubernetes-sigs / kubebuilder / 22064282680

16 Feb 2026 01:18PM UTC coverage: 73.433% (-0.2%) from 73.667%
22064282680

push

github

web-flow
Merge pull request #5484 from kubernetes-sigs/master

📖 Update docs for release v4.12.0

493 of 682 new or added lines in 34 files covered. (72.29%)

5 existing lines in 2 files now uncovered.

7004 of 9538 relevant lines covered (73.43%)

44.85 hits per line

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

40.68
/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
}
56

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

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

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

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

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

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

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

1✔
79
# Typical workflow:
1✔
80
  make build-installer  # Generate dist/install.yaml with latest changes
1✔
81
  %[1]s edit --plugins=%[2]s  # Generate/update Helm chart in dist/chart/
1✔
82

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

1✔
89
The generated chart structure mirrors your config/ directory:
1✔
90
<output>/chart/
1✔
91
├── Chart.yaml
1✔
92
├── values.yaml
1✔
93
├── .helmignore
1✔
94
└── templates/
1✔
95
    ├── NOTES.txt
1✔
96
    ├── _helpers.tpl
1✔
97
    ├── rbac/
1✔
98
    ├── manager/
1✔
99
    ├── webhook/
1✔
100
    └── ...
1✔
101
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
1✔
102
}
1✔
103

104
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
1✔
105
        fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
1✔
106
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
1✔
107
                "path to the YAML file containing Kubernetes manifests from kustomize output")
1✔
108
        fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
1✔
109
}
1✔
110

111
func (p *editSubcommand) InjectConfig(c config.Config) error {
1✔
112
        p.config = c
1✔
113
        return nil
1✔
114
}
1✔
115

116
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
×
117
        // If using default manifests file, ensure it exists by running make build-installer
×
118
        if p.manifestsFile == DefaultManifestsFile {
×
119
                if err := p.ensureManifestsExist(); err != nil {
×
120
                        slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
×
121
                }
×
122
        }
123

124
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
125
        scaffolder.InjectFS(fs)
×
126
        err := scaffolder.Scaffold()
×
127
        if err != nil {
×
128
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
129
        }
×
130

131
        // Remove deprecated v1-alpha plugin entry from PROJECT file
132
        // This must happen in Scaffold (before config is saved) to be persisted
NEW
133
        p.removeV1AlphaPluginEntry()
×
NEW
134

×
135
        // Save plugin config to PROJECT file
×
136
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
137
        canonicalKey := plugin.KeyFor(Plugin{})
×
138
        cfg := pluginConfig{}
×
NEW
139
        isFirstRun := false
×
140
        if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
×
141
                switch {
×
142
                case errors.As(err, &config.UnsupportedFieldError{}):
×
143
                        // Config version doesn't support plugin metadata
×
144
                        return nil
×
145
                case errors.As(err, &config.PluginKeyNotFoundError{}):
×
NEW
146
                        // This is the first time the plugin is run
×
NEW
147
                        isFirstRun = true
×
148
                        if key != canonicalKey {
×
149
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
150
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
151
                                                return nil
×
152
                                        }
×
153
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
154
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
155
                                        }
×
NEW
156
                                } else {
×
NEW
157
                                        // Found config under canonical key, not first run
×
NEW
158
                                        isFirstRun = false
×
UNCOV
159
                                }
×
160
                        }
161
                default:
×
162
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
163
                }
164
        }
165

166
        // Update configuration with current parameters
167
        cfg.ManifestsFile = p.manifestsFile
×
168
        cfg.OutputDir = p.outputDir
×
169

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

174
        // Add Helm deployment targets to Makefile only on first run
NEW
175
        if isFirstRun {
×
NEW
176
                slog.Info("adding Helm deployment targets to Makefile...")
×
NEW
177
                // Extract namespace from manifests for accurate Makefile generation
×
NEW
178
                namespace := p.extractNamespaceFromManifests()
×
NEW
179
                if err := p.addHelmMakefileTargets(namespace); err != nil {
×
NEW
180
                        slog.Warn("failed to add Helm targets to Makefile", "error", err)
×
NEW
181
                }
×
182
        }
183

UNCOV
184
        return nil
×
185
}
186

187
// ensureManifestsExist runs make build-installer to generate the default manifests file
188
func (p *editSubcommand) ensureManifestsExist() error {
×
189
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
190

×
191
        // Run the required make targets to generate the manifests file
×
192
        targets := []string{"manifests", "generate", "build-installer"}
×
193
        for _, target := range targets {
×
194
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
195
                        return fmt.Errorf("make %s failed: %w", target, err)
×
196
                }
×
197
        }
198

199
        // Verify the file was created
200
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
201
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
202
        }
×
203

204
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
205
        return nil
×
206
}
207

208
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
209
func (p *editSubcommand) PostScaffold() error {
2✔
210
        hasWebhooks := hasWebhooksWith(p.config)
2✔
211

2✔
212
        if hasWebhooks {
2✔
213
                workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
×
214
                if _, err := os.Stat(workflowFile); err != nil {
×
215
                        slog.Info(
×
216
                                "Workflow file not found, unable to uncomment cert-manager installation",
×
217
                                "error", err,
×
218
                                "file", workflowFile,
×
219
                        )
×
220
                        return nil
×
221
                }
×
222
                target := `
×
223
#      - name: Install cert-manager via Helm (wait for readiness)
×
224
#        run: |
×
225
#          helm repo add jetstack https://charts.jetstack.io
×
226
#          helm repo update
×
227
#          helm install cert-manager jetstack/cert-manager \
×
228
#            --namespace cert-manager \
×
229
#            --create-namespace \
×
230
#            --set crds.enabled=true \
×
231
#            --wait \
×
232
#            --timeout 300s`
×
233
                if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
×
234
                        hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
×
235
                        if !hasUncommented || errCheck != nil {
×
236
                                slog.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
×
237
                        }
×
238
                } else {
×
239
                        target = `# TODO: Uncomment if cert-manager is enabled`
×
240
                        _ = util.ReplaceInFile(workflowFile, target, "")
×
241
                }
×
242
        }
243
        return nil
2✔
244
}
245

246
// addHelmMakefileTargets appends Helm deployment targets to the Makefile if they don't already exist
247
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
248
        makefilePath := "Makefile"
3✔
249
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
250
                return fmt.Errorf("makefile not found")
1✔
251
        }
1✔
252

253
        // Get the Helm Makefile targets
254
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
2✔
255

2✔
256
        // Append the targets if they don't already exist
2✔
257
        if err := util.AppendCodeIfNotExist(makefilePath, helmTargets); err != nil {
2✔
NEW
258
                return fmt.Errorf("failed to append Helm targets to Makefile: %w", err)
×
NEW
259
        }
×
260

261
        slog.Info("added Helm deployment targets to Makefile",
2✔
262
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
2✔
263
        return nil
2✔
264
}
265

266
// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
267
// Returns projectName-system if manifests don't exist or namespace not found.
NEW
268
func (p *editSubcommand) extractNamespaceFromManifests() string {
×
NEW
269
        // Default to project-name-system pattern
×
NEW
270
        defaultNamespace := p.config.GetProjectName() + "-system"
×
NEW
271

×
NEW
272
        // If manifests file doesn't exist, use default
×
NEW
273
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
×
NEW
274
                return defaultNamespace
×
NEW
275
        }
×
276

277
        // Parse the manifests to get the namespace
NEW
278
        file, err := os.Open(p.manifestsFile)
×
NEW
279
        if err != nil {
×
NEW
280
                return defaultNamespace
×
NEW
281
        }
×
NEW
282
        defer func() {
×
NEW
283
                _ = file.Close()
×
NEW
284
        }()
×
285

286
        // Parse YAML documents looking for the manager Deployment
NEW
287
        decoder := yaml.NewDecoder(file)
×
NEW
288
        for {
×
NEW
289
                var doc map[string]any
×
NEW
290
                if err := decoder.Decode(&doc); err != nil {
×
NEW
291
                        if err == io.EOF {
×
NEW
292
                                break
×
293
                        }
NEW
294
                        continue
×
295
                }
296

297
                // Check if this is a Deployment (manager)
NEW
298
                if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
×
NEW
299
                        if metadata, ok := doc["metadata"].(map[string]any); ok {
×
NEW
300
                                // Check if it's the manager deployment
×
NEW
301
                                if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
×
NEW
302
                                        // Extract namespace from the manager Deployment
×
NEW
303
                                        if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
×
NEW
304
                                                return namespace
×
NEW
305
                                        }
×
306
                                }
307
                        }
308
                }
309
        }
310

311
        // Fallback to default if manager Deployment not found
NEW
312
        return defaultNamespace
×
313
}
314

315
// getHelmMakefileTargets returns the Helm Makefile targets as a string
316
// following the same patterns as the existing Makefile deployment section
317
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
5✔
318
        if outputDir == "" {
5✔
NEW
319
                outputDir = "dist"
×
NEW
320
        }
×
321

322
        // Use the project name as default for release name
323
        release := projectName
5✔
324

5✔
325
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
326
}
327

328
// helmMakefileTemplate returns the Helm deployment section template
329
// This follows the same pattern as the Kustomize deployment section in the Go plugin
330
const helmMakefileTemplateFormat = `
331
##@ Helm Deployment
332

333
## Helm binary to use for deploying the chart
334
HELM ?= helm
335
## Namespace to deploy the Helm release
336
HELM_NAMESPACE ?= %s
337
## Name of the Helm release
338
HELM_RELEASE ?= %s
339
## Path to the Helm chart directory
340
HELM_CHART_DIR ?= %s/chart
341
## Additional arguments to pass to helm commands
342
HELM_EXTRA_ARGS ?=
343

344
.PHONY: install-helm
345
install-helm: ## Install the latest version of Helm.
346
        @command -v $(HELM) >/dev/null 2>&1 || { \
347
                echo "Installing Helm..." && \
348
                curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
349
        }
350

351
.PHONY: helm-deploy
352
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
353
        $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
354
                --namespace $(HELM_NAMESPACE) \
355
                --create-namespace \
356
                --set manager.image.repository=$${IMG%%:*} \
357
                --set manager.image.tag=$${IMG##*:} \
358
                --wait \
359
                --timeout 5m \
360
                $(HELM_EXTRA_ARGS)
361

362
.PHONY: helm-uninstall
363
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
364
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
365

366
.PHONY: helm-status
367
helm-status: ## Show Helm release status.
368
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
369

370
.PHONY: helm-history
371
helm-history: ## Show Helm release history.
372
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
373

374
.PHONY: helm-rollback
375
helm-rollback: ## Rollback to previous Helm release.
376
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
377
`
378

379
func helmMakefileTemplate(namespace, release, outputDir string) string {
5✔
380
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
5✔
381
}
5✔
382

383
func hasWebhooksWith(c config.Config) bool {
3✔
384
        resources, err := c.GetResources()
3✔
385
        if err != nil {
3✔
386
                return false
×
387
        }
×
388

389
        for _, res := range resources {
3✔
390
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
391
                        return true
×
392
                }
×
393
        }
394

395
        return false
3✔
396
}
397

398
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
399
// This must be called from Scaffold (before config is saved) for changes to be persisted.
400
func (p *editSubcommand) removeV1AlphaPluginEntry() {
4✔
401
        // Only attempt to remove if using v3 config (which supports plugin configs)
4✔
402
        cfg, ok := p.config.(*cfgv3.Cfg)
4✔
403
        if !ok {
4✔
NEW
404
                return
×
NEW
405
        }
×
406

407
        // Check if v1-alpha plugin entry exists
408
        if cfg.Plugins == nil {
6✔
409
                return
2✔
410
        }
2✔
411

412
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
413
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
414
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
415
        }
2✔
416
}
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