github.com/joelanford/operator-sdk@v0.8.2/internal/pkg/scaffold/scaffold.go (about)

     1  // Copyright 2018 The Operator-SDK Authors
     2  //
     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  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Modified from github.com/kubernetes-sigs/controller-tools/pkg/scaffold/scaffold.go
    16  
    17  package scaffold
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"go/parser"
    23  	"go/token"
    24  	"io"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"text/template"
    29  
    30  	"github.com/operator-framework/operator-sdk/internal/pkg/scaffold/input"
    31  	"github.com/operator-framework/operator-sdk/internal/util/fileutil"
    32  
    33  	"github.com/pkg/errors"
    34  	log "github.com/sirupsen/logrus"
    35  	"github.com/spf13/afero"
    36  	"golang.org/x/tools/imports"
    37  )
    38  
    39  // Scaffold writes Templates to scaffold new files
    40  type Scaffold struct {
    41  	// Repo is the go project package
    42  	Repo string
    43  	// AbsProjectPath is the absolute path to the project root, including the project directory.
    44  	AbsProjectPath string
    45  	// ProjectName is the operator's name, ex. app-operator
    46  	ProjectName string
    47  	// Fs is the filesystem GetWriter uses to write scaffold files.
    48  	Fs afero.Fs
    49  	// GetWriter returns a writer for writing scaffold files.
    50  	GetWriter func(path string, mode os.FileMode) (io.Writer, error)
    51  	// BoilerplatePath is the path to a file containing Go boilerplate text.
    52  	BoilerplatePath string
    53  
    54  	// boilerplateBytes are bytes of Go boilerplate text.
    55  	boilerplateBytes []byte
    56  }
    57  
    58  func (s *Scaffold) setFieldsAndValidate(t input.File) error {
    59  	if b, ok := t.(input.Repo); ok {
    60  		b.SetRepo(s.Repo)
    61  	}
    62  	if b, ok := t.(input.AbsProjectPath); ok {
    63  		b.SetAbsProjectPath(s.AbsProjectPath)
    64  	}
    65  	if b, ok := t.(input.ProjectName); ok {
    66  		b.SetProjectName(s.ProjectName)
    67  	}
    68  
    69  	// Validate the template is ok
    70  	if v, ok := t.(input.Validate); ok {
    71  		if err := v.Validate(); err != nil {
    72  			return err
    73  		}
    74  	}
    75  	return nil
    76  }
    77  
    78  func (s *Scaffold) configure(cfg *input.Config) {
    79  	s.Repo = cfg.Repo
    80  	s.AbsProjectPath = cfg.AbsProjectPath
    81  	s.ProjectName = cfg.ProjectName
    82  }
    83  
    84  func validateBoilerplateBytes(b []byte) error {
    85  	// Append a 'package main' so we can parse the file.
    86  	fset := token.NewFileSet()
    87  	f, err := parser.ParseFile(fset, "", append([]byte("package main\n"), b...), parser.ParseComments)
    88  	if err != nil {
    89  		return fmt.Errorf("parse boilerplate comments: %v", err)
    90  	}
    91  	if len(f.Comments) == 0 {
    92  		return fmt.Errorf("boilerplate does not contain comments")
    93  	}
    94  	var cb []byte
    95  	for _, cg := range f.Comments {
    96  		for _, c := range cg.List {
    97  			cb = append(cb, []byte(strings.TrimSpace(c.Text)+"\n")...)
    98  		}
    99  	}
   100  	var tb []byte
   101  	tb, cb = bytes.TrimSpace(b), bytes.TrimSpace(cb)
   102  	if bytes.Compare(tb, cb) != 0 {
   103  		return fmt.Errorf(`boilerplate contains text other than comments:\n"%s"\n`, tb)
   104  	}
   105  	return nil
   106  }
   107  
   108  func wrapBoilerplateErr(err error, bp string) error {
   109  	return errors.Wrapf(err, `boilerplate file "%s"`, bp)
   110  }
   111  
   112  func (s *Scaffold) setBoilerplate() (err error) {
   113  	// If we've already set boilerplate bytes, don't overwrite them.
   114  	if len(s.boilerplateBytes) == 0 {
   115  		bp := s.BoilerplatePath
   116  		if bp == "" {
   117  			i, err := (&Boilerplate{}).GetInput()
   118  			if err != nil {
   119  				return wrapBoilerplateErr(err, i.Path)
   120  			}
   121  			if _, err := s.Fs.Stat(i.Path); err == nil {
   122  				bp = i.Path
   123  			}
   124  		}
   125  		if bp != "" {
   126  			b, err := afero.ReadFile(s.Fs, bp)
   127  			if err != nil {
   128  				return wrapBoilerplateErr(err, bp)
   129  			}
   130  			if err = validateBoilerplateBytes(b); err != nil {
   131  				return wrapBoilerplateErr(err, bp)
   132  			}
   133  			s.boilerplateBytes = append(bytes.TrimSpace(b), '\n', '\n')
   134  		}
   135  	}
   136  	return nil
   137  }
   138  
   139  // Execute executes scaffolding the Files
   140  func (s *Scaffold) Execute(cfg *input.Config, files ...input.File) error {
   141  	if s.Fs == nil {
   142  		s.Fs = afero.NewOsFs()
   143  	}
   144  	if s.GetWriter == nil {
   145  		s.GetWriter = fileutil.NewFileWriterFS(s.Fs).WriteCloser
   146  	}
   147  
   148  	// Generate boilerplate file first so new Go files get headers.
   149  	if err := s.setBoilerplate(); err != nil {
   150  		return err
   151  	}
   152  
   153  	// Configure s using common fields from cfg.
   154  	s.configure(cfg)
   155  
   156  	for _, f := range files {
   157  		if err := s.doFile(f); err != nil {
   158  			return err
   159  		}
   160  	}
   161  	return nil
   162  }
   163  
   164  // doFile scaffolds a single file
   165  func (s *Scaffold) doFile(e input.File) error {
   166  	// Set common fields
   167  	err := s.setFieldsAndValidate(e)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	// Get the template input params
   173  	i, err := e.GetInput()
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	// Ensure we use the absolute file path; i.Path is relative to the project root.
   179  	absFilePath := filepath.Join(s.AbsProjectPath, i.Path)
   180  
   181  	// Check if the file to write already exists
   182  	if _, err := s.Fs.Stat(absFilePath); err == nil || os.IsExist(err) {
   183  		switch i.IfExistsAction {
   184  		case input.Overwrite:
   185  		case input.Skip:
   186  			return nil
   187  		case input.Error:
   188  			return fmt.Errorf("%s already exists", absFilePath)
   189  		}
   190  	}
   191  
   192  	return s.doRender(i, e, absFilePath)
   193  }
   194  
   195  const goFileExt = ".go"
   196  
   197  func isGoFile(p string) bool {
   198  	return filepath.Ext(p) == goFileExt
   199  }
   200  
   201  func (s *Scaffold) doRender(i input.Input, e input.File, absPath string) error {
   202  	var mode os.FileMode = fileutil.DefaultFileMode
   203  	if i.IsExec {
   204  		mode = fileutil.DefaultExecFileMode
   205  	}
   206  	f, err := s.GetWriter(absPath, mode)
   207  	if err != nil {
   208  		return err
   209  	}
   210  	if c, ok := f.(io.Closer); ok {
   211  		defer func() {
   212  			if err := c.Close(); err != nil {
   213  				log.Fatal(err)
   214  			}
   215  		}()
   216  	}
   217  
   218  	var b []byte
   219  	if c, ok := e.(CustomRenderer); ok {
   220  		c.SetFS(s.Fs)
   221  		// CustomRenderers have a non-template method of file rendering.
   222  		if b, err = c.CustomRender(); err != nil {
   223  			return err
   224  		}
   225  	} else {
   226  		// All other files are rendered via their templates.
   227  		temp, err := newTemplate(i)
   228  		if err != nil {
   229  			return err
   230  		}
   231  
   232  		out := &bytes.Buffer{}
   233  		if err = temp.Execute(out, e); err != nil {
   234  			return err
   235  		}
   236  		b = out.Bytes()
   237  	}
   238  
   239  	// gofmt the imports
   240  	if isGoFile(absPath) {
   241  		b, err = imports.Process(absPath, b, nil)
   242  		if err != nil {
   243  			return err
   244  		}
   245  	}
   246  
   247  	// Files being overwritten must be trucated to len 0 so no old bytes remain.
   248  	if _, err = s.Fs.Stat(absPath); err == nil && i.IfExistsAction == input.Overwrite {
   249  		if file, ok := f.(afero.File); ok {
   250  			if err = file.Truncate(0); err != nil {
   251  				return err
   252  			}
   253  		}
   254  	}
   255  
   256  	if isGoFile(absPath) && len(s.boilerplateBytes) != 0 {
   257  		if _, err = f.Write(s.boilerplateBytes); err != nil {
   258  			return err
   259  		}
   260  	}
   261  	_, err = f.Write(b)
   262  	log.Infoln("Created", i.Path)
   263  	return err
   264  }
   265  
   266  // newTemplate returns a new template named by i.Path with common functions and
   267  // the input's TemplateFuncs.
   268  func newTemplate(i input.Input) (*template.Template, error) {
   269  	t := template.New(i.Path).Funcs(template.FuncMap{
   270  		"title": strings.Title,
   271  		"lower": strings.ToLower,
   272  	})
   273  	if len(i.TemplateFuncs) > 0 {
   274  		t.Funcs(i.TemplateFuncs)
   275  	}
   276  	if i.Delims[0] != "" && i.Delims[1] != "" {
   277  		t.Delims(i.Delims[0], i.Delims[1])
   278  	}
   279  	return t.Parse(i.TemplateBody)
   280  }