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

kubernetes-sigs / kubebuilder / 20694092930

04 Jan 2026 02:06PM UTC coverage: 67.164% (-4.2%) from 71.354%
20694092930

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.

6521 of 9709 relevant lines covered (67.16%)

27.1 hits per line

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

29.28
/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**: The plugin preserves customizations in values.yaml, Chart.yaml, _helpers.tpl, and .helmignore
1✔
83
unless --force is used. All template files are regenerated to match your current kustomize output.
1✔
84

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

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

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

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

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

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

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

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

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

166
        return nil
×
167
}
168

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

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

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

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

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

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

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

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

240
        return false
3✔
241
}
242

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

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

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

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

NEW
268
        deletedCount := 0
×
NEW
269
        warnCount := 0
×
NEW
270

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

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

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

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

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

×
NEW
321
        return nil
×
322
}
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