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

kubernetes-sigs / kubebuilder / 23032648315

13 Mar 2026 01:53AM UTC coverage: 69.957% (-9.8%) from 79.762%
23032648315

Pull #5352

github

camilamacedo86
(chore) Add delete interface and implementation for all options

Add delete functionality to remove APIs and webhooks from projects.
Users can now clean up scaffolded resources.

Generated-by: Cursor/Claude
Pull Request #5352: WIP ✨ Added Delete API and implemented a unified interface across all commands and plugin options

402 of 1618 new or added lines in 24 files covered. (24.85%)

1 existing line in 1 file now uncovered.

6308 of 9017 relevant lines covered (69.96%)

30.25 hits per line

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

31.29
/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/afero"
29
        "github.com/spf13/pflag"
30
        "go.yaml.in/yaml/v3"
31

32
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
33
        cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
36
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
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
        // DefaultOutputDir is the default output directory for Helm charts
44
        DefaultOutputDir = "dist"
45
        // v1AlphaPluginKey is the deprecated v1-alpha plugin key
46
        v1AlphaPluginKey = "helm.kubebuilder.io/v1-alpha"
47
)
48

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

51
type editSubcommand struct {
52
        config        config.Config
53
        force         bool
54
        manifestsFile string
55
        outputDir     string
56
        delete        bool // Delete flag to remove Helm chart generation
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
# Typical workflow:
1✔
82
  make build-installer  # Generate dist/install.yaml with latest changes
1✔
83
  %[1]s edit --plugins=%[2]s  # Generate/update Helm chart in dist/chart/
1✔
84

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

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

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

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

119
func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
×
NEW
120
        // Handle delete mode
×
NEW
121
        if p.delete {
×
NEW
122
                return p.deleteHelmChart(fs)
×
NEW
123
        }
×
124

125
        // Normal scaffold mode
126
        // If using default manifests file, ensure it exists by running make build-installer
127
        if p.manifestsFile == DefaultManifestsFile {
×
128
                if err := p.ensureManifestsExist(); err != nil {
×
129
                        slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
×
130
                }
×
131
        }
132

133
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
134
        scaffolder.InjectFS(fs)
×
135
        err := scaffolder.Scaffold()
×
136
        if err != nil {
×
137
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
138
        }
×
139

140
        // Remove deprecated v1-alpha plugin entry from PROJECT file
141
        // This must happen in Scaffold (before config is saved) to be persisted
142
        p.removeV1AlphaPluginEntry()
×
143

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

175
        // Update configuration with current parameters
176
        cfg.ManifestsFile = p.manifestsFile
×
177
        cfg.OutputDir = p.outputDir
×
178

×
179
        if err = p.config.EncodePluginConfig(key, cfg); err != nil {
×
180
                return fmt.Errorf("error encoding plugin configuration: %w", err)
×
181
        }
×
182

183
        // Add Helm deployment targets to Makefile only on first run
184
        if isFirstRun {
×
185
                slog.Info("adding Helm deployment targets to Makefile...")
×
186
                // Extract namespace from manifests for accurate Makefile generation
×
187
                namespace := p.extractNamespaceFromManifests()
×
188
                if err := p.addHelmMakefileTargets(namespace); err != nil {
×
189
                        slog.Warn("failed to add Helm targets to Makefile", "error", err)
×
190
                }
×
191
        }
192

193
        return nil
×
194
}
195

196
// ensureManifestsExist runs make build-installer to generate the default manifests file
197
func (p *editSubcommand) ensureManifestsExist() error {
×
198
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
199

×
200
        // Run the required make targets to generate the manifests file
×
201
        targets := []string{"manifests", "generate", "build-installer"}
×
202
        for _, target := range targets {
×
203
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
204
                        return fmt.Errorf("make %s failed: %w", target, err)
×
205
                }
×
206
        }
207

208
        // Verify the file was created
209
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
210
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
211
        }
×
212

213
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
214
        return nil
×
215
}
216

217
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
218
func (p *editSubcommand) PostScaffold() error {
2✔
219
        hasWebhooks := hasWebhooksWith(p.config)
2✔
220

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

255
// addHelmMakefileTargets appends Helm deployment targets to the Makefile if they don't already exist
256
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
3✔
257
        makefilePath := "Makefile"
3✔
258
        if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
4✔
259
                return fmt.Errorf("makefile not found")
1✔
260
        }
1✔
261

262
        // Get the Helm Makefile targets
263
        helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)
2✔
264

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

270
        slog.Info("added Helm deployment targets to Makefile",
2✔
271
                "targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
2✔
272
        return nil
2✔
273
}
274

275
// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
276
// Returns projectName-system if manifests don't exist or namespace not found.
277
func (p *editSubcommand) extractNamespaceFromManifests() string {
×
278
        // Default to project-name-system pattern
×
279
        defaultNamespace := p.config.GetProjectName() + "-system"
×
280

×
281
        // If manifests file doesn't exist, use default
×
282
        if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
×
283
                return defaultNamespace
×
284
        }
×
285

286
        // Parse the manifests to get the namespace
287
        file, err := os.Open(p.manifestsFile)
×
288
        if err != nil {
×
289
                return defaultNamespace
×
290
        }
×
291
        defer func() {
×
292
                _ = file.Close()
×
293
        }()
×
294

295
        // Parse YAML documents looking for the manager Deployment
296
        decoder := yaml.NewDecoder(file)
×
297
        for {
×
298
                var doc map[string]any
×
299
                if err := decoder.Decode(&doc); err != nil {
×
300
                        if err == io.EOF {
×
301
                                break
×
302
                        }
303
                        continue
×
304
                }
305

306
                // Check if this is a Deployment (manager)
307
                if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
×
308
                        if metadata, ok := doc["metadata"].(map[string]any); ok {
×
309
                                // Check if it's the manager deployment
×
310
                                if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
×
311
                                        // Extract namespace from the manager Deployment
×
312
                                        if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
×
313
                                                return namespace
×
314
                                        }
×
315
                                }
316
                        }
317
                }
318
        }
319

320
        // Fallback to default if manager Deployment not found
321
        return defaultNamespace
×
322
}
323

324
// getHelmMakefileTargets returns the Helm Makefile targets as a string
325
// following the same patterns as the existing Makefile deployment section
326
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
5✔
327
        if outputDir == "" {
5✔
328
                outputDir = "dist"
×
329
        }
×
330

331
        // Use the project name as default for release name
332
        release := projectName
5✔
333

5✔
334
        return helmMakefileTemplate(namespace, release, outputDir)
5✔
335
}
336

337
// helmMakefileTemplate returns the Helm deployment section template
338
// This follows the same pattern as the Kustomize deployment section in the Go plugin
339
const helmMakefileTemplateFormat = `
340
##@ Helm Deployment
341

342
## Helm binary to use for deploying the chart
343
HELM ?= helm
344
## Namespace to deploy the Helm release
345
HELM_NAMESPACE ?= %s
346
## Name of the Helm release
347
HELM_RELEASE ?= %s
348
## Path to the Helm chart directory
349
HELM_CHART_DIR ?= %s/chart
350
## Additional arguments to pass to helm commands
351
HELM_EXTRA_ARGS ?=
352

353
.PHONY: install-helm
354
install-helm: ## Install the latest version of Helm.
355
        @command -v $(HELM) >/dev/null 2>&1 || { \
356
                echo "Installing Helm..." && \
357
                curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
358
        }
359

360
.PHONY: helm-deploy
361
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
362
        $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
363
                --namespace $(HELM_NAMESPACE) \
364
                --create-namespace \
365
                --set manager.image.repository=$${IMG%%:*} \
366
                --set manager.image.tag=$${IMG##*:} \
367
                --wait \
368
                --timeout 5m \
369
                $(HELM_EXTRA_ARGS)
370

371
.PHONY: helm-uninstall
372
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
373
        $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
374

375
.PHONY: helm-status
376
helm-status: ## Show Helm release status.
377
        $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
378

379
.PHONY: helm-history
380
helm-history: ## Show Helm release history.
381
        $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
382

383
.PHONY: helm-rollback
384
helm-rollback: ## Rollback to previous Helm release.
385
        $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
386
`
387

388
func helmMakefileTemplate(namespace, release, outputDir string) string {
5✔
389
        return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
5✔
390
}
5✔
391

392
func hasWebhooksWith(c config.Config) bool {
3✔
393
        resources, err := c.GetResources()
3✔
394
        if err != nil {
3✔
395
                return false
×
396
        }
×
397

398
        for _, res := range resources {
3✔
399
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
400
                        return true
×
401
                }
×
402
        }
403

404
        return false
3✔
405
}
406

407
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
408
// This must be called from Scaffold (before config is saved) for changes to be persisted.
409
func (p *editSubcommand) removeV1AlphaPluginEntry() {
4✔
410
        // Only attempt to remove if using v3 config (which supports plugin configs)
4✔
411
        cfg, ok := p.config.(*cfgv3.Cfg)
4✔
412
        if !ok {
4✔
413
                return
×
414
        }
×
415

416
        // Check if v1-alpha plugin entry exists
417
        if cfg.Plugins == nil {
6✔
418
                return
2✔
419
        }
2✔
420

421
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
422
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
423
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
424
        }
2✔
425
}
426

427
// deleteHelmChart removes Helm chart files and configuration (best effort)
NEW
428
func (p *editSubcommand) deleteHelmChart(fs machinery.Filesystem) error {
×
NEW
429
        slog.Info("Deleting Helm chart files...")
×
NEW
430

×
NEW
431
        // Get plugin config to find output directory
×
NEW
432
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
NEW
433
        canonicalKey := plugin.KeyFor(Plugin{})
×
NEW
434
        cfg := pluginConfig{}
×
NEW
435

×
NEW
436
        err := p.config.DecodePluginConfig(key, &cfg)
×
NEW
437
        if err != nil {
×
NEW
438
                if errors.As(err, &config.PluginKeyNotFoundError{}) && key != canonicalKey {
×
NEW
439
                        _ = p.config.DecodePluginConfig(canonicalKey, &cfg)
×
NEW
440
                }
×
441
        }
442

443
        // Use configured output dir or default
NEW
444
        outputDir := p.outputDir
×
NEW
445
        if outputDir == "" {
×
NEW
446
                outputDir = cfg.OutputDir
×
NEW
447
        }
×
NEW
448
        if outputDir == "" {
×
NEW
449
                outputDir = DefaultOutputDir
×
NEW
450
        }
×
451

NEW
452
        deletedCount := 0
×
NEW
453
        warnCount := 0
×
NEW
454

×
NEW
455
        // Delete chart directory (best effort)
×
NEW
456
        chartDir := filepath.Join(outputDir, "chart")
×
NEW
457
        if exists, _ := afero.DirExists(fs.FS, chartDir); exists {
×
NEW
458
                if err := fs.FS.RemoveAll(chartDir); err != nil {
×
NEW
459
                        slog.Warn("Failed to delete Helm chart directory", "path", chartDir, "error", err)
×
NEW
460
                        warnCount++
×
NEW
461
                } else {
×
NEW
462
                        slog.Info("Deleted Helm chart directory", "path", chartDir)
×
NEW
463
                        deletedCount++
×
NEW
464
                }
×
NEW
465
        } else {
×
NEW
466
                slog.Warn("Helm chart directory not found", "path", chartDir)
×
NEW
467
                warnCount++
×
NEW
468
        }
×
469

470
        // Delete test workflow (best effort)
NEW
471
        testChartPath := filepath.Join(".github", "workflows", "test-chart.yml")
×
NEW
472
        if exists, _ := afero.Exists(fs.FS, testChartPath); exists {
×
NEW
473
                if err := fs.FS.Remove(testChartPath); err != nil {
×
NEW
474
                        slog.Warn("Failed to delete test-chart.yml", "path", testChartPath, "error", err)
×
NEW
475
                        warnCount++
×
NEW
476
                } else {
×
NEW
477
                        slog.Info("Deleted test-chart workflow", "path", testChartPath)
×
NEW
478
                        deletedCount++
×
NEW
479
                }
×
NEW
480
        } else {
×
NEW
481
                slog.Warn("Test chart workflow not found", "path", testChartPath)
×
NEW
482
                warnCount++
×
NEW
483
        }
×
484

485
        // Remove plugin config from PROJECT by encoding empty struct
NEW
486
        if encErr := p.config.EncodePluginConfig(key, struct{}{}); encErr != nil {
×
NEW
487
                // Try canonical key if different
×
NEW
488
                if key != canonicalKey {
×
NEW
489
                        if encErr2 := p.config.EncodePluginConfig(canonicalKey, struct{}{}); encErr2 != nil {
×
NEW
490
                                slog.Warn("Failed to remove plugin configuration from PROJECT file",
×
NEW
491
                                        "provided_key_error", encErr, "canonical_key_error", encErr2)
×
NEW
492
                                warnCount++
×
NEW
493
                        }
×
NEW
494
                } else {
×
NEW
495
                        slog.Warn("Failed to remove plugin configuration from PROJECT file", "error", encErr)
×
NEW
496
                        warnCount++
×
NEW
497
                }
×
498
        }
499

NEW
500
        fmt.Printf("\nSuccessfully completed Helm plugin deletion\n")
×
NEW
501
        if deletedCount > 0 {
×
NEW
502
                fmt.Printf("Deleted: %d item(s)\n", deletedCount)
×
NEW
503
        }
×
NEW
504
        if warnCount > 0 {
×
NEW
505
                fmt.Printf("Warnings: %d item(s) - some files may not exist or couldn't be deleted (see logs)\n", warnCount)
×
NEW
506
        }
×
507

NEW
508
        return nil
×
509
}
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