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

kubernetes-sigs / kubebuilder / 21778493952

07 Feb 2026 10:13AM UTC coverage: 73.863% (-0.09%) from 73.954%
21778493952

Pull #5448

github

camilamacedo86
fix(CLI/API) prevent --help from being validated as plugin name

- Add PersistentPreRunE hook to detect help flags in plugin values
- Filter help flags before plugin validation
- Show plugin descriptions instead of project versions
- Use short plugin keys (go/v4 vs go.kubebuilder.io/v4)
- Filter plugins by subcommand (init/edit/create)
- Hide deprecated plugins from help output
- Add documentation links for each plugin
- Improve root help wording for better getting-started experience

Generate-by: Cursour/Claude
Pull Request #5448: 🐛 fix(CLI/API) prevent --help from being validated as plugin name

174 of 227 new or added lines in 15 files covered. (76.65%)

1 existing line in 1 file now uncovered.

6887 of 9324 relevant lines covered (73.86%)

43.55 hits per line

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

82.55
/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
        "strings"
25

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

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

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

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

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

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

70
        /* Internal fields */
71

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

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

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

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

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

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

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

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

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

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

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

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

144
        return c, nil
24✔
145
}
146

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

12✔
151
        var uve config.UnsupportedVersionError
12✔
152

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

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

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

10✔
180
        return nil
10✔
181
}
182

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

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

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

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

10✔
206
        return nil
10✔
207
}
208

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

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

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

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

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

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

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

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

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

272
        return false
12✔
273
}
274

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

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

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

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

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

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

319
        return nil
×
320
}
321

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

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

334
        return nil
2✔
335
}
336

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

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

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

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

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

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

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

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

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

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

410
                c.pluginKeys = validPluginKeys
7✔
411
        }
412

413
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
414
        // else validate the provided project version
415
        if projectVersionStr != "" {
26✔
416
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
417
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
418
                }
1✔
419
        }
420

421
        return nil
20✔
422
}
423

424
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
425
func (c *CLI) getInfoFromDefaults() {
14✔
426
        // Should not use default values if a plugin was already set
14✔
427
        // This checks includes the case where a project configuration file was found,
14✔
428
        // as it will always have at least one plugin key set by now
14✔
429
        if len(c.pluginKeys) != 0 {
16✔
430
                // We don't assign a default value for project version here because we may be able to
2✔
431
                // resolve the project version after resolving the plugins.
2✔
432
                return
2✔
433
        }
2✔
434

435
        // If the user provided a project version, use the default plugins for that project version
436
        if c.projectVersion.Validate() == nil {
13✔
437
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
438
                return
1✔
439
        }
1✔
440

441
        // Else try to use the default plugins for the default project version
442
        if c.defaultProjectVersion.Validate() == nil {
13✔
443
                var found bool
2✔
444
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
445
                        c.projectVersion = c.defaultProjectVersion
2✔
446
                        return
2✔
447
                }
2✔
448
        }
449

450
        // Else check if only default plugins for a project version were provided
451
        if len(c.defaultPlugins) == 1 {
16✔
452
                for projectVersion, defaultPlugins := range c.defaultPlugins {
14✔
453
                        c.pluginKeys = defaultPlugins
7✔
454
                        c.projectVersion = projectVersion
7✔
455
                        return
7✔
456
                }
7✔
457
        }
458
}
459

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

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

27✔
467
        for _, pluginKey := range c.pluginKeys {
55✔
468
                var extraErrMsg string
28✔
469

28✔
470
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
28✔
471
                for _, p := range c.plugins {
295✔
472
                        plugins = append(plugins, p)
267✔
473
                }
267✔
474
                // We can omit the error because plugin keys have already been validated
475
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
28✔
476
                if knownProjectVersion {
47✔
477
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
19✔
478
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
19✔
479
                }
19✔
480

481
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
482
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
483
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
484
                if _, version := plugin.SplitKey(pluginKey); version != "" {
42✔
485
                        var ver plugin.Version
14✔
486
                        if err := ver.Parse(version); err != nil {
14✔
487
                                return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err)
×
488
                        }
×
489
                        if !ver.IsStable() {
14✔
490
                                extraErrMsg += unstablePluginMsg
×
491
                        }
×
492
                }
493

494
                // Only 1 plugin can match
495
                switch len(plugins) {
28✔
496
                case 1:
19✔
497
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
19✔
498
                case 0:
6✔
499
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
6✔
500
                default:
3✔
501
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
502
                }
503
        }
504

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

4✔
510
                // If there is only one common supported project version, resolve to it
4✔
511
        ProjectNumberVersionSwitch:
4✔
512
                switch len(supportedProjectVersions) {
4✔
513
                case 1:
1✔
514
                        c.projectVersion = supportedProjectVersions[0]
1✔
515
                case 0:
1✔
516
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
517
                default:
2✔
518
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
519
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
520
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
521
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
522
                                        c.projectVersion = c.defaultProjectVersion
1✔
523
                                        break ProjectNumberVersionSwitch
1✔
524
                                }
525
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
526
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
527
                        }
528
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
529
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
530
                }
531
        }
532

533
        return nil
16✔
534
}
535

536
// addSubcommands returns a root command with a subcommand tree reflecting the
537
// current project's state.
538
func (c *CLI) addSubcommands() {
10✔
539
        // add the alpha command if it has any subcommands enabled
10✔
540
        c.addAlphaCmd()
10✔
541

10✔
542
        // kubebuilder completion
10✔
543
        // Only add completion if requested
10✔
544
        if c.completionCommand {
11✔
545
                c.cmd.AddCommand(c.newCompletionCmd())
1✔
546
        }
1✔
547

548
        // kubebuilder create
549
        createCmd := c.newCreateCmd()
10✔
550
        // kubebuilder create api
10✔
551
        createCmd.AddCommand(c.newCreateAPICmd())
10✔
552
        createCmd.AddCommand(c.newCreateWebhookCmd())
10✔
553
        if createCmd.HasSubCommands() {
20✔
554
                c.cmd.AddCommand(createCmd)
10✔
555
        }
10✔
556

557
        // kubebuilder edit
558
        c.cmd.AddCommand(c.newEditCmd())
10✔
559

10✔
560
        // kubebuilder init
10✔
561
        c.cmd.AddCommand(c.newInitCmd())
10✔
562

10✔
563
        // kubebuilder version
10✔
564
        // Only add version if a version string was provided
10✔
565
        if c.version != "" {
11✔
566
                c.cmd.AddCommand(c.newVersionCmd())
1✔
567
        }
1✔
568
}
569

570
// addExtraCommands adds the additional commands.
571
func (c *CLI) addExtraCommands() error {
9✔
572
        for _, cmd := range c.extraCommands {
11✔
573
                for _, subCmd := range c.cmd.Commands() {
10✔
574
                        if cmd.Name() == subCmd.Name() {
9✔
575
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
576
                        }
1✔
577
                }
578
                c.cmd.AddCommand(cmd)
1✔
579
        }
580
        return nil
8✔
581
}
582

583
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
584
func (c CLI) printDeprecationWarnings() {
7✔
585
        for _, p := range c.resolvedPlugins {
12✔
586
                if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 {
6✔
587
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning()))
1✔
588
                }
1✔
589
        }
590
}
591

592
// metadata returns CLI's metadata.
593
func (c CLI) metadata() plugin.CLIMetadata {
26✔
594
        return plugin.CLIMetadata{
26✔
595
                CommandName: c.commandName,
26✔
596
        }
26✔
597
}
26✔
598

599
// Run executes the CLI utility.
600
//
601
// If an error is found, command help and examples will be printed.
602
func (c CLI) Run() error {
1✔
603
        if err := c.cmd.Execute(); err != nil {
2✔
604
                // Don't return error if help was displayed (from --plugins --help pattern)
1✔
605
                if err == errHelpDisplayed {
1✔
NEW
606
                        return nil
×
NEW
607
                }
×
608
                return fmt.Errorf("error executing command: %w", err)
1✔
609
        }
610

611
        return nil
×
612
}
613

614
// Command returns the underlying root command.
615
func (c CLI) Command() *cobra.Command {
2✔
616
        return c.cmd
2✔
617
}
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