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

kubernetes-sigs / kubebuilder / 21110785101

18 Jan 2026 10:19AM UTC coverage: 69.245% (-4.3%) from 73.565%
21110785101

Pull #5352

github

camilamacedo86
Add delete api and delete webhook commands

Add delete functionality to remove APIs and webhooks from projects.
Users can now clean up scaffolded resources with proper validation.
Pull Request #5352: WIP ✨ Add delete api and delete webhook commands

341 of 1049 new or added lines in 18 files covered. (32.51%)

1 existing line in 1 file now uncovered.

6892 of 9953 relevant lines covered (69.25%)

35.71 hits per line

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

29.91
/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, _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
    ├── _helpers.tpl
1✔
94
    ├── rbac/
1✔
95
    ├── manager/
1✔
96
    ├── webhook/
1✔
97
    └── ...
1✔
98
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
1✔
99
}
1✔
100

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

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

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

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

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

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

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

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

168
        return nil
×
169
}
170

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

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

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

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

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

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

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

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

242
        return false
3✔
243
}
244

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

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

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

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

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

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

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

303
        // Remove plugin config from PROJECT file (best effort)
NEW
304
        if err := p.config.EncodePluginConfig(key, pluginConfig{}); err != nil {
×
NEW
305
                slog.Warn("Failed to remove plugin configuration from PROJECT file", "error", err)
×
NEW
306
                warnCount++
×
NEW
307
        } else {
×
NEW
308
                slog.Info("Removed plugin configuration from PROJECT file")
×
NEW
309
        }
×
310

NEW
311
        fmt.Printf("\nSuccessfully completed Helm plugin deletion\n")
×
NEW
312
        if deletedCount > 0 {
×
NEW
313
                fmt.Printf("Deleted: %d item(s)\n", deletedCount)
×
NEW
314
        }
×
NEW
315
        if warnCount > 0 {
×
NEW
316
                fmt.Printf("Warnings: %d item(s) - some files may not exist or couldn't be deleted (see logs)\n", warnCount)
×
NEW
317
        }
×
318

NEW
319
        fmt.Println("\nNext steps:")
×
NEW
320
        fmt.Println("1. Review and remove any Helm-related CI/CD pipelines if present")
×
NEW
321
        fmt.Println("2. Update documentation to remove Helm chart references")
×
NEW
322

×
NEW
323
        return nil
×
324
}
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