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

kubernetes-sigs / kubebuilder / 19029306515

03 Nov 2025 09:08AM UTC coverage: 66.088% (-0.8%) from 66.847%
19029306515

push

github

web-flow
Merge pull request #5165 from camilamacedo86/make-cf-external

✨ (feat): Expose ProjectConfig to external plugins

8 of 10 new or added lines in 1 file covered. (80.0%)

59 existing lines in 1 file now uncovered.

3352 of 5072 relevant lines covered (66.09%)

39.42 hits per line

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

36.72
/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 {
14✔
39
        return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file"
14✔
40
}
14✔
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 {
6✔
49
        return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand)
6✔
50
}
6✔
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) {
17✔
55
        cmd.Long = fmt.Sprintf("%s\nNote: %v", cmd.Long, err)
17✔
56
        cmd.RunE = errCmdFunc(err)
17✔
57
}
17✔
58

59
// errCmdFunc returns a cobra RunE function that returns the provided error
60
func errCmdFunc(err error) func(*cobra.Command, []string) error {
49✔
61
        return func(*cobra.Command, []string) error {
52✔
62
                return err
3✔
63
        }
3✔
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
type pluginChainSetter interface {
76
        SetPluginChain([]string)
77
}
78

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

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

106
// applySubcommandHooks runs the initialization hooks and configures the commands pre-run,
107
// run, and post-run hooks with the appropriate execution hooks.
108
func (c *CLI) applySubcommandHooks(
UNCOV
109
        cmd *cobra.Command,
×
UNCOV
110
        subcommands []keySubcommandTuple,
×
UNCOV
111
        errorMessage string,
×
UNCOV
112
        createConfig bool,
×
113
) {
26✔
114
        commandPluginChain := make([]string, len(subcommands))
26✔
115
        for i, tuple := range subcommands {
54✔
116
                commandPluginChain[i] = tuple.key
28✔
117
        }
28✔
118
        for _, tuple := range subcommands {
54✔
119
                if setter, ok := tuple.subcommand.(pluginChainSetter); ok {
32✔
120
                        setter.SetPluginChain(commandPluginChain)
4✔
121
                }
4✔
NEW
122
        }
×
NEW
123

×
UNCOV
124
        // In case we create a new project configuration we need to compute the plugin chain.
×
125
        pluginChain := make([]string, 0, len(c.resolvedPlugins))
26✔
126
        if createConfig {
33✔
127
                // We extract the plugin keys again instead of using the ones obtained when filtering subcommands
7✔
128
                // as these plugins are unbundled but we want to keep bundle names in the plugin chain.
7✔
129
                for _, p := range c.resolvedPlugins {
15✔
130
                        pluginChain = append(pluginChain, plugin.KeyFor(p))
8✔
131
                }
8✔
UNCOV
132
        }
×
UNCOV
133

×
134
        options := initializationHooks(cmd, subcommands, c.metadata())
26✔
135

26✔
136
        factory := executionHooksFactory{
26✔
137
                fs:             c.fs,
26✔
138
                store:          yamlstore.New(c.fs),
26✔
139
                subcommands:    subcommands,
26✔
140
                errorMessage:   errorMessage,
26✔
141
                projectVersion: c.projectVersion,
26✔
142
                pluginChain:    pluginChain,
26✔
143
                cliVersion:     c.cliVersion,
26✔
144
        }
26✔
145
        cmd.PreRunE = factory.preRunEFunc(options, createConfig)
26✔
146
        cmd.RunE = factory.runEFunc()
26✔
147
        cmd.PostRunE = factory.postRunEFunc()
26✔
UNCOV
148
}
×
UNCOV
149

×
UNCOV
150
// initializationHooks executes update metadata and bind flags plugin hooks.
×
151
func initializationHooks(
UNCOV
152
        cmd *cobra.Command,
×
UNCOV
153
        subcommands []keySubcommandTuple,
×
UNCOV
154
        meta plugin.CLIMetadata,
×
155
) *resourceOptions {
26✔
156
        // Update metadata hook.
26✔
157
        subcmdMeta := plugin.SubcommandMetadata{
26✔
158
                Description: cmd.Long,
26✔
159
                Examples:    cmd.Example,
26✔
160
        }
26✔
161
        for _, tuple := range subcommands {
54✔
162
                if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata {
52✔
163
                        subcommand.UpdateMetadata(meta, &subcmdMeta)
24✔
164
                }
24✔
UNCOV
165
        }
×
166
        cmd.Long = subcmdMeta.Description
26✔
167
        cmd.Example = subcmdMeta.Examples
26✔
168

26✔
169
        // Before binding specific plugin flags, bind common ones.
26✔
170
        requiresResource := false
26✔
171
        for _, tuple := range subcommands {
54✔
172
                if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource {
40✔
173
                        break
12✔
UNCOV
174
                }
×
175
        }
176
        var options *resourceOptions
26✔
177
        if requiresResource {
38✔
178
                options = bindResourceFlags(cmd.Flags())
12✔
179
        }
12✔
180

181
        // Bind flags hook.
182
        for _, tuple := range subcommands {
54✔
183
                if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags {
52✔
184
                        subcommand.BindFlags(cmd.Flags())
24✔
185
                }
24✔
186
        }
187

188
        return options
26✔
189
}
190

191
type executionHooksFactory struct {
192
        // fs is the filesystem abstraction to scaffold files to.
193
        fs machinery.Filesystem
194
        // store is the backend used to load/save the project configuration.
UNCOV
195
        store store.Store
×
UNCOV
196
        // subcommands are the tuples representing the set of subcommands provided by the resolved plugins.
×
UNCOV
197
        subcommands []keySubcommandTuple
×
UNCOV
198
        // errorMessage is prepended to returned errors.
×
199
        errorMessage string
200
        // projectVersion is the project version that will be used to create new project configurations.
UNCOV
201
        // It is only used for initialization.
×
UNCOV
202
        projectVersion config.Version
×
UNCOV
203
        // pluginChain is the plugin chain configured for this project.
×
UNCOV
204
        pluginChain []string
×
UNCOV
205
        // cliVersion is the version of the CLI.
×
206
        cliVersion string
UNCOV
207
}
×
UNCOV
208

×
209
func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error {
×
210
        for i, tuple := range factory.subcommands {
×
211
                if tuple.skip {
×
212
                        continue
×
UNCOV
213
                }
×
214

215
                err := cb(tuple.subcommand)
×
216

×
217
                var exitError plugin.ExitError
×
218
                switch {
×
219
                case err == nil:
×
220
                        // No error do nothing
221
                case errors.As(err, &exitError):
×
222
                        // Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped
×
223
                        factory.subcommands[i].skip = true
×
224
                        fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason)
×
225
                default:
×
226
                        // Any other error, wrap it
×
227
                        return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err)
×
UNCOV
228
                }
×
UNCOV
229
        }
×
UNCOV
230

×
231
        return nil
×
232
}
233

UNCOV
234
// preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource,
×
UNCOV
235
// and executes inject config, inject resource, and pre-scaffold hooks.
×
UNCOV
236
func (factory *executionHooksFactory) preRunEFunc(
×
UNCOV
237
        options *resourceOptions,
×
UNCOV
238
        createConfig bool,
×
239
) func(*cobra.Command, []string) error {
26✔
240
        return func(*cobra.Command, []string) error {
26✔
241
                if createConfig {
×
242
                        // Check if a project configuration is already present.
×
243
                        if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) {
×
244
                                return fmt.Errorf("%s: already initialized", factory.errorMessage)
×
245
                        }
×
UNCOV
246

×
UNCOV
247
                        // Initialize the project configuration.
×
248
                        if err := factory.store.New(factory.projectVersion); err != nil {
×
249
                                return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err)
×
250
                        }
×
251
                } else {
×
252
                        // Load the project configuration.
×
253
                        if err := factory.store.Load(); os.IsNotExist(err) {
×
254
                                return fmt.Errorf("%s: unable to find configuration file, project must be initialized",
×
255
                                        factory.errorMessage)
×
256
                        } else if err != nil {
×
257
                                return fmt.Errorf("%s: unable to load configuration file: %w", factory.errorMessage, err)
×
258
                        }
×
UNCOV
259
                }
×
260
                cfg := factory.store.Config()
×
261

×
262
                // Set the CLI version if creating a new project configuration.
×
263
                if createConfig {
×
264
                        _ = cfg.SetCliVersion(factory.cliVersion)
×
265
                }
×
UNCOV
266

×
267
                // Set the pluginChain field.
268
                if len(factory.pluginChain) != 0 {
×
269
                        _ = cfg.SetPluginChain(factory.pluginChain)
×
270
                }
×
UNCOV
271

×
UNCOV
272
                // Create the resource if non-nil options provided
×
273
                var res *resource.Resource
×
274
                if options != nil {
×
275
                        // TODO: offer a flag instead of hard-coding project-wide domain
×
276
                        options.Domain = cfg.GetDomain()
×
277
                        if err := options.validate(); err != nil {
×
278
                                return fmt.Errorf("%s: unable to create resource: %w", factory.errorMessage, err)
×
279
                        }
×
280
                        res = options.newResource()
×
UNCOV
281
                }
×
UNCOV
282

×
UNCOV
283
                // Inject config hook.
×
284
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
285
                        if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig {
×
286
                                return subcommand.InjectConfig(cfg)
×
287
                        }
×
288
                        return nil
×
289
                }, "unable to inject the configuration to"); err != nil {
×
290
                        return err
×
291
                }
×
UNCOV
292

×
293
                if res != nil {
×
294
                        // Inject resource hook.
×
295
                        if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
296
                                if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource {
×
297
                                        return subcommand.InjectResource(res)
×
298
                                }
×
299
                                return nil
×
300
                        }, "unable to inject the resource to"); err != nil {
×
301
                                return err
×
302
                        }
×
UNCOV
303

×
304
                        if err := res.Validate(); err != nil {
×
305
                                return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
×
306
                        }
×
307
                }
308

309
                // Pre-scaffold hook.
310
                //nolint:revive
311
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
312
                        if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold {
×
313
                                return subcommand.PreScaffold(factory.fs)
×
314
                        }
×
315
                        return nil
×
316
                }, "unable to run pre-scaffold tasks of"); err != nil {
×
317
                        return err
×
318
                }
×
UNCOV
319

×
320
                return nil
×
UNCOV
321
        }
×
322
}
323

324
// runEFunc returns a cobra RunE function that executes the scaffold hook.
325
func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error {
26✔
326
        return func(*cobra.Command, []string) error {
26✔
327
                // Scaffold hook.
×
328
                //nolint:revive
×
329
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
330
                        return subcommand.Scaffold(factory.fs)
×
331
                }, "unable to scaffold with"); err != nil {
×
332
                        return err
×
333
                }
×
334

335
                return nil
×
UNCOV
336
        }
×
UNCOV
337
}
×
UNCOV
338

×
UNCOV
339
// postRunEFunc returns a cobra RunE function that saves the configuration
×
UNCOV
340
// and executes the post-scaffold hook.
×
341
func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error {
26✔
342
        return func(*cobra.Command, []string) error {
26✔
343
                if err := factory.store.Save(); err != nil {
×
344
                        return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err)
×
345
                }
×
346

347
                // Post-scaffold hook.
348
                //nolint:revive
349
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
350
                        if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold {
×
351
                                return subcommand.PostScaffold()
×
352
                        }
×
353
                        return nil
×
354
                }, "unable to run post-scaffold tasks of"); err != nil {
×
355
                        return err
×
356
                }
×
357

358
                return nil
×
359
        }
360
}
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