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

kubernetes-sigs / kubebuilder / 15372570272

01 Jun 2025 07:40AM UTC coverage: 71.498% (+0.1%) from 71.371%
15372570272

Pull #4844

github

camilamacedo86
(cli) fix: allow 'alpha generate' to work with unsupported plugins by patching PROJECT file in-memory

This change enables the 'kubebuilder alpha generate' command to work with projects that use old and unsupported plugin versions (e.g., go.kubebuilder.io/v2 or v3). It does so by loading the PROJECT config file into memory and replacing deprecated plugin keys with the latest supported version (v4) before parsing.

This prevents config loading errors and preserves support for CLI flags like --help and others.

Related to: https://github.com/kubernetes-sigs/kubebuilder/issues/4433
Pull Request #4844: 🐛 (cli) fix: allow 'alpha generate' to work with unsupported plugins by patching PROJECT file in-memory

33 of 72 new or added lines in 1 file covered. (45.83%)

2343 of 3277 relevant lines covered (71.5%)

16.54 hits per line

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

84.13
/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
        "os"
23
        "strings"
24

25
        log "github.com/sirupsen/logrus"
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) {
40✔
128
        // Default CLI options.
40✔
129
        c := &CLI{
40✔
130
                commandName: "kubebuilder",
40✔
131
                description: `CLI tool for building Kubernetes extensions and tools.
40✔
132
`,
40✔
133
                plugins:        make(map[string]plugin.Plugin),
40✔
134
                defaultPlugins: make(map[config.Version][]string),
40✔
135
                fs:             machinery.Filesystem{FS: afero.NewOsFs()},
40✔
136
        }
40✔
137

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

145
        return c, nil
23✔
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✔
NEW
225
                // Patch raw file bytes before unmarshalling
×
NEW
226
                if err := patchProjectFileInMemoryIfNeeded(c.fs.FS, yamlstore.DefaultPath); err != nil {
×
NEW
227
                        return err
×
NEW
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 := 0; i < len(args); i++ {
75✔
245
                arg := args[i]
63✔
246

63✔
247
                // Skip flags and their values
63✔
248
                if strings.HasPrefix(arg, "-") {
124✔
249
                        // If the flag is in --flag=value format, skip only this one
61✔
250
                        if strings.Contains(arg, "=") {
109✔
251
                                continue
48✔
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✔
NEW
268
                if positional[i] == "alpha" && positional[i+1] == "generate" {
×
NEW
269
                        return true
×
NEW
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.
NEW
282
func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error {
×
NEW
283
        type pluginReplacement struct {
×
NEW
284
                Old string
×
NEW
285
                New string
×
NEW
286
        }
×
NEW
287

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

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

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

×
NEW
302
        for _, rep := range replacements {
×
NEW
303
                if strings.Contains(modified, rep.Old) {
×
NEW
304
                        modified = strings.ReplaceAll(modified, rep.Old, rep.New)
×
NEW
305
                        log.Warnf("This project is using an old and no longer supported plugin layout %q. "+
×
NEW
306
                                "Replace in memory to %q to allow `alpha generate` to work.",
×
NEW
307
                                rep.Old, rep.New)
×
NEW
308
                }
×
309
        }
310

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

NEW
318
        return nil
×
319
}
320

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

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

333
        return nil
2✔
334
}
335

336
// getInfoFromFlags obtains the project version and plugin keys from flags.
337
func (c *CLI) getInfoFromFlags(hasConfigFile bool) error {
22✔
338
        // Partially parse the command line arguments
22✔
339
        fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
22✔
340

22✔
341
        // Load the base command global flags
22✔
342
        fs.AddFlagSet(c.cmd.PersistentFlags())
22✔
343

22✔
344
        // If we were unable to load the project configuration, we should also accept the project version flag
22✔
345
        var projectVersionStr string
22✔
346
        if !hasConfigFile {
44✔
347
                fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version")
22✔
348
        }
22✔
349

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

22✔
354
        // Omit unknown flags to avoid parsing errors
22✔
355
        fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
22✔
356

22✔
357
        // Parse the arguments
22✔
358
        if err := fs.Parse(os.Args[1:]); err != nil {
22✔
359
                return fmt.Errorf("could not parse flags: %w", err)
×
360
        }
×
361

362
        // If any plugin key was provided, replace those from the project configuration file
363
        if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil {
22✔
364
                return fmt.Errorf("invalid flag %q: %w", pluginsFlag, err)
×
365
        } else if len(pluginKeys) != 0 {
30✔
366
                // Remove leading and trailing spaces and validate the plugin keys
8✔
367
                for i, key := range pluginKeys {
24✔
368
                        pluginKeys[i] = strings.TrimSpace(key)
16✔
369
                        if err := plugin.ValidateKey(pluginKeys[i]); err != nil {
17✔
370
                                return fmt.Errorf("invalid plugin %q found in flags: %w", pluginKeys[i], err)
1✔
371
                        }
1✔
372
                }
373

374
                c.pluginKeys = pluginKeys
7✔
375
        }
376

377
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
378
        // else validate the provided project version
379
        if projectVersionStr != "" {
26✔
380
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
381
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
382
                }
1✔
383
        }
384

385
        return nil
20✔
386
}
387

388
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
389
func (c *CLI) getInfoFromDefaults() {
14✔
390
        // Should not use default values if a plugin was already set
14✔
391
        // This checks includes the case where a project configuration file was found,
14✔
392
        // as it will always have at least one plugin key set by now
14✔
393
        if len(c.pluginKeys) != 0 {
16✔
394
                // We don't assign a default value for project version here because we may be able to
2✔
395
                // resolve the project version after resolving the plugins.
2✔
396
                return
2✔
397
        }
2✔
398

399
        // If the user provided a project version, use the default plugins for that project version
400
        if c.projectVersion.Validate() == nil {
13✔
401
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
402
                return
1✔
403
        }
1✔
404

405
        // Else try to use the default plugins for the default project version
406
        if c.defaultProjectVersion.Validate() == nil {
13✔
407
                var found bool
2✔
408
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
409
                        c.projectVersion = c.defaultProjectVersion
2✔
410
                        return
2✔
411
                }
2✔
412
        }
413

414
        // Else check if only default plugins for a project version were provided
415
        if len(c.defaultPlugins) == 1 {
16✔
416
                for projectVersion, defaultPlugins := range c.defaultPlugins {
14✔
417
                        c.pluginKeys = defaultPlugins
7✔
418
                        c.projectVersion = projectVersion
7✔
419
                        return
7✔
420
                }
7✔
421
        }
422
}
423

424
const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " +
425
        "https://kubebuilder.io/migration/plugin/plugins.html)"
426

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

27✔
431
        for _, pluginKey := range c.pluginKeys {
55✔
432
                var extraErrMsg string
28✔
433

28✔
434
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
28✔
435
                for _, p := range c.plugins {
295✔
436
                        plugins = append(plugins, p)
267✔
437
                }
267✔
438
                // We can omit the error because plugin keys have already been validated
439
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
28✔
440
                if knownProjectVersion {
47✔
441
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
19✔
442
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
19✔
443
                }
19✔
444

445
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
446
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
447
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
448
                if _, version := plugin.SplitKey(pluginKey); version != "" {
42✔
449
                        var ver plugin.Version
14✔
450
                        if err := ver.Parse(version); err != nil {
14✔
451
                                return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err)
×
452
                        }
×
453
                        if !ver.IsStable() {
14✔
454
                                extraErrMsg += unstablePluginMsg
×
455
                        }
×
456
                }
457

458
                // Only 1 plugin can match
459
                switch len(plugins) {
28✔
460
                case 1:
19✔
461
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
19✔
462
                case 0:
6✔
463
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
6✔
464
                default:
3✔
465
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
466
                }
467
        }
468

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

4✔
474
                // If there is only one common supported project version, resolve to it
4✔
475
        ProjectNumberVersionSwitch:
4✔
476
                switch len(supportedProjectVersions) {
4✔
477
                case 1:
1✔
478
                        c.projectVersion = supportedProjectVersions[0]
1✔
479
                case 0:
1✔
480
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
481
                default:
2✔
482
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
483
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
484
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
485
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
486
                                        c.projectVersion = c.defaultProjectVersion
1✔
487
                                        break ProjectNumberVersionSwitch
1✔
488
                                }
489
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
490
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
491
                        }
492
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
493
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
494
                }
495
        }
496

497
        return nil
16✔
498
}
499

500
// addSubcommands returns a root command with a subcommand tree reflecting the
501
// current project's state.
502
func (c *CLI) addSubcommands() {
10✔
503
        // add the alpha command if it has any subcommands enabled
10✔
504
        c.addAlphaCmd()
10✔
505

10✔
506
        // kubebuilder completion
10✔
507
        // Only add completion if requested
10✔
508
        if c.completionCommand {
11✔
509
                c.cmd.AddCommand(c.newCompletionCmd())
1✔
510
        }
1✔
511

512
        // kubebuilder create
513
        createCmd := c.newCreateCmd()
10✔
514
        // kubebuilder create api
10✔
515
        createCmd.AddCommand(c.newCreateAPICmd())
10✔
516
        createCmd.AddCommand(c.newCreateWebhookCmd())
10✔
517
        if createCmd.HasSubCommands() {
20✔
518
                c.cmd.AddCommand(createCmd)
10✔
519
        }
10✔
520

521
        // kubebuilder edit
522
        c.cmd.AddCommand(c.newEditCmd())
10✔
523

10✔
524
        // kubebuilder init
10✔
525
        c.cmd.AddCommand(c.newInitCmd())
10✔
526

10✔
527
        // kubebuilder version
10✔
528
        // Only add version if a version string was provided
10✔
529
        if c.version != "" {
11✔
530
                c.cmd.AddCommand(c.newVersionCmd())
1✔
531
        }
1✔
532
}
533

534
// addExtraCommands adds the additional commands.
535
func (c *CLI) addExtraCommands() error {
9✔
536
        for _, cmd := range c.extraCommands {
11✔
537
                for _, subCmd := range c.cmd.Commands() {
10✔
538
                        if cmd.Name() == subCmd.Name() {
9✔
539
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
540
                        }
1✔
541
                }
542
                c.cmd.AddCommand(cmd)
1✔
543
        }
544
        return nil
8✔
545
}
546

547
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
548
func (c CLI) printDeprecationWarnings() {
7✔
549
        for _, p := range c.resolvedPlugins {
12✔
550
                if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 {
6✔
551
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning()))
1✔
552
                }
1✔
553
        }
554
}
555

556
// metadata returns CLI's metadata.
557
func (c CLI) metadata() plugin.CLIMetadata {
24✔
558
        return plugin.CLIMetadata{
24✔
559
                CommandName: c.commandName,
24✔
560
        }
24✔
561
}
24✔
562

563
// Run executes the CLI utility.
564
//
565
// If an error is found, command help and examples will be printed.
566
func (c CLI) Run() error {
1✔
567
        if err := c.cmd.Execute(); err != nil {
2✔
568
                return fmt.Errorf("error executing command: %w", err)
1✔
569
        }
1✔
570

571
        return nil
×
572
}
573

574
// Command returns the underlying root command.
575
func (c CLI) Command() *cobra.Command {
2✔
576
        return c.cmd
2✔
577
}
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