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

kubernetes-sigs / kubebuilder / 15232042200

24 May 2025 11:38PM UTC coverage: 71.57% (-1.5%) from 73.023%
15232042200

push

github

web-flow
📖 Update the docs with release v4.6.0 (#4822)

* chore: simplify test variable declarations for CLI tests

Replaced redundant multi-line `var` blocks with single-line declarations in
`version_test.go` and `completion_test.go`. This reduces visual noise and aligns
with idiomatic Go style for concise test setup.

* chore: improve readability of test case for alternative delimiters

Reformatted the inline struct definition in `scaffold_test.go` to a multi-line
layout.

* chore: add gofumpt to golangci-lint configuration

Enabled the `gofumpt` linter in `.golangci.yml` to enforce stricter formatting rules
and ensure consistency with gofumpt standards across the codebase.

* :book: bump sigs.k8s.io/kubebuilder/v4

Bumps [sigs.k8s.io/kubebuilder/v4](https://github.com/kubernetes-sigs/kubebuilder) from 4.5.1 to 4.5.2.
- [Release notes](https://github.com/kubernetes-sigs/kubebuilder/releases)
- [Changelog](https://github.com/kubernetes-sigs/kubebuilder/blob/master/RELEASE.md)
- [Commits](https://github.com/kubernetes-sigs/kubebuilder/compare/v4.5.1...v4.5.2)

---
updated-dependencies:
- dependency-name: sigs.k8s.io/kubebuilder/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: unify import alias for network-policy

Replaces import alias `network_policy` with `networkpolicy` to follow Go naming conventions.

* chore: normalize error messages and wrap errors using %w

- Use %w consistently in fmt.Errorf for proper error wrapping
- Normalize error message casing for consistency
- Adjust tests to reflect updated error strings

* chore: normalize error messages and wrap errors using %w

- Use %w consistently in fmt.Errorf for proper error wrapping

* chore: normalize error messages and wrap errors using %w

- Use %w consistently in fmt.Errorf for proper error wrapping

* 🌱 (chore): wrap errors with `%w` and normalize formatting in `kustomize/v2`

Replaced uses of `%v... (continued)

66 of 297 new or added lines in 17 files covered. (22.22%)

34 existing lines in 7 files now uncovered.

2311 of 3229 relevant lines covered (71.57%)

16.53 hits per line

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

93.26
/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 {
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
        // Full CLI version string.
51
        version string
52
        // CLI version string (just the CLI version number, no extra information).
53
        cliVersion string
54
        // CLI root's command description.
55
        description string
56
        // Plugins registered in the CLI.
57
        plugins map[string]plugin.Plugin
58
        // Default plugins in case none is provided and a config file can't be found.
59
        defaultPlugins map[config.Version][]string
60
        // Default project version in case none is provided and a config file can't be found.
61
        defaultProjectVersion config.Version
62
        // Commands injected by options.
63
        extraCommands []*cobra.Command
64
        // Alpha commands injected by options.
65
        extraAlphaCommands []*cobra.Command
66
        // Whether to add a completion command to the CLI.
67
        completionCommand bool
68

69
        /* Internal fields */
70

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

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

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

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

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

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

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

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

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

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

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

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

143
        return c, nil
23✔
144
}
145

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

12✔
150
        var uve config.UnsupportedVersionError
12✔
151

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

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

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

184
        // Add the subcommands
185
        c.addSubcommands()
10✔
186

10✔
187
        return nil
10✔
188
}
189

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

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

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

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

10✔
213
        return nil
10✔
214
}
215

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

224
        return c.getInfoFromConfig(cfg.Config())
×
225
}
226

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

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

239
        return nil
2✔
240
}
241

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

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

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

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

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

22✔
263
        // Parse the arguments
22✔
264
        if err := fs.Parse(os.Args[1:]); err != nil {
22✔
NEW
265
                return fmt.Errorf("could not parse flags: %w", err)
×
266
        }
×
267

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

280
                c.pluginKeys = pluginKeys
7✔
281
        }
282

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

291
        return nil
20✔
292
}
293

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

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

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

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

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

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

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

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

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

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

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

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

403
        return nil
16✔
404
}
405

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

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

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

427
        // kubebuilder edit
428
        c.cmd.AddCommand(c.newEditCmd())
10✔
429

10✔
430
        // kubebuilder init
10✔
431
        c.cmd.AddCommand(c.newInitCmd())
10✔
432

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

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

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

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

469
// Run executes the CLI utility.
470
//
471
// If an error is found, command help and examples will be printed.
472
func (c CLI) Run() error {
1✔
473
        if err := c.cmd.Execute(); err != nil {
2✔
474
                return fmt.Errorf("error executing command: %w", err)
1✔
475
        }
1✔
476

NEW
477
        return nil
×
478
}
479

480
// Command returns the underlying root command.
481
func (c CLI) Command() *cobra.Command {
2✔
482
        return c.cmd
2✔
483
}
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