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

kubernetes-sigs / kubebuilder / 13900935872

17 Mar 2025 01:39PM UTC coverage: 72.986%. First build
13900935872

Pull #4572

github

web-flow
Update and rename test-alpha-generate.yaml to alphagenerate.yaml
Pull Request #4572: :sparkles: Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins

2 of 49 new or added lines in 3 files covered. (4.08%)

2310 of 3165 relevant lines covered (72.99%)

13.98 hits per line

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

93.58
/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
        // Workaround for kubebuilder alpha generate
12✔
151
        if len(os.Args) > 2 && os.Args[1] == "alpha" && os.Args[2] == "generate" {
12✔
NEW
152
                err := updateProjectFileForAlphaGenerate()
×
NEW
153
                if err != nil {
×
NEW
154
                        return fmt.Errorf("failed to update PROJECT file: %w", err)
×
NEW
155
                }
×
156
        }
157

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

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

182
        // Add the subcommands
183
        c.addSubcommands()
10✔
184

10✔
185
        return nil
10✔
186
}
187

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

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

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

208
        // Get project version and plugin info from defaults
209
        c.getInfoFromDefaults()
10✔
210

10✔
211
        return nil
10✔
212
}
213

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

222
        return c.getInfoFromConfig(cfg.Config())
×
223
}
224

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

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

237
        return nil
2✔
238
}
239

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

22✔
245
        // Load the base command global flags
22✔
246
        fs.AddFlagSet(c.cmd.PersistentFlags())
22✔
247

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

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

22✔
258
        // Omit unknown flags to avoid parsing errors
22✔
259
        fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true}
22✔
260

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

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

278
                c.pluginKeys = pluginKeys
7✔
279
        }
280

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

289
        return nil
20✔
290
}
291

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

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

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

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

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

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

27✔
335
        for _, pluginKey := range c.pluginKeys {
55✔
336
                var extraErrMsg string
28✔
337

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

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

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

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

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

401
        return nil
16✔
402
}
403

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

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

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

425
        // kubebuilder edit
426
        c.cmd.AddCommand(c.newEditCmd())
10✔
427

10✔
428
        // kubebuilder init
10✔
429
        c.cmd.AddCommand(c.newInitCmd())
10✔
430

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

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

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

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

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

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