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

kubernetes-sigs / kubebuilder / 13454291151

21 Feb 2025 09:50AM UTC coverage: 73.061% (-1.0%) from 74.078%
13454291151

Pull #4572

github

sarthaksarthak9
Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins
Pull Request #4572: :sparkles: Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins

1 of 45 new or added lines in 3 files covered. (2.22%)

107 existing lines in 3 files now uncovered.

2308 of 3159 relevant lines covered (73.06%)

14.01 hits per line

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

93.89
/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
        "github.com/spf13/afero"
26
        "github.com/spf13/cobra"
27
        "github.com/spf13/pflag"
28

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

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

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

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

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

67
        /* Internal fields */
68

69
        // Plugin keys to scaffold with.
70
        pluginKeys []string
71
        // Project version to scaffold.
72
        projectVersion config.Version
73

74
        // A filtered set of plugins that should be used by command constructors.
75
        resolvedPlugins []plugin.Plugin
76

77
        // Root command.
78
        cmd *cobra.Command
79

80
        // Underlying fs
81
        fs machinery.Filesystem
82
}
83

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

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

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

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

115
        // Write deprecation notices after all commands have been constructed.
116
        c.printDeprecationWarnings()
7✔
117

7✔
118
        return c, nil
7✔
119
}
120

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

40✔
134
        // Apply provided options.
40✔
135
        for _, option := range options {
91✔
136
                if err := option(c); err != nil {
68✔
137
                        return nil, err
17✔
138
                }
17✔
139
        }
140

141
        return c, nil
23✔
142
}
143

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

12✔
148
        var uve config.UnsupportedVersionError
12✔
149

12✔
150
        // Get project version and plugin keys.
12✔
151
        switch err := c.getInfo(); {
12✔
152
        case err == nil:
10✔
153
        case errors.As(err, &uve) && uve.Version.Compare(config.Version{Number: 3, Stage: stage.Alpha}) == 0:
1✔
154
                stableVersion := config.Version{
1✔
155
                        Number: uve.Version.Number,
1✔
156
                }
1✔
157
                if config.IsRegistered(stableVersion) {
2✔
158
                        c.projectVersion = stableVersion
1✔
159
                } else {
1✔
UNCOV
160
                        return err
×
UNCOV
161
                }
×
162
        default:
1✔
163
                return err
1✔
164
        }
165

166
        // Workaround for kubebuilder alpha generate
167
        if c.cmd.CalledAs() == "alpha generate" {
11✔
NEW
UNCOV
168
                err := updateProjectFileForAlphaGenerate()
×
NEW
UNCOV
169
                if err != nil {
×
NEW
UNCOV
170
                        return fmt.Errorf("failed to update PROJECT file: %w", err)
×
NEW
UNCOV
171
                }
×
172
        }
173

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

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

10✔
182
        return nil
10✔
183
}
184

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

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

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

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

10✔
208
        return nil
10✔
209
}
210

211
// getInfoFromConfigFile obtains the project version and plugin keys from the project config file.
212
func (c *CLI) getInfoFromConfigFile() error {
12✔
213
        // Read the project configuration file
12✔
214
        cfg := yamlstore.New(c.fs)
12✔
215
        if err := cfg.Load(); err != nil {
24✔
216
                return err
12✔
217
        }
12✔
218

UNCOV
219
        return c.getInfoFromConfig(cfg.Config())
×
220
}
221

222
// getInfoFromConfig obtains the project version and plugin keys from the project config.
223
// It is extracted from getInfoFromConfigFile for testing purposes.
224
func (c *CLI) getInfoFromConfig(projectConfig config.Config) error {
3✔
225
        c.pluginKeys = projectConfig.GetPluginChain()
3✔
226
        c.projectVersion = projectConfig.GetVersion()
3✔
227

3✔
228
        for _, pluginKey := range c.pluginKeys {
7✔
229
                if err := plugin.ValidateKey(pluginKey); err != nil {
5✔
230
                        return fmt.Errorf("invalid plugin key found in project configuration file: %w", err)
1✔
231
                }
1✔
232
        }
233

234
        return nil
2✔
235
}
236

237
// getInfoFromFlags obtains the project version and plugin keys from flags.
238
func (c *CLI) getInfoFromFlags(hasConfigFile bool) error {
22✔
239
        // Partially parse the command line arguments
22✔
240
        fs := pflag.NewFlagSet("base", pflag.ContinueOnError)
22✔
241

22✔
242
        // Load the base command global flags
22✔
243
        fs.AddFlagSet(c.cmd.PersistentFlags())
22✔
244

22✔
245
        // If we were unable to load the project configuration, we should also accept the project version flag
22✔
246
        var projectVersionStr string
22✔
247
        if !hasConfigFile {
44✔
248
                fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version")
22✔
249
        }
22✔
250

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

22✔
255
        // Omit unknown flags to avoid parsing errors
22✔
256
        fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
22✔
257

22✔
258
        // Parse the arguments
22✔
259
        if err := fs.Parse(os.Args[1:]); err != nil {
22✔
260
                return err
×
UNCOV
261
        }
×
262

263
        // If any plugin key was provided, replace those from the project configuration file
264
        if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil {
22✔
UNCOV
265
                return err
×
266
        } else if len(pluginKeys) != 0 {
30✔
267
                // Remove leading and trailing spaces and validate the plugin keys
8✔
268
                for i, key := range pluginKeys {
24✔
269
                        pluginKeys[i] = strings.TrimSpace(key)
16✔
270
                        if err := plugin.ValidateKey(pluginKeys[i]); err != nil {
17✔
271
                                return fmt.Errorf("invalid plugin %q found in flags: %w", pluginKeys[i], err)
1✔
272
                        }
1✔
273
                }
274

275
                c.pluginKeys = pluginKeys
7✔
276
        }
277

278
        // If the project version flag was accepted but not provided keep the empty version and try to resolve it later,
279
        // else validate the provided project version
280
        if projectVersionStr != "" {
26✔
281
                if err := c.projectVersion.Parse(projectVersionStr); err != nil {
6✔
282
                        return fmt.Errorf("invalid project version flag: %w", err)
1✔
283
                }
1✔
284
        }
285

286
        return nil
20✔
287
}
288

289
// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values
290
func (c *CLI) getInfoFromDefaults() {
14✔
291
        // Should not use default values if a plugin was already set
14✔
292
        // This checks includes the case where a project configuration file was found,
14✔
293
        // as it will always have at least one plugin key set by now
14✔
294
        if len(c.pluginKeys) != 0 {
16✔
295
                // We don't assign a default value for project version here because we may be able to
2✔
296
                // resolve the project version after resolving the plugins.
2✔
297
                return
2✔
298
        }
2✔
299

300
        // If the user provided a project version, use the default plugins for that project version
301
        if c.projectVersion.Validate() == nil {
13✔
302
                c.pluginKeys = c.defaultPlugins[c.projectVersion]
1✔
303
                return
1✔
304
        }
1✔
305

306
        // Else try to use the default plugins for the default project version
307
        if c.defaultProjectVersion.Validate() == nil {
13✔
308
                var found bool
2✔
309
                if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found {
4✔
310
                        c.projectVersion = c.defaultProjectVersion
2✔
311
                        return
2✔
312
                }
2✔
313
        }
314

315
        // Else check if only default plugins for a project version were provided
316
        if len(c.defaultPlugins) == 1 {
16✔
317
                for projectVersion, defaultPlugins := range c.defaultPlugins {
14✔
318
                        c.pluginKeys = defaultPlugins
7✔
319
                        c.projectVersion = projectVersion
7✔
320
                        return
7✔
321
                }
7✔
322
        }
323
}
324

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

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

27✔
332
        for _, pluginKey := range c.pluginKeys {
55✔
333
                var extraErrMsg string
28✔
334

28✔
335
                plugins := make([]plugin.Plugin, 0, len(c.plugins))
28✔
336
                for _, p := range c.plugins {
295✔
337
                        plugins = append(plugins, p)
267✔
338
                }
267✔
339
                // We can omit the error because plugin keys have already been validated
340
                plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey)
28✔
341
                if knownProjectVersion {
47✔
342
                        plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion)
19✔
343
                        extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion)
19✔
344
                }
19✔
345

346
                // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable".
347
                // This upgrade effectively removes a plugin, which is fine because unstable plugins are
348
                // under no support contract. However users should be notified _why_ their plugin cannot be found.
349
                if _, version := plugin.SplitKey(pluginKey); version != "" {
42✔
350
                        var ver plugin.Version
14✔
351
                        if err := ver.Parse(version); err != nil {
14✔
UNCOV
352
                                return fmt.Errorf("error parsing input plugin version from key %q: %v", pluginKey, err)
×
UNCOV
353
                        }
×
354
                        if !ver.IsStable() {
14✔
UNCOV
355
                                extraErrMsg += unstablePluginMsg
×
UNCOV
356
                        }
×
357
                }
358

359
                // Only 1 plugin can match
360
                switch len(plugins) {
28✔
361
                case 1:
19✔
362
                        c.resolvedPlugins = append(c.resolvedPlugins, plugins[0])
19✔
363
                case 0:
6✔
364
                        return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg)
6✔
365
                default:
3✔
366
                        return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg)
3✔
367
                }
368
        }
369

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

4✔
375
                // If there is only one common supported project version, resolve to it
4✔
376
        ProjectNumberVersionSwitch:
4✔
377
                switch len(supportedProjectVersions) {
4✔
378
                case 1:
1✔
379
                        c.projectVersion = supportedProjectVersions[0]
1✔
380
                case 0:
1✔
381
                        return fmt.Errorf("no project version supported by all the resolved plugins")
1✔
382
                default:
2✔
383
                        supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions))
2✔
384
                        for _, supportedProjectVersion := range supportedProjectVersions {
6✔
385
                                // In case one of the multiple supported versions is the default one, choose that and exit the switch
4✔
386
                                if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 {
5✔
387
                                        c.projectVersion = c.defaultProjectVersion
1✔
388
                                        break ProjectNumberVersionSwitch
1✔
389
                                }
390
                                supportedProjectVersionStrings = append(supportedProjectVersionStrings,
3✔
391
                                        fmt.Sprintf("%q", supportedProjectVersion))
3✔
392
                        }
393
                        return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s",
1✔
394
                                strings.Join(supportedProjectVersionStrings, ", "))
1✔
395
                }
396
        }
397

398
        return nil
16✔
399
}
400

401
// addSubcommands returns a root command with a subcommand tree reflecting the
402
// current project's state.
403
func (c *CLI) addSubcommands() {
10✔
404
        // add the alpha command if it has any subcommands enabled
10✔
405
        c.addAlphaCmd()
10✔
406

10✔
407
        // kubebuilder completion
10✔
408
        // Only add completion if requested
10✔
409
        if c.completionCommand {
11✔
410
                c.cmd.AddCommand(c.newCompletionCmd())
1✔
411
        }
1✔
412

413
        // kubebuilder create
414
        createCmd := c.newCreateCmd()
10✔
415
        // kubebuilder create api
10✔
416
        createCmd.AddCommand(c.newCreateAPICmd())
10✔
417
        createCmd.AddCommand(c.newCreateWebhookCmd())
10✔
418
        if createCmd.HasSubCommands() {
20✔
419
                c.cmd.AddCommand(createCmd)
10✔
420
        }
10✔
421

422
        // kubebuilder edit
423
        c.cmd.AddCommand(c.newEditCmd())
10✔
424

10✔
425
        // kubebuilder init
10✔
426
        c.cmd.AddCommand(c.newInitCmd())
10✔
427

10✔
428
        // kubebuilder version
10✔
429
        // Only add version if a version string was provided
10✔
430
        if c.version != "" {
11✔
431
                c.cmd.AddCommand(c.newVersionCmd())
1✔
432
        }
1✔
433
}
434

435
// addExtraCommands adds the additional commands.
436
func (c *CLI) addExtraCommands() error {
9✔
437
        for _, cmd := range c.extraCommands {
11✔
438
                for _, subCmd := range c.cmd.Commands() {
10✔
439
                        if cmd.Name() == subCmd.Name() {
9✔
440
                                return fmt.Errorf("command %q already exists", cmd.Name())
1✔
441
                        }
1✔
442
                }
443
                c.cmd.AddCommand(cmd)
1✔
444
        }
445
        return nil
8✔
446
}
447

448
// printDeprecationWarnings prints the deprecation warnings of the resolved plugins.
449
func (c CLI) printDeprecationWarnings() {
7✔
450
        for _, p := range c.resolvedPlugins {
12✔
451
                if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 {
6✔
452
                        _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning()))
1✔
453
                }
1✔
454
        }
455
}
456

457
// metadata returns CLI's metadata.
458
func (c CLI) metadata() plugin.CLIMetadata {
24✔
459
        return plugin.CLIMetadata{
24✔
460
                CommandName: c.commandName,
24✔
461
        }
24✔
462
}
24✔
463

464
// Run executes the CLI utility.
465
//
466
// If an error is found, command help and examples will be printed.
467
func (c CLI) Run() error {
1✔
468
        return c.cmd.Execute()
1✔
469
}
1✔
470

471
// Command returns the underlying root command.
472
func (c CLI) Command() *cobra.Command {
2✔
473
        return c.cmd
2✔
474
}
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

© 2025 Coveralls, Inc