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

kubernetes-sigs / kubebuilder / 22015043146

14 Feb 2026 09:28AM UTC coverage: 73.343% (-0.05%) from 73.395%
22015043146

Pull #5478

github

camilamacedo86
chore(helm/v2-alpha): remove helm test feature (reverts pre-release addition)
Pull Request #5478: ⚠️ (helm/v2-alpha): remove helm test feature (reverts pre-release addition to be safe)

1 of 1 new or added line in 1 file covered. (100.0%)

22 existing lines in 1 file now uncovered.

6983 of 9521 relevant lines covered (73.34%)

44.9 hits per line

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

40.93
/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
    ├── tests/
1✔
101
    └── ...
1✔
102
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
1✔
103
}
1✔
104

105
func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
1✔
106
        fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
1✔
107
        fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
1✔
108
                "path to the YAML file containing Kubernetes manifests from kustomize output")
1✔
109
        fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
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)
×
UNCOV
122
                }
×
123
        }
124

125
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(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)
×
UNCOV
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
×
UNCOV
160
                                }
×
161
                        }
162
                default:
×
UNCOV
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)
×
UNCOV
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)
×
UNCOV
182
                }
×
183
        }
184

UNCOV
185
        return nil
×
186
}
187

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

396
        return false
3✔
397
}
398

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

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

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