• 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

0.0
/pkg/cli/alpha/internal/generate.go
1
/*
2
Copyright 2023 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 internal
18

19
import (
20
        "errors"
21
        "fmt"
22
        "os"
23
        "os/exec"
24
        "strings"
25

26
        log "github.com/sirupsen/logrus"
27

28
        "sigs.k8s.io/kubebuilder/v4/pkg/cli/alpha/internal/common"
29
        "sigs.k8s.io/kubebuilder/v4/pkg/config"
30
        "sigs.k8s.io/kubebuilder/v4/pkg/config/store"
31
        "sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
32
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin"
33
        "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
34
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
35
        "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha"
36
        hemlv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha"
37
)
38

39
// Generate store the required info for the command
40
type Generate struct {
41
        InputDir  string
42
        OutputDir string
43
}
44

45
// Generate handles the migration and scaffolding process.
46
func (opts *Generate) Generate() error {
×
47
        projectConfig, err := common.LoadProjectConfig(opts.InputDir)
×
48
        if err != nil {
×
49
                return fmt.Errorf("error loading project config: %v", err)
×
50
        }
×
51

52
        if opts.OutputDir == "" {
×
53
                cwd, getWdErr := os.Getwd()
×
54
                if getWdErr != nil {
×
55
                        return fmt.Errorf("failed to get working directory: %w", getWdErr)
×
56
                }
×
57
                opts.OutputDir = cwd
×
58
                if _, err = os.Stat(opts.OutputDir); err == nil {
×
59
                        log.Warn("Using current working directory to re-scaffold the project")
×
60
                        log.Warn("This directory will be cleaned up and all files removed before the re-generation")
×
61

×
62
                        // Ensure we clean the correct directory
×
63
                        log.Info("Cleaning directory:", opts.OutputDir)
×
64

×
65
                        // Use an absolute path to target files directly
×
66
                        cleanupCmd := fmt.Sprintf("rm -rf %s/*", opts.OutputDir)
×
67
                        err = util.RunCmd("Running cleanup", "sh", "-c", cleanupCmd)
×
68
                        if err != nil {
×
69
                                log.Error("Cleanup failed:", err)
×
70
                                return fmt.Errorf("cleanup failed: %w", err)
×
71
                        }
×
72

73
                        // Note that we should remove ALL files except the PROJECT file and .git directory
74
                        cleanupCmd = fmt.Sprintf(
×
75
                                `find %q -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} +`,
×
76
                                opts.OutputDir,
×
77
                        )
×
78
                        err = util.RunCmd("Running cleanup", "sh", "-c", cleanupCmd)
×
79
                        if err != nil {
×
80
                                log.Error("Cleanup failed:", err)
×
81
                                return fmt.Errorf("cleanup failed: %w", err)
×
82
                        }
×
83
                }
84
        }
85

86
        if err = createDirectory(opts.OutputDir); err != nil {
×
87
                return fmt.Errorf("error creating output directory %q: %w", opts.OutputDir, err)
×
88
        }
×
89

90
        if err = changeWorkingDirectory(opts.OutputDir); err != nil {
×
91
                return fmt.Errorf("error changing working directory %q: %w", opts.OutputDir, err)
×
92
        }
×
93

94
        if err = kubebuilderInit(projectConfig); err != nil {
×
95
                return fmt.Errorf("error initializing project config: %w", err)
×
96
        }
×
97

98
        if err = kubebuilderEdit(projectConfig); err != nil {
×
99
                return fmt.Errorf("error editing project config: %w", err)
×
100
        }
×
101

102
        if err = kubebuilderCreate(projectConfig); err != nil {
×
103
                return fmt.Errorf("error creating project config: %w", err)
×
104
        }
×
105

106
        if err = migrateGrafanaPlugin(projectConfig, opts.InputDir, opts.OutputDir); err != nil {
×
107
                return fmt.Errorf("error migrating Grafana plugin: %w", err)
×
108
        }
×
109

110
        if hasHelmPlugin(projectConfig) {
×
111
                if err = kubebuilderHelmEdit(); err != nil {
×
112
                        return fmt.Errorf("error editing Helm plugin: %w", err)
×
113
                }
×
114
        }
115

116
        if err = migrateDeployImagePlugin(projectConfig); err != nil {
×
117
                return fmt.Errorf("error migrating deploy-image plugin: %w", err)
×
118
        }
×
119

120
        // Run make targets to ensure the project is properly set up.
121
        // These steps are performed on a best-effort basis: if any of the targets fail,
122
        // we log a warning to inform the user, but we do not stop the process or return an error.
123
        // This is to avoid blocking the migration flow due to non-critical issues during setup.
124
        targets := []string{"manifests", "generate", "fmt", "vet", "lint-fix"}
×
125
        for _, target := range targets {
×
126
                log.Infof("Running: make %s", target)
×
127
                err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target)
×
128
                if err != nil {
×
129
                        log.Warnf("make %s failed: %v", target, err)
×
130
                }
×
131
        }
132

133
        return nil
×
134
}
135

136
// Validate ensures the options are valid and kubebuilder is installed.
137
func (opts *Generate) Validate() error {
×
138
        var err error
×
139
        opts.InputDir, err = common.GetInputPath(opts.InputDir)
×
140
        if err != nil {
×
141
                return fmt.Errorf("error getting input path %q: %w", opts.InputDir, err)
×
142
        }
×
143

144
        _, err = exec.LookPath("kubebuilder")
×
145
        if err != nil {
×
146
                return fmt.Errorf("kubebuilder not found in the path: %w", err)
×
147
        }
×
148

149
        return nil
×
150
}
151

152
// Helper function to create the output directory.
153
func createDirectory(outputDir string) error {
×
154
        if err := os.MkdirAll(outputDir, 0o755); err != nil {
×
155
                return fmt.Errorf("failed to create output directory %q: %w", outputDir, err)
×
156
        }
×
157
        return nil
×
158
}
159

160
// Helper function to change the current working directory.
161
func changeWorkingDirectory(outputDir string) error {
×
162
        if err := os.Chdir(outputDir); err != nil {
×
163
                return fmt.Errorf("failed to change the working directory to %q: %w", outputDir, err)
×
164
        }
×
165
        return nil
×
166
}
167

168
// Initializes the project with Kubebuilder.
169
func kubebuilderInit(s store.Store) error {
×
170
        args := append([]string{"init"}, getInitArgs(s)...)
×
171
        if err := util.RunCmd("kubebuilder init", "kubebuilder", args...); err != nil {
×
172
                return fmt.Errorf("failed to run kubebuilder init command: %w", err)
×
173
        }
×
174

175
        return nil
×
176
}
177

178
// Edits the project to enable or disable multigroup layout.
179
func kubebuilderEdit(s store.Store) error {
×
180
        if s.Config().IsMultiGroup() {
×
181
                args := []string{"edit", "--multigroup"}
×
182
                if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil {
×
183
                        return fmt.Errorf("failed to run kubebuilder edit command: %w", err)
×
184
                }
×
185
        }
186

187
        return nil
×
188
}
189

190
// Creates APIs and Webhooks for the project.
191
func kubebuilderCreate(s store.Store) error {
×
192
        resources, err := s.Config().GetResources()
×
193
        if err != nil {
×
194
                return fmt.Errorf("failed to get resources: %w", err)
×
195
        }
×
196

197
        // First, scaffold all APIs
198
        for _, r := range resources {
×
199
                if err = createAPI(r); err != nil {
×
200
                        return fmt.Errorf("failed to create API for %s/%s/%s: %w", r.Group, r.Version, r.Kind, err)
×
201
                }
×
202
        }
203

204
        // Then, scaffold all webhooks
205
        // We cannot create a webhook for an API that does not exist
206
        for _, r := range resources {
×
207
                if err = createWebhook(r); err != nil {
×
208
                        return fmt.Errorf("failed to create webhook for %s/%s/%s: %w", r.Group, r.Version, r.Kind, err)
×
209
                }
×
210
        }
211

212
        return nil
×
213
}
214

215
// Migrates the Grafana plugin.
216
func migrateGrafanaPlugin(s store.Store, src, des string) error {
×
217
        var grafanaPlugin struct{}
×
218
        err := s.Config().DecodePluginConfig(plugin.KeyFor(v1alpha.Plugin{}), grafanaPlugin)
×
219
        if errors.As(err, &config.PluginKeyNotFoundError{}) {
×
220
                log.Info("Grafana plugin not found, skipping migration")
×
221
                return nil
×
222
        } else if err != nil {
×
223
                return fmt.Errorf("failed to decode grafana plugin config: %w", err)
×
224
        }
×
225

226
        if err = kubebuilderGrafanaEdit(); err != nil {
×
227
                return fmt.Errorf("error editing Grafana plugin: %w", err)
×
228
        }
×
229

230
        if err = grafanaConfigMigrate(src, des); err != nil {
×
231
                return fmt.Errorf("error migrating Grafana config: %w", err)
×
232
        }
×
233

234
        return kubebuilderGrafanaEdit()
×
235
}
236

237
// Migrates the Deploy Image plugin.
238
func migrateDeployImagePlugin(s store.Store) error {
×
239
        var deployImagePlugin v1alpha1.PluginConfig
×
240
        err := s.Config().DecodePluginConfig(plugin.KeyFor(v1alpha1.Plugin{}), &deployImagePlugin)
×
241
        if errors.As(err, &config.PluginKeyNotFoundError{}) {
×
242
                log.Info("Deploy-image plugin not found, skipping migration")
×
243
                return nil
×
244
        } else if err != nil {
×
245
                return fmt.Errorf("failed to decode deploy-image plugin config: %w", err)
×
246
        }
×
247

248
        for _, r := range deployImagePlugin.Resources {
×
249
                if err := createAPIWithDeployImage(r); err != nil {
×
250
                        return fmt.Errorf("failed to create API with deploy-image: %w", err)
×
251
                }
×
252
        }
253

254
        return nil
×
255
}
256

257
// Creates an API with Deploy Image plugin.
258
func createAPIWithDeployImage(resourceData v1alpha1.ResourceData) error {
×
259
        args := append([]string{"create", "api"}, getGVKFlagsFromDeployImage(resourceData)...)
×
260
        args = append(args, getDeployImageOptions(resourceData)...)
×
261
        if err := util.RunCmd("kubebuilder create api", "kubebuilder", args...); err != nil {
×
262
                return fmt.Errorf("failed to run kubebuilder create api command: %w", err)
×
263
        }
×
264

265
        return nil
×
266
}
267

268
// Helper function to get Init arguments for Kubebuilder.
269
func getInitArgs(s store.Store) []string {
×
270
        var args []string
×
271
        plugins := s.Config().GetPluginChain()
×
272

×
273
        // Define outdated plugin versions that need replacement
×
274
        outdatedPlugins := map[string]string{
×
275
                "go.kubebuilder.io/v3":       "go.kubebuilder.io/v4",
×
276
                "go.kubebuilder.io/v3-alpha": "go.kubebuilder.io/v4",
×
277
                "go.kubebuilder.io/v2":       "go.kubebuilder.io/v4",
×
278
        }
×
279

×
280
        // Replace outdated plugins and exit after the first replacement
×
281
        for i, plg := range plugins {
×
282
                if newPlugin, exists := outdatedPlugins[plg]; exists {
×
283
                        log.Warnf("We checked that your PROJECT file is configured with the layout '%s', which is no longer supported.\n"+
×
284
                                "However, we will try our best to re-generate the project using '%s'.", plg, newPlugin)
×
285
                        plugins[i] = newPlugin
×
286
                        break
×
287
                }
288
        }
289

290
        if len(plugins) > 0 {
×
291
                args = append(args, "--plugins", strings.Join(plugins, ","))
×
292
        }
×
293
        if domain := s.Config().GetDomain(); domain != "" {
×
294
                args = append(args, "--domain", domain)
×
295
        }
×
296
        if repo := s.Config().GetRepository(); repo != "" {
×
297
                args = append(args, "--repo", repo)
×
298
        }
×
299
        return args
×
300
}
301

302
// Gets the GVK flags for a resource.
303
func getGVKFlags(res resource.Resource) []string {
×
304
        var args []string
×
305
        if res.Plural != "" {
×
306
                args = append(args, "--plural", res.Plural)
×
307
        }
×
308
        if res.Group != "" {
×
309
                args = append(args, "--group", res.Group)
×
310
        }
×
311
        if res.Version != "" {
×
312
                args = append(args, "--version", res.Version)
×
313
        }
×
314
        if res.Kind != "" {
×
315
                args = append(args, "--kind", res.Kind)
×
316
        }
×
317
        return args
×
318
}
319

320
// Gets the GVK flags for a Deploy Image resource.
321
func getGVKFlagsFromDeployImage(resourceData v1alpha1.ResourceData) []string {
×
322
        var args []string
×
323
        if resourceData.Group != "" {
×
324
                args = append(args, "--group", resourceData.Group)
×
325
        }
×
326
        if resourceData.Version != "" {
×
327
                args = append(args, "--version", resourceData.Version)
×
328
        }
×
329
        if resourceData.Kind != "" {
×
330
                args = append(args, "--kind", resourceData.Kind)
×
331
        }
×
332
        return args
×
333
}
334

335
// Gets the options for a Deploy Image resource.
336
func getDeployImageOptions(resourceData v1alpha1.ResourceData) []string {
×
337
        var args []string
×
338
        if resourceData.Options.Image != "" {
×
339
                args = append(args, fmt.Sprintf("--image=%s", resourceData.Options.Image))
×
340
        }
×
341
        if resourceData.Options.ContainerCommand != "" {
×
342
                args = append(args, fmt.Sprintf("--image-container-command=%s", resourceData.Options.ContainerCommand))
×
343
        }
×
344
        if resourceData.Options.ContainerPort != "" {
×
345
                args = append(args, fmt.Sprintf("--image-container-port=%s", resourceData.Options.ContainerPort))
×
346
        }
×
347
        if resourceData.Options.RunAsUser != "" {
×
348
                args = append(args, fmt.Sprintf("--run-as-user=%s", resourceData.Options.RunAsUser))
×
349
        }
×
350
        args = append(args, fmt.Sprintf("--plugins=%s", plugin.KeyFor(v1alpha1.Plugin{})))
×
351
        return args
×
352
}
353

354
// Creates an API resource.
355
func createAPI(res resource.Resource) error {
×
356
        args := append([]string{"create", "api"}, getGVKFlags(res)...)
×
357
        args = append(args, getAPIResourceFlags(res)...)
×
358

×
359
        // Add the external API path flag if the resource is external
×
360
        if res.IsExternal() {
×
361
                args = append(args, "--external-api-path", res.Path)
×
362
                args = append(args, "--external-api-domain", res.Domain)
×
363
        }
×
364

365
        if err := util.RunCmd("kubebuilder create api", "kubebuilder", args...); err != nil {
×
366
                return fmt.Errorf("failed to run kubebuilder create api command: %w", err)
×
367
        }
×
368

369
        return nil
×
370
}
371

372
// Gets flags for API resource creation.
373
func getAPIResourceFlags(res resource.Resource) []string {
×
374
        var args []string
×
375

×
376
        if res.API == nil || res.API.IsEmpty() {
×
377
                args = append(args, "--resource=false")
×
378
        } else {
×
379
                args = append(args, "--resource")
×
380
                if res.API.Namespaced {
×
381
                        args = append(args, "--namespaced")
×
382
                } else {
×
383
                        args = append(args, "--namespaced=false")
×
384
                }
×
385
        }
386
        if res.Controller {
×
387
                args = append(args, "--controller")
×
388
        } else {
×
389
                args = append(args, "--controller=false")
×
390
        }
×
391
        return args
×
392
}
393

394
// Creates a webhook resource.
395
func createWebhook(res resource.Resource) error {
×
396
        if res.Webhooks == nil || res.Webhooks.IsEmpty() {
×
397
                return nil
×
398
        }
×
399
        args := append([]string{"create", "webhook"}, getGVKFlags(res)...)
×
400
        args = append(args, getWebhookResourceFlags(res)...)
×
401

×
402
        if err := util.RunCmd("kubebuilder create webhook", "kubebuilder", args...); err != nil {
×
403
                return fmt.Errorf("failed to run kubebuilder create webhook command: %w", err)
×
404
        }
×
405

406
        return nil
×
407
}
408

409
// Gets flags for webhook creation.
410
func getWebhookResourceFlags(res resource.Resource) []string {
×
411
        var args []string
×
412
        if res.IsExternal() {
×
413
                args = append(args, "--external-api-path", res.Path)
×
414
                args = append(args, "--external-api-domain", res.Domain)
×
415
        }
×
416
        if res.HasValidationWebhook() {
×
417
                args = append(args, "--programmatic-validation")
×
418
        }
×
419
        if res.HasDefaultingWebhook() {
×
420
                args = append(args, "--defaulting")
×
421
        }
×
422
        if res.HasConversionWebhook() {
×
423
                args = append(args, "--conversion")
×
424
                if len(res.Webhooks.Spoke) > 0 {
×
425
                        for _, spoke := range res.Webhooks.Spoke {
×
426
                                args = append(args, "--spoke", spoke)
×
427
                        }
×
428
                }
429
        }
430
        return args
×
431
}
432

433
// Copies files from source to destination.
434
func copyFile(src, des string) error {
×
435
        bytesRead, err := os.ReadFile(src)
×
436
        if err != nil {
×
437
                return fmt.Errorf("source file path %q does not exist: %w", src, err)
×
438
        }
×
439
        if err = os.WriteFile(des, bytesRead, 0o755); err != nil {
×
440
                return fmt.Errorf("failed to write file %q: %w", des, err)
×
441
        }
×
442

443
        return nil
×
444
}
445

446
// Migrates Grafana configuration files.
447
func grafanaConfigMigrate(src, des string) error {
×
448
        grafanaConfig := fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", src)
×
449
        if _, err := os.Stat(grafanaConfig); os.IsNotExist(err) {
×
450
                return fmt.Errorf("grafana config path %s does not exist: %w", grafanaConfig, err)
×
451
        }
×
452
        return copyFile(grafanaConfig, fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", des))
×
453
}
454

455
// Edits the project to include the Grafana plugin.
456
func kubebuilderGrafanaEdit() error {
×
457
        args := []string{"edit", "--plugins", plugin.KeyFor(v1alpha.Plugin{})}
×
458
        if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil {
×
459
                return fmt.Errorf("failed to run edit subcommand for Grafana plugin: %w", err)
×
460
        }
×
461
        return nil
×
462
}
463

464
// Edits the project to include the Helm plugin.
465
func kubebuilderHelmEdit() error {
×
466
        args := []string{"edit", "--plugins", plugin.KeyFor(hemlv1alpha.Plugin{})}
×
467
        if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil {
×
468
                return fmt.Errorf("failed to run edit subcommand for Helm plugin: %w", err)
×
469
        }
×
470
        return nil
×
471
}
472

473
// hasHelmPlugin checks if the Helm plugin is present by inspecting the plugin chain or configuration.
474
func hasHelmPlugin(cfg store.Store) bool {
×
475
        var pluginConfig map[string]interface{}
×
476

×
477
        // Decode the Helm plugin configuration to check if it's present
×
478
        err := cfg.Config().DecodePluginConfig(plugin.KeyFor(hemlv1alpha.Plugin{}), &pluginConfig)
×
479
        if err != nil {
×
480
                // If the Helm plugin is not found, return false
×
481
                if errors.As(err, &config.PluginKeyNotFoundError{}) {
×
482
                        return false
×
483
                }
×
484
                // Log other errors if needed
485
                log.Errorf("error decoding Helm plugin config: %v", err)
×
486
                return false
×
487
        }
488

489
        // Helm plugin is present
490
        return true
×
491
}
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