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

kubernetes-sigs / kubebuilder / 23032648315

13 Mar 2026 01:53AM UTC coverage: 69.957% (-9.8%) from 79.762%
23032648315

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

1 existing line in 1 file now uncovered.

6308 of 9017 relevant lines covered (69.96%)

30.25 hits per line

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

81.61
/pkg/cli/cli.go
1
/*
2
Copyright 2020 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
        "errors"
21
        "fmt"
22
        log "log/slog"
23
        "os"
24
        "slices"
25
        "strings"
26

27
        "github.com/spf13/afero"
28
        "github.com/spf13/cobra"
29
        "github.com/spf13/pflag"
30

31
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
32
        yamlstore "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
36
)
37

38
const (
39
        noticeColor    = "\033[1;33m%s\033[0m"
40
        deprecationFmt = "[Deprecation Notice] %s\n\n"
41

42
        pluginsFlag        = "plugins"
43
        projectVersionFlag = "project-version"
44
)
45

46
// CLI is the command line utility that is used to scaffold kubebuilder project files.
47
type CLI struct {
48
        /* Fields set by Option */
49

50
        // Root command name. It is injected downstream to provide correct help, usage, examples and errors.
51
        commandName string
52
        // Full CLI version string.
53
        version string
54
        // CLI version string (just the CLI version number, no extra information).
55
        cliVersion string
56
        // CLI root's command description.
57
        description string
58
        // Plugins registered in the CLI.
59
        plugins map[string]plugin.Plugin
60
        // Default plugins in case none is provided and a config file can't be found.
61
        defaultPlugins map[config.Version][]string
62
        // Default project version in case none is provided and a config file can't be found.
63
        defaultProjectVersion config.Version
64
        // Commands injected by options.
65
        extraCommands []*cobra.Command
66
        // Alpha commands injected by options.
67
        extraAlphaCommands []*cobra.Command
68
        // Whether to add a completion command to the CLI.
69
        completionCommand bool
70

71
        /* Internal fields */
72

73
        // Plugin keys to scaffold with.
74
        pluginKeys []string
75
        // Project version to scaffold.
76
        projectVersion config.Version
77

78
        // A filtered set of plugins that should be used by command constructors.
79
        resolvedPlugins []plugin.Plugin
80

81
        // Root command.
82
        cmd *cobra.Command
83

84
        // Underlying fs
85
        fs machinery.Filesystem
86
}
87

88
// New creates a new CLI instance.
89
//
90
// It follows the functional options pattern in order to customize the resulting CLI.
91
//
92
// It returns an error if any of the provided options fails. As some processing needs
93
// to be done, execution errors may be found here. Instead of returning an error, this
94
// function will return a valid CLI that errors in Run so that help is provided to the
95
// user.
96
func New(options ...Option) (*CLI, error) {
11✔
97
        // Create the CLI.
11✔
98
        c, err := newCLI(options...)
11✔
99
        if err != nil {
12✔
100
                return nil, err
1✔
101
        }
1✔
102

103
        // Build the cmd tree.
104
        if err := c.buildCmd(); err != nil {
11✔
105
                c.cmd.RunE = errCmdFunc(err)
1✔
106
                return c, nil
1✔
107
        }
1✔
108

109
        // Add extra commands injected by options.
110
        if err := c.addExtraCommands(); err != nil {
10✔
111
                return nil, err
1✔
112
        }
1✔
113

114
        // Add extra alpha commands injected by options.
115
        if err := c.addExtraAlphaCommands(); err != nil {
9✔
116
                return nil, err
1✔
117
        }
1✔
118

119
        // Write deprecation notices after all commands have been constructed.
120
        c.printDeprecationWarnings()
7✔
121

7✔
122
        return c, nil
7✔
123
}
124

125
// newCLI creates a default CLI instance and applies the provided options.
126
// It is as a separate function for test purposes.
127
func newCLI(options ...Option) (*CLI, error) {
41✔
128
        // Default CLI options.
41✔
129
        c := &CLI{
41✔
130
                commandName: "kubebuilder",
41✔
131
                description: `CLI tool for building Kubernetes extensions and tools.
41✔
132
`,
41✔
133
                plugins:        make(map[string]plugin.Plugin),
41✔
134
                defaultPlugins: make(map[config.Version][]string),
41✔
135
                fs:             machinery.Filesystem{FS: afero.NewOsFs()},
41✔
136
        }
41✔
137

41✔
138
        // Apply provided options.
41✔
139
        for _, option := range options {
93✔
140
                if err := option(c); err != nil {
69✔
141
                        return nil, err
17✔
142
                }
17✔
143
        }
144

145
        return c, nil
24✔
146
}
147

148
// buildCmd creates the underlying cobra command and stores it internally.
149
func (c *CLI) buildCmd() error {
12✔
150
        c.cmd = c.newRootCmd()
12✔
151

12✔
152
        var uve config.UnsupportedVersionError
12✔
153

12✔
154
        // Get project version and plugin keys.
12✔
155
        switch err := c.getInfo(); {
12✔
156
        case err == nil:
10✔
157
        case errors.As(err, &uve) && uve.Version.Compare(config.Version{Number: 3, Stage: stage.Alpha}) == 0:
1✔
158
                // Check if the corresponding stable version exists, set c.projectVersion and break
1✔
159
                stableVersion := config.Version{
1✔
160
                        Number: uve.Version.Number,
1✔
161
                }
1✔
162
                if config.IsRegistered(stableVersion) {
2✔
163
                        // Use the stableVersion
1✔
164
                        c.projectVersion = stableVersion
1✔
165
                } else {
1✔
166
                        // stable version not registered, let's bail out
×
167
                        return err
×
168
                }
×
169
        default:
1✔
170
                return err
1✔
171
        }
172

173
        // Resolve plugins for project version and plugin keys.
174
        if err := c.resolvePlugins(); err != nil {
12✔
175
                return err
1✔
176
        }
1✔
177

178
        // Add the subcommands
179
        c.addSubcommands()
10✔
180

10✔
181
        return nil
10✔
182
}
183

184
// getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags.
185
func (c *CLI) getInfo() error {
12✔
186
        // Get plugin keys and project version from project configuration file
12✔
187
        // We discard the error if file doesn't exist because not being able to read a project configuration
12✔
188
        // file is not fatal for some commands. The ones that require it need to check its existence later.
12✔
189
        hasConfigFile := true
12✔
190
        if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) {
22✔
191
                hasConfigFile = false
10✔
192
        } else if err != nil {
14✔
193
                return err
2✔
194
        }
2✔
195

196
        // We can't early return here in case a project configuration file was found because
197
        // this command call may override the project plugins.
198

199
        // Get project version and plugin info from flags
200
        if err := c.getInfoFromFlags(hasConfigFile); err != nil {
10✔
201
                return err
×
202
        }
×
203

204
        // Get project version and plugin info from defaults
205
        c.getInfoFromDefaults()
10✔
206

10✔
207
        return nil
10✔
208
}
209

210
// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
211
func (c *CLI) getInfoFromConfigFile() error {
12✔
212
        // Read the project configuration file
12✔
213
        cfg := yamlstore.New(c.fs)
12✔
214

12✔
215
        // Workaround for https://github.com/kubernetes-sigs/kubebuilder/issues/4433
12✔
216
        //
12✔
217
        // This allows the `kubebuilder alpha generate` command to work with old projects
12✔
218
        // that use plugin versions no longer supported (like go.kubebuilder.io/v3).
12✔
219
        //
12✔
220
        // We read the PROJECT file into memory and update the plugin version (e.g. from v3 to v4)
12✔
221
        // before the CLI tries to load it. This avoids errors during config loading
12✔
222
        // and lets users migrate their project layout from go/v3 to go/v4.
12✔
223

12✔
224
        if isAlphaGenerateCommand(os.Args[1:]) {
12✔
225
                // Patch raw file bytes before unmarshalling
×
226
                if err := patchProjectFileInMemoryIfNeeded(c.fs.FS, yamlstore.DefaultPath); err != nil {
×
227
                        return err
×
228
                }
×
229
        }
230

231
        if err := cfg.Load(); err != nil {
24✔
232
                return fmt.Errorf("error loading configuration: %w", err)
12✔
233
        }
12✔
234

235
        return c.getInfoFromConfig(cfg.Config())
×
236
}
237

238
// isAlphaGenerateCommand checks if the command invocation is `kubebuilder alpha generate`
239
// by scanning os.Args (excluding global flags). It returns true if "alpha" is followed by "generate".
240
func isAlphaGenerateCommand(args []string) bool {
12✔
241
        positional := []string{}
12✔
242
        skip := false
12✔
243

12✔
244
        for i := range args {
87✔
245
                arg := args[i]
75✔
246

75✔
247
                // Skip flags and their values
75✔
248
                if strings.HasPrefix(arg, "-") {
148✔
249
                        // If the flag is in --flag=value format, skip only this one
73✔
250
                        if strings.Contains(arg, "=") {
133✔
251
                                continue
60✔
252
                        }
253
                        // If it's --flag value format, skip next one too
254
                        if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
14✔
255
                                skip = true
1✔
256
                        }
1✔
257
                        continue
13✔
258
                }
259
                if skip {
3✔
260
                        skip = false
1✔
261
                        continue
1✔
262
                }
263
                positional = append(positional, arg)
1✔
264
        }
265

266
        // Check for `alpha generate` in positional arguments
267
        for i := 0; i < len(positional)-1; i++ {
12✔
268
                if positional[i] == "alpha" && positional[i+1] == "generate" {
×
269
                        return true
×
270
                }
×
271
        }
272

273
        return false
12✔
274
}
275

276
// patchProjectFileInMemoryIfNeeded updates deprecated plugin keys in the PROJECT file in place,
277
// so that users can run `kubebuilder alpha generate` even with older plugin layouts.
278
//
279
// See: https://github.com/kubernetes-sigs/kubebuilder/issues/4433
280
//
281
// This ensures the CLI can successfully load the config without failing on unsupported plugin versions.
282
func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error {
×
283
        type pluginReplacement struct {
×
284
                Old string
×
285
                New string
×
286
        }
×
287

×
288
        replacements := []pluginReplacement{
×
289
                {"go.kubebuilder.io/v2", "go.kubebuilder.io/v4"},
×
290
                {"go.kubebuilder.io/v3", "go.kubebuilder.io/v4"},
×
291
                {"go.kubebuilder.io/v3-alpha", "go.kubebuilder.io/v4"},
×
292
        }
×
293

×
294
        content, err := afero.ReadFile(fs, path)
×
295
        if err != nil {
×
296
                return nil
×
297
        }
×
298

299
        original := string(content)
×
300
        modified := original
×
301

×
302
        for _, rep := range replacements {
×
303
                if strings.Contains(modified, rep.Old) {
×
304
                        modified = strings.ReplaceAll(modified, rep.Old, rep.New)
×
305
                        log.Warn("Project is using an old and unsupported plugin layout",
×
306
                                "old_layout", rep.Old,
×
307
                                "new_layout", rep.New,
×
308
                                "note", "Replace in memory to allow `alpha generate` to work.",
×
309
                        )
×
310
                }
×
311
        }
312

313
        if modified != original {
×
314
                err := afero.WriteFile(fs, path, []byte(modified), machinery.DefaultFilePermission)
×
315
                if err != nil {
×
316
                        return fmt.Errorf("failed to write patched PROJECT file: %w", err)
×
317
                }
×
318
        }
319

320
        return nil
×
321
}
322

323
// getInfoFromConfig obtains the project version and plugin keys from the project config.
324
// It is extracted from getInfoFromConfigFile for testing purposes.
325
func (c *CLI) getInfoFromConfig(projectConfig config.Config) error {
3✔
326
        c.pluginKeys = projectConfig.GetPluginChain()
3✔
327
        c.projectVersion = projectConfig.GetVersion()
3✔
328

3✔
329
        for _, pluginKey := range c.pluginKeys {
7✔
330
                if err := plugin.ValidateKey(pluginKey); err != nil {
5✔
331
                        return fmt.Errorf("invalid plugin key found in project configuration file: %w", err)
1✔
332
                }
1✔
333
        }
334

335
        return nil
2✔
336
}
337

338
// getInfoFromFlags obtains the project version and plugin keys from flags.
339
func (c *CLI) getInfoFromFlags(hasConfigFile bool) error {
24✔
340
        // Check if --plugins is followed by --help or -h to avoid parsing help as a plugin value
24✔
341
        // This fixes: kubebuilder init --plugins --help
24✔
342
        for i := 0; i < len(os.Args)-1; i++ {
218✔
343
                if os.Args[i] == "--plugins" || os.Args[i] == "--plugins=" {
204✔
344
                        nextArg := os.Args[i+1]
10✔
345
                        if isHelpFlag(nextArg) {
12✔
346
                                // Help was requested, return early to let Cobra handle it
2✔
347
                                return nil
2✔
348
                        }
2✔
349
                }
350
        }
351

352
        // Partially parse the command line arguments
353
        fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
22✔
354

22✔
355
        // Load the base command global flags
22✔
356
        fs.AddFlagSet(c.cmd.PersistentFlags())
22✔
357

22✔
358
        // If we were unable to load the project configuration, we should also accept the project version flag
22✔
359
        var projectVersionStr string
22✔
360
        if !hasConfigFile {
44✔
361
                fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version")
22✔
362
        }
22✔
363

364
        // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default
365
        // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command.
366
        fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName))
22✔
367

22✔
368
        // Omit unknown flags to avoid parsing errors
22✔
369
        fs.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true}
22✔
370

22✔
371
        // Parse the arguments
22✔
372
        if err := fs.Parse(os.Args[1:]); err != nil {
22✔
373
                return fmt.Errorf("could not parse flags: %w", err)
×
374
        }
×
375

376
        // If any plugin key was provided, replace those from the project configuration file
377
        // Exception: for delete commands, APPEND to layout plugins instead of replacing
378
        if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil {
22✔
379
                return fmt.Errorf("invalid flag %q: %w", pluginsFlag, err)
×
380
        } else if len(pluginKeys) != 0 {
30✔
381
                // Filter out help flags that may have been incorrectly parsed as plugin values
8✔
382
                // This fixes the issue where "kubebuilder edit --plugins --help" treats --help as a plugin
8✔
383
                validPluginKeys := make([]string, 0, len(pluginKeys))
8✔
384
                helpRequested := false
8✔
385
                for _, key := range pluginKeys {
24✔
386
                        key = strings.TrimSpace(key)
16✔
387
                        // Skip help flags
16✔
388
                        if isHelpFlag(key) {
16✔
389
                                helpRequested = true
×
390
                                continue
×
391
                        }
392
                        validPluginKeys = append(validPluginKeys, key)
16✔
393
                }
394

395
                // If help was requested via --plugins flag, set the help flag to trigger Cobra's help display
396
                // This prevents command execution and shows help instead
397
                if helpRequested {
8✔
398
                        if err := fs.Set("help", "true"); err == nil {
×
399
                                return nil
×
400
                        }
×
401
                        // If setting help flag fails, still return nil to avoid validation errors
402
                        return nil
×
403
                }
404

405
                // Validate the remaining plugin keys
406
                for i, key := range validPluginKeys {
24✔
407
                        if err := plugin.ValidateKey(key); err != nil {
17✔
408
                                return fmt.Errorf("invalid plugin %q found in flags: %w", validPluginKeys[i], err)
1✔
409
                        }
1✔
410
                }
411

412
                // For delete commands, if we have layout plugins from PROJECT, keep them
413
                // and append the user-specified plugins (avoiding duplicates).
414
                // Extract the command name from positional args (after flags are parsed).
415
                isDeleteCommand := c.isDeleteCommand(fs.Args())
7✔
416

7✔
417
                if isDeleteCommand && len(c.pluginKeys) > 0 {
7✔
NEW
418
                        // Append user plugins to layout plugins (deduplicate)
×
NEW
419
                        combined := append([]string{}, c.pluginKeys...)
×
NEW
420
                        for _, userPlugin := range validPluginKeys {
×
NEW
421
                                found := slices.Contains(combined, userPlugin)
×
NEW
422
                                if !found {
×
NEW
423
                                        combined = append(combined, userPlugin)
×
NEW
424
                                }
×
425
                        }
NEW
426
                        c.pluginKeys = combined
×
427
                } else {
7✔
428
                        c.pluginKeys = validPluginKeys
7✔
429
                }
7✔
430
        }
431

432
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
433
        // else validate the provided project version
434
        if projectVersionStr != "" {
26✔
435
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
436
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
437
                }
1✔
438
        }
439

440
        return nil
20✔
441
}
442

443
// isDeleteCommand checks if the command being executed is a delete command
444
// by examining the positional arguments (excluding flags).
445
func (c *CLI) isDeleteCommand(args []string) bool {
7✔
446
        // Args contains positional arguments after pflag parsing (flags are removed).
7✔
447
        // Check if the first positional argument is "delete".
7✔
448
        return len(args) > 0 && args[0] == "delete"
7✔
449
}
7✔
450

451
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
452
func (c *CLI) getInfoFromDefaults() {
14✔
453
        // Should not use default values if a plugin was already set
14✔
454
        // This checks includes the case where a project configuration file was found,
14✔
455
        // as it will always have at least one plugin key set by now
14✔
456
        if len(c.pluginKeys) != 0 {
16✔
457
                // We don't assign a default value for project version here because we may be able to
2✔
458
                // resolve the project version after resolving the plugins.
2✔
459
                return
2✔
460
        }
2✔
461

462
        // If the user provided a project version, use the default plugins for that project version
463
        if c.projectVersion.Validate() == nil {
13✔
464
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
465
                return
1✔
466
        }
1✔
467

468
        // Else try to use the default plugins for the default project version
469
        if c.defaultProjectVersion.Validate() == nil {
13✔
470
                var found bool
2✔
471
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
472
                        c.projectVersion = c.defaultProjectVersion
2✔
473
                        return
2✔
474
                }
2✔
475
        }
476

477
        // Else check if only default plugins for a project version were provided
478
        if len(c.defaultPlugins) == 1 {
16✔
479
                for projectVersion, defaultPlugins := range c.defaultPlugins {
14✔
480
                        c.pluginKeys = defaultPlugins
7✔
481
                        c.projectVersion = projectVersion
7✔
482
                        return
7✔
483
                }
7✔
484
        }
485
}
486

487
const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " +
488
        "https://kubebuilder.io/plugins/plugins-versioning)"
489

490
// resolvePlugins selects from the available plugins those that match the project version and plugin keys provided.
491
func (c *CLI) resolvePlugins() error {
27✔
492
        knownProjectVersion := c.projectVersion.Validate() == nil
27✔
493

27✔
494
        for _, pluginKey := range c.pluginKeys {
55✔
495
                var extraErrMsg string
28✔
496

28✔
497
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
28✔
498
                for _, p := range c.plugins {
295✔
499
                        plugins = append(plugins, p)
267✔
500
                }
267✔
501
                // We can omit the error because plugin keys have already been validated
502
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
28✔
503
                if knownProjectVersion {
47✔
504
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
19✔
505
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
19✔
506
                }
19✔
507

508
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
509
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
510
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
511
                if _, version := plugin.SplitKey(pluginKey); version != "" {
42✔
512
                        var ver plugin.Version
14✔
513
                        if err := ver.Parse(version); err != nil {
14✔
514
                                return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err)
×
515
                        }
×
516
                        if !ver.IsStable() {
14✔
517
                                extraErrMsg += unstablePluginMsg
×
518
                        }
×
519
                }
520

521
                // Only 1 plugin can match
522
                switch len(plugins) {
28✔
523
                case 1:
19✔
524
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
19✔
525
                case 0:
6✔
526
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
6✔
527
                default:
3✔
528
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
529
                }
530
        }
531

532
        // Now we can try to resolve the project version if not known by this point
533
        if !knownProjectVersion && len(c.resolvedPlugins) > 0 {
22✔
534
                // Extract the common supported project versions
4✔
535
                supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...)
4✔
536

4✔
537
                // If there is only one common supported project version, resolve to it
4✔
538
        ProjectNumberVersionSwitch:
4✔
539
                switch len(supportedProjectVersions) {
4✔
540
                case 1:
1✔
541
                        c.projectVersion = supportedProjectVersions[0]
1✔
542
                case 0:
1✔
543
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
544
                default:
2✔
545
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
546
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
547
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
548
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
549
                                        c.projectVersion = c.defaultProjectVersion
1✔
550
                                        break ProjectNumberVersionSwitch
1✔
551
                                }
552
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
553
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
554
                        }
555
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
556
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
557
                }
558
        }
559

560
        return nil
16✔
561
}
562

563
// addSubcommands returns a root command with a subcommand tree reflecting the
564
// current project's state.
565
func (c *CLI) addSubcommands() {
10✔
566
        // add the alpha command if it has any subcommands enabled
10✔
567
        c.addAlphaCmd()
10✔
568

10✔
569
        // kubebuilder completion
10✔
570
        // Only add completion if requested
10✔
571
        if c.completionCommand {
11✔
572
                c.cmd.AddCommand(c.newCompletionCmd())
1✔
573
        }
1✔
574

575
        // kubebuilder create
576
        createCmd := c.newCreateCmd()
10✔
577
        // kubebuilder create api
10✔
578
        createCmd.AddCommand(c.newCreateAPICmd())
10✔
579
        createCmd.AddCommand(c.newCreateWebhookCmd())
10✔
580
        if createCmd.HasSubCommands() {
20✔
581
                c.cmd.AddCommand(createCmd)
10✔
582
        }
10✔
583

584
        // kubebuilder delete
585
        deleteCmd := c.newDeleteCmd()
10✔
586
        // kubebuilder delete api
10✔
587
        deleteCmd.AddCommand(c.newDeleteAPICmd())
10✔
588
        deleteCmd.AddCommand(c.newDeleteWebhookCmd())
10✔
589
        if deleteCmd.HasSubCommands() {
20✔
590
                c.cmd.AddCommand(deleteCmd)
10✔
591
        }
10✔
592

593
        // kubebuilder edit
594
        c.cmd.AddCommand(c.newEditCmd())
10✔
595

10✔
596
        // kubebuilder init
10✔
597
        c.cmd.AddCommand(c.newInitCmd())
10✔
598

10✔
599
        // kubebuilder version
10✔
600
        // Only add version if a version string was provided
10✔
601
        if c.version != "" {
11✔
602
                c.cmd.AddCommand(c.newVersionCmd())
1✔
603
        }
1✔
604
}
605

606
// addExtraCommands adds the additional commands.
607
func (c *CLI) addExtraCommands() error {
9✔
608
        for _, cmd := range c.extraCommands {
11✔
609
                for _, subCmd := range c.cmd.Commands() {
12✔
610
                        if cmd.Name() == subCmd.Name() {
11✔
611
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
612
                        }
1✔
613
                }
614
                c.cmd.AddCommand(cmd)
1✔
615
        }
616
        return nil
8✔
617
}
618

619
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
620
func (c CLI) printDeprecationWarnings() {
7✔
621
        for _, p := range c.resolvedPlugins {
12✔
622
                if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 {
6✔
623
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning()))
1✔
624
                }
1✔
625
        }
626
}
627

628
// metadata returns CLI's metadata.
629
func (c CLI) metadata() plugin.CLIMetadata {
38✔
630
        return plugin.CLIMetadata{
38✔
631
                CommandName: c.commandName,
38✔
632
        }
38✔
633
}
38✔
634

635
// Run executes the CLI utility.
636
//
637
// If an error is found, command help and examples will be printed.
638
func (c CLI) Run() error {
1✔
639
        if err := c.cmd.Execute(); err != nil {
2✔
640
                // Don't return error if help was displayed (from --plugins --help pattern)
1✔
641
                if err == errHelpDisplayed {
1✔
642
                        return nil
×
643
                }
×
644
                return fmt.Errorf("error executing command: %w", err)
1✔
645
        }
646

647
        return nil
×
648
}
649

650
// Command returns the underlying root command.
651
func (c CLI) Command() *cobra.Command {
2✔
652
        return c.cmd
2✔
653
}
2✔
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