sigs.k8s.io/kubebuilder/v3@v3.14.0/pkg/machinery/scaffold_test.go (about) 1 /* 2 Licensed under the Apache License, Version 2.0 (the "License"); 3 you may not use this file except in compliance with the License. 4 You may obtain a copy of the License at 5 6 http://www.apache.org/licenses/LICENSE-2.0 7 8 Unless required by applicable law or agreed to in writing, software 9 distributed under the License is distributed on an "AS IS" BASIS, 10 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 See the License for the specific language governing permissions and 12 limitations under the License. 13 */ 14 15 package machinery 16 17 import ( 18 "errors" 19 "os" 20 21 . "github.com/onsi/ginkgo/v2" 22 . "github.com/onsi/gomega" 23 "github.com/spf13/afero" 24 25 cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" 26 "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" 27 ) 28 29 var _ = Describe("Scaffold", func() { 30 Describe("NewScaffold", func() { 31 It("should succeed for no option", func() { 32 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}) 33 Expect(s.fs).NotTo(BeNil()) 34 Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) 35 Expect(s.filePerm).To(Equal(defaultFilePermission)) 36 Expect(s.injector.config).To(BeNil()) 37 Expect(s.injector.boilerplate).To(Equal("")) 38 Expect(s.injector.resource).To(BeNil()) 39 }) 40 41 It("should succeed with directory permissions option", func() { 42 const dirPermissions os.FileMode = 0o755 43 44 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithDirectoryPermissions(dirPermissions)) 45 Expect(s.fs).NotTo(BeNil()) 46 Expect(s.dirPerm).To(Equal(dirPermissions)) 47 Expect(s.filePerm).To(Equal(defaultFilePermission)) 48 Expect(s.injector.config).To(BeNil()) 49 Expect(s.injector.boilerplate).To(Equal("")) 50 Expect(s.injector.resource).To(BeNil()) 51 }) 52 53 It("should succeed with file permissions option", func() { 54 const filePermissions os.FileMode = 0o755 55 56 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithFilePermissions(filePermissions)) 57 Expect(s.fs).NotTo(BeNil()) 58 Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) 59 Expect(s.filePerm).To(Equal(filePermissions)) 60 Expect(s.injector.config).To(BeNil()) 61 Expect(s.injector.boilerplate).To(Equal("")) 62 Expect(s.injector.resource).To(BeNil()) 63 }) 64 65 It("should succeed with config option", func() { 66 cfg := cfgv3.New() 67 68 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithConfig(cfg)) 69 Expect(s.fs).NotTo(BeNil()) 70 Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) 71 Expect(s.filePerm).To(Equal(defaultFilePermission)) 72 Expect(s.injector.config).NotTo(BeNil()) 73 Expect(s.injector.config.GetVersion().Compare(cfgv3.Version)).To(Equal(0)) 74 Expect(s.injector.boilerplate).To(Equal("")) 75 Expect(s.injector.resource).To(BeNil()) 76 }) 77 78 It("should succeed with boilerplate option", func() { 79 const boilerplate = "Copyright" 80 81 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithBoilerplate(boilerplate)) 82 Expect(s.fs).NotTo(BeNil()) 83 Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) 84 Expect(s.filePerm).To(Equal(defaultFilePermission)) 85 Expect(s.injector.config).To(BeNil()) 86 Expect(s.injector.boilerplate).To(Equal(boilerplate)) 87 Expect(s.injector.resource).To(BeNil()) 88 }) 89 90 It("should succeed with resource option", func() { 91 res := &resource.Resource{GVK: resource.GVK{ 92 Group: "group", 93 Domain: "my.domain", 94 Version: "v1", 95 Kind: "Kind", 96 }} 97 98 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithResource(res)) 99 Expect(s.fs).NotTo(BeNil()) 100 Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) 101 Expect(s.filePerm).To(Equal(defaultFilePermission)) 102 Expect(s.injector.config).To(BeNil()) 103 Expect(s.injector.boilerplate).To(Equal("")) 104 Expect(s.injector.resource).NotTo(BeNil()) 105 Expect(s.injector.resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) 106 }) 107 }) 108 109 Describe("Scaffold.Execute", func() { 110 const ( 111 path = "filename" 112 pathGo = path + ".go" 113 pathYaml = path + ".yaml" 114 content = "Hello world!" 115 ) 116 117 var ( 118 testErr = errors.New("error text") 119 120 s *Scaffold 121 ) 122 123 BeforeEach(func() { 124 s = &Scaffold{fs: afero.NewMemMapFs()} 125 }) 126 127 DescribeTable("successes", 128 func(path, expected string, files ...Builder) { 129 Expect(s.Execute(files...)).To(Succeed()) 130 131 b, err := afero.ReadFile(s.fs, path) 132 Expect(err).NotTo(HaveOccurred()) 133 Expect(string(b)).To(Equal(expected)) 134 }, 135 Entry("should write the file", 136 path, content, 137 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, 138 ), 139 Entry("should skip optional models if already have one", 140 path, content, 141 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, 142 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, 143 ), 144 Entry("should overwrite required models if already have one", 145 path, content, 146 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, 147 &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content}, 148 ), 149 Entry("should format a go file", 150 pathGo, "package file\n", 151 &fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: "package file"}, 152 ), 153 154 Entry("should render actions correctly", 155 path, "package testValue", 156 &fakeTemplate{fakeBuilder: fakeBuilder{path: path, TestField: "testValue"}, body: "package {{.TestField}}"}, 157 ), 158 159 Entry("should render actions with alternative delimiters correctly", 160 path, "package testValue", 161 &fakeTemplate{fakeBuilder: fakeBuilder{path: path, TestField: "testValue"}, 162 body: "package [[.TestField]]", parseDelimLeft: "[[", parseDelimRight: "]]"}, 163 ), 164 ) 165 166 DescribeTable("file builders related errors", 167 func(errType interface{}, files ...Builder) { 168 err := s.Execute(files...) 169 Expect(err).To(HaveOccurred()) 170 Expect(errors.As(err, errType)).To(BeTrue()) 171 }, 172 Entry("should fail if unable to validate a file builder", 173 &ValidateError{}, 174 fakeRequiresValidation{validateErr: testErr}, 175 ), 176 Entry("should fail if unable to set default values for a template", 177 &SetTemplateDefaultsError{}, 178 &fakeTemplate{err: testErr}, 179 ), 180 Entry("should fail if an unexpected previous model is found", 181 &ModelAlreadyExistsError{}, 182 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, 183 &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}}, 184 ), 185 Entry("should fail if behavior if-exists-action is not defined", 186 &UnknownIfExistsActionError{}, 187 &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, 188 &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: -1}}, 189 ), 190 ) 191 192 // Following errors are unwrapped, so we need to check for substrings 193 DescribeTable("template related errors", 194 func(errMsg string, files ...Builder) { 195 err := s.Execute(files...) 196 Expect(err).To(HaveOccurred()) 197 Expect(err.Error()).To(ContainSubstring(errMsg)) 198 }, 199 Entry("should fail if a template is broken", 200 "template: ", 201 &fakeTemplate{body: "{{ .Field }"}, 202 ), 203 Entry("should fail if a template params aren't provided", 204 "template: ", 205 &fakeTemplate{body: "{{ .Field }}"}, 206 ), 207 Entry("should fail if unable to format a go file", 208 "expected 'package', found ", 209 &fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: content}, 210 ), 211 ) 212 213 DescribeTable("insert strings", 214 func(path, input, expected string, files ...Builder) { 215 Expect(afero.WriteFile(s.fs, path, []byte(input), 0o666)).To(Succeed()) 216 217 Expect(s.Execute(files...)).To(Succeed()) 218 219 b, err := afero.ReadFile(s.fs, path) 220 Expect(err).NotTo(HaveOccurred()) 221 Expect(string(b)).To(Equal(expected)) 222 }, 223 Entry("should insert lines for go files", 224 pathGo, 225 `package test 226 227 //+kubebuilder:scaffold:- 228 `, 229 `package test 230 231 var a int 232 var b int 233 234 //+kubebuilder:scaffold:- 235 `, 236 fakeInserter{ 237 fakeBuilder: fakeBuilder{path: pathGo}, 238 codeFragments: CodeFragmentsMap{ 239 NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, 240 }, 241 }, 242 ), 243 Entry("should insert lines for yaml files", 244 pathYaml, 245 ` 246 #+kubebuilder:scaffold:- 247 `, 248 ` 249 1 250 2 251 #+kubebuilder:scaffold:- 252 `, 253 fakeInserter{ 254 fakeBuilder: fakeBuilder{path: pathYaml}, 255 codeFragments: CodeFragmentsMap{ 256 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 257 }, 258 }, 259 ), 260 Entry("should use models if there is no file", 261 pathYaml, 262 "", 263 ` 264 1 265 2 266 #+kubebuilder:scaffold:- 267 `, 268 &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` 269 #+kubebuilder:scaffold:- 270 `}, 271 fakeInserter{ 272 fakeBuilder: fakeBuilder{path: pathYaml}, 273 codeFragments: CodeFragmentsMap{ 274 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 275 }, 276 }, 277 ), 278 Entry("should use required models over files", 279 pathYaml, 280 content, 281 ` 282 1 283 2 284 #+kubebuilder:scaffold:- 285 `, 286 &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` 287 #+kubebuilder:scaffold:- 288 `}, 289 fakeInserter{ 290 fakeBuilder: fakeBuilder{path: pathYaml}, 291 codeFragments: CodeFragmentsMap{ 292 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 293 }, 294 }, 295 ), 296 Entry("should use files over optional models", 297 pathYaml, 298 ` 299 #+kubebuilder:scaffold:- 300 `, 301 ` 302 1 303 2 304 #+kubebuilder:scaffold:- 305 `, 306 &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml}, body: content}, 307 fakeInserter{ 308 fakeBuilder: fakeBuilder{path: pathYaml}, 309 codeFragments: CodeFragmentsMap{ 310 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 311 }, 312 }, 313 ), 314 Entry("should filter invalid markers", 315 pathYaml, 316 ` 317 #+kubebuilder:scaffold:- 318 #+kubebuilder:scaffold:* 319 `, 320 ` 321 1 322 2 323 #+kubebuilder:scaffold:- 324 #+kubebuilder:scaffold:* 325 `, 326 fakeInserter{ 327 fakeBuilder: fakeBuilder{path: pathYaml}, 328 markers: []Marker{NewMarkerFor(pathYaml, "-")}, 329 codeFragments: CodeFragmentsMap{ 330 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 331 NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, 332 }, 333 }, 334 ), 335 Entry("should filter already existing one-line code fragments", 336 pathYaml, 337 ` 338 1 339 #+kubebuilder:scaffold:- 340 3 341 4 342 #+kubebuilder:scaffold:* 343 `, 344 ` 345 1 346 2 347 #+kubebuilder:scaffold:- 348 3 349 4 350 #+kubebuilder:scaffold:* 351 `, 352 fakeInserter{ 353 fakeBuilder: fakeBuilder{path: pathYaml}, 354 codeFragments: CodeFragmentsMap{ 355 NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, 356 NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, 357 }, 358 }, 359 ), 360 Entry("should filter already existing multi-line indented code fragments", 361 pathGo, 362 `package test 363 364 func init() { 365 if err := something(); err != nil { 366 return err 367 } 368 369 //+kubebuilder:scaffold:- 370 } 371 `, 372 `package test 373 374 func init() { 375 if err := something(); err != nil { 376 return err 377 } 378 379 //+kubebuilder:scaffold:- 380 } 381 `, 382 fakeInserter{ 383 fakeBuilder: fakeBuilder{path: pathGo}, 384 codeFragments: CodeFragmentsMap{ 385 NewMarkerFor(pathGo, "-"): {"if err := something(); err != nil {\n\treturn err\n}\n\n"}, 386 }, 387 }, 388 ), 389 Entry("should not insert anything if no code fragment", 390 pathYaml, 391 ` 392 #+kubebuilder:scaffold:- 393 `, 394 ` 395 #+kubebuilder:scaffold:- 396 `, 397 fakeInserter{ 398 fakeBuilder: fakeBuilder{path: pathYaml}, 399 codeFragments: CodeFragmentsMap{ 400 NewMarkerFor(pathYaml, "-"): {}, 401 }, 402 }, 403 ), 404 ) 405 406 DescribeTable("insert strings related errors", 407 func(errType interface{}, files ...Builder) { 408 Expect(afero.WriteFile(s.fs, path, []byte{}, 0o666)).To(Succeed()) 409 410 err := s.Execute(files...) 411 Expect(err).To(HaveOccurred()) 412 Expect(errors.As(err, errType)).To(BeTrue()) 413 }, 414 Entry("should fail if inserting into a model that fails when a file exists and it does exist", 415 &FileAlreadyExistsError{}, 416 &fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: Error}}, 417 fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, 418 ), 419 Entry("should fail if inserting into a model with unknown behavior if the file exists and it does exist", 420 &UnknownIfExistsActionError{}, 421 &fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, 422 fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, 423 ), 424 ) 425 426 Context("write when the file already exists", func() { 427 BeforeEach(func() { 428 _ = afero.WriteFile(s.fs, path, []byte{}, 0o666) 429 }) 430 431 It("should skip the file by default", func() { 432 Expect(s.Execute(&fakeTemplate{ 433 fakeBuilder: fakeBuilder{path: path}, 434 body: content, 435 })).To(Succeed()) 436 437 b, err := afero.ReadFile(s.fs, path) 438 Expect(err).NotTo(HaveOccurred()) 439 Expect(string(b)).To(BeEmpty()) 440 }) 441 442 It("should write the file if configured to do so", func() { 443 Expect(s.Execute(&fakeTemplate{ 444 fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, 445 body: content, 446 })).To(Succeed()) 447 448 b, err := afero.ReadFile(s.fs, path) 449 Expect(err).NotTo(HaveOccurred()) 450 Expect(string(b)).To(Equal(content)) 451 }) 452 453 It("should error if configured to do so", func() { 454 err := s.Execute(&fakeTemplate{ 455 fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}, 456 body: content, 457 }) 458 Expect(err).To(HaveOccurred()) 459 Expect(errors.As(err, &FileAlreadyExistsError{})).To(BeTrue()) 460 }) 461 }) 462 }) 463 }) 464 465 var _ Builder = fakeBuilder{} 466 467 // fakeBuilder is used to mock a Builder 468 type fakeBuilder struct { 469 path string 470 ifExistsAction IfExistsAction 471 TestField string // test go template actions 472 } 473 474 // GetPath implements Builder 475 func (f fakeBuilder) GetPath() string { 476 return f.path 477 } 478 479 // GetIfExistsAction implements Builder 480 func (f fakeBuilder) GetIfExistsAction() IfExistsAction { 481 return f.ifExistsAction 482 } 483 484 var _ RequiresValidation = fakeRequiresValidation{} 485 486 // fakeRequiresValidation is used to mock a RequiresValidation in order to test Scaffold 487 type fakeRequiresValidation struct { 488 fakeBuilder 489 490 validateErr error 491 } 492 493 // Validate implements RequiresValidation 494 func (f fakeRequiresValidation) Validate() error { 495 return f.validateErr 496 } 497 498 var _ Template = &fakeTemplate{} 499 500 // fakeTemplate is used to mock a File in order to test Scaffold 501 type fakeTemplate struct { 502 fakeBuilder 503 504 body string 505 err error 506 parseDelimLeft string 507 parseDelimRight string 508 } 509 510 func (f *fakeTemplate) SetDelim(left, right string) { 511 f.parseDelimLeft = left 512 f.parseDelimRight = right 513 } 514 515 func (f *fakeTemplate) GetDelim() (string, string) { 516 return f.parseDelimLeft, f.parseDelimRight 517 } 518 519 // GetBody implements Template 520 func (f *fakeTemplate) GetBody() string { 521 return f.body 522 } 523 524 // SetTemplateDefaults implements Template 525 func (f *fakeTemplate) SetTemplateDefaults() error { 526 if f.err != nil { 527 return f.err 528 } 529 530 return nil 531 } 532 533 type fakeInserter struct { 534 fakeBuilder 535 536 markers []Marker 537 codeFragments CodeFragmentsMap 538 } 539 540 // GetMarkers implements Inserter 541 func (f fakeInserter) GetMarkers() []Marker { 542 if f.markers != nil { 543 return f.markers 544 } 545 546 markers := make([]Marker, 0, len(f.codeFragments)) 547 for marker := range f.codeFragments { 548 markers = append(markers, marker) 549 } 550 return markers 551 } 552 553 // GetCodeFragments implements Inserter 554 func (f fakeInserter) GetCodeFragments() CodeFragmentsMap { 555 return f.codeFragments 556 }