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 }