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

kubernetes-sigs / kubebuilder / 19031748799

03 Nov 2025 10:41AM UTC coverage: 69.632% (-0.2%) from 69.807%
19031748799

push

github

web-flow
Merge pull request #5166 from camilamacedo86/fix-domain

🐛 (fix): Fix plugin configuration tracking when wrapped in bundles with custom domains

120 of 219 new or added lines in 3 files covered. (54.79%)

36 existing lines in 3 files now uncovered.

3726 of 5351 relevant lines covered (69.63%)

38.13 hits per line

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

45.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 {
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 pairs a plugin key with its subcommand.
67
// key is the plugin's own key, configKey is the bundle key (if wrapped in a bundle).
68
type keySubcommandTuple struct {
69
        key        string
70
        configKey  string
71
        subcommand plugin.Subcommand
72

73
        // skip marks subcommands that should be skipped after a plugin.ExitError.
74
        skip bool
75
}
76

77
type pluginChainSetter interface {
78
        SetPluginChain([]string)
UNCOV
79
}
×
UNCOV
80

×
NEW
81
// filterSubcommands returns plugin keys and subcommands from resolved plugins.
×
82
func (c *CLI) filterSubcommands(
×
83
        filter func(plugin.Plugin) bool,
×
84
        extract func(plugin.Plugin) plugin.Subcommand,
×
85
) []keySubcommandTuple {
29✔
86
        tuples := make([]keySubcommandTuple, 0, len(c.resolvedPlugins))
29✔
87
        for _, p := range c.resolvedPlugins {
59✔
88
                tuples = append(tuples, collectSubcommands(p, plugin.KeyFor(p), filter, extract)...)
30✔
89
        }
30✔
90
        return tuples
29✔
NEW
91
}
×
UNCOV
92

×
NEW
93
func collectSubcommands(
×
NEW
94
        p plugin.Plugin,
×
NEW
95
        configKey string,
×
NEW
96
        filter func(plugin.Plugin) bool,
×
NEW
97
        extract func(plugin.Plugin) plugin.Subcommand,
×
98
) []keySubcommandTuple {
32✔
99
        if bundle, isBundle := p.(plugin.Bundle); isBundle {
34✔
100
                collected := make([]keySubcommandTuple, 0, len(bundle.Plugins()))
2✔
101
                for _, nested := range bundle.Plugins() {
4✔
102
                        collected = append(collected, collectSubcommands(nested, configKey, filter, extract)...)
2✔
103
                }
2✔
104
                return collected
2✔
105
        }
106

107
        if !filter(p) {
34✔
108
                return nil
4✔
109
        }
4✔
NEW
110

×
111
        return []keySubcommandTuple{{
26✔
112
                key:        plugin.KeyFor(p),
26✔
113
                configKey:  configKey,
26✔
114
                subcommand: extract(p),
26✔
115
        }}
26✔
UNCOV
116
}
×
UNCOV
117

×
118
// applySubcommandHooks runs the initialization hooks and configures the commands pre-run,
119
// run, and post-run hooks with the appropriate execution hooks.
UNCOV
120
func (c *CLI) applySubcommandHooks(
×
121
        cmd *cobra.Command,
×
122
        subcommands []keySubcommandTuple,
×
123
        errorMessage string,
×
124
        createConfig bool,
×
125
) {
26✔
126
        commandPluginChain := make([]string, len(subcommands))
26✔
127
        for i, tuple := range subcommands {
54✔
128
                commandPluginChain[i] = tuple.key
28✔
129
        }
28✔
130
        for _, tuple := range subcommands {
54✔
131
                if setter, ok := tuple.subcommand.(pluginChainSetter); ok {
32✔
132
                        setter.SetPluginChain(commandPluginChain)
4✔
133
                }
4✔
134
        }
135

136
        // In case we create a new project configuration we need to compute the plugin chain.
137
        pluginChain := make([]string, 0, len(c.resolvedPlugins))
26✔
138
        if createConfig {
33✔
139
                // We extract the plugin keys again instead of using the ones obtained when filtering subcommands
7✔
140
                // as these plugins are unbundled but we want to keep bundle names in the plugin chain.
7✔
141
                for _, p := range c.resolvedPlugins {
15✔
142
                        pluginChain = append(pluginChain, plugin.KeyFor(p))
8✔
143
                }
8✔
144
        }
×
145

×
146
        options := initializationHooks(cmd, subcommands, c.metadata())
26✔
147

26✔
148
        factory := executionHooksFactory{
26✔
149
                fs:             c.fs,
26✔
150
                store:          yamlstore.New(c.fs),
26✔
151
                subcommands:    subcommands,
26✔
152
                errorMessage:   errorMessage,
26✔
153
                projectVersion: c.projectVersion,
26✔
154
                pluginChain:    pluginChain,
26✔
155
                cliVersion:     c.cliVersion,
26✔
156
        }
26✔
157
        cmd.PreRunE = factory.preRunEFunc(options, createConfig)
26✔
158
        cmd.RunE = factory.runEFunc()
26✔
159
        cmd.PostRunE = factory.postRunEFunc()
26✔
160
}
161

162
// initializationHooks executes update metadata and bind flags plugin hooks.
×
UNCOV
163
func initializationHooks(
×
164
        cmd *cobra.Command,
×
165
        subcommands []keySubcommandTuple,
×
166
        meta plugin.CLIMetadata,
167
) *resourceOptions {
26✔
168
        // Update metadata hook.
26✔
169
        subcmdMeta := plugin.SubcommandMetadata{
26✔
170
                Description: cmd.Long,
26✔
171
                Examples:    cmd.Example,
26✔
172
        }
26✔
173
        for _, tuple := range subcommands {
54✔
174
                if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata {
52✔
175
                        subcommand.UpdateMetadata(meta, &subcmdMeta)
24✔
176
                }
24✔
177
        }
178
        cmd.Long = subcmdMeta.Description
26✔
179
        cmd.Example = subcmdMeta.Examples
26✔
180

26✔
181
        // Before binding specific plugin flags, bind common ones.
26✔
182
        requiresResource := false
26✔
183
        for _, tuple := range subcommands {
54✔
184
                if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource {
40✔
185
                        break
12✔
186
                }
187
        }
188
        var options *resourceOptions
26✔
189
        if requiresResource {
38✔
190
                options = bindResourceFlags(cmd.Flags())
12✔
191
        }
12✔
192

193
        // Bind flags hook.
194
        for _, tuple := range subcommands {
54✔
195
                if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags {
52✔
196
                        subcommand.BindFlags(cmd.Flags())
24✔
197
                }
24✔
UNCOV
198
        }
×
199

200
        return options
26✔
UNCOV
201
}
×
UNCOV
202

×
UNCOV
203
type executionHooksFactory struct {
×
UNCOV
204
        // fs is the filesystem abstraction to scaffold files to.
×
UNCOV
205
        fs machinery.Filesystem
×
206
        // store is the backend used to load/save the project configuration.
207
        store store.Store
×
208
        // subcommands are the tuples representing the set of subcommands provided by the resolved plugins.
×
209
        subcommands []keySubcommandTuple
×
210
        // errorMessage is prepended to returned errors.
×
UNCOV
211
        errorMessage string
×
UNCOV
212
        // projectVersion is the project version that will be used to create new project configurations.
×
213
        // It is only used for initialization.
×
214
        projectVersion config.Version
215
        // pluginChain is the plugin chain configured for this project.
216
        pluginChain []string
217
        // cliVersion is the version of the CLI.
×
218
        cliVersion string
219
}
220

221
func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error {
1✔
222
        for i, tuple := range factory.subcommands {
3✔
223
                if tuple.skip {
2✔
224
                        continue
×
225
                }
×
UNCOV
226

×
227
                err := factory.withPluginChain(tuple, func() error {
4✔
228
                        return cb(tuple.subcommand)
2✔
229
                })
2✔
230

×
231
                var exitError plugin.ExitError
2✔
232
                switch {
2✔
233
                case err == nil:
2✔
UNCOV
234
                        // No error do nothing
×
235
                case errors.As(err, &exitError):
×
236
                        // Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped
×
237
                        factory.subcommands[i].skip = true
×
238
                        fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason)
×
239
                default:
×
240
                        // Any other error, wrap it
×
241
                        return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err)
×
242
                }
×
243
        }
×
244

×
245
        return nil
1✔
UNCOV
246
}
×
UNCOV
247

×
248
func (factory *executionHooksFactory) withPluginChain(tuple keySubcommandTuple, cb func() error) (err error) {
2✔
249
        if tuple.configKey == "" {
2✔
NEW
250
                return cb()
×
NEW
251
        }
×
252

253
        cfg := factory.store.Config()
2✔
254
        if cfg == nil {
2✔
NEW
255
                return cb()
×
NEW
256
        }
×
257

258
        // Temporarily move configKey to the front so GetPluginKeyForConfig finds it first.
NEW
259
        // This ensures each bundled plugin saves config under the right key.
×
260
        original := append([]string(nil), cfg.GetPluginChain()...)
2✔
261
        newChain := moveKeyToFront(original, tuple.configKey)
2✔
262
        changed := !equalStringSlices(original, newChain)
2✔
263
        if changed {
3✔
264
                if setErr := cfg.SetPluginChain(newChain); setErr != nil {
1✔
NEW
265
                        return fmt.Errorf("unable to set plugin chain for %q: %w", tuple.configKey, setErr)
×
NEW
266
                }
×
267
                defer func() {
2✔
268
                        if resetErr := cfg.SetPluginChain(original); resetErr != nil && err == nil {
1✔
NEW
269
                                err = fmt.Errorf("unable to reset plugin chain: %w", resetErr)
×
NEW
270
                        }
×
NEW
271
                }()
×
NEW
272
        }
×
NEW
273

×
274
        return cb()
2✔
NEW
275
}
×
NEW
276

×
277
func moveKeyToFront(chain []string, key string) []string {
2✔
278
        if len(chain) == 0 {
2✔
NEW
279
                return []string{key}
×
NEW
280
        }
×
NEW
281

×
282
        if chain[0] == key {
3✔
283
                return chain
1✔
284
        }
1✔
NEW
285

×
286
        newChain := make([]string, 0, len(chain)+1)
1✔
287
        newChain = append(newChain, key)
1✔
288
        for _, existing := range chain {
3✔
289
                if existing == key {
3✔
290
                        continue
1✔
NEW
291
                }
×
292
                newChain = append(newChain, existing)
1✔
293
        }
294
        return newChain
1✔
295
}
296

297
func equalStringSlices(a, b []string) bool {
2✔
298
        if len(a) != len(b) {
2✔
NEW
299
                return false
×
NEW
300
        }
×
301
        for i := range a {
5✔
302
                if a[i] != b[i] {
4✔
303
                        return false
1✔
304
                }
1✔
305
        }
306
        return true
1✔
307
}
308

309
// preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource,
310
// and executes inject config, inject resource, and pre-scaffold hooks.
311
func (factory *executionHooksFactory) preRunEFunc(
×
312
        options *resourceOptions,
×
313
        createConfig bool,
×
314
) func(*cobra.Command, []string) error {
26✔
315
        return func(*cobra.Command, []string) error {
26✔
316
                if createConfig {
×
317
                        // Check if a project configuration is already present.
×
318
                        if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) {
×
319
                                return fmt.Errorf("%s: already initialized", factory.errorMessage)
×
320
                        }
×
321

×
322
                        // Initialize the project configuration.
323
                        if err := factory.store.New(factory.projectVersion); err != nil {
×
324
                                return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err)
×
325
                        }
×
326
                } else {
×
327
                        // Load the project configuration.
×
328
                        if err := factory.store.Load(); os.IsNotExist(err) {
×
329
                                return fmt.Errorf("%s: unable to find configuration file, project must be initialized",
×
330
                                        factory.errorMessage)
×
331
                        } else if err != nil {
×
332
                                return fmt.Errorf("%s: unable to load configuration file: %w", factory.errorMessage, err)
×
333
                        }
×
334
                }
335
                cfg := factory.store.Config()
×
336

×
337
                // Set the CLI version if creating a new project configuration.
×
338
                if createConfig {
×
339
                        _ = cfg.SetCliVersion(factory.cliVersion)
×
340
                }
×
341

×
UNCOV
342
                // Set the pluginChain field.
×
343
                if len(factory.pluginChain) != 0 {
×
344
                        _ = cfg.SetPluginChain(factory.pluginChain)
×
345
                }
×
346

347
                // Create the resource if non-nil options provided
348
                var res *resource.Resource
×
349
                if options != nil {
×
350
                        // TODO: offer a flag instead of hard-coding project-wide domain
×
351
                        options.Domain = cfg.GetDomain()
×
352
                        if err := options.validate(); err != nil {
×
353
                                return fmt.Errorf("%s: unable to create resource: %w", factory.errorMessage, err)
×
354
                        }
×
355
                        res = options.newResource()
×
356
                }
357

358
                // Inject config hook.
359
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
360
                        if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig {
×
361
                                return subcommand.InjectConfig(cfg)
×
362
                        }
×
363
                        return nil
×
364
                }, "unable to inject the configuration to"); err != nil {
×
365
                        return err
×
366
                }
×
367

368
                if res != nil {
×
369
                        // Inject resource hook.
×
370
                        if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
371
                                if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource {
×
372
                                        return subcommand.InjectResource(res)
×
373
                                }
×
374
                                return nil
×
375
                        }, "unable to inject the resource to"); err != nil {
×
376
                                return err
×
377
                        }
×
378

379
                        if err := res.Validate(); err != nil {
×
380
                                return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
×
381
                        }
×
382
                }
383

384
                // Pre-scaffold hook.
385
                //nolint:revive
386
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
387
                        if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold {
×
388
                                return subcommand.PreScaffold(factory.fs)
×
389
                        }
×
390
                        return nil
×
391
                }, "unable to run pre-scaffold tasks of"); err != nil {
×
392
                        return err
×
393
                }
×
394

395
                return nil
×
396
        }
397
}
398

399
// runEFunc returns a cobra RunE function that executes the scaffold hook.
400
func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error {
26✔
401
        return func(*cobra.Command, []string) error {
26✔
402
                // Scaffold hook.
×
403
                //nolint:revive
×
404
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
405
                        return subcommand.Scaffold(factory.fs)
×
406
                }, "unable to scaffold with"); err != nil {
×
407
                        return err
×
408
                }
×
409

410
                return nil
×
411
        }
412
}
413

414
// postRunEFunc returns a cobra RunE function that saves the configuration
415
// and executes the post-scaffold hook.
416
func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error {
26✔
417
        return func(*cobra.Command, []string) error {
26✔
418
                if err := factory.store.Save(); err != nil {
×
419
                        return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err)
×
420
                }
×
421

422
                // Post-scaffold hook.
423
                //nolint:revive
424
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
425
                        if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold {
×
426
                                return subcommand.PostScaffold()
×
427
                        }
×
428
                        return nil
×
429
                }, "unable to run post-scaffold tasks of"); err != nil {
×
430
                        return err
×
431
                }
×
432

433
                return nil
×
434
        }
435
}
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