sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go (about)

     1  /*
     2  Copyright 2022 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 scaffolds
    18  
    19  import (
    20  	"fmt"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	log "github.com/sirupsen/logrus"
    25  	"github.com/spf13/afero"
    26  
    27  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    28  	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
    29  	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
    30  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/plugin/util"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins"
    33  	kustomizev1scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v1/scaffolds"
    34  	kustomizev2scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/common/kustomize/v2/scaffolds"
    35  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api"
    36  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples"
    37  	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers"
    38  	golangv3scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds"
    39  	golangv4scaffolds "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v4/scaffolds"
    40  )
    41  
    42  var _ plugins.Scaffolder = &apiScaffolder{}
    43  
    44  // apiScaffolder contains configuration for generating scaffolding for Go type
    45  // representing the API and controller that implements the behavior for the API.
    46  type apiScaffolder struct {
    47  	config    config.Config
    48  	resource  resource.Resource
    49  	image     string
    50  	command   string
    51  	port      string
    52  	runAsUser string
    53  
    54  	// fs is the filesystem that will be used by the scaffolder
    55  	fs machinery.Filesystem
    56  }
    57  
    58  // NewDeployImageScaffolder returns a new Scaffolder for declarative
    59  // nolint: lll
    60  func NewDeployImageScaffolder(config config.Config, res resource.Resource, image,
    61  	command, port, runAsUser string,
    62  ) plugins.Scaffolder {
    63  	return &apiScaffolder{
    64  		config:    config,
    65  		resource:  res,
    66  		image:     image,
    67  		command:   command,
    68  		port:      port,
    69  		runAsUser: runAsUser,
    70  	}
    71  }
    72  
    73  // InjectFS implements cmdutil.Scaffolder
    74  func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
    75  	s.fs = fs
    76  }
    77  
    78  // Scaffold implements cmdutil.Scaffolder
    79  func (s *apiScaffolder) Scaffold() error {
    80  	log.Println("Writing scaffold for you to edit...")
    81  
    82  	//nolint: staticcheck
    83  	isGoV3 := plugin.IsLegacyLayout(s.config)
    84  
    85  	if err := s.scaffoldCreateAPIFromPlugins(isGoV3); err != nil {
    86  		return err
    87  	}
    88  
    89  	// Load the boilerplate
    90  	boilerplate, err := afero.ReadFile(s.fs.FS, filepath.Join("hack", "boilerplate.go.txt"))
    91  	if err != nil {
    92  		return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
    93  	}
    94  
    95  	// Initialize the machinery.Scaffold that will write the files to disk
    96  	scaffold := machinery.NewScaffold(s.fs,
    97  		machinery.WithConfig(s.config),
    98  		machinery.WithBoilerplate(string(boilerplate)),
    99  		machinery.WithResource(&s.resource),
   100  	)
   101  
   102  	if err := scaffold.Execute(
   103  		&api.Types{Port: s.port, IsLegacyLayout: isGoV3},
   104  	); err != nil {
   105  		return fmt.Errorf("error updating APIs: %v", err)
   106  	}
   107  
   108  	if err := scaffold.Execute(
   109  		&samples.CRDSample{Port: s.port},
   110  	); err != nil {
   111  		return fmt.Errorf("error updating config/samples: %v", err)
   112  	}
   113  
   114  	controller := &controllers.Controller{
   115  		ControllerRuntimeVersion: golangv3scaffolds.ControllerRuntimeVersion,
   116  		IsLegacyLayout:           isGoV3,
   117  	}
   118  
   119  	if !isGoV3 {
   120  		controller.ControllerRuntimeVersion = golangv4scaffolds.ControllerRuntimeVersion
   121  	}
   122  
   123  	if err := scaffold.Execute(
   124  		controller,
   125  	); err != nil {
   126  		return fmt.Errorf("error scaffolding controller: %v", err)
   127  	}
   128  
   129  	if err := s.updateControllerCode(*controller); err != nil {
   130  		return fmt.Errorf("error updating controller: %v", err)
   131  	}
   132  
   133  	defaultMainPath := "cmd/main.go"
   134  	if isGoV3 {
   135  		defaultMainPath = "main.go"
   136  	}
   137  	if err := s.updateMainByAddingEventRecorder(defaultMainPath); err != nil {
   138  		return fmt.Errorf("error updating main.go: %v", err)
   139  	}
   140  
   141  	if err := scaffold.Execute(
   142  		&controllers.ControllerTest{Port: s.port, IsLegacyLayout: isGoV3},
   143  	); err != nil {
   144  		return fmt.Errorf("error creating controller/**_controller_test.go: %v", err)
   145  	}
   146  
   147  	if err := s.addEnvVarIntoManager(); err != nil {
   148  		return err
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  // addEnvVarIntoManager will update the config/manager/manager.yaml by adding
   155  // a new ENV VAR for to store the image informed which will be used in the
   156  // controller to create the Pod for the Kind
   157  func (s *apiScaffolder) addEnvVarIntoManager() error {
   158  	managerPath := filepath.Join("config", "manager", "manager.yaml")
   159  	err := util.ReplaceInFile(managerPath, `env:`, `env:`)
   160  	if err != nil {
   161  		if err := util.InsertCode(managerPath, `name: manager`, `
   162          env:`); err != nil {
   163  			return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml")
   164  		}
   165  	}
   166  
   167  	if err = util.InsertCode(managerPath, `env:`,
   168  		fmt.Sprintf(envVarTemplate, strings.ToUpper(s.resource.Kind), s.image)); err != nil {
   169  		return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml")
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // scaffoldCreateAPIFromPlugins will reuse the code from the kustomize and base golang
   176  // plugins to do the default scaffolds which an API is created
   177  func (s *apiScaffolder) scaffoldCreateAPIFromPlugins(isLegacyLayout bool) error {
   178  	if err := s.scaffoldCreateAPIFromGolang(isLegacyLayout); err != nil {
   179  		return fmt.Errorf("error scaffolding golang files for the new API: %v", err)
   180  	}
   181  
   182  	if err := s.scaffoldCreateAPIFromKustomize(isLegacyLayout); err != nil {
   183  		return fmt.Errorf("error scaffolding kustomize manifests for the new API: %v", err)
   184  	}
   185  	return nil
   186  }
   187  
   188  // TODO: replace this implementation by creating its own MainUpdater
   189  // which will have its own controller template which set the recorder so that we can use it
   190  // in the reconciliation to create an event inside for the finalizer
   191  func (s *apiScaffolder) updateMainByAddingEventRecorder(defaultMainPath string) error {
   192  	if err := util.InsertCode(
   193  		defaultMainPath,
   194  		fmt.Sprintf(
   195  			`%sReconciler{
   196  		Client: mgr.GetClient(),
   197  		Scheme: mgr.GetScheme(),`, s.resource.Kind),
   198  		fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)),
   199  	); err != nil {
   200  		return fmt.Errorf("error scaffolding event recorder in %s: %v", defaultMainPath, err)
   201  	}
   202  
   203  	return nil
   204  }
   205  
   206  // updateControllerCode will update the code generate on the template to add the Container information
   207  func (s *apiScaffolder) updateControllerCode(controller controllers.Controller) error {
   208  	if err := util.ReplaceInFile(
   209  		controller.Path,
   210  		"//TODO: scaffold container",
   211  		fmt.Sprintf(containerTemplate, // value for the image
   212  			strings.ToLower(s.resource.Kind), // value for the name of the container
   213  		),
   214  	); err != nil {
   215  		return fmt.Errorf("error scaffolding container in the controller path (%s): %v",
   216  			controller.Path, err)
   217  	}
   218  
   219  	// Scaffold the command if informed
   220  	if len(s.command) > 0 {
   221  		// TODO: improve it to be an spec in the sample and api instead so that
   222  		// users can change the values
   223  		var res string
   224  		for _, value := range strings.Split(s.command, ",") {
   225  			res += fmt.Sprintf(" \"%s\",", strings.TrimSpace(value))
   226  		}
   227  		// remove the latest ,
   228  		res = res[:len(res)-1]
   229  		// remove the first space to not fail in the go fmt ./...
   230  		res = strings.TrimLeft(res, " ")
   231  
   232  		if err := util.InsertCode(controller.Path, `SecurityContext: &corev1.SecurityContext{
   233  							RunAsNonRoot:             &[]bool{true}[0],
   234  							AllowPrivilegeEscalation: &[]bool{false}[0],
   235  							Capabilities: &corev1.Capabilities{
   236  								Drop: []corev1.Capability{
   237  									"ALL",
   238  								},
   239  							},
   240  						},`, fmt.Sprintf(commandTemplate, res)); err != nil {
   241  			return fmt.Errorf("error scaffolding command in the  controller path (%s): %v",
   242  				controller.Path, err)
   243  		}
   244  	}
   245  
   246  	// Scaffold the port if informed
   247  	if len(s.port) > 0 {
   248  		if err := util.InsertCode(
   249  			controller.Path,
   250  			`SecurityContext: &corev1.SecurityContext{
   251  							RunAsNonRoot:             &[]bool{true}[0],
   252  							AllowPrivilegeEscalation: &[]bool{false}[0],
   253  							Capabilities: &corev1.Capabilities{
   254  								Drop: []corev1.Capability{
   255  									"ALL",
   256  								},
   257  							},
   258  						},`,
   259  			fmt.Sprintf(
   260  				portTemplate,
   261  				strings.ToLower(s.resource.Kind),
   262  				strings.ToLower(s.resource.Kind)),
   263  		); err != nil {
   264  			return fmt.Errorf("error scaffolding container port in the controller path (%s): %v",
   265  				controller.Path,
   266  				err)
   267  		}
   268  	}
   269  
   270  	if len(s.runAsUser) > 0 {
   271  		if err := util.InsertCode(
   272  			controller.Path,
   273  			`RunAsNonRoot:             &[]bool{true}[0],`,
   274  			fmt.Sprintf(runAsUserTemplate, s.runAsUser),
   275  		); err != nil {
   276  			return fmt.Errorf("error scaffolding user-id in the controller path (%s): %v",
   277  				controller.Path, err)
   278  		}
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  func (s *apiScaffolder) scaffoldCreateAPIFromKustomize(isLegacyLayout bool) error {
   285  	// Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API
   286  	// todo: when we have the go/v4 plugin we will also need to check what is the plugin used
   287  	// in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha
   288  	var kustomizeScaffolder plugins.Scaffolder
   289  
   290  	if isLegacyLayout {
   291  		kustomizeScaffolder = kustomizev1scaffolds.NewAPIScaffolder(
   292  			s.config,
   293  			s.resource,
   294  			true,
   295  		)
   296  	} else {
   297  		kustomizeScaffolder = kustomizev2scaffolds.NewAPIScaffolder(
   298  			s.config,
   299  			s.resource,
   300  			true,
   301  		)
   302  	}
   303  
   304  	kustomizeScaffolder.InjectFS(s.fs)
   305  
   306  	if err := kustomizeScaffolder.Scaffold(); err != nil {
   307  		return fmt.Errorf("error scaffolding kustomize files for the APIs: %v", err)
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func (s *apiScaffolder) scaffoldCreateAPIFromGolang(isLegacyLayout bool) error {
   314  	// Now we need call the kustomize/v1 plugin to do its scaffolds when we create a new API
   315  	// todo: when we have the go/v4 plugin we will also need to check what is the plugin used
   316  	// in the Project layout to know if we should use kustomize/v1 OR kustomize/v2-alpha
   317  	if isLegacyLayout {
   318  		golangV3Scaffolder := golangv3scaffolds.NewAPIScaffolder(s.config,
   319  			s.resource, true)
   320  		golangV3Scaffolder.InjectFS(s.fs)
   321  		return golangV3Scaffolder.Scaffold()
   322  	}
   323  	golangV4Scaffolder := golangv4scaffolds.NewAPIScaffolder(s.config,
   324  		s.resource, true)
   325  	golangV4Scaffolder.InjectFS(s.fs)
   326  	return golangV4Scaffolder.Scaffold()
   327  }
   328  
   329  const containerTemplate = `Containers: []corev1.Container{{
   330  						Image:           image,
   331  						Name:            "%s",
   332  						ImagePullPolicy: corev1.PullIfNotPresent,
   333  						// Ensure restrictive context for the container
   334  						// More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
   335  						SecurityContext: &corev1.SecurityContext{
   336  							RunAsNonRoot:             &[]bool{true}[0],
   337  							AllowPrivilegeEscalation: &[]bool{false}[0],
   338  							Capabilities: &corev1.Capabilities{
   339  								Drop: []corev1.Capability{
   340  									"ALL",
   341  								},
   342  							},
   343  						},
   344  					}}`
   345  
   346  const runAsUserTemplate = `
   347  							RunAsUser:                &[]int64{%s}[0],`
   348  
   349  const commandTemplate = `
   350  						Command: []string{%s},`
   351  
   352  const portTemplate = `
   353  						Ports: []corev1.ContainerPort{{
   354  							ContainerPort: %s.Spec.ContainerPort,
   355  							Name:          "%s",
   356  						}},`
   357  
   358  const recorderTemplate = `
   359  		Recorder: mgr.GetEventRecorderFor("%s-controller"),`
   360  
   361  const envVarTemplate = `
   362          - name: %s_IMAGE
   363            value: %s`