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

kubernetes-sigs / kubebuilder / 24537942620

16 Apr 2026 10:47PM UTC coverage: 82.376% (+0.004%) from 82.372%
24537942620

push

github

web-flow
Merge pull request #5637 from camilamacedo86/remove-comment-redu

🌱 refactor(helm/v2-alpha): Remove redundant code comments

7698 of 9345 relevant lines covered (82.38%)

73.8 hits per line

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

41.18
/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/internal/common"
37
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
38
)
39

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

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

49
type editSubcommand struct {
50
        config        config.Config
51
        force         bool
52
        manifestsFile string
53
        outputDir     string
54
}
55

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

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

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

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

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

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

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

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

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

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

103
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
1✔
104
        fs.BoolVar(&p.force, "force", false, "If set, regenerate all files except Chart.yaml")
1✔
105
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
1✔
106
                "Path to the YAML file containing Kubernetes manifests from kustomize output "+
1✔
107
                        "(e.g., dist/install.yaml). Defaults to dist/install.yaml if unset")
1✔
108
        fs.StringVar(&p.outputDir, "output-dir", common.DefaultOutputDir,
1✔
109
                "Output directory for the generated Helm chart (e.g., charts). Defaults to dist if unset")
1✔
110
}
1✔
111

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

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

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

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

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

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

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

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

185
        return nil
×
186
}
187

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
func (p *editSubcommand) PostScaffold() error {
2✔
209
        hasWebhooks := hasWebhooksWith(p.config)
2✔
210

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

245
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
246
        makefilePath := "Makefile"
3✔
247
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
248
                return fmt.Errorf("makefile not found")
1✔
249
        }
1✔
250

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

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

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

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

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

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

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

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

309
        // Fallback to default if manager Deployment not found
310
        return defaultNamespace
×
311
}
312

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

320
        // Use the project name as default for release name
321
        release := projectName
5✔
322

5✔
323
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
324
}
325

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

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

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

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

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

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

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

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

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

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

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

393
        return false
3✔
394
}
395

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

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

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