diff --git a/pkg/cli/alpha/generate.go b/pkg/cli/alpha/generate.go index 48e991fbda3..fc71bbb5212 100644 --- a/pkg/cli/alpha/generate.go +++ b/pkg/cli/alpha/generate.go @@ -17,12 +17,13 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v4/pkg/rescaffold" + + "sigs.k8s.io/kubebuilder/v4/pkg/command/generate" ) // NewScaffoldCommand return a new scaffold command func NewScaffoldCommand() *cobra.Command { - opts := rescaffold.MigrateOptions{} + opts := generate.Generate{} scaffoldCmd := &cobra.Command{ Use: "generate", Short: "Re-scaffold an existing Kuberbuilder project", @@ -36,8 +37,8 @@ Then we will re-scaffold the project by Kubebuilder in the directory specified b return opts.Validate() }, Run: func(_ *cobra.Command, _ []string) { - if err := opts.Rescaffold(); err != nil { - log.Fatalf("Failed to rescaffold %s", err) + if err := opts.Generate(); err != nil { + log.Fatalf("Failed to command %s", err) } }, } diff --git a/pkg/rescaffold/migrate.go b/pkg/command/generate/generate.go similarity index 54% rename from pkg/rescaffold/migrate.go rename to pkg/command/generate/generate.go index 410a3f4db8f..265bfa4fe01 100644 --- a/pkg/rescaffold/migrate.go +++ b/pkg/command/generate/generate.go @@ -1,9 +1,12 @@ /* Copyright 2023 The Kubernetes Authors. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -11,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package rescaffold +package generate import ( "errors" @@ -32,103 +35,112 @@ import ( "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" ) -type MigrateOptions struct { +type Generate struct { InputDir string OutputDir string } -const DefaultOutputDir = "output-dir" -const grafanaPluginKey = "grafana.kubebuilder.io/v1-alpha" +const ( + defaultOutputDir = "output-dir" + grafanaPluginKey = "grafana.kubebuilder.io/v1-alpha" + deployImagePluginKey = "deploy-image.go.kubebuilder.io/v1-alpha" +) -func (opts *MigrateOptions) Rescaffold() error { - config := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()}) - if err := config.LoadFrom(fmt.Sprintf("%s/%s", opts.InputDir, yaml.DefaultPath)); err != nil { - log.Fatalf("Failed to load PROJECT file %v", err) +// Generate handles the migration and scaffolding process. +func (opts *Generate) Generate() error { + config, err := loadProjectConfig(opts.InputDir) + if err != nil { + return err } - // create output directory - // nolint: gosec - if err := os.MkdirAll(opts.OutputDir, 0755); err != nil { - log.Fatalf("Failed to create output directory %v", err) + + if err := createDirectory(opts.OutputDir); err != nil { + return err } - // use the new directory to set up the new project - if err := os.Chdir(opts.OutputDir); err != nil { - log.Fatalf("Failed to change the current working directory %v", err) + + if err := changeWorkingDirectory(opts.OutputDir); err != nil { + return err } - // init project with plugins + if err := kubebuilderInit(config); err != nil { - log.Fatalf("Failed to run init subcommand %v", err) + return err } - // call edit subcommands to enable or disable multigroup layout + if err := kubebuilderEdit(config); err != nil { - log.Fatalf("Failed to run edit subcommand %v", err) + return err } - // create APIs and Webhooks + if err := kubebuilderCreate(config); err != nil { - log.Fatalf("Failed to run create API subcommand %v", err) + return err } - // plugin specific migration + if err := migrateGrafanaPlugin(config, opts.InputDir, opts.OutputDir); err != nil { - log.Fatalf("Failed to run grafana plugin migration %v", err) + return err } + if err := migrateDeployImagePlugin(config); err != nil { - log.Fatalf("Failed to run deploy-image plugin migration %v", err) + return err } + return nil } -func (opts *MigrateOptions) Validate() error { +// Validate ensures the options are valid and kubebuilder is installed. +func (opts *Generate) Validate() error { cwd, err := os.Getwd() if err != nil { - log.Fatal(err) + return fmt.Errorf("failed to get working directory: %w", err) } - // get PROJECT path from command args - inputPath, err := getInputPath(cwd, opts.InputDir) + + opts.InputDir, err = getInputPath(cwd, opts.InputDir) if err != nil { - log.Fatal(err) + return err } - opts.InputDir = inputPath - // get output path from command args + opts.OutputDir, err = getOutputPath(cwd, opts.OutputDir) if err != nil { - log.Fatal(err) + return err } - // check whether the kubebuilder binary is accessible + _, err = exec.LookPath("kubebuilder") - return err + if err != nil { + return fmt.Errorf("kubebuilder not found in the path: %w", err) + } + + return nil } -func getInputPath(currentWorkingDirectory string, inputPath string) (string, error) { - if inputPath == "" { - inputPath = currentWorkingDirectory - } - projectPath := fmt.Sprintf("%s/%s", inputPath, yaml.DefaultPath) - if _, err := os.Stat(projectPath); os.IsNotExist(err) { - return "", fmt.Errorf("PROJECT path: %s does not exist. %v", projectPath, err) +// Helper function to load the project configuration. +func loadProjectConfig(inputDir string) (store.Store, error) { + config := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()}) + if err := config.LoadFrom(fmt.Sprintf("%s/%s", inputDir, yaml.DefaultPath)); err != nil { + return nil, fmt.Errorf("failed to load PROJECT file: %w", err) } - return inputPath, nil + return config, nil } -func getOutputPath(currentWorkingDirectory, outputPath string) (string, error) { - if outputPath == "" { - outputPath = fmt.Sprintf("%s/%s", currentWorkingDirectory, DefaultOutputDir) +// Helper function to create the output directory. +func createDirectory(outputDir string) error { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outputDir, err) } - _, err := os.Stat(outputPath) - if err == nil { - return "", fmt.Errorf("Output path: %s already exists. %v", outputPath, err) - } - if os.IsNotExist(err) { - return outputPath, nil + return nil +} + +// Helper function to change the current working directory. +func changeWorkingDirectory(outputDir string) error { + if err := os.Chdir(outputDir); err != nil { + return fmt.Errorf("failed to change the working directory to %s: %w", outputDir, err) } - return "", err + return nil } +// Initializes the project with Kubebuilder. func kubebuilderInit(store store.Store) error { - var args []string - args = append(args, "init") - args = append(args, getInitArgs(store)...) + args := append([]string{"init"}, getInitArgs(store)...) return util.RunCmd("kubebuilder init", "kubebuilder", args...) } +// Edits the project to enable or disable multigroup layout. func kubebuilderEdit(store store.Store) error { if store.Config().IsMultiGroup() { args := []string{"edit", "--multigroup"} @@ -137,151 +149,173 @@ func kubebuilderEdit(store store.Store) error { return nil } +// Creates APIs and Webhooks for the project. func kubebuilderCreate(store store.Store) error { resources, err := store.Config().GetResources() if err != nil { - return err + return fmt.Errorf("failed to get resources: %w", err) } for _, r := range resources { - if err = createAPI(r); err != nil { - return err + if err := createAPI(r); err != nil { + return fmt.Errorf("failed to create API: %w", err) } - if err = createWebhook(r); err != nil { - return err + if err := createWebhook(r); err != nil { + return fmt.Errorf("failed to create webhook: %w", err) } } return nil } +// Migrates the Grafana plugin. func migrateGrafanaPlugin(store store.Store, src, des string) error { var grafanaPlugin struct{} err := store.Config().DecodePluginConfig(grafanaPluginKey, grafanaPlugin) - // If the grafana plugin is not found, we don't need to migrate - if err != nil { - if errors.As(err, &config.PluginKeyNotFoundError{}) { - log.Info("Grafana plugin is not found, skip the migration") - return nil - } - return fmt.Errorf("failed to decode grafana plugin config %v", err) + if errors.As(err, &config.PluginKeyNotFoundError{}) { + log.Info("Grafana plugin not found, skipping migration") + return nil + } else if err != nil { + return fmt.Errorf("failed to decode grafana plugin config: %w", err) } - err = kubebuilderGrafanaEdit() - if err != nil { + + if err := kubebuilderGrafanaEdit(); err != nil { return err } - err = grafanaConfigMigrate(src, des) - if err != nil { + + if err := grafanaConfigMigrate(src, des); err != nil { return err } + return kubebuilderGrafanaEdit() } +// Migrates the Deploy Image plugin. func migrateDeployImagePlugin(store store.Store) error { - deployImagePlugin := v1alpha1.PluginConfig{} - err := store.Config().DecodePluginConfig("deploy-image.go.kubebuilder.io/v1-alpha", &deployImagePlugin) - // If the deploy-image plugin is not found, we don't need to migrate - if err != nil { - if errors.As(err, &config.PluginKeyNotFoundError{}) { - log.Printf("deploy-image plugin is not found, skip the migration") - return nil - } - return fmt.Errorf("failed to decode deploy-image plugin config %v", err) + var deployImagePlugin v1alpha1.PluginConfig + err := store.Config().DecodePluginConfig(deployImagePluginKey, &deployImagePlugin) + if errors.As(err, &config.PluginKeyNotFoundError{}) { + log.Info("Deploy-image plugin not found, skipping migration") + return nil + } else if err != nil { + return fmt.Errorf("failed to decode deploy-image plugin config: %w", err) } for _, r := range deployImagePlugin.Resources { - if err = createAPIWithDeployImage(r); err != nil { - return err + if err := createAPIWithDeployImage(r); err != nil { + return fmt.Errorf("failed to create API with deploy-image: %w", err) } } + return nil } +// Creates an API with Deploy Image plugin. func createAPIWithDeployImage(resource v1alpha1.ResourceData) error { - var args []string - args = append(args, "create") - args = append(args, "api") - args = append(args, getGVKFlagsFromDeployImage(resource)...) + args := append([]string{"create", "api"}, getGVKFlagsFromDeployImage(resource)...) args = append(args, getDeployImageOptions(resource)...) return util.RunCmd("kubebuilder create api", "kubebuilder", args...) } +// Helper function to get input path. +func getInputPath(currentWorkingDirectory, inputPath string) (string, error) { + if inputPath == "" { + inputPath = currentWorkingDirectory + } + projectPath := fmt.Sprintf("%s/%s", inputPath, yaml.DefaultPath) + if _, err := os.Stat(projectPath); os.IsNotExist(err) { + return "", fmt.Errorf("project path %s does not exist: %w", projectPath, err) + } + return inputPath, nil +} + +// Helper function to get output path. +func getOutputPath(currentWorkingDirectory, outputPath string) (string, error) { + if outputPath == "" { + outputPath = fmt.Sprintf("%s/%s", currentWorkingDirectory, defaultOutputDir) + } + if _, err := os.Stat(outputPath); err == nil { + return "", fmt.Errorf("output path %s already exists", outputPath) + } + return outputPath, nil +} + +// Helper function to get Init arguments for Kubebuilder. func getInitArgs(store store.Store) []string { var args []string plugins := store.Config().GetPluginChain() if len(plugins) > 0 { args = append(args, "--plugins", strings.Join(plugins, ",")) } - domain := store.Config().GetDomain() - if domain != "" { + if domain := store.Config().GetDomain(); domain != "" { args = append(args, "--domain", domain) } return args } +// Gets the GVK flags for a resource. func getGVKFlags(resource resource.Resource) []string { var args []string - - if len(resource.Plural) > 0 { + if resource.Plural != "" { args = append(args, "--plural", resource.Plural) } - if len(resource.Group) > 0 { + if resource.Group != "" { args = append(args, "--group", resource.Group) } - if len(resource.Version) > 0 { + if resource.Version != "" { args = append(args, "--version", resource.Version) } - if len(resource.Kind) > 0 { + if resource.Kind != "" { args = append(args, "--kind", resource.Kind) } return args } +// Gets the GVK flags for a Deploy Image resource. func getGVKFlagsFromDeployImage(resource v1alpha1.ResourceData) []string { var args []string - if len(resource.Group) > 0 { + if resource.Group != "" { args = append(args, "--group", resource.Group) } - if len(resource.Version) > 0 { + if resource.Version != "" { args = append(args, "--version", resource.Version) } - if len(resource.Kind) > 0 { + if resource.Kind != "" { args = append(args, "--kind", resource.Kind) } return args } +// Gets the options for a Deploy Image resource. func getDeployImageOptions(resource v1alpha1.ResourceData) []string { var args []string - if len(resource.Options.Image) > 0 { + if resource.Options.Image != "" { args = append(args, fmt.Sprintf("--image=%s", resource.Options.Image)) } - if len(resource.Options.ContainerCommand) > 0 { + if resource.Options.ContainerCommand != "" { args = append(args, fmt.Sprintf("--image-container-command=%s", resource.Options.ContainerCommand)) } - if len(resource.Options.ContainerPort) > 0 { + if resource.Options.ContainerPort != "" { args = append(args, fmt.Sprintf("--image-container-port=%s", resource.Options.ContainerPort)) } - if len(resource.Options.RunAsUser) > 0 { + if resource.Options.RunAsUser != "" { args = append(args, fmt.Sprintf("--run-as-user=%s", resource.Options.RunAsUser)) } - args = append(args, fmt.Sprintf("--plugins=\"%s\"", "deploy-image/v1-alpha")) + args = append(args, fmt.Sprintf("--plugins=%s", "deploy-image/v1-alpha")) return args } +// Creates an API resource. func createAPI(resource resource.Resource) error { - var args []string - args = append(args, "create") - args = append(args, "api") - args = append(args, getGVKFlags(resource)...) + args := append([]string{"create", "api"}, getGVKFlags(resource)...) args = append(args, getAPIResourceFlags(resource)...) return util.RunCmd("kubebuilder create api", "kubebuilder", args...) } +// Gets flags for API resource creation. func getAPIResourceFlags(resource resource.Resource) []string { var args []string if resource.API == nil || resource.API.IsEmpty() { - // create API without creating resource args = append(args, "--resource=false") } else { args = append(args, "--resource") @@ -291,7 +325,6 @@ func getAPIResourceFlags(resource resource.Resource) []string { args = append(args, "--namespaced=false") } } - if resource.Controller { args = append(args, "--controller") } else { @@ -300,18 +333,17 @@ func getAPIResourceFlags(resource resource.Resource) []string { return args } +// Creates a webhook resource. func createWebhook(resource resource.Resource) error { if resource.Webhooks == nil || resource.Webhooks.IsEmpty() { return nil } - var args []string - args = append(args, "create") - args = append(args, "webhook") - args = append(args, getGVKFlags(resource)...) + args := append([]string{"create", "webhook"}, getGVKFlags(resource)...) args = append(args, getWebhookResourceFlags(resource)...) return util.RunCmd("kubebuilder create webhook", "kubebuilder", args...) } +// Gets flags for webhook creation. func getWebhookResourceFlags(resource resource.Resource) []string { var args []string if resource.HasConversionWebhook() { @@ -326,30 +358,29 @@ func getWebhookResourceFlags(resource resource.Resource) []string { return args } +// Copies files from source to destination. func copyFile(src, des string) error { - // nolint:gosec bytesRead, err := os.ReadFile(src) if err != nil { - return fmt.Errorf("Source file path: %s does not exist. %v", src, err) + return fmt.Errorf("source file path %s does not exist: %w", src, err) } - // Copy all the contents to the desitination file - // nolint:gosec return os.WriteFile(des, bytesRead, 0755) } +// Migrates Grafana configuration files. func grafanaConfigMigrate(src, des string) error { - grafanaConfig := fmt.Sprintf("%s/%s", src, "grafana/custom-metrics/config.yaml") + grafanaConfig := fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", src) if _, err := os.Stat(grafanaConfig); os.IsNotExist(err) { - return fmt.Errorf("Grafana Config path: %s does not exist. %v", grafanaConfig, err) + return fmt.Errorf("Grafana config path %s does not exist: %w", grafanaConfig, err) } - return copyFile(grafanaConfig, fmt.Sprintf("%s/%s", des, "grafana/custom-metrics/config.yaml")) + return copyFile(grafanaConfig, fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", des)) } +// Edits the project to include the Grafana plugin. func kubebuilderGrafanaEdit() error { args := []string{"edit", "--plugins", grafanaPluginKey} - err := util.RunCmd("kubebuilder edit", "kubebuilder", args...) - if err != nil { - return fmt.Errorf("Failed to run edit subcommand for Grafana Plugin %v", err) + if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil { + return fmt.Errorf("failed to run edit subcommand for Grafana plugin: %w", err) } return nil }