Skip to content

Commit 7b91689

Browse files
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.
1 parent bd63fdc commit 7b91689

35 files changed

+3209
-3
lines changed

docs/book/src/plugins/extending/extending_cli_features_and_plugins.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ of the following CLI commands:
6262
- `init`: Initializes the project structure.
6363
- `create api`: Scaffolds a new API and controller.
6464
- `create webhook`: Scaffolds a new webhook.
65-
- `edit`: edit the project structure.
65+
- `delete api`: Deletes an API and its associated files.
66+
- `delete webhook`: Deletes a webhook and its associated files.
67+
- `edit`: Updates the project structure.
6668

6769
Here’s an example of using the `init` subcommand with a custom plugin:
6870

docs/book/src/plugins/extending/external-plugins.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ External plugins can support the following Kubebuilder subcommands:
120120
- `init`: Project initialization
121121
- `create api`: Scaffold Kubernetes API definitions
122122
- `create webhook`: Scaffold Kubernetes webhooks
123+
- `delete api`: Delete Kubernetes API definitions and associated files
124+
- `delete webhook`: Delete Kubernetes webhooks and associated files
123125
- `edit`: Update project configuration
124126

125127
**Optional subcommands for enhanced user experience:**

docs/book/src/plugins/plugins.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ kubebuilder create api --plugins=pluginA,pluginB,pluginC
3131
OR
3232
kubebuilder create webhook --plugins=pluginA,pluginB,pluginC
3333
OR
34+
kubebuilder delete api --plugins=pluginA,pluginB,pluginC
35+
OR
36+
kubebuilder delete webhook --plugins=pluginA,pluginB,pluginC
37+
OR
3438
kubebuilder edit --plugins=pluginA,pluginB,pluginC
3539
```
3640

pkg/cli/cli.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,15 @@ func (c *CLI) addSubcommands() {
519519
c.cmd.AddCommand(createCmd)
520520
}
521521

522+
// kubebuilder delete
523+
deleteCmd := c.newDeleteCmd()
524+
// kubebuilder delete api
525+
deleteCmd.AddCommand(c.newDeleteAPICmd())
526+
deleteCmd.AddCommand(c.newDeleteWebhookCmd())
527+
if deleteCmd.HasSubCommands() {
528+
c.cmd.AddCommand(deleteCmd)
529+
}
530+
522531
// kubebuilder edit
523532
c.cmd.AddCommand(c.newEditCmd())
524533

pkg/cli/delete.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2026 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 cli
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
)
22+
23+
func (CLI) newDeleteCmd() *cobra.Command {
24+
return &cobra.Command{
25+
Use: "delete",
26+
Short: "Delete a Kubernetes API or webhook",
27+
Long: `Delete a Kubernetes API or webhook and associated files.`,
28+
}
29+
}

pkg/cli/delete_api.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2026 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 cli
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
24+
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
25+
)
26+
27+
const deleteAPIErrorMsg = "failed to delete API"
28+
29+
func (c CLI) newDeleteAPICmd() *cobra.Command {
30+
cmd := &cobra.Command{
31+
Use: "api",
32+
Short: "Delete a Kubernetes API",
33+
Long: `Delete a Kubernetes API and its associated files.
34+
`,
35+
RunE: errCmdFunc(
36+
fmt.Errorf("api subcommand requires an existing project"),
37+
),
38+
}
39+
40+
// In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of
41+
// this subcommand. This allows the use of subcommands that do not require resolved plugins like help.
42+
if len(c.resolvedPlugins) == 0 {
43+
cmdErr(cmd, noResolvedPluginError{})
44+
return cmd
45+
}
46+
47+
// Obtain the plugin keys and subcommands from the plugins that implement plugin.DeleteAPI.
48+
subcommands := c.filterSubcommands(
49+
func(p plugin.Plugin) bool {
50+
_, isValid := p.(plugin.DeleteAPI)
51+
return isValid
52+
},
53+
func(p plugin.Plugin) plugin.Subcommand {
54+
return p.(plugin.DeleteAPI).GetDeleteAPISubcommand()
55+
},
56+
)
57+
58+
// Verify that there is at least one remaining plugin.
59+
if len(subcommands) == 0 {
60+
cmdErr(cmd, noAvailablePluginError{"API deletion"})
61+
return cmd
62+
}
63+
64+
c.applySubcommandHooks(cmd, subcommands, deleteAPIErrorMsg, false)
65+
66+
return cmd
67+
}

pkg/cli/delete_webhook.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2026 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 cli
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/spf13/cobra"
23+
24+
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
25+
)
26+
27+
const deleteWebhookErrorMsg = "failed to delete webhook"
28+
29+
func (c CLI) newDeleteWebhookCmd() *cobra.Command {
30+
cmd := &cobra.Command{
31+
Use: "webhook",
32+
Short: "Delete a webhook for an API resource",
33+
Long: `Delete a webhook for an API resource and associated files.
34+
`,
35+
RunE: errCmdFunc(
36+
fmt.Errorf("webhook subcommand requires an existing project"),
37+
),
38+
}
39+
40+
// In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of
41+
// this subcommand. This allows the use of subcommands that do not require resolved plugins like help.
42+
if len(c.resolvedPlugins) == 0 {
43+
cmdErr(cmd, noResolvedPluginError{})
44+
return cmd
45+
}
46+
47+
// Obtain the plugin keys and subcommands from the plugins that implement plugin.DeleteWebhook.
48+
subcommands := c.filterSubcommands(
49+
func(p plugin.Plugin) bool {
50+
_, isValid := p.(plugin.DeleteWebhook)
51+
return isValid
52+
},
53+
func(p plugin.Plugin) plugin.Subcommand {
54+
return p.(plugin.DeleteWebhook).GetDeleteWebhookSubcommand()
55+
},
56+
)
57+
58+
// Verify that there is at least one remaining plugin.
59+
if len(subcommands) == 0 {
60+
cmdErr(cmd, noAvailablePluginError{"webhook deletion"})
61+
return cmd
62+
}
63+
64+
c.applySubcommandHooks(cmd, subcommands, deleteWebhookErrorMsg, false)
65+
66+
return cmd
67+
}

pkg/config/interface.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ type Config interface {
8282
AddResource(res resource.Resource) error
8383
// UpdateResource adds the provided resource if it was not present, modifies it if it was already present.
8484
UpdateResource(res resource.Resource) error
85+
// RemoveResource removes the resource matching the provided GVK from the config.
86+
RemoveResource(gvk resource.GVK) error
87+
// SetResourceWebhooks sets the webhook configuration for a resource, replacing existing configuration.
88+
// Unlike UpdateResource which merges, this completely replaces the webhook config.
89+
SetResourceWebhooks(gvk resource.GVK, webhooks *resource.Webhooks) error
8590

8691
// HasGroup checks if the provided group is the same as any of the tracked resources.
8792
HasGroup(group string) bool

pkg/config/v3/config.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,48 @@ func (c *Cfg) UpdateResource(res resource.Resource) error {
259259
return nil
260260
}
261261

262+
// RemoveResource implements config.Config
263+
func (c *Cfg) RemoveResource(gvk resource.GVK) error {
264+
indexToRemove := -1
265+
for i, r := range c.Resources {
266+
// Match by Group, Version, Kind (not domain, as core types may not have domain set)
267+
if r.Group == gvk.Group && r.Version == gvk.Version && r.Kind == gvk.Kind {
268+
indexToRemove = i
269+
break
270+
}
271+
}
272+
273+
if indexToRemove == -1 {
274+
return fmt.Errorf("failed to remove resource: resource with GVK {%q %q %q} not found",
275+
gvk.Group, gvk.Version, gvk.Kind)
276+
}
277+
278+
// Remove the resource by slicing around it
279+
c.Resources = append(c.Resources[:indexToRemove], c.Resources[indexToRemove+1:]...)
280+
return nil
281+
}
282+
283+
// SetResourceWebhooks implements config.Config
284+
func (c *Cfg) SetResourceWebhooks(gvk resource.GVK, webhooks *resource.Webhooks) error {
285+
for i, r := range c.Resources {
286+
// Match by Group, Version, Kind (not domain)
287+
if r.Group == gvk.Group && r.Version == gvk.Version && r.Kind == gvk.Kind {
288+
if webhooks == nil {
289+
c.Resources[i].Webhooks = nil
290+
} else {
291+
if c.Resources[i].Webhooks == nil {
292+
c.Resources[i].Webhooks = &resource.Webhooks{}
293+
}
294+
c.Resources[i].Webhooks.Set(webhooks)
295+
}
296+
return nil
297+
}
298+
}
299+
300+
return fmt.Errorf("failed to set webhooks: resource with GVK {%q %q %q} not found",
301+
gvk.Group, gvk.Version, gvk.Kind)
302+
}
303+
262304
// HasGroup implements config.Config
263305
func (c Cfg) HasGroup(group string) bool {
264306
// Return true if the target group is found in the tracked resources

pkg/config/v3/config_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,89 @@ var _ = Describe("Cfg", func() {
276276
checkResource(c.Resources[0], resWithoutPlural)
277277
})
278278

279+
It("RemoveResource should remove the resource if it exists", func() {
280+
c.Resources = append(c.Resources, res)
281+
l := len(c.Resources)
282+
Expect(c.RemoveResource(res.GVK)).To(Succeed())
283+
Expect(c.Resources).To(HaveLen(l - 1))
284+
Expect(c.HasResource(res.GVK)).To(BeFalse())
285+
})
286+
287+
It("RemoveResource should return an error if the resource does not exist", func() {
288+
Expect(c.RemoveResource(res.GVK)).NotTo(Succeed())
289+
})
290+
291+
It("RemoveResource should remove only the specified resource", func() {
292+
res2 := resource.Resource{
293+
GVK: resource.GVK{
294+
Group: "group",
295+
Version: "v1",
296+
Kind: "OtherKind",
297+
},
298+
}
299+
c.Resources = append(c.Resources, res, res2)
300+
Expect(c.Resources).To(HaveLen(2))
301+
302+
Expect(c.RemoveResource(res.GVK)).To(Succeed())
303+
Expect(c.Resources).To(HaveLen(1))
304+
Expect(c.HasResource(res.GVK)).To(BeFalse())
305+
Expect(c.HasResource(res2.GVK)).To(BeTrue())
306+
})
307+
308+
It("SetResourceWebhooks should update webhook configuration", func() {
309+
resWithWebhooks := resource.Resource{
310+
GVK: resource.GVK{
311+
Group: "group",
312+
Version: "v1",
313+
Kind: "Kind",
314+
},
315+
Webhooks: &resource.Webhooks{
316+
WebhookVersion: "v1",
317+
Defaulting: true,
318+
Validation: true,
319+
},
320+
}
321+
c.Resources = append(c.Resources, resWithWebhooks)
322+
323+
// Update to remove defaulting webhook
324+
newWebhooks := &resource.Webhooks{
325+
WebhookVersion: "v1",
326+
Defaulting: false,
327+
Validation: true,
328+
}
329+
Expect(c.SetResourceWebhooks(resWithWebhooks.GVK, newWebhooks)).To(Succeed())
330+
331+
updated, err := c.GetResource(resWithWebhooks.GVK)
332+
Expect(err).NotTo(HaveOccurred())
333+
Expect(updated.Webhooks.Defaulting).To(BeFalse())
334+
Expect(updated.Webhooks.Validation).To(BeTrue())
335+
})
336+
337+
It("SetResourceWebhooks should clear webhooks when nil provided", func() {
338+
resWithWebhooks := resource.Resource{
339+
GVK: resource.GVK{
340+
Group: "group",
341+
Version: "v1",
342+
Kind: "Kind",
343+
},
344+
Webhooks: &resource.Webhooks{
345+
WebhookVersion: "v1",
346+
Defaulting: true,
347+
},
348+
}
349+
c.Resources = append(c.Resources, resWithWebhooks)
350+
351+
Expect(c.SetResourceWebhooks(resWithWebhooks.GVK, nil)).To(Succeed())
352+
353+
updated, err := c.GetResource(resWithWebhooks.GVK)
354+
Expect(err).NotTo(HaveOccurred())
355+
Expect(updated.Webhooks).To(BeNil())
356+
})
357+
358+
It("SetResourceWebhooks should fail if resource not found", func() {
359+
Expect(c.SetResourceWebhooks(res.GVK, nil)).NotTo(Succeed())
360+
})
361+
279362
It("HasGroup should return false with no tracked resources", func() {
280363
Expect(c.HasGroup(res.Group)).To(BeFalse())
281364
})

0 commit comments

Comments
 (0)