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

kubernetes-sigs / kubebuilder / 13900935872

17 Mar 2025 01:39PM UTC coverage: 72.986%. First build
13900935872

Pull #4572

github

web-flow
Update and rename test-alpha-generate.yaml to alphagenerate.yaml
Pull Request #4572: :sparkles: Ensure 'kubebuilder alpha generate' rescaffolds with outdated plugins

2 of 49 new or added lines in 3 files covered. (4.08%)

2310 of 3165 relevant lines covered (72.99%)

13.98 hits per line

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

40.57
/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✔
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 {
×
196
                if tuple.skip {
×
197
                        continue
×
198
                }
199

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

×
202
                var exitError plugin.ExitError
×
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:
×
211
                        // Any other error, wrap it
×
212
                        return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err)
×
213
                }
214
        }
215

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) {
×
229
                                return fmt.Errorf("%s: already initialized", factory.errorMessage)
×
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 {
×
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 {
×
249
                        _ = cfg.SetPluginChain(factory.pluginChain)
×
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)
×
259
                        }
×
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 {
×
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 {
×
281
                                return err
×
282
                        }
×
283

284
                        if err := res.Validate(); err != nil {
×
285
                                return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err)
×
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 {
×
297
                        return err
×
298
                }
×
299

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 {
×
312
                        return err
×
313
                }
×
314

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 {
×
324
                        return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err)
×
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 {
×
335
                        return err
×
336
                }
×
337

338
                return nil
×
339
        }
340
}
341

NEW
342
func updateProjectFileForAlphaGenerate() error {
×
NEW
343
        projectFilePath := "PROJECT"
×
NEW
344

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

NEW
350
        projectStr := string(content)
×
NEW
351

×
NEW
352
        // Define outdated plugin versions that need replacement
×
NEW
353
        outdatedPlugins := []string{"go.kubebuilder.io/v3", "go.kubebuilder.io/v3-alpha", "go.kubebuilder.io/v2"}
×
NEW
354
        updated := false
×
NEW
355

×
NEW
356
        for _, oldPlugin := range outdatedPlugins {
×
NEW
357
                if strings.Contains(projectStr, oldPlugin) {
×
NEW
358
                        log.Warnf("Detected '%s' in PROJECT file.", oldPlugin)
×
NEW
359
                        log.Warnf("Kubebuilder v4 no longer supports this. It will be replaced with 'go.kubebuilder.io/v4'.")
×
NEW
360

×
NEW
361
                        projectStr = strings.ReplaceAll(projectStr, oldPlugin, "go.kubebuilder.io/v4")
×
NEW
362
                        updated = true
×
NEW
363
                        break
×
364
                }
365
        }
366

367
        // Only update the file if changes were made
NEW
368
        if updated {
×
NEW
369
                err = os.WriteFile(projectFilePath, []byte(projectStr), 0644)
×
NEW
370
                if err != nil {
×
NEW
371
                        return fmt.Errorf("failed to update PROJECT file: %w", err)
×
NEW
372
                }
×
NEW
373
                log.Infof("PROJECT file updated successfully.")
×
374
        }
375

NEW
376
        return nil
×
377
}
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