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

kubernetes-sigs / kubebuilder / 29161841219

11 Jul 2026 05:33PM UTC coverage: 84.311% (+0.2%) from 84.081%
29161841219

push

github

web-flow
Merge pull request #5845 from immanuwell/fix/version-help-ignore-bad-project

🐛 (CLI) Ignore invalid PROJECT paths for version and help

49 of 50 new or added lines in 4 files covered. (98.0%)

8163 of 9682 relevant lines covered (84.31%)

145.97 hits per line

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

83.59
/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
        pluginsFlagArg     = "--plugins"
44
        projectVersionFlag = "project-version"
45
        helpFlagArg        = "--help"
46
        shellZsh           = "zsh"
47

48
        kubebuilderCommandName          = "kubebuilder"
49
        kubebuilderSubcommandHelp       = "help"
50
        kubebuilderSubcommandInit       = "init"
51
        kubebuilderSubcommandVersion    = "version"
52
        kubebuilderSubcommandCompletion = "completion"
53
        pluginGoKubebuilderV4           = "go.kubebuilder.io/v4"
54
        pluginGoKubebuilderV2           = "go.kubebuilder.io/v2"
55

56
        pluginsFlagDescription = "Comma-separated list of plugin keys to use. " +
57
                "If unset, Kubebuilder uses the plugin chain from PROJECT or the CLI default"
58
        projectVersionFlagDescription = "Project config version used to select compatible plugins and write PROJECT " +
59
                "(e.g., 3). If unset, Kubebuilder uses the version from PROJECT or the CLI default"
60
)
61

62
// CLI is the command line utility that is used to scaffold kubebuilder project files.
63
type CLI struct {
64
        /* Fields set by Option */
65

66
        // Root command name. It is injected downstream to provide correct help, usage, examples and errors.
67
        commandName string
68
        // Full CLI version string.
69
        version string
70
        // CLI version string (just the CLI version number, no extra information).
71
        cliVersion string
72
        // CLI root's command description.
73
        description string
74
        // Plugins registered in the CLI.
75
        plugins map[string]plugin.Plugin
76
        // Default plugins in case none is provided and a config file can't be found.
77
        defaultPlugins map[config.Version][]string
78
        // Default project version in case none is provided and a config file can't be found.
79
        defaultProjectVersion config.Version
80
        // Commands injected by options.
81
        extraCommands []*cobra.Command
82
        // Alpha commands injected by options.
83
        extraAlphaCommands []*cobra.Command
84
        // Whether to add a completion command to the CLI.
85
        completionCommand bool
86

87
        /* Internal fields */
88

89
        // Plugin keys to scaffold with.
90
        pluginKeys []string
91
        // Project version to scaffold.
92
        projectVersion config.Version
93

94
        // A filtered set of plugins that should be used by command constructors.
95
        resolvedPlugins []plugin.Plugin
96

97
        // Root command.
98
        cmd *cobra.Command
99

100
        // Underlying fs
101
        fs machinery.Filesystem
102
}
103

104
// New creates a new CLI instance.
105
//
106
// It follows the functional options pattern in order to customize the resulting CLI.
107
//
108
// It returns an error if any of the provided options fails. As some processing needs
109
// to be done, execution errors may be found here. Instead of returning an error, this
110
// function will return a valid CLI that errors in Run so that help is provided to the
111
// user.
112
func New(options ...Option) (*CLI, error) {
19✔
113
        // Create the CLI.
19✔
114
        c, err := newCLI(options...)
19✔
115
        if err != nil {
20✔
116
                return nil, err
1✔
117
        }
1✔
118

119
        // Build the cmd tree.
120
        if err := c.buildCmd(); err != nil {
19✔
121
                c.cmd.RunE = errCmdFunc(err)
1✔
122
                return c, nil
1✔
123
        }
1✔
124

125
        // Add extra commands injected by options.
126
        if err := c.addExtraCommands(); err != nil {
18✔
127
                return nil, err
1✔
128
        }
1✔
129

130
        // Add extra alpha commands injected by options.
131
        if err := c.addExtraAlphaCommands(); err != nil {
17✔
132
                return nil, err
1✔
133
        }
1✔
134

135
        // Write deprecation notices after all commands have been constructed.
136
        c.printDeprecationWarnings()
15✔
137

15✔
138
        return c, nil
15✔
139
}
140

141
// newCLI creates a default CLI instance and applies the provided options.
142
// It is as a separate function for test purposes.
143
func newCLI(options ...Option) (*CLI, error) {
49✔
144
        // Default CLI options.
49✔
145
        c := &CLI{
49✔
146
                commandName: kubebuilderCommandName,
49✔
147
                description: `CLI tool for building Kubernetes extensions and tools.
49✔
148
`,
49✔
149
                plugins:        make(map[string]plugin.Plugin),
49✔
150
                defaultPlugins: make(map[config.Version][]string),
49✔
151
                fs:             machinery.Filesystem{FS: afero.NewOsFs()},
49✔
152
        }
49✔
153

49✔
154
        // Apply provided options.
49✔
155
        for _, option := range options {
138✔
156
                if err := option(c); err != nil {
106✔
157
                        return nil, err
17✔
158
                }
17✔
159
        }
160

161
        return c, nil
32✔
162
}
163

164
// buildCmd creates the underlying cobra command and stores it internally.
165
func (c *CLI) buildCmd() error {
20✔
166
        c.cmd = c.newRootCmd()
20✔
167

20✔
168
        var uve config.UnsupportedVersionError
20✔
169

20✔
170
        // Get project version and plugin keys.
20✔
171
        switch err := c.getInfo(); {
20✔
172
        case err == nil:
18✔
173
        case errors.As(err, &uve) && uve.Version.Compare(config.Version{Number: 3, Stage: stage.Alpha}) == 0:
1✔
174
                // Check if the corresponding stable version exists, set c.projectVersion and break
1✔
175
                stableVersion := config.Version{
1✔
176
                        Number: uve.Version.Number,
1✔
177
                }
1✔
178
                if config.IsRegistered(stableVersion) {
2✔
179
                        // Use the stableVersion
1✔
180
                        c.projectVersion = stableVersion
1✔
181
                } else {
1✔
182
                        // stable version not registered, let's bail out
×
183
                        return err
×
184
                }
×
185
        default:
1✔
186
                return err
1✔
187
        }
188

189
        // Resolve plugins for project version and plugin keys.
190
        if err := c.resolvePlugins(); err != nil {
21✔
191
                if !shouldIgnoreConfigLoadError(os.Args[1:]) {
3✔
192
                        return err
1✔
193
                }
1✔
194
        }
195

196
        // Add the subcommands
197
        c.addSubcommands()
18✔
198

18✔
199
        return nil
18✔
200
}
201

202
// getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags.
203
func (c *CLI) getInfo() error {
20✔
204
        // Get plugin keys and project version from project configuration file
20✔
205
        // We discard the error if file doesn't exist because not being able to read a project configuration
20✔
206
        // file is not fatal for some commands. The ones that require it need to check its existence later.
20✔
207
        hasConfigFile := true
20✔
208
        if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) {
30✔
209
                hasConfigFile = false
10✔
210
        } else if err != nil {
30✔
211
                if shouldIgnoreConfigLoadError(os.Args[1:]) {
18✔
212
                        hasConfigFile = false
8✔
213
                } else {
10✔
214
                        return err
2✔
215
                }
2✔
216
        }
217

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

221
        // Get project version and plugin info from flags
222
        if err := c.getInfoFromFlags(hasConfigFile); err != nil {
18✔
223
                return err
×
224
        }
×
225

226
        // Get project version and plugin info from defaults
227
        c.getInfoFromDefaults()
18✔
228

18✔
229
        return nil
18✔
230
}
231

232
// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
233
func (c *CLI) getInfoFromConfigFile() error {
20✔
234
        // Read the project configuration file
20✔
235
        cfg := yamlstore.New(c.fs)
20✔
236

20✔
237
        // Workaround for https://github.com/kubernetes-sigs/kubebuilder/issues/4433
20✔
238
        //
20✔
239
        // This allows the `kubebuilder alpha generate` command to work with old projects
20✔
240
        // that use plugin versions no longer supported (like go.kubebuilder.io/v3).
20✔
241
        //
20✔
242
        // We read the PROJECT file into memory and update the plugin version (e.g. from v3 to v4)
20✔
243
        // before the CLI tries to load it. This avoids errors during config loading
20✔
244
        // and lets users migrate their project layout from go/v3 to go/v4.
20✔
245

20✔
246
        if isAlphaGenerateCommand(os.Args[1:]) {
20✔
247
                // Patch raw file bytes before unmarshalling
×
248
                if err := patchProjectFileInMemoryIfNeeded(c.fs.FS, yamlstore.DefaultPath); err != nil {
×
249
                        return err
×
250
                }
×
251
        }
252

253
        if err := cfg.Load(); err != nil {
40✔
254
                return fmt.Errorf("error loading configuration: %w", err)
20✔
255
        }
20✔
256

257
        return c.getInfoFromConfig(cfg.Config())
×
258
}
259

260
// positionalArgs returns arguments that are not flags or flag values.
261
func positionalArgs(args []string) []string {
28✔
262
        positional := []string{}
28✔
263

28✔
264
        for i := 0; i < len(args); i++ {
140✔
265
                arg := args[i]
112✔
266
                if !strings.HasPrefix(arg, "-") {
125✔
267
                        positional = append(positional, arg)
13✔
268
                        continue
13✔
269
                }
270

271
                // A flag in --flag=value form consumes no additional argument.
272
                if strings.Contains(arg, "=") {
174✔
273
                        continue
75✔
274
                }
275

276
                // A flag consumes the following argument only when it is not another flag.
277
                if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
30✔
278
                        i++
6✔
279
                }
6✔
280
        }
281

282
        return positional
28✔
283
}
284

285
// hasOnlyRootFlags returns true when args consists solely of root flags and their values.
286
func hasOnlyRootFlags(args []string) bool {
4✔
287
        for i := 0; i < len(args); i++ {
8✔
288
                switch args[i] {
4✔
289
                case pluginsFlagArg, "--" + projectVersionFlag:
2✔
290
                        if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") {
4✔
291
                                i++
2✔
292
                        }
2✔
293
                default:
2✔
294
                        if strings.HasPrefix(args[i], pluginsFlagArg+"=") ||
2✔
295
                                strings.HasPrefix(args[i], "--"+projectVersionFlag+"=") {
2✔
NEW
296
                                continue
×
297
                        }
298
                        return false
2✔
299
                }
300
        }
301

302
        return true
2✔
303
}
304

305
// isAlphaGenerateCommand checks if the command invocation is `kubebuilder alpha generate`
306
// by scanning os.Args (excluding global flags). It returns true if "alpha" is followed by "generate".
307
func isAlphaGenerateCommand(args []string) bool {
20✔
308
        positional := positionalArgs(args)
20✔
309

20✔
310
        // Check for `alpha generate` in positional arguments
20✔
311
        for i := 0; i < len(positional)-1; i++ {
22✔
312
                if positional[i] == "alpha" && positional[i+1] == "generate" {
2✔
313
                        return true
×
314
                }
×
315
        }
316

317
        return false
20✔
318
}
319

320
// shouldIgnoreConfigLoadError returns true for commands that do not require a valid PROJECT file.
321
// This keeps context-free commands like help, version, and completion working from arbitrary directories.
322
func shouldIgnoreConfigLoadError(args []string) bool {
12✔
323
        if slices.ContainsFunc(args, isHelpFlag) {
16✔
324
                return true
4✔
325
        }
4✔
326

327
        positional := positionalArgs(args)
8✔
328
        // With no subcommand, Cobra displays root help. Configuration is not required.
8✔
329
        if len(positional) == 0 {
13✔
330
                return len(args) == 0 || hasOnlyRootFlags(args)
5✔
331
        }
5✔
332

333
        return positional[0] == kubebuilderSubcommandHelp ||
3✔
334
                positional[0] == kubebuilderSubcommandVersion ||
3✔
335
                positional[0] == kubebuilderSubcommandCompletion
3✔
336
}
337

338
// patchProjectFileInMemoryIfNeeded updates deprecated plugin keys in the PROJECT file in place,
339
// so that users can run `kubebuilder alpha generate` even with older plugin layouts.
340
//
341
// See: https://github.com/kubernetes-sigs/kubebuilder/issues/4433
342
//
343
// This ensures the CLI can successfully load the config without failing on unsupported plugin versions.
344
func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error {
×
345
        type pluginReplacement struct {
×
346
                Old string
×
347
                New string
×
348
        }
×
349

×
350
        replacements := []pluginReplacement{
×
351
                {pluginGoKubebuilderV2, pluginGoKubebuilderV4},
×
352
                {"go.kubebuilder.io/v3", pluginGoKubebuilderV4},
×
353
                {"go.kubebuilder.io/v3-alpha", pluginGoKubebuilderV4},
×
354
        }
×
355

×
356
        content, err := afero.ReadFile(fs, path)
×
357
        if err != nil {
×
358
                return nil
×
359
        }
×
360

361
        original := string(content)
×
362
        modified := original
×
363

×
364
        for _, rep := range replacements {
×
365
                if strings.Contains(modified, rep.Old) {
×
366
                        modified = strings.ReplaceAll(modified, rep.Old, rep.New)
×
367
                        log.Warn("Project is using an old and unsupported plugin layout",
×
368
                                "old_layout", rep.Old,
×
369
                                "new_layout", rep.New,
×
370
                                "note", "Replace in memory to allow `alpha generate` to work.",
×
371
                        )
×
372
                }
×
373
        }
374

375
        if modified != original {
×
376
                err := afero.WriteFile(fs, path, []byte(modified), machinery.DefaultFilePermission)
×
377
                if err != nil {
×
378
                        return fmt.Errorf("failed to write patched PROJECT file: %w", err)
×
379
                }
×
380
        }
381

382
        return nil
×
383
}
384

385
// getInfoFromConfig obtains the project version and plugin keys from the project config.
386
// It is extracted from getInfoFromConfigFile for testing purposes.
387
func (c *CLI) getInfoFromConfig(projectConfig config.Config) error {
3✔
388
        c.pluginKeys = projectConfig.GetPluginChain()
3✔
389
        c.projectVersion = projectConfig.GetVersion()
3✔
390

3✔
391
        for _, pluginKey := range c.pluginKeys {
7✔
392
                if err := plugin.ValidateKey(pluginKey); err != nil {
5✔
393
                        return fmt.Errorf("invalid plugin key found in project configuration file: %w", err)
1✔
394
                }
1✔
395
        }
396

397
        return nil
2✔
398
}
399

400
// getInfoFromFlags obtains the project version and plugin keys from flags.
401
func (c *CLI) getInfoFromFlags(hasConfigFile bool) error {
34✔
402
        // Check if --plugins is followed by --help or -h to avoid parsing help as a plugin value
34✔
403
        // This fixes: kubebuilder init --plugins --help
34✔
404
        for i := 0; i < len(os.Args)-1; i++ {
261✔
405
                if os.Args[i] == pluginsFlagArg || os.Args[i] == pluginsFlagArg+"=" {
239✔
406
                        nextArg := os.Args[i+1]
12✔
407
                        if isHelpFlag(nextArg) {
15✔
408
                                // Help was requested, return early to let Cobra handle it
3✔
409
                                return nil
3✔
410
                        }
3✔
411
                }
412
        }
413

414
        // Partially parse the command line arguments
415
        fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
31✔
416

31✔
417
        // Load the base command global flags
31✔
418
        fs.AddFlagSet(c.cmd.PersistentFlags())
31✔
419

31✔
420
        // If we were unable to load the project configuration, we should also accept the project version flag
31✔
421
        var projectVersionStr string
31✔
422
        if !hasConfigFile {
62✔
423
                fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version")
31✔
424
        }
31✔
425

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

31✔
430
        // Omit unknown flags to avoid parsing errors
31✔
431
        fs.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true}
31✔
432

31✔
433
        // Parse the arguments
31✔
434
        if err := fs.Parse(os.Args[1:]); err != nil {
31✔
435
                return fmt.Errorf("could not parse flags: %w", err)
×
436
        }
×
437

438
        // If any plugin key was provided, replace those from the project configuration file
439
        if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil {
31✔
440
                return fmt.Errorf("invalid flag %q: %w", pluginsFlag, err)
×
441
        } else if len(pluginKeys) != 0 {
42✔
442
                // Filter out help flags that may have been incorrectly parsed as plugin values
11✔
443
                // This fixes the issue where "kubebuilder edit --plugins --help" treats --help as a plugin
11✔
444
                validPluginKeys := make([]string, 0, len(pluginKeys))
11✔
445
                helpRequested := false
11✔
446
                for _, key := range pluginKeys {
30✔
447
                        key = strings.TrimSpace(key)
19✔
448
                        // Skip help flags
19✔
449
                        if isHelpFlag(key) {
19✔
450
                                helpRequested = true
×
451
                                continue
×
452
                        }
453
                        validPluginKeys = append(validPluginKeys, key)
19✔
454
                }
455

456
                // If help was requested via --plugins flag, set the help flag to trigger Cobra's help display
457
                // This prevents command execution and shows help instead
458
                if helpRequested {
11✔
459
                        if err := fs.Set("help", "true"); err == nil {
×
460
                                return nil
×
461
                        }
×
462
                        // If setting help flag fails, still return nil to avoid validation errors
463
                        return nil
×
464
                }
465

466
                // Validate the remaining plugin keys
467
                for i, key := range validPluginKeys {
30✔
468
                        if err := plugin.ValidateKey(key); err != nil {
20✔
469
                                return fmt.Errorf("invalid plugin %q found in flags: %w", validPluginKeys[i], err)
1✔
470
                        }
1✔
471
                }
472

473
                c.pluginKeys = validPluginKeys
10✔
474
        }
475

476
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
477
        // else validate the provided project version
478
        if projectVersionStr != "" {
35✔
479
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
480
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
481
                }
1✔
482
        }
483

484
        return nil
29✔
485
}
486

487
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
488
func (c *CLI) getInfoFromDefaults() {
22✔
489
        // Should not use default values if a plugin was already set
22✔
490
        // This checks includes the case where a project configuration file was found,
22✔
491
        // as it will always have at least one plugin key set by now
22✔
492
        if len(c.pluginKeys) != 0 {
25✔
493
                // We don't assign a default value for project version here because we may be able to
3✔
494
                // resolve the project version after resolving the plugins.
3✔
495
                return
3✔
496
        }
3✔
497

498
        // If the user provided a project version, use the default plugins for that project version
499
        if c.projectVersion.Validate() == nil {
20✔
500
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
501
                return
1✔
502
        }
1✔
503

504
        // Else try to use the default plugins for the default project version
505
        if c.defaultProjectVersion.Validate() == nil {
20✔
506
                var found bool
2✔
507
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
508
                        c.projectVersion = c.defaultProjectVersion
2✔
509
                        return
2✔
510
                }
2✔
511
        }
512

513
        // Else check if only default plugins for a project version were provided
514
        if len(c.defaultPlugins) == 1 {
30✔
515
                for projectVersion, defaultPlugins := range c.defaultPlugins {
28✔
516
                        c.pluginKeys = defaultPlugins
14✔
517
                        c.projectVersion = projectVersion
14✔
518
                        return
14✔
519
                }
14✔
520
        }
521
}
522

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

526
// resolvePlugins selects from the available plugins those that match the project version and plugin keys provided.
527
func (c *CLI) resolvePlugins() error {
35✔
528
        knownProjectVersion := c.projectVersion.Validate() == nil
35✔
529

35✔
530
        for _, pluginKey := range c.pluginKeys {
71✔
531
                var extraErrMsg string
36✔
532

36✔
533
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
36✔
534
                for _, p := range c.plugins {
311✔
535
                        plugins = append(plugins, p)
275✔
536
                }
275✔
537
                // We can omit the error because plugin keys have already been validated
538
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
36✔
539
                if knownProjectVersion {
62✔
540
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
26✔
541
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
26✔
542
                }
26✔
543

544
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
545
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
546
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
547
                if _, version := plugin.SplitKey(pluginKey); version != "" {
58✔
548
                        var ver plugin.Version
22✔
549
                        if err := ver.Parse(version); err != nil {
22✔
550
                                return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err)
×
551
                        }
×
552
                        if !ver.IsStable() {
22✔
553
                                extraErrMsg += unstablePluginMsg
×
554
                        }
×
555
                }
556

557
                // Only 1 plugin can match
558
                switch len(plugins) {
36✔
559
                case 1:
26✔
560
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
26✔
561
                case 0:
7✔
562
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
7✔
563
                default:
3✔
564
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
565
                }
566
        }
567

568
        // Now we can try to resolve the project version if not known by this point
569
        if !knownProjectVersion && len(c.resolvedPlugins) > 0 {
29✔
570
                // Extract the common supported project versions
4✔
571
                supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...)
4✔
572

4✔
573
                // If there is only one common supported project version, resolve to it
4✔
574
        ProjectNumberVersionSwitch:
4✔
575
                switch len(supportedProjectVersions) {
4✔
576
                case 1:
1✔
577
                        c.projectVersion = supportedProjectVersions[0]
1✔
578
                case 0:
1✔
579
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
580
                default:
2✔
581
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
582
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
583
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
584
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
585
                                        c.projectVersion = c.defaultProjectVersion
1✔
586
                                        break ProjectNumberVersionSwitch
1✔
587
                                }
588
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
589
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
590
                        }
591
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
592
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
593
                }
594
        }
595

596
        return nil
23✔
597
}
598

599
// addSubcommands returns a root command with a subcommand tree reflecting the
600
// current project's state.
601
func (c *CLI) addSubcommands() {
18✔
602
        // add the alpha command if it has any subcommands enabled
18✔
603
        c.addAlphaCmd()
18✔
604

18✔
605
        // kubebuilder completion
18✔
606
        // Only add completion if requested
18✔
607
        if c.completionCommand {
25✔
608
                c.cmd.AddCommand(c.newCompletionCmd())
7✔
609
        }
7✔
610

611
        // kubebuilder create
612
        createCmd := c.newCreateCmd()
18✔
613
        // kubebuilder create api
18✔
614
        createCmd.AddCommand(c.newCreateAPICmd())
18✔
615
        createCmd.AddCommand(c.newCreateWebhookCmd())
18✔
616
        if createCmd.HasSubCommands() {
36✔
617
                c.cmd.AddCommand(createCmd)
18✔
618
        }
18✔
619

620
        // kubebuilder edit
621
        c.cmd.AddCommand(c.newEditCmd())
18✔
622

18✔
623
        // kubebuilder init
18✔
624
        c.cmd.AddCommand(c.newInitCmd())
18✔
625

18✔
626
        // kubebuilder version
18✔
627
        // Only add version if a version string was provided
18✔
628
        if c.version != "" {
26✔
629
                c.cmd.AddCommand(c.newVersionCmd())
8✔
630
        }
8✔
631
}
632

633
// addExtraCommands adds the additional commands.
634
func (c *CLI) addExtraCommands() error {
17✔
635
        for _, cmd := range c.extraCommands {
19✔
636
                for _, subCmd := range c.cmd.Commands() {
10✔
637
                        if cmd.Name() == subCmd.Name() {
9✔
638
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
639
                        }
1✔
640
                }
641
                c.cmd.AddCommand(cmd)
1✔
642
        }
643
        return nil
16✔
644
}
645

646
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
647
func (c CLI) printDeprecationWarnings() {
15✔
648
        for _, p := range c.resolvedPlugins {
27✔
649
                if p == nil {
12✔
650
                        continue
×
651
                }
652
                if deprecated, ok := p.(plugin.Deprecated); ok && len(deprecated.DeprecationWarning()) > 0 {
13✔
653
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, deprecated.DeprecationWarning()))
1✔
654
                }
1✔
655
        }
656
}
657

658
// metadata returns CLI's metadata.
659
func (c CLI) metadata() plugin.CLIMetadata {
54✔
660
        return plugin.CLIMetadata{
54✔
661
                CommandName: c.commandName,
54✔
662
        }
54✔
663
}
54✔
664

665
// Run executes the CLI utility.
666
//
667
// If an error is found, command help and examples will be printed.
668
func (c CLI) Run() error {
1✔
669
        if err := c.cmd.Execute(); err != nil {
2✔
670
                // Don't return error if help was displayed (from --plugins --help pattern)
1✔
671
                if err == errHelpDisplayed {
1✔
672
                        return nil
×
673
                }
×
674
                return fmt.Errorf("error executing command: %w", err)
1✔
675
        }
676

677
        return nil
×
678
}
679

680
// Command returns the underlying root command.
681
func (c CLI) Command() *cobra.Command {
2✔
682
        return c.cmd
2✔
683
}
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