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

kubernetes-sigs / kubebuilder / 21705957068

05 Feb 2026 09:24AM UTC coverage: 66.755% (-7.2%) from 73.919%
21705957068

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.
Pull Request #5352: ✨ Added Delete API and implemented a unified interface across all commands and plugin options

394 of 1568 new or added lines in 21 files covered. (25.13%)

1 existing line in 1 file now uncovered.

7092 of 10624 relevant lines covered (66.75%)

37.96 hits per line

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

29.44
/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
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
31
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
32
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
34
)
35

36
const (
37
        // DefaultManifestsFile is the default path for kustomize output manifests
38
        DefaultManifestsFile = "dist/install.yaml"
39
        // DefaultOutputDir is the default output directory for Helm charts
40
        DefaultOutputDir = "dist"
41
)
42

43
var _ plugin.EditSubcommand = &editSubcommand{}
44

45
type editSubcommand struct {
46
        config        config.Config
47
        force         bool
48
        manifestsFile string
49
        outputDir     string
50
        delete        bool // Delete flag to remove Helm chart generation
51
}
52

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

1✔
57
This plugin dynamically generates Helm chart templates by parsing the output of 'make build-installer' 
1✔
58
(dist/install.yaml by default). The generated chart preserves all customizations made to your kustomize 
1✔
59
configuration including environment variables, labels, and annotations.
1✔
60

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

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

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

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

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

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

129
        scaffolder := scaffolds.NewKustomizeHelmScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
×
130
        scaffolder.InjectFS(fs)
×
131
        err := scaffolder.Scaffold()
×
132
        if err != nil {
×
133
                return fmt.Errorf("error scaffolding Helm chart: %w", err)
×
134
        }
×
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
        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{}):
×
146
                        if key != canonicalKey {
×
147
                                if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
×
148
                                        if errors.As(err2, &config.UnsupportedFieldError{}) {
×
149
                                                return nil
×
150
                                        }
×
151
                                        if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
×
152
                                                return fmt.Errorf("error decoding plugin configuration: %w", err2)
×
153
                                        }
×
154
                                }
155
                        }
156
                default:
×
157
                        return fmt.Errorf("error decoding plugin configuration: %w", err)
×
158
                }
159
        }
160

161
        // Update configuration with current parameters
162
        cfg.ManifestsFile = p.manifestsFile
×
163
        cfg.OutputDir = p.outputDir
×
164

×
165
        if err = p.config.EncodePluginConfig(key, cfg); err != nil {
×
166
                return fmt.Errorf("error encoding plugin configuration: %w", err)
×
167
        }
×
168

169
        return nil
×
170
}
171

172
// ensureManifestsExist runs make build-installer to generate the default manifests file
173
func (p *editSubcommand) ensureManifestsExist() error {
×
174
        slog.Info("Generating default manifests file", "file", p.manifestsFile)
×
175

×
176
        // Run the required make targets to generate the manifests file
×
177
        targets := []string{"manifests", "generate", "build-installer"}
×
178
        for _, target := range targets {
×
179
                if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
×
180
                        return fmt.Errorf("make %s failed: %w", target, err)
×
181
                }
×
182
        }
183

184
        // Verify the file was created
185
        if _, err := os.Stat(p.manifestsFile); err != nil {
×
186
                return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
×
187
        }
×
188

189
        slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
×
190
        return nil
×
191
}
192

193
// PostScaffold automatically uncomments cert-manager installation when webhooks are present
194
func (p *editSubcommand) PostScaffold() error {
2✔
195
        hasWebhooks := hasWebhooksWith(p.config)
2✔
196

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

231
func hasWebhooksWith(c config.Config) bool {
3✔
232
        resources, err := c.GetResources()
3✔
233
        if err != nil {
3✔
234
                return false
×
235
        }
×
236

237
        for _, res := range resources {
3✔
238
                if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
×
239
                        return true
×
240
                }
×
241
        }
242

243
        return false
3✔
244
}
245

246
// deleteHelmChart removes Helm chart files and configuration (best effort)
NEW
247
func (p *editSubcommand) deleteHelmChart(fs machinery.Filesystem) error {
×
NEW
248
        slog.Info("Deleting Helm chart files...")
×
NEW
249

×
NEW
250
        // Get plugin config to find output directory
×
NEW
251
        key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
×
NEW
252
        canonicalKey := plugin.KeyFor(Plugin{})
×
NEW
253
        cfg := pluginConfig{}
×
NEW
254

×
NEW
255
        err := p.config.DecodePluginConfig(key, &cfg)
×
NEW
256
        if err != nil {
×
NEW
257
                if errors.As(err, &config.PluginKeyNotFoundError{}) && key != canonicalKey {
×
NEW
258
                        _ = p.config.DecodePluginConfig(canonicalKey, &cfg)
×
NEW
259
                }
×
260
        }
261

262
        // Use configured output dir or default
NEW
263
        outputDir := p.outputDir
×
NEW
264
        if outputDir == "" {
×
NEW
265
                outputDir = cfg.OutputDir
×
NEW
266
        }
×
NEW
267
        if outputDir == "" {
×
NEW
268
                outputDir = DefaultOutputDir
×
NEW
269
        }
×
270

NEW
271
        deletedCount := 0
×
NEW
272
        warnCount := 0
×
NEW
273

×
NEW
274
        // Delete chart directory (best effort)
×
NEW
275
        chartDir := filepath.Join(outputDir, "chart")
×
NEW
276
        if exists, _ := afero.DirExists(fs.FS, chartDir); exists {
×
NEW
277
                if err := fs.FS.RemoveAll(chartDir); err != nil {
×
NEW
278
                        slog.Warn("Failed to delete Helm chart directory", "path", chartDir, "error", err)
×
NEW
279
                        warnCount++
×
NEW
280
                } else {
×
NEW
281
                        slog.Info("Deleted Helm chart directory", "path", chartDir)
×
NEW
282
                        deletedCount++
×
NEW
283
                }
×
NEW
284
        } else {
×
NEW
285
                slog.Warn("Helm chart directory not found", "path", chartDir)
×
NEW
286
                warnCount++
×
NEW
287
        }
×
288

289
        // Delete test workflow (best effort)
NEW
290
        testChartPath := filepath.Join(".github", "workflows", "test-chart.yml")
×
NEW
291
        if exists, _ := afero.Exists(fs.FS, testChartPath); exists {
×
NEW
292
                if err := fs.FS.Remove(testChartPath); err != nil {
×
NEW
293
                        slog.Warn("Failed to delete test-chart.yml", "path", testChartPath, "error", err)
×
NEW
294
                        warnCount++
×
NEW
295
                } else {
×
NEW
296
                        slog.Info("Deleted test-chart workflow", "path", testChartPath)
×
NEW
297
                        deletedCount++
×
NEW
298
                }
×
NEW
299
        } else {
×
NEW
300
                slog.Warn("Test chart workflow not found", "path", testChartPath)
×
NEW
301
                warnCount++
×
NEW
302
        }
×
303

304
        // Remove plugin config from PROJECT by encoding empty struct
NEW
305
        if encErr := p.config.EncodePluginConfig(key, struct{}{}); encErr != nil {
×
NEW
306
                // Try canonical key if different
×
NEW
307
                if key != canonicalKey {
×
NEW
308
                        if encErr2 := p.config.EncodePluginConfig(canonicalKey, struct{}{}); encErr2 != nil {
×
NEW
309
                                slog.Warn("Failed to remove plugin configuration from PROJECT file", "error", encErr2)
×
NEW
310
                                warnCount++
×
NEW
311
                        } else {
×
NEW
312
                                slog.Info("Removed plugin configuration from PROJECT file")
×
NEW
313
                        }
×
NEW
314
                } else {
×
NEW
315
                        slog.Warn("Failed to remove plugin configuration from PROJECT file", "error", encErr)
×
NEW
316
                        warnCount++
×
NEW
317
                }
×
NEW
318
        } else {
×
NEW
319
                slog.Info("Removed plugin configuration from PROJECT file")
×
NEW
320
        }
×
321

NEW
322
        fmt.Printf("\nSuccessfully completed Helm plugin deletion\n")
×
NEW
323
        if deletedCount > 0 {
×
NEW
324
                fmt.Printf("Deleted: %d item(s)\n", deletedCount)
×
NEW
325
        }
×
NEW
326
        if warnCount > 0 {
×
NEW
327
                fmt.Printf("Warnings: %d item(s) - some files may not exist or couldn't be deleted (see logs)\n", warnCount)
×
NEW
328
        }
×
329

NEW
330
        return nil
×
331
}
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