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 }