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

kubernetes-sigs / kubebuilder / 13454291151

21 Feb 2025 09:50AM UTC coverage: 73.061% (-1.0%) from 74.078%
13454291151

Pull #4572

github

sarthaksarthak9
Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins
Pull Request #4572: :sparkles: Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins

1 of 45 new or added lines in 3 files covered. (2.22%)

107 existing lines in 3 files now uncovered.

2308 of 3159 relevant lines covered (73.06%)

14.01 hits per line

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

39.27
/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
        "strings"
24

25
        log "github.com/sirupsen/logrus"
26
        "github.com/spf13/cobra"
27

28
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
29
        "sigs.k8s.io/kubebuilder/v4/pkg/config/store"
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/resource"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
34
)
35

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

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

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

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

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

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

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

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

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

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

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

122
        options := initializationHooks(cmd, subcommands, c.metadata())
24✔
123

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

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

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

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

175
        return options
24✔
176
}
177

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

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

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

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

UNCOV
216
        return nil
×
217
}
218

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

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

×
247
                // Set the pluginChain field.
×
248
                if len(factory.pluginChain) != 0 {
×
UNCOV
249
                        _ = cfg.SetPluginChain(factory.pluginChain)
×
UNCOV
250
                }
×
251

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

263
                // Inject config hook.
264
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
265
                        if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig {
×
266
                                return subcommand.InjectConfig(cfg)
×
267
                        }
×
268
                        return nil
×
269
                }, "unable to inject the configuration to"); err != nil {
×
UNCOV
270
                        return err
×
271
                }
×
272

273
                if res != nil {
×
274
                        // Inject resource hook.
×
275
                        if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
276
                                if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource {
×
277
                                        return subcommand.InjectResource(res)
×
278
                                }
×
279
                                return nil
×
280
                        }, "unable to inject the resource to"); err != nil {
×
UNCOV
281
                                return err
×
282
                        }
×
283

284
                        if err := res.Validate(); err != nil {
×
UNCOV
285
                                return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
×
UNCOV
286
                        }
×
287
                }
288

289
                // Pre-scaffold hook.
290
                //nolint:revive
291
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
292
                        if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold {
×
293
                                return subcommand.PreScaffold(factory.fs)
×
294
                        }
×
295
                        return nil
×
296
                }, "unable to run pre-scaffold tasks of"); err != nil {
×
UNCOV
297
                        return err
×
298
                }
×
299

UNCOV
300
                return nil
×
301
        }
302
}
303

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

UNCOV
315
                return nil
×
316
        }
317
}
318

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

327
                // Post-scaffold hook.
328
                //nolint:revive
329
                if err := factory.forEach(func(subcommand plugin.Subcommand) error {
×
330
                        if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold {
×
331
                                return subcommand.PostScaffold()
×
332
                        }
×
333
                        return nil
×
334
                }, "unable to run post-scaffold tasks of"); err != nil {
×
UNCOV
335
                        return err
×
336
                }
×
337

UNCOV
338
                return nil
×
339
        }
340
}
341

342
// updateProjectFileForAlphaGenerate updates the PROJECT file to replace unsupported
343
// plugins with a supported version before running `kubebuilder alpha generate`.
344

NEW
UNCOV
345
func updateProjectFileForAlphaGenerate() error {
×
NEW
UNCOV
346
        projectFilePath := "PROJECT"
×
NEW
UNCOV
347

×
NEW
UNCOV
348
        content, err := os.ReadFile(projectFilePath)
×
NEW
UNCOV
349
        if err != nil {
×
NEW
UNCOV
350
                return fmt.Errorf("failed to read PROJECT file: %w", err)
×
NEW
UNCOV
351
        }
×
352

NEW
UNCOV
353
        projectStr := string(content)
×
NEW
UNCOV
354

×
NEW
UNCOV
355
        // No need to update if v3 is not found
×
NEW
UNCOV
356
        if !strings.Contains(projectStr, "go.kubebuilder.io/v3") {
×
NEW
UNCOV
357
                return nil 
×
NEW
UNCOV
358
        }
×
359

NEW
UNCOV
360
        log.Warnf("Detected 'go.kubebuilder.io/v3' in PROJECT file.")
×
NEW
UNCOV
361
        log.Warnf("Kubebuilder v4 no longer supports this. It will be replaced with 'go.kubebuilder.io/v4'.")
×
NEW
UNCOV
362

×
NEW
UNCOV
363
        fmt.Print("Do you want to proceed? (y/N): ")
×
NEW
UNCOV
364

×
NEW
UNCOV
365
        var response string
×
NEW
UNCOV
366
        fmt.Scanln(&response)
×
NEW
UNCOV
367
        response = strings.TrimSpace(strings.ToLower(response))
×
NEW
UNCOV
368

×
NEW
UNCOV
369
        if response != "y" {
×
NEW
UNCOV
370
                log.Warnf("Aborting kubebuilder alpha generate.")
×
NEW
UNCOV
371
                return fmt.Errorf("user aborted operation")
×
NEW
UNCOV
372
        }
×
373

NEW
UNCOV
374
        updatedProjectStr := strings.ReplaceAll(projectStr, "go.kubebuilder.io/v3", "go.kubebuilder.io/v4")
×
NEW
UNCOV
375

×
NEW
UNCOV
376
        err = os.WriteFile(projectFilePath, []byte(updatedProjectStr), 0644)
×
NEW
UNCOV
377
        if err != nil {
×
NEW
UNCOV
378
                return fmt.Errorf("failed to update PROJECT file: %w", err)
×
NEW
UNCOV
379
        }
×
380

NEW
UNCOV
381
        log.Infof("PROJECT file successfully updated. Proceeding with kubebuilder alpha generate.")
×
NEW
UNCOV
382

×
NEW
UNCOV
383
        return nil
×
384
}
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