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

kubernetes-sigs / kubebuilder / 16522004449

25 Jul 2025 12:30PM UTC coverage: 64.151%. Remained the same
16522004449

Pull #4898

github

web-flow
Merge branch 'master' into version/refactor-add-tests
Pull Request #4898: ✨ Improve version command output: add runtime fallbacks and unit tests.

2627 of 4095 relevant lines covered (64.15%)

13.59 hits per line

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

46.28
/pkg/cli/cmd_helpers.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

24
        "github.com/spf13/cobra"
25

26
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
27
        "sigs.k8s.io/kubebuilder/v4/pkg/config/store"
28
        yamlstore "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
29
        "sigs.k8s.io/kubebuilder/v4/pkg/machinery"
30
        "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
31
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
32
)
33

34
// noResolvedPluginError is returned by subcommands that require a plugin when none was resolved.
35
type noResolvedPluginError struct{}
36

37
// Error implements error interface.
38
func (e noResolvedPluginError) Error() string {
12✔
39
        return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file"
12✔
40
}
12✔
41

42
// noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found.
43
type noAvailablePluginError struct {
44
        subcommand string
45
}
46

47
// Error implements error interface.
48
func (e noAvailablePluginError) Error() string {
4✔
49
        return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand)
4✔
50
}
4✔
51

52
// cmdErr updates a cobra command to output error information when executed
53
// or used with the help flag.
54
func cmdErr(cmd *cobra.Command, err error) {
16✔
55
        cmd.Long = fmt.Sprintf("%s\nNote: %v", cmd.Long, err)
16✔
56
        cmd.RunE = errCmdFunc(err)
16✔
57
}
16✔
58

59
// errCmdFunc returns a cobra RunE function that returns the provided error
60
func errCmdFunc(err error) func(*cobra.Command, []string) error {
47✔
61
        return func(*cobra.Command, []string) error {
48✔
62
                return err
1✔
63
        }
1✔
64
}
65

66
// keySubcommandTuple represents a pairing of the key of a plugin with a plugin.Subcommand.
67
type keySubcommandTuple struct {
68
        key        string
69
        subcommand plugin.Subcommand
70

71
        // skip will be used to flag subcommands that should be skipped after any hook returned a plugin.ExitError.
72
        skip bool
73
}
74

75
// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins.
76
func (c *CLI) filterSubcommands(
77
        filter func(plugin.Plugin) bool,
78
        extract func(plugin.Plugin) plugin.Subcommand,
79
) []keySubcommandTuple {
28✔
80
        // Unbundle plugins
28✔
81
        plugins := make([]plugin.Plugin, 0, len(c.resolvedPlugins))
28✔
82
        for _, p := range c.resolvedPlugins {
56✔
83
                if bundle, isBundle := p.(plugin.Bundle); isBundle {
28✔
84
                        plugins = append(plugins, bundle.Plugins()...)
×
85
                } else {
28✔
86
                        plugins = append(plugins, p)
28✔
87
                }
28✔
88
        }
89

90
        tuples := make([]keySubcommandTuple, 0, len(plugins))
28✔
91
        for _, p := range plugins {
56✔
92
                if filter(p) {
52✔
93
                        tuples = append(tuples, keySubcommandTuple{
24✔
94
                                key:        plugin.KeyFor(p),
24✔
95
                                subcommand: extract(p),
24✔
96
                        })
24✔
97
                }
24✔
98
        }
99
        return tuples
28✔
100
}
101

102
// applySubcommandHooks runs the initialization hooks and configures the commands pre-run,
103
// run, and post-run hooks with the appropriate execution hooks.
104
func (c *CLI) applySubcommandHooks(
105
        cmd *cobra.Command,
106
        subcommands []keySubcommandTuple,
107
        errorMessage string,
108
        createConfig bool,
109
) {
24✔
110
        // In case we create a new project configuration we need to compute the plugin chain.
24✔
111
        pluginChain := make([]string, 0, len(c.resolvedPlugins))
24✔
112
        if createConfig {
30✔
113
                // We extract the plugin keys again instead of using the ones obtained when filtering subcommands
6✔
114
                // as these plugins are unbundled but we want to keep bundle names in the plugin chain.
6✔
115
                for _, p := range c.resolvedPlugins {
12✔
116
                        pluginChain = append(pluginChain, plugin.KeyFor(p))
6✔
117
                }
6✔
118
        }
119

120
        options := initializationHooks(cmd, subcommands, c.metadata())
24✔
121

24✔
122
        factory := executionHooksFactory{
24✔
123
                fs:             c.fs,
24✔
124
                store:          yamlstore.New(c.fs),
24✔
125
                subcommands:    subcommands,
24✔
126
                errorMessage:   errorMessage,
24✔
127
                projectVersion: c.projectVersion,
24✔
128
                pluginChain:    pluginChain,
24✔
129
                cliVersion:     c.cliVersion,
24✔
130
        }
24✔
131
        cmd.PreRunE = factory.preRunEFunc(options, createConfig)
24✔
132
        cmd.RunE = factory.runEFunc()
24✔
133
        cmd.PostRunE = factory.postRunEFunc()
24✔
134
}
135

136
// initializationHooks executes update metadata and bind flags plugin hooks.
137
func initializationHooks(
138
        cmd *cobra.Command,
139
        subcommands []keySubcommandTuple,
140
        meta plugin.CLIMetadata,
141
) *resourceOptions {
24✔
142
        // Update metadata hook.
24✔
143
        subcmdMeta := plugin.SubcommandMetadata{
24✔
144
                Description: cmd.Long,
24✔
145
                Examples:    cmd.Example,
24✔
146
        }
24✔
147
        for _, tuple := range subcommands {
48✔
148
                if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata {
48✔
149
                        subcommand.UpdateMetadata(meta, &subcmdMeta)
24✔
150
                }
24✔
151
        }
152
        cmd.Long = subcmdMeta.Description
24✔
153
        cmd.Example = subcmdMeta.Examples
24✔
154

24✔
155
        // Before binding specific plugin flags, bind common ones.
24✔
156
        requiresResource := false
24✔
157
        for _, tuple := range subcommands {
48✔
158
                if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource {
36✔
159
                        break
12✔
160
                }
161
        }
162
        var options *resourceOptions
24✔
163
        if requiresResource {
36✔
164
                options = bindResourceFlags(cmd.Flags())
12✔
165
        }
12✔
166

167
        // Bind flags hook.
168
        for _, tuple := range subcommands {
48✔
169
                if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags {
48✔
170
                        subcommand.BindFlags(cmd.Flags())
24✔
171
                }
24✔
172
        }
173

174
        return options
24✔
175
}
176

177
type executionHooksFactory struct {
178
        // fs is the filesystem abstraction to scaffold files to.
179
        fs machinery.Filesystem
180
        // store is the backend used to load/save the project configuration.
181
        store store.Store
182
        // subcommands are the tuples representing the set of subcommands provided by the resolved plugins.
183
        subcommands []keySubcommandTuple
184
        // errorMessage is prepended to returned errors.
185
        errorMessage string
186
        // projectVersion is the project version that will be used to create new project configurations.
187
        // It is only used for initialization.
188
        projectVersion config.Version
189
        // pluginChain is the plugin chain configured for this project.
190
        pluginChain []string
191
        // cliVersion is the version of the CLI.
192
        cliVersion string
193
}
194

195
func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error {
×
196
        for i, tuple := range factory.subcommands {
×
197
                if tuple.skip {
×
198
                        continue
×
199
                }
200

201
                err := cb(tuple.subcommand)
×
202

×
203
                var exitError plugin.ExitError
×
204
                switch {
×
205
                case err == nil:
×
206
                        // No error do nothing
207
                case errors.As(err, &exitError):
×
208
                        // Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped
×
209
                        factory.subcommands[i].skip = true
×
210
                        fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason)
×
211
                default:
×
212
                        // Any other error, wrap it
×
213
                        return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err)
×
214
                }
215
        }
216

217
        return nil
×
218
}
219

220
// preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource,
221
// and executes inject config, inject resource, and pre-scaffold hooks.
222
func (factory *executionHooksFactory) preRunEFunc(
223
        options *resourceOptions,
224
        createConfig bool,
225
) func(*cobra.Command, []string) error {
24✔
226
        return func(*cobra.Command, []string) error {
24✔
227
                if createConfig {
×
228
                        // Check if a project configuration is already present.
×
229
                        if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) {
×
230
                                return fmt.Errorf("%s: already initialized", factory.errorMessage)
×
231
                        }
×
232

233
                        // Initialize the project configuration.
234
                        if err := factory.store.New(factory.projectVersion); err != nil {
×
235
                                return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err)
×
236
                        }
×
237
                } else {
×
238
                        // Load the project configuration.
×
239
                        if err := factory.store.Load(); os.IsNotExist(err) {
×
240
                                return fmt.Errorf("%s: unable to find configuration file, project must be initialized",
×
241
                                        factory.errorMessage)
×
242
                        } else if err != nil {
×
243
                                return fmt.Errorf("%s: unable to load configuration file: %w", factory.errorMessage, err)
×
244
                        }
×
245
                }
246
                cfg := factory.store.Config()
×
247

×
248
                // Set the CLI version if creating a new project configuration.
×
249
                if createConfig {
×
250
                        _ = cfg.SetCliVersion(factory.cliVersion)
×
251
                }
×
252

253
                // Set the pluginChain field.
254
                if len(factory.pluginChain) != 0 {
×
255
                        _ = cfg.SetPluginChain(factory.pluginChain)
×
256
                }
×
257

258
                // Create the resource if non-nil options provided
259
                var res *resource.Resource
×
260
                if options != nil {
×
261
                        // TODO: offer a flag instead of hard-coding project-wide domain
×
262
                        options.Domain = cfg.GetDomain()
×
263
                        if err := options.validate(); err != nil {
×
264
                                return fmt.Errorf("%s: unable to create resource: %w", factory.errorMessage, err)
×
265
                        }
×
266
                        res = options.newResource()
×
267
                }
268

269
                // Inject config hook.
270
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
271
                        if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig {
×
272
                                return subcommand.InjectConfig(cfg)
×
273
                        }
×
274
                        return nil
×
275
                }, "unable to inject the configuration to"); err != nil {
×
276
                        return err
×
277
                }
×
278

279
                if res != nil {
×
280
                        // Inject resource hook.
×
281
                        if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
282
                                if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource {
×
283
                                        return subcommand.InjectResource(res)
×
284
                                }
×
285
                                return nil
×
286
                        }, "unable to inject the resource to"); err != nil {
×
287
                                return err
×
288
                        }
×
289

290
                        if err := res.Validate(); err != nil {
×
291
                                return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
×
292
                        }
×
293
                }
294

295
                // Pre-scaffold hook.
296
                //nolint:revive
297
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
298
                        if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold {
×
299
                                return subcommand.PreScaffold(factory.fs)
×
300
                        }
×
301
                        return nil
×
302
                }, "unable to run pre-scaffold tasks of"); err != nil {
×
303
                        return err
×
304
                }
×
305

306
                return nil
×
307
        }
308
}
309

310
// runEFunc returns a cobra RunE function that executes the scaffold hook.
311
func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error {
24✔
312
        return func(*cobra.Command, []string) error {
24✔
313
                // Scaffold hook.
×
314
                //nolint:revive
×
315
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
316
                        return subcommand.Scaffold(factory.fs)
×
317
                }, "unable to scaffold with"); err != nil {
×
318
                        return err
×
319
                }
×
320

321
                return nil
×
322
        }
323
}
324

325
// postRunEFunc returns a cobra RunE function that saves the configuration
326
// and executes the post-scaffold hook.
327
func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error {
24✔
328
        return func(*cobra.Command, []string) error {
24✔
329
                if err := factory.store.Save(); err != nil {
×
330
                        return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err)
×
331
                }
×
332

333
                // Post-scaffold hook.
334
                //nolint:revive
335
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
336
                        if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold {
×
337
                                return subcommand.PostScaffold()
×
338
                        }
×
339
                        return nil
×
340
                }, "unable to run post-scaffold tasks of"); err != nil {
×
341
                        return err
×
342
                }
×
343

344
                return nil
×
345
        }
346
}
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