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

kubernetes-sigs / kubebuilder / 21719734236

05 Feb 2026 04:30PM UTC coverage: 66.464% (-7.4%) from 73.91%
21719734236

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 1627 new or added lines in 24 files covered. (24.71%)

1 existing line in 1 file now uncovered.

7111 of 10699 relevant lines covered (66.46%)

37.75 hits per line

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

32.51
/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
        "log/slog"
23
        "os"
24
        "path/filepath"
25

26
        "github.com/spf13/afero"
27
        "github.com/spf13/pflag"
28

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

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

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

48
type editSubcommand struct {
49
        config        config.Config
50
        force         bool
51
        manifestsFile string
52
        outputDir     string
53
        delete        bool // Delete flag to remove Helm chart generation
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
This plugin dynamically generates Helm chart templates by parsing the output of 'make build-installer' 
1✔
61
(dist/install.yaml by default). The generated chart preserves all customizations made to your kustomize 
1✔
62
configuration including environment variables, labels, and annotations.
1✔
63

1✔
64
The chart structure mirrors your config/ directory organization for easy maintenance.`
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, and
1✔
87
.github/workflows/test-chart.yml. All template files in templates/ are always regenerated
1✔
88
to match your current kustomize output. Use --force to regenerate all files except Chart.yaml.
1✔
89

1✔
90
The generated chart structure mirrors your config/ directory:
1✔
91
<output>/chart/
1✔
92
├── Chart.yaml
1✔
93
├── values.yaml
1✔
94
├── .helmignore
1✔
95
└── templates/
1✔
96
    ├── NOTES.txt
1✔
97
    ├── _helpers.tpl
1✔
98
    ├── rbac/
1✔
99
    ├── manager/
1✔
100
    ├── webhook/
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
        fs.BoolVar(&p.delete, "delete", false, "delete Helm chart generation from the project")
1✔
111
}
1✔
112

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

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

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

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

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

×
143
        // Save plugin config to PROJECT file
×
144
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
145
        canonicalKey := plugin.KeyFor(Plugin{})
×
146
        cfg := pluginConfig{}
×
147
        if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
×
148
                switch {
×
149
                case errors.As(err, &config.UnsupportedFieldError{}):
×
150
                        // Config version doesn't support plugin metadata
×
151
                        return nil
×
152
                case errors.As(err, &config.PluginKeyNotFoundError{}):
×
153
                        if key != canonicalKey {
×
154
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
155
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
156
                                                return nil
×
157
                                        }
×
158
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
159
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
160
                                        }
×
161
                                }
162
                        }
163
                default:
×
164
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
165
                }
166
        }
167

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

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

176
        return nil
×
177
}
178

179
// ensureManifestsExist runs make build-installer to generate the default manifests file
180
func (p *editSubcommand) ensureManifestsExist() error {
×
181
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
182

×
183
        // Run the required make targets to generate the manifests file
×
184
        targets := []string{"manifests", "generate", "build-installer"}
×
185
        for _, target := range targets {
×
186
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
187
                        return fmt.Errorf("make %s failed: %w", target, err)
×
188
                }
×
189
        }
190

191
        // Verify the file was created
192
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
193
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
194
        }
×
195

196
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
197
        return nil
×
198
}
199

200
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
201
func (p *editSubcommand) PostScaffold() error {
2✔
202
        hasWebhooks := hasWebhooksWith(p.config)
2✔
203

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

238
func hasWebhooksWith(c config.Config) bool {
3✔
239
        resources, err := c.GetResources()
3✔
240
        if err != nil {
3✔
241
                return false
×
242
        }
×
243

244
        for _, res := range resources {
3✔
245
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
246
                        return true
×
247
                }
×
248
        }
249

250
        return false
3✔
251
}
252

253
// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
254
// This must be called from Scaffold (before config is saved) for changes to be persisted.
255
func (p *editSubcommand) removeV1AlphaPluginEntry() {
4✔
256
        // Only attempt to remove if using v3 config (which supports plugin configs)
4✔
257
        cfg, ok := p.config.(*cfgv3.Cfg)
4✔
258
        if !ok {
4✔
259
                return
×
260
        }
×
261

262
        // Check if v1-alpha plugin entry exists
263
        if cfg.Plugins == nil {
6✔
264
                return
2✔
265
        }
2✔
266

267
        if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
4✔
268
                delete(cfg.Plugins, v1AlphaPluginKey)
2✔
269
                slog.Info("removed deprecated v1-alpha plugin entry")
2✔
270
        }
2✔
271
}
272

273
// deleteHelmChart removes Helm chart files and configuration (best effort)
NEW
274
func (p *editSubcommand) deleteHelmChart(fs machinery.Filesystem) error {
×
NEW
275
        slog.Info("Deleting Helm chart files...")
×
NEW
276

×
NEW
277
        // Get plugin config to find output directory
×
NEW
278
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
NEW
279
        canonicalKey := plugin.KeyFor(Plugin{})
×
NEW
280
        cfg := pluginConfig{}
×
NEW
281

×
NEW
282
        err := p.config.DecodePluginConfig(key, &cfg)
×
NEW
283
        if err != nil {
×
NEW
284
                if errors.As(err, &config.PluginKeyNotFoundError{}) && key != canonicalKey {
×
NEW
285
                        _ = p.config.DecodePluginConfig(canonicalKey, &cfg)
×
NEW
286
                }
×
287
        }
288

289
        // Use configured output dir or default
NEW
290
        outputDir := p.outputDir
×
NEW
291
        if outputDir == "" {
×
NEW
292
                outputDir = cfg.OutputDir
×
NEW
293
        }
×
NEW
294
        if outputDir == "" {
×
NEW
295
                outputDir = DefaultOutputDir
×
NEW
296
        }
×
297

NEW
298
        deletedCount := 0
×
NEW
299
        warnCount := 0
×
NEW
300

×
NEW
301
        // Delete chart directory (best effort)
×
NEW
302
        chartDir := filepath.Join(outputDir, "chart")
×
NEW
303
        if exists, _ := afero.DirExists(fs.FS, chartDir); exists {
×
NEW
304
                if err := fs.FS.RemoveAll(chartDir); err != nil {
×
NEW
305
                        slog.Warn("Failed to delete Helm chart directory", "path", chartDir, "error", err)
×
NEW
306
                        warnCount++
×
NEW
307
                } else {
×
NEW
308
                        slog.Info("Deleted Helm chart directory", "path", chartDir)
×
NEW
309
                        deletedCount++
×
NEW
310
                }
×
NEW
311
        } else {
×
NEW
312
                slog.Warn("Helm chart directory not found", "path", chartDir)
×
NEW
313
                warnCount++
×
NEW
314
        }
×
315

316
        // Delete test workflow (best effort)
NEW
317
        testChartPath := filepath.Join(".github", "workflows", "test-chart.yml")
×
NEW
318
        if exists, _ := afero.Exists(fs.FS, testChartPath); exists {
×
NEW
319
                if err := fs.FS.Remove(testChartPath); err != nil {
×
NEW
320
                        slog.Warn("Failed to delete test-chart.yml", "path", testChartPath, "error", err)
×
NEW
321
                        warnCount++
×
NEW
322
                } else {
×
NEW
323
                        slog.Info("Deleted test-chart workflow", "path", testChartPath)
×
NEW
324
                        deletedCount++
×
NEW
325
                }
×
NEW
326
        } else {
×
NEW
327
                slog.Warn("Test chart workflow not found", "path", testChartPath)
×
NEW
328
                warnCount++
×
NEW
329
        }
×
330

331
        // Remove plugin config from PROJECT by encoding empty struct
NEW
332
        if encErr := p.config.EncodePluginConfig(key, struct{}{}); encErr != nil {
×
NEW
333
                // Try canonical key if different
×
NEW
334
                if key != canonicalKey {
×
NEW
335
                        if encErr2 := p.config.EncodePluginConfig(canonicalKey, struct{}{}); encErr2 != nil {
×
NEW
336
                                slog.Warn("Failed to remove plugin configuration from PROJECT file",
×
NEW
337
                                        "provided_key_error", encErr, "canonical_key_error", encErr2)
×
NEW
338
                                warnCount++
×
NEW
339
                        }
×
NEW
340
                } else {
×
NEW
341
                        slog.Warn("Failed to remove plugin configuration from PROJECT file", "error", encErr)
×
NEW
342
                        warnCount++
×
NEW
343
                }
×
344
        }
345

NEW
346
        fmt.Printf("\nSuccessfully completed Helm plugin deletion\n")
×
NEW
347
        if deletedCount > 0 {
×
NEW
348
                fmt.Printf("Deleted: %d item(s)\n", deletedCount)
×
NEW
349
        }
×
NEW
350
        if warnCount > 0 {
×
NEW
351
                fmt.Printf("Warnings: %d item(s) - some files may not exist or couldn't be deleted (see logs)\n", warnCount)
×
NEW
352
        }
×
353

NEW
354
        return nil
×
355
}
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