From b3c1e7b6585599f9dd5cd788e1579ea21a3a0fc7 Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Sun, 22 Sep 2024 08:08:53 +0100 Subject: [PATCH] cleanup/refactor: Alpha Generate command - Refactored the code implementation to improve error handling, encapsulate logic, and streamline execution flow. - Moved the code implementation from `pkg` to the CLI since it pertains directly to a command. - Additionally, moved the implementation to `internal` to prevent exposing the `Generate` code and avoid unintentional extensions on this alpha feature. --- pkg/cli/alpha/{generate.go => command.go} | 9 +- .../alpha/internal/generate.go} | 275 ++++++++++-------- 2 files changed, 157 insertions(+), 127 deletions(-) rename pkg/cli/alpha/{generate.go => command.go} (91%) rename pkg/{rescaffold/migrate.go => cli/alpha/internal/generate.go} (54%) diff --git a/pkg/cli/alpha/generate.go b/pkg/cli/alpha/command.go similarity index 91% rename from pkg/cli/alpha/generate.go rename to pkg/cli/alpha/command.go index 48e991fbda3..cf9395d24a5 100644 --- a/pkg/cli/alpha/generate.go +++ b/pkg/cli/alpha/command.go @@ -15,14 +15,13 @@ package alpha import ( log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v4/pkg/rescaffold" + "sigs.k8s.io/kubebuilder/v4/pkg/cli/alpha/internal" ) // NewScaffoldCommand return a new scaffold command func NewScaffoldCommand() *cobra.Command { - opts := rescaffold.MigrateOptions{} + opts := internal.Generate{} scaffoldCmd := &cobra.Command{ Use: "generate", Short: "Re-scaffold an existing Kuberbuilder project", @@ -36,8 +35,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/cli/alpha/internal/generate.go similarity index 54% rename from pkg/rescaffold/migrate.go rename to pkg/cli/alpha/internal/generate.go index bde0bd78b4d..414fefa5102 100644 --- a/pkg/rescaffold/migrate.go +++ b/pkg/cli/alpha/internal/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 internal 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,143 +149,165 @@ 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)...) // Add the external API path flag if the resource is external @@ -285,11 +319,11 @@ func createAPI(resource resource.Resource) error { 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") @@ -299,7 +333,6 @@ func getAPIResourceFlags(resource resource.Resource) []string { args = append(args, "--namespaced=false") } } - if resource.Controller { args = append(args, "--controller") } else { @@ -308,18 +341,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() { @@ -334,30 +366,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 }