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  }