sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/machinery/scaffold.go (about)

     1  /*
     2  Copyright 2018 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 machinery
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"text/template"
    27  
    28  	"github.com/spf13/afero"
    29  	"golang.org/x/tools/imports"
    30  
    31  	"sigs.k8s.io/kubebuilder/v3/pkg/config"
    32  	"sigs.k8s.io/kubebuilder/v3/pkg/model/resource"
    33  )
    34  
    35  const (
    36  	createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
    37  
    38  	defaultDirectoryPermission os.FileMode = 0700
    39  	defaultFilePermission      os.FileMode = 0600
    40  )
    41  
    42  var options = imports.Options{
    43  	Comments:   true,
    44  	TabIndent:  true,
    45  	TabWidth:   8,
    46  	FormatOnly: true,
    47  }
    48  
    49  // Scaffold uses templates to scaffold new files
    50  type Scaffold struct {
    51  	// fs allows to mock the file system for tests
    52  	fs afero.Fs
    53  
    54  	// permissions for new directories and files
    55  	dirPerm  os.FileMode
    56  	filePerm os.FileMode
    57  
    58  	// injector is used to provide several fields to the templates
    59  	injector injector
    60  }
    61  
    62  // ScaffoldOption allows to provide optional arguments to the Scaffold
    63  type ScaffoldOption func(*Scaffold)
    64  
    65  // NewScaffold returns a new Scaffold with the provided plugins
    66  func NewScaffold(fs Filesystem, options ...ScaffoldOption) *Scaffold {
    67  	s := &Scaffold{
    68  		fs:       fs.FS,
    69  		dirPerm:  defaultDirectoryPermission,
    70  		filePerm: defaultFilePermission,
    71  	}
    72  
    73  	for _, option := range options {
    74  		option(s)
    75  	}
    76  
    77  	return s
    78  }
    79  
    80  // WithDirectoryPermissions sets the permissions for new directories
    81  func WithDirectoryPermissions(dirPerm os.FileMode) ScaffoldOption {
    82  	return func(s *Scaffold) {
    83  		s.dirPerm = dirPerm
    84  	}
    85  }
    86  
    87  // WithFilePermissions sets the permissions for new files
    88  func WithFilePermissions(filePerm os.FileMode) ScaffoldOption {
    89  	return func(s *Scaffold) {
    90  		s.filePerm = filePerm
    91  	}
    92  }
    93  
    94  // WithConfig provides the project configuration to the Scaffold
    95  func WithConfig(cfg config.Config) ScaffoldOption {
    96  	return func(s *Scaffold) {
    97  		s.injector.config = cfg
    98  
    99  		if cfg != nil && cfg.GetRepository() != "" {
   100  			imports.LocalPrefix = cfg.GetRepository()
   101  		}
   102  	}
   103  }
   104  
   105  // WithBoilerplate provides the boilerplate to the Scaffold
   106  func WithBoilerplate(boilerplate string) ScaffoldOption {
   107  	return func(s *Scaffold) {
   108  		s.injector.boilerplate = boilerplate
   109  	}
   110  }
   111  
   112  // WithResource provides the resource to the Scaffold
   113  func WithResource(resource *resource.Resource) ScaffoldOption {
   114  	return func(s *Scaffold) {
   115  		s.injector.resource = resource
   116  	}
   117  }
   118  
   119  // Execute writes to disk the provided files
   120  func (s *Scaffold) Execute(builders ...Builder) error {
   121  	// Initialize the files
   122  	files := make(map[string]*File, len(builders))
   123  
   124  	for _, builder := range builders {
   125  		// Inject common fields
   126  		s.injector.injectInto(builder)
   127  
   128  		// Validate file builders
   129  		if reqValBuilder, requiresValidation := builder.(RequiresValidation); requiresValidation {
   130  			if err := reqValBuilder.Validate(); err != nil {
   131  				return ValidateError{err}
   132  			}
   133  		}
   134  
   135  		// Build models for Template builders
   136  		if t, isTemplate := builder.(Template); isTemplate {
   137  			if err := s.buildFileModel(t, files); err != nil {
   138  				return err
   139  			}
   140  		}
   141  
   142  		// Build models for Inserter builders
   143  		if i, isInserter := builder.(Inserter); isInserter {
   144  			if err := s.updateFileModel(i, files); err != nil {
   145  				return err
   146  			}
   147  		}
   148  	}
   149  
   150  	// Persist the files to disk
   151  	for _, f := range files {
   152  		if err := s.writeFile(f); err != nil {
   153  			return err
   154  		}
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // buildFileModel scaffolds a single file
   161  func (Scaffold) buildFileModel(t Template, models map[string]*File) error {
   162  	// Set the template default values
   163  	if err := t.SetTemplateDefaults(); err != nil {
   164  		return SetTemplateDefaultsError{err}
   165  	}
   166  
   167  	path := t.GetPath()
   168  
   169  	// Handle already existing models
   170  	if _, found := models[path]; found {
   171  		switch t.GetIfExistsAction() {
   172  		case SkipFile:
   173  			return nil
   174  		case Error:
   175  			return ModelAlreadyExistsError{path}
   176  		case OverwriteFile:
   177  		default:
   178  			return UnknownIfExistsActionError{path, t.GetIfExistsAction()}
   179  		}
   180  	}
   181  
   182  	b, err := doTemplate(t)
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	models[path] = &File{
   188  		Path:           path,
   189  		Contents:       string(b),
   190  		IfExistsAction: t.GetIfExistsAction(),
   191  	}
   192  	return nil
   193  }
   194  
   195  // doTemplate executes the template for a file using the input
   196  func doTemplate(t Template) ([]byte, error) {
   197  	// Create a new template.Template using the type of the Template as the name
   198  	temp := template.New(fmt.Sprintf("%T", t))
   199  	leftDelim, rightDelim := t.GetDelim()
   200  	if leftDelim != "" && rightDelim != "" {
   201  		temp.Delims(leftDelim, rightDelim)
   202  	}
   203  
   204  	// Set the function map to be used
   205  	fm := DefaultFuncMap()
   206  	if templateWithFuncMap, hasCustomFuncMap := t.(UseCustomFuncMap); hasCustomFuncMap {
   207  		fm = templateWithFuncMap.GetFuncMap()
   208  	}
   209  	temp.Funcs(fm)
   210  
   211  	// Set the template body
   212  	if _, err := temp.Parse(t.GetBody()); err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	// Execute the template
   217  	out := &bytes.Buffer{}
   218  	if err := temp.Execute(out, t); err != nil {
   219  		return nil, err
   220  	}
   221  	b := out.Bytes()
   222  
   223  	// TODO(adirio): move go-formatting to write step
   224  	// gofmt the imports
   225  	if filepath.Ext(t.GetPath()) == ".go" {
   226  		var err error
   227  		if b, err = imports.Process(t.GetPath(), b, &options); err != nil {
   228  			return nil, err
   229  		}
   230  	}
   231  
   232  	return b, nil
   233  }
   234  
   235  // updateFileModel updates a single file
   236  func (s Scaffold) updateFileModel(i Inserter, models map[string]*File) error {
   237  	m, err := s.loadPreviousModel(i, models)
   238  	if err != nil {
   239  		return err
   240  	}
   241  
   242  	// Get valid code fragments
   243  	codeFragments := getValidCodeFragments(i)
   244  
   245  	// Remove code fragments that already were applied
   246  	err = filterExistingValues(m.Contents, codeFragments)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	// If no code fragment to insert, we are done
   252  	if len(codeFragments) == 0 {
   253  		return nil
   254  	}
   255  
   256  	content, err := insertStrings(m.Contents, codeFragments)
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	// TODO(adirio): move go-formatting to write step
   262  	formattedContent := content
   263  	if ext := filepath.Ext(i.GetPath()); ext == ".go" {
   264  		formattedContent, err = imports.Process(i.GetPath(), content, nil)
   265  		if err != nil {
   266  			return err
   267  		}
   268  	}
   269  
   270  	m.Contents = string(formattedContent)
   271  	m.IfExistsAction = OverwriteFile
   272  	models[m.Path] = m
   273  	return nil
   274  }
   275  
   276  // loadPreviousModel gets the previous model from the models map or the actual file
   277  func (s Scaffold) loadPreviousModel(i Inserter, models map[string]*File) (*File, error) {
   278  	path := i.GetPath()
   279  
   280  	// Lets see if we already have a model for this file
   281  	if m, found := models[path]; found {
   282  		// Check if there is already an scaffolded file
   283  		exists, err := afero.Exists(s.fs, path)
   284  		if err != nil {
   285  			return nil, ExistsFileError{err}
   286  		}
   287  
   288  		// If there is a model but no scaffolded file we return the model
   289  		if !exists {
   290  			return m, nil
   291  		}
   292  
   293  		// If both a model and a file are found, check which has preference
   294  		switch m.IfExistsAction {
   295  		case SkipFile:
   296  			// File has preference
   297  			fromFile, err := s.loadModelFromFile(path)
   298  			if err != nil {
   299  				return m, nil
   300  			}
   301  			return fromFile, nil
   302  		case Error:
   303  			// Writing will result in an error, so we can return error now
   304  			return nil, FileAlreadyExistsError{path}
   305  		case OverwriteFile:
   306  			// Model has preference
   307  			return m, nil
   308  		default:
   309  			return nil, UnknownIfExistsActionError{path, m.IfExistsAction}
   310  		}
   311  	}
   312  
   313  	// There was no model
   314  	return s.loadModelFromFile(path)
   315  }
   316  
   317  // loadModelFromFile gets the previous model from the actual file
   318  func (s Scaffold) loadModelFromFile(path string) (f *File, err error) {
   319  	reader, err := s.fs.Open(path)
   320  	if err != nil {
   321  		return nil, OpenFileError{err}
   322  	}
   323  	defer func() {
   324  		if closeErr := reader.Close(); err == nil && closeErr != nil {
   325  			err = CloseFileError{closeErr}
   326  		}
   327  	}()
   328  
   329  	content, err := afero.ReadAll(reader)
   330  	if err != nil {
   331  		return nil, ReadFileError{err}
   332  	}
   333  
   334  	return &File{Path: path, Contents: string(content)}, nil
   335  }
   336  
   337  // getValidCodeFragments obtains the code fragments from a file.Inserter
   338  func getValidCodeFragments(i Inserter) CodeFragmentsMap {
   339  	// Get the code fragments
   340  	codeFragments := i.GetCodeFragments()
   341  
   342  	// Validate the code fragments
   343  	validMarkers := i.GetMarkers()
   344  	for marker := range codeFragments {
   345  		valid := false
   346  		for _, validMarker := range validMarkers {
   347  			if marker == validMarker {
   348  				valid = true
   349  				break
   350  			}
   351  		}
   352  		if !valid {
   353  			delete(codeFragments, marker)
   354  		}
   355  	}
   356  
   357  	return codeFragments
   358  }
   359  
   360  // filterExistingValues removes code fragments that already exist in the content.
   361  func filterExistingValues(content string, codeFragmentsMap CodeFragmentsMap) error {
   362  	for marker, codeFragments := range codeFragmentsMap {
   363  		codeFragmentsOut := codeFragments[:0]
   364  
   365  		for _, codeFragment := range codeFragments {
   366  			exists, err := codeFragmentExists(content, codeFragment)
   367  			if err != nil {
   368  				return err
   369  			}
   370  			if !exists {
   371  				codeFragmentsOut = append(codeFragmentsOut, codeFragment)
   372  			}
   373  		}
   374  
   375  		if len(codeFragmentsOut) == 0 {
   376  			delete(codeFragmentsMap, marker)
   377  		} else {
   378  			codeFragmentsMap[marker] = codeFragmentsOut
   379  		}
   380  	}
   381  
   382  	return nil
   383  }
   384  
   385  // codeFragmentExists checks if the codeFragment exists in the content.
   386  func codeFragmentExists(content, codeFragment string) (exists bool, err error) {
   387  	// Trim space on each line in order to match different levels of indentation.
   388  	var sb strings.Builder
   389  	for _, line := range strings.Split(codeFragment, "\n") {
   390  		_, _ = sb.WriteString(strings.TrimSpace(line))
   391  		_ = sb.WriteByte('\n')
   392  	}
   393  
   394  	codeFragmentTrimmed := strings.TrimSpace(sb.String())
   395  	scanLines := 1 + strings.Count(codeFragmentTrimmed, "\n")
   396  	scanFunc := func(contentGroup string) bool {
   397  		if contentGroup == codeFragmentTrimmed {
   398  			exists = true
   399  			return false
   400  		}
   401  		return true
   402  	}
   403  
   404  	if err := scanMultiline(content, scanLines, scanFunc); err != nil {
   405  		return false, err
   406  	}
   407  
   408  	return exists, nil
   409  }
   410  
   411  // scanMultiline scans a string while buffering the specified number of scanLines. It calls scanFunc
   412  // for every group of lines. The content passed to scanFunc will have trimmed whitespace. It
   413  // continues scanning the content as long as scanFunc returns true.
   414  func scanMultiline(content string, scanLines int, scanFunc func(contentGroup string) bool) error {
   415  	scanner := bufio.NewScanner(strings.NewReader(content))
   416  
   417  	// Optimized simple case.
   418  	if scanLines == 1 {
   419  		for scanner.Scan() {
   420  			if !scanFunc(strings.TrimSpace(scanner.Text())) {
   421  				return scanner.Err()
   422  			}
   423  		}
   424  		return scanner.Err()
   425  	}
   426  
   427  	// Complex case.
   428  	bufferedLines := make([]string, scanLines)
   429  	bufferedLinesIndex := 0
   430  	var sb strings.Builder
   431  
   432  	for scanner.Scan() {
   433  		// Trim space on each line in order to match different levels of indentation.
   434  		bufferedLines[bufferedLinesIndex] = strings.TrimSpace(scanner.Text())
   435  		bufferedLinesIndex = (bufferedLinesIndex + 1) % scanLines
   436  
   437  		sb.Reset()
   438  		for i := 0; i < scanLines; i++ {
   439  			_, _ = sb.WriteString(bufferedLines[(bufferedLinesIndex+i)%scanLines])
   440  			_ = sb.WriteByte('\n')
   441  		}
   442  
   443  		if !scanFunc(strings.TrimSpace(sb.String())) {
   444  			return scanner.Err()
   445  		}
   446  	}
   447  
   448  	return scanner.Err()
   449  }
   450  
   451  func insertStrings(content string, codeFragmentsMap CodeFragmentsMap) ([]byte, error) {
   452  	out := new(bytes.Buffer)
   453  
   454  	scanner := bufio.NewScanner(strings.NewReader(content))
   455  	for scanner.Scan() {
   456  		line := scanner.Text()
   457  
   458  		for marker, codeFragments := range codeFragmentsMap {
   459  			if marker.EqualsLine(line) {
   460  				for _, codeFragment := range codeFragments {
   461  					_, _ = out.WriteString(codeFragment) // bytes.Buffer.WriteString always returns nil errors
   462  				}
   463  			}
   464  		}
   465  
   466  		_, _ = out.WriteString(line + "\n") // bytes.Buffer.WriteString always returns nil errors
   467  	}
   468  	if err := scanner.Err(); err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	return out.Bytes(), nil
   473  }
   474  
   475  func (s Scaffold) writeFile(f *File) (err error) {
   476  	// Check if the file to write already exists
   477  	exists, err := afero.Exists(s.fs, f.Path)
   478  	if err != nil {
   479  		return ExistsFileError{err}
   480  	}
   481  	if exists {
   482  		switch f.IfExistsAction {
   483  		case OverwriteFile:
   484  			// By not returning, the file is written as if it didn't exist
   485  		case SkipFile:
   486  			// By returning nil, the file is not written but the process will carry on
   487  			return nil
   488  		case Error:
   489  			// By returning an error, the file is not written and the process will fail
   490  			return FileAlreadyExistsError{f.Path}
   491  		}
   492  	}
   493  
   494  	// Create the directory if needed
   495  	if err := s.fs.MkdirAll(filepath.Dir(f.Path), s.dirPerm); err != nil {
   496  		return CreateDirectoryError{err}
   497  	}
   498  
   499  	// Create or truncate the file
   500  	writer, err := s.fs.OpenFile(f.Path, createOrUpdate, s.filePerm)
   501  	if err != nil {
   502  		return CreateFileError{err}
   503  	}
   504  	defer func() {
   505  		if closeErr := writer.Close(); err == nil && closeErr != nil {
   506  			err = CloseFileError{err}
   507  		}
   508  	}()
   509  
   510  	if _, err := writer.Write([]byte(f.Contents)); err != nil {
   511  		return WriteFileError{err}
   512  	}
   513  
   514  	return nil
   515  }