sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/rescaffold/migrate.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  	http://www.apache.org/licenses/LICENSE-2.0
     7  Unless required by applicable law or agreed to in writing, software
     8  distributed under the License is distributed on an "AS IS" BASIS,
     9  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  See the License for the specific language governing permissions and
    11  limitations under the License.
    12  */
    13  
    14  package rescaffold
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"os"
    20  	"os/exec"
    21  	"strings"
    22  
    23  	log "github.com/sirupsen/logrus"
    24  
    25  	"github.com/spf13/afero"
    26  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    27  	"sigs.k8s.io/kubebuilder/v3/pkg/config/store"
    28  	"sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml"
    29  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    30  	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1"
    33  )
    34  
    35  type MigrateOptions struct {
    36  	InputDir  string
    37  	OutputDir string
    38  }
    39  
    40  const DefaultOutputDir = "output-dir"
    41  const grafanaPluginKey = "grafana.kubebuilder.io/v1-alpha"
    42  
    43  func (opts *MigrateOptions) Rescaffold() error {
    44  	config := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()})
    45  	if err := config.LoadFrom(fmt.Sprintf("%s/%s", opts.InputDir, yaml.DefaultPath)); err != nil {
    46  		log.Fatalf("Failed to load PROJECT file %v", err)
    47  	}
    48  	// create output directory
    49  	// nolint: gosec
    50  	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
    51  		log.Fatalf("Failed to create output directory %v", err)
    52  	}
    53  	// use the new directory to set up the new project
    54  	if err := os.Chdir(opts.OutputDir); err != nil {
    55  		log.Fatalf("Failed to change the current working directory %v", err)
    56  	}
    57  	// init project with plugins
    58  	if err := kubebuilderInit(config); err != nil {
    59  		log.Fatalf("Failed to run init subcommand %v", err)
    60  	}
    61  	// call edit subcommands to enable or disable multigroup layout
    62  	if err := kubebuilderEdit(config); err != nil {
    63  		log.Fatalf("Failed to run edit subcommand %v", err)
    64  	}
    65  	// create APIs and Webhooks
    66  	if err := kubebuilderCreate(config); err != nil {
    67  		log.Fatalf("Failed to run create API subcommand %v", err)
    68  	}
    69  	// plugin specific migration
    70  	if err := migrateGrafanaPlugin(config, opts.InputDir, opts.OutputDir); err != nil {
    71  		log.Fatalf("Failed to run grafana plugin migration %v", err)
    72  	}
    73  	if err := migrateDeployImagePlugin(config); err != nil {
    74  		log.Fatalf("Failed to run deploy-image plugin migration %v", err)
    75  	}
    76  	return nil
    77  }
    78  
    79  func (opts *MigrateOptions) Validate() error {
    80  	cwd, err := os.Getwd()
    81  	if err != nil {
    82  		log.Fatal(err)
    83  	}
    84  	// get PROJECT path from command args
    85  	inputPath, err := getInputPath(cwd, opts.InputDir)
    86  	if err != nil {
    87  		log.Fatal(err)
    88  	}
    89  	opts.InputDir = inputPath
    90  	// get output path from command args
    91  	opts.OutputDir, err = getOutputPath(cwd, opts.OutputDir)
    92  	if err != nil {
    93  		log.Fatal(err)
    94  	}
    95  	// check whether the kubebuilder binary is accessible
    96  	_, err = exec.LookPath("kubebuilder")
    97  	return err
    98  }
    99  
   100  func getInputPath(currentWorkingDirectory string, inputPath string) (string, error) {
   101  	if inputPath == "" {
   102  		inputPath = currentWorkingDirectory
   103  	}
   104  	projectPath := fmt.Sprintf("%s/%s", inputPath, yaml.DefaultPath)
   105  	if _, err := os.Stat(projectPath); os.IsNotExist(err) {
   106  		return "", fmt.Errorf("PROJECT path: %s does not exist. %v", projectPath, err)
   107  	}
   108  	return inputPath, nil
   109  }
   110  
   111  func getOutputPath(currentWorkingDirectory, outputPath string) (string, error) {
   112  	if outputPath == "" {
   113  		outputPath = fmt.Sprintf("%s/%s", currentWorkingDirectory, DefaultOutputDir)
   114  	}
   115  	_, err := os.Stat(outputPath)
   116  	if err == nil {
   117  		return "", fmt.Errorf("Output path: %s already exists. %v", outputPath, err)
   118  	}
   119  	if os.IsNotExist(err) {
   120  		return outputPath, nil
   121  	}
   122  	return "", err
   123  }
   124  
   125  func kubebuilderInit(store store.Store) error {
   126  	var args []string
   127  	args = append(args, "init")
   128  	args = append(args, getInitArgs(store)...)
   129  	return util.RunCmd("kubebuilder init", "kubebuilder", args...)
   130  }
   131  
   132  func kubebuilderEdit(store store.Store) error {
   133  	if store.Config().IsMultiGroup() {
   134  		args := []string{"edit", "--multigroup"}
   135  		return util.RunCmd("kubebuilder edit", "kubebuilder", args...)
   136  	}
   137  	return nil
   138  }
   139  
   140  func kubebuilderCreate(store store.Store) error {
   141  	resources, err := store.Config().GetResources()
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	for _, r := range resources {
   147  		if err = createAPI(r); err != nil {
   148  			return err
   149  		}
   150  		if err = createWebhook(r); err != nil {
   151  			return err
   152  		}
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func migrateGrafanaPlugin(store store.Store, src, des string) error {
   159  	var grafanaPlugin struct{}
   160  	err := store.Config().DecodePluginConfig(grafanaPluginKey, grafanaPlugin)
   161  	// If the grafana plugin is not found, we don't need to migrate
   162  	if err != nil {
   163  		if errors.As(err, &config.PluginKeyNotFoundError{}) {
   164  			log.Info("Grafana plugin is not found, skip the migration")
   165  			return nil
   166  		}
   167  		return fmt.Errorf("failed to decode grafana plugin config %v", err)
   168  	}
   169  	err = kubebuilderGrafanaEdit()
   170  	if err != nil {
   171  		return err
   172  	}
   173  	err = grafanaConfigMigrate(src, des)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	return kubebuilderGrafanaEdit()
   178  }
   179  
   180  func migrateDeployImagePlugin(store store.Store) error {
   181  	deployImagePlugin := v1alpha1.PluginConfig{}
   182  	err := store.Config().DecodePluginConfig("deploy-image.go.kubebuilder.io/v1-alpha", &deployImagePlugin)
   183  	// If the deploy-image plugin is not found, we don't need to migrate
   184  	if err != nil {
   185  		if errors.As(err, &config.PluginKeyNotFoundError{}) {
   186  			log.Printf("deploy-image plugin is not found, skip the migration")
   187  			return nil
   188  		}
   189  		return fmt.Errorf("failed to decode deploy-image plugin config %v", err)
   190  	}
   191  
   192  	for _, r := range deployImagePlugin.Resources {
   193  		if err = createAPIWithDeployImage(r); err != nil {
   194  			return err
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  func createAPIWithDeployImage(resource v1alpha1.ResourceData) error {
   201  	var args []string
   202  	args = append(args, "create")
   203  	args = append(args, "api")
   204  	args = append(args, getGVKFlagsFromDeployImage(resource)...)
   205  	args = append(args, getDeployImageOptions(resource)...)
   206  	return util.RunCmd("kubebuilder create api", "kubebuilder", args...)
   207  }
   208  
   209  func getInitArgs(store store.Store) []string {
   210  	var args []string
   211  	plugins := store.Config().GetPluginChain()
   212  	if len(plugins) > 0 {
   213  		args = append(args, "--plugins", strings.Join(plugins, ","))
   214  	}
   215  	domain := store.Config().GetDomain()
   216  	if domain != "" {
   217  		args = append(args, "--domain", domain)
   218  	}
   219  	return args
   220  }
   221  
   222  func getGVKFlags(resource resource.Resource) []string {
   223  	var args []string
   224  
   225  	if len(resource.Plural) > 0 {
   226  		args = append(args, "--plural", resource.Plural)
   227  	}
   228  	if len(resource.Group) > 0 {
   229  		args = append(args, "--group", resource.Group)
   230  	}
   231  	if len(resource.Version) > 0 {
   232  		args = append(args, "--version", resource.Version)
   233  	}
   234  	if len(resource.Kind) > 0 {
   235  		args = append(args, "--kind", resource.Kind)
   236  	}
   237  	return args
   238  }
   239  
   240  func getGVKFlagsFromDeployImage(resource v1alpha1.ResourceData) []string {
   241  	var args []string
   242  	if len(resource.Group) > 0 {
   243  		args = append(args, "--group", resource.Group)
   244  	}
   245  	if len(resource.Version) > 0 {
   246  		args = append(args, "--version", resource.Version)
   247  	}
   248  	if len(resource.Kind) > 0 {
   249  		args = append(args, "--kind", resource.Kind)
   250  	}
   251  	return args
   252  }
   253  
   254  func getDeployImageOptions(resource v1alpha1.ResourceData) []string {
   255  	var args []string
   256  	if len(resource.Options.Image) > 0 {
   257  		args = append(args, fmt.Sprintf("--image=%s", resource.Options.Image))
   258  	}
   259  	if len(resource.Options.ContainerCommand) > 0 {
   260  		args = append(args, fmt.Sprintf("--image-container-command=%s", resource.Options.ContainerCommand))
   261  	}
   262  	if len(resource.Options.ContainerPort) > 0 {
   263  		args = append(args, fmt.Sprintf("--image-container-port=%s", resource.Options.ContainerPort))
   264  	}
   265  	if len(resource.Options.RunAsUser) > 0 {
   266  		args = append(args, fmt.Sprintf("--run-as-user=%s", resource.Options.RunAsUser))
   267  	}
   268  	args = append(args, fmt.Sprintf("--plugins=\"%s\"", "deploy-image/v1-alpha"))
   269  	return args
   270  }
   271  
   272  func createAPI(resource resource.Resource) error {
   273  	var args []string
   274  	args = append(args, "create")
   275  	args = append(args, "api")
   276  	args = append(args, getGVKFlags(resource)...)
   277  	args = append(args, getAPIResourceFlags(resource)...)
   278  	return util.RunCmd("kubebuilder create api", "kubebuilder", args...)
   279  }
   280  
   281  func getAPIResourceFlags(resource resource.Resource) []string {
   282  	var args []string
   283  	if resource.API == nil || resource.API.IsEmpty() {
   284  		// create API without creating resource
   285  		args = append(args, "--resource=false")
   286  	} else {
   287  		args = append(args, "--resource")
   288  		if resource.API.Namespaced {
   289  			args = append(args, "--namespaced")
   290  		}
   291  	}
   292  
   293  	if resource.Controller {
   294  		args = append(args, "--controller")
   295  	} else {
   296  		args = append(args, "--controller=false")
   297  	}
   298  	return args
   299  }
   300  
   301  func createWebhook(resource resource.Resource) error {
   302  	if resource.Webhooks == nil || resource.Webhooks.IsEmpty() {
   303  		return nil
   304  	}
   305  	var args []string
   306  	args = append(args, "create")
   307  	args = append(args, "webhook")
   308  	args = append(args, getGVKFlags(resource)...)
   309  	args = append(args, getWebhookResourceFlags(resource)...)
   310  	return util.RunCmd("kubebuilder create webhook", "kubebuilder", args...)
   311  }
   312  
   313  func getWebhookResourceFlags(resource resource.Resource) []string {
   314  	var args []string
   315  	if resource.HasConversionWebhook() {
   316  		args = append(args, "--conversion")
   317  	}
   318  	if resource.HasValidationWebhook() {
   319  		args = append(args, "--programmatic-validation")
   320  	}
   321  	if resource.HasDefaultingWebhook() {
   322  		args = append(args, "--defaulting")
   323  	}
   324  	return args
   325  }
   326  
   327  func copyFile(src, des string) error {
   328  	// nolint:gosec
   329  	bytesRead, err := os.ReadFile(src)
   330  	if err != nil {
   331  		return fmt.Errorf("Source file path: %s does not exist. %v", src, err)
   332  	}
   333  	//Copy all the contents to the desitination file
   334  	// nolint:gosec
   335  	return os.WriteFile(des, bytesRead, 0755)
   336  }
   337  
   338  func grafanaConfigMigrate(src, des string) error {
   339  	grafanaConfig := fmt.Sprintf("%s/%s", src, "grafana/custom-metrics/config.yaml")
   340  	if _, err := os.Stat(grafanaConfig); os.IsNotExist(err) {
   341  		return fmt.Errorf("Grafana Config path: %s does not exist. %v", grafanaConfig, err)
   342  	}
   343  	return copyFile(grafanaConfig, fmt.Sprintf("%s/%s", des, "grafana/custom-metrics/config.yaml"))
   344  }
   345  
   346  func kubebuilderGrafanaEdit() error {
   347  	args := []string{"edit", "--plugins", grafanaPluginKey}
   348  	err := util.RunCmd("kubebuilder edit", "kubebuilder", args...)
   349  	if err != nil {
   350  		return fmt.Errorf("Failed to run edit subcommand for Grafana Plugin %v", err)
   351  	}
   352  	return nil
   353  }