diff --git a/cmd/api.go b/cmd/api.go index ae4a8af689f..ec8b7026421 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -29,6 +29,7 @@ import ( "sigs.k8s.io/kubebuilder/cmd/internal" "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" "sigs.k8s.io/kubebuilder/plugins/addon" @@ -202,7 +203,7 @@ func (o *apiOptions) validate(c *config.Config) error { } func (o *apiOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { - plugins := make([]scaffold.Plugin, 0) + plugins := make([]model.Plugin, 0) switch strings.ToLower(o.pattern) { case "": // Default pattern diff --git a/pkg/model/plugin.go b/pkg/model/plugin.go new file mode 100644 index 00000000000..06f8854933e --- /dev/null +++ b/pkg/model/plugin.go @@ -0,0 +1,24 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package model + +// Plugin is the interface that a plugin must implement +// We will (later) have an ExecPlugin that implements this by exec-ing a binary +type Plugin interface { + // Pipe is the core plugin interface, that transforms a UniverseModel + Pipe(*Universe) error +} diff --git a/pkg/scaffold/api.go b/pkg/scaffold/api.go index cbfc6033625..7e0919ab024 100644 --- a/pkg/scaffold/api.go +++ b/pkg/scaffold/api.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/kubebuilder/internal/config" "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" controllerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/controller" crdv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/crd" @@ -38,7 +39,7 @@ type apiScaffolder struct { config *config.Config resource *resource.Resource // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []Plugin + plugins []model.Plugin // doResource indicates whether to scaffold API Resource or not doResource bool // doController indicates whether to scaffold controller files or not @@ -49,7 +50,7 @@ func NewAPIScaffolder( config *config.Config, res *resource.Resource, doResource, doController bool, - plugins []Plugin, + plugins []model.Plugin, ) Scaffolder { return &apiScaffolder{ plugins: plugins, @@ -93,7 +94,7 @@ func (s *apiScaffolder) scaffoldV1() error { return fmt.Errorf("error building API scaffold: %v", err) } - if err := (&Scaffold{}).Execute( + if err := (&machinery.Scaffold{}).Execute( universe, input.Options{}, &crdv1.Register{Resource: s.resource}, @@ -126,7 +127,7 @@ func (s *apiScaffolder) scaffoldV1() error { return fmt.Errorf("error building controller scaffold: %v", err) } - if err := (&Scaffold{}).Execute( + if err := (&machinery.Scaffold{}).Execute( universe, input.Options{}, &controllerv1.Controller{Resource: s.resource}, @@ -165,7 +166,7 @@ func (s *apiScaffolder) scaffoldV2() error { return fmt.Errorf("error building API scaffold: %v", err) } - if err := (&Scaffold{Plugins: s.plugins}).Execute( + if err := (&machinery.Scaffold{Plugins: s.plugins}).Execute( universe, input.Options{}, &scaffoldv2.Types{Input: input.Input{Path: path}, Resource: s.resource}, @@ -185,7 +186,7 @@ func (s *apiScaffolder) scaffoldV2() error { } kustomizationFile := &crdv2.Kustomization{Resource: s.resource} - if err := (&Scaffold{}).Execute( + if err := (&machinery.Scaffold{}).Execute( universe, input.Options{}, kustomizationFile, @@ -221,7 +222,7 @@ func (s *apiScaffolder) scaffoldV2() error { } suiteTestFile := &controllerv2.SuiteTest{Resource: s.resource} - if err := (&Scaffold{Plugins: s.plugins}).Execute( + if err := (&machinery.Scaffold{Plugins: s.plugins}).Execute( universe, input.Options{}, suiteTestFile, diff --git a/pkg/scaffold/init.go b/pkg/scaffold/init.go index 34f67e70a78..7601aea1735 100644 --- a/pkg/scaffold/init.go +++ b/pkg/scaffold/init.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/kubebuilder/internal/config" "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" scaffoldv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1" managerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" @@ -75,7 +76,7 @@ func (s *initScaffolder) Scaffold() error { return fmt.Errorf("error initializing project: %v", err) } - if err := (&Scaffold{BoilerplateOptional: true}).Execute( + if err := (&machinery.Scaffold{BoilerplateOptional: true}).Execute( universe, input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, &project.Boilerplate{ @@ -95,7 +96,7 @@ func (s *initScaffolder) Scaffold() error { return fmt.Errorf("error initializing project: %v", err) } - if err := (&Scaffold{}).Execute( + if err := (&machinery.Scaffold{}).Execute( universe, input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, &project.GitIgnore{}, @@ -124,7 +125,7 @@ func (s *initScaffolder) scaffoldV1() error { return fmt.Errorf("error initializing project: %v", err) } - return (&Scaffold{}).Execute( + return (&machinery.Scaffold{}).Execute( universe, input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, &project.KustomizeRBAC{}, @@ -154,7 +155,7 @@ func (s *initScaffolder) scaffoldV2() error { return fmt.Errorf("error initializing project: %v", err) } - return (&Scaffold{}).Execute( + return (&machinery.Scaffold{}).Execute( universe, input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, &metricsauthv2.AuthProxyPatch{}, diff --git a/pkg/scaffold/internal/filesystem/errors.go b/pkg/scaffold/internal/filesystem/errors.go new file mode 100644 index 00000000000..6591eb074dc --- /dev/null +++ b/pkg/scaffold/internal/filesystem/errors.go @@ -0,0 +1,93 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesystem + +import ( + "fmt" +) + +// This file contains the errors returned by the file system wrapper +// They are not exported as they should not be created outside of this package +// Exported functions are provided to check which kind of error was returned + +// createDirectoryError is returned if the directory could not be created +type createDirectoryError struct { + path string + err error +} + +func (e createDirectoryError) Error() string { + return fmt.Sprintf("failed to create directory for %s: %v", e.path, e.err) +} + +// IsCreateDirectoryError checks if the returned error is because the directory +// could not be created +func IsCreateDirectoryError(e error) bool { + _, ok := e.(createDirectoryError) + return ok +} + +// createFileError is returned if the file could not be created +type createFileError struct { + path string + err error +} + +func (e createFileError) Error() string { + return fmt.Sprintf("failed to create %s: %v", e.path, e.err) +} + +// IsCreateFileError checks if the returned error is because the file could not +// be created +func IsCreateFileError(e error) bool { + _, ok := e.(createFileError) + return ok +} + +// writeFileError is returned if the filed could not be written to +type writeFileError struct { + path string + err error +} + +func (e writeFileError) Error() string { + return fmt.Sprintf("failed to write to %s: %v", e.path, e.err) +} + +// IsWriteFileError checks if the returned error is because the file could not +// be written to +func IsWriteFileError(e error) bool { + _, ok := e.(writeFileError) + return ok +} + +// closeFileError is returned if the file could not be created +type closeFileError struct { + path string + err error +} + +func (e closeFileError) Error() string { + return fmt.Sprintf("failed to close %s: %v", e.path, e.err) +} + +// IsCloseFileError checks if the returned error is because the file could not +// be closed +func IsCloseFileError(e error) bool { + _, ok := e.(closeFileError) + return ok +} diff --git a/pkg/scaffold/internal/filesystem/filesystem.go b/pkg/scaffold/internal/filesystem/filesystem.go new file mode 100644 index 00000000000..d89fda56376 --- /dev/null +++ b/pkg/scaffold/internal/filesystem/filesystem.go @@ -0,0 +1,132 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesystem + +import ( + "io" + "log" + "os" + "path/filepath" + + "github.com/spf13/afero" +) + +const ( + createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC + + defaultDirectoryPermission os.FileMode = 0700 + defaultFilePermission os.FileMode = 0600 +) + +// FileSystem is an IO wrapper to create files +type FileSystem interface { + // Exists checks if the file exists + Exists(path string) (bool, error) + + // Create creates the directory and file and returns a self-closing + // io.Writer pointing to that file. If the file exists, it truncates it. + Create(path string) (io.Writer, error) +} + +// fileSystem implements FileSystem +type fileSystem struct { + fs afero.Fs + dirPerm os.FileMode + filePerm os.FileMode + fileMode int +} + +// New returns a new FileSystem +func New(options ...Options) FileSystem { + // Default values + fs := fileSystem{ + fs: afero.NewOsFs(), + dirPerm: defaultDirectoryPermission, + filePerm: defaultFilePermission, + fileMode: createOrUpdate, + } + + // Apply options + for _, option := range options { + option(&fs) + } + + return fs +} + +// Options configure FileSystem +type Options func(system *fileSystem) + +// DirectoryPermissions makes FileSystem.Create use the provided directory +// permissions +func DirectoryPermissions(dirPerm os.FileMode) Options { + return func(fs *fileSystem) { + fs.dirPerm = dirPerm + } +} + +// FilePermissions makes FileSystem.Create use the provided file permissions +func FilePermissions(filePerm os.FileMode) Options { + return func(fs *fileSystem) { + fs.filePerm = filePerm + } +} + +// Exists implements FileSystem.Exists +func (fs fileSystem) Exists(path string) (bool, error) { + return afero.Exists(fs.fs, path) +} + +// Create implements FileSystem.Create +func (fs fileSystem) Create(path string) (io.Writer, error) { + // Create the directory if needed + if err := fs.fs.MkdirAll(filepath.Dir(path), fs.dirPerm); err != nil { + return nil, createDirectoryError{path, err} + } + + // Create or truncate the file + wc, err := fs.fs.OpenFile(path, fs.fileMode, fs.filePerm) + if err != nil { + return nil, createFileError{path, err} + } + + return &file{path, wc}, nil +} + +// file implements io.Writer +type file struct { + path string + io.WriteCloser +} + +// Write implements io.Writer.Write +func (f *file) Write(content []byte) (int, error) { + // Close the file when we end writing + defer func() { + if err := f.Close(); err != nil { + log.Fatal(closeFileError{f.path, err}) + } + }() + + // Write the content + n, err := f.WriteCloser.Write(content) + if err != nil { + return n, writeFileError{f.path, err} + } + + return n, nil +} diff --git a/pkg/scaffold/internal/filesystem/mock.go b/pkg/scaffold/internal/filesystem/mock.go new file mode 100644 index 00000000000..b83b080da39 --- /dev/null +++ b/pkg/scaffold/internal/filesystem/mock.go @@ -0,0 +1,166 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesystem + +import ( + "bytes" + "io" + "log" + + "github.com/onsi/ginkgo" + "github.com/onsi/gomega" +) + +// mockFileSystem implements FileSystem +type mockFileSystem struct { + path string + exists func(path string) bool + existsError error + createDirError error + createFileError error + output *bytes.Buffer + writeFileError error + closeFileError error +} + +// NewMock returns a new FileSystem +func NewMock(options ...MockOptions) FileSystem { + // Default values + fs := mockFileSystem{ + exists: func(_ string) bool { return false }, + output: new(bytes.Buffer), + } + + // Apply options + for _, option := range options { + option(&fs) + } + + return fs +} + +// MockOptions configure FileSystem +type MockOptions func(system *mockFileSystem) + +// MockPath ensures that the file created with this scaffold is at path +func MockPath(path string) MockOptions { + return func(fs *mockFileSystem) { + fs.path = path + } +} + +// MockExists makes FileSystem.Exists use the provided function to check if the +// file exists +func MockExists(exists func(path string) bool) MockOptions { + return func(fs *mockFileSystem) { + fs.exists = exists + } +} + +// MockExistsError makes FileSystem.Exists return err +func MockExistsError(err error) MockOptions { + return func(fs *mockFileSystem) { + fs.existsError = err + } +} + +// MockCreateDirError makes FileSystem.Create return err +func MockCreateDirError(err error) MockOptions { + return func(fs *mockFileSystem) { + fs.createDirError = err + } +} + +// MockCreateFileError makes FileSystem.Create return err +func MockCreateFileError(err error) MockOptions { + return func(fs *mockFileSystem) { + fs.createFileError = err + } +} + +// MockOutput provides a buffer where the content will be written +func MockOutput(output *bytes.Buffer) MockOptions { + return func(fs *mockFileSystem) { + fs.output = output + } +} + +// MockWriteFileError makes the Write method (of the io.Writer returned by +// FileSystem.Create) return err +func MockWriteFileError(err error) MockOptions { + return func(fs *mockFileSystem) { + fs.writeFileError = err + } +} + +// MockCloseFileError makes the Write method (of the io.Writer returned by +// FileSystem.Create) return err +func MockCloseFileError(err error) MockOptions { + return func(fs *mockFileSystem) { + fs.closeFileError = err + } +} + +// Exists implements FileSystem.Exists +func (fs mockFileSystem) Exists(path string) (bool, error) { + if fs.existsError != nil { + return false, fs.existsError + } + + return fs.exists(path), nil +} + +// Create implements FileSystem.Create +func (fs mockFileSystem) Create(path string) (io.Writer, error) { + if fs.path != "" { + ginkgo.GinkgoRecover() + gomega.Expect(path).To(gomega.Equal(fs.path)) + } + + if fs.createDirError != nil { + return nil, createDirectoryError{path, fs.createDirError} + } + + if fs.createFileError != nil { + return nil, createFileError{path, fs.createFileError} + } + + return &mockFile{path, fs.output, fs.writeFileError, fs.closeFileError}, nil +} + +// mockFile implements io.Writer mocking a file for tests +type mockFile struct { + path string + content *bytes.Buffer + writeFileError error + closeFileError error +} + +// Write implements io.Writer.Write +func (f *mockFile) Write(content []byte) (int, error) { + defer func() { + if f.closeFileError != nil { + log.Fatal(closeFileError{path: f.path, err: f.closeFileError}) + } + }() + + if f.writeFileError != nil { + return 0, writeFileError{path: f.path, err: f.writeFileError} + } + + return f.content.Write(content) +} diff --git a/pkg/scaffold/internal/machinery/mock.go b/pkg/scaffold/internal/machinery/mock.go new file mode 100644 index 00000000000..bd90306e14f --- /dev/null +++ b/pkg/scaffold/internal/machinery/mock.go @@ -0,0 +1,86 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package machinery + +import ( + "bytes" + "go/build" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/filesystem" +) + +// TestResult is the result of running the scaffolding. +type TestResult struct { + // Actual is the bytes written to a scaffolded file. + Actual bytes.Buffer + + // Golden is the golden file contents read from the controller-tools/testdata package + Golden string +} + +func prjPath() string { + return path.Join(build.Default.GOPATH, "src", "sigs.k8s.io", "kubebuilder", "testdata", "gopath") +} + +func prjRoot() string { + return path.Join(prjPath(), "src", "project") +} + +// MockOptions are the options for scaffolding in the controller-tools/testdata directory +func MockOptions() input.Options { + return input.Options{ + BoilerplatePath: filepath.Join(prjRoot(), "hack", "boilerplate.go.txt"), + ProjectPath: filepath.Join(prjRoot(), "PROJECT"), + } +} + +// NewMockScaffold returns a new Scaffold and TestResult instance for testing +func NewMockScaffold(writeToPath, goldenPath string) (*Scaffold, *TestResult) { + // Check that the test-GOPATH exists + if _, err := os.Stat(prjPath()); err != nil { + panic(err) + } + + // Create the result + r := &TestResult{} + + // Setup scaffold + s := &Scaffold{ + fs: filesystem.NewMock( + filesystem.MockPath(writeToPath), + filesystem.MockExists(func(path string) bool { return path != writeToPath }), + filesystem.MockOutput(&r.Actual), + ), + ConfigPath: prjRoot(), + } + + // Read the golden file into the result + if len(goldenPath) > 0 { + b, err := ioutil.ReadFile(filepath.Join(prjRoot(), goldenPath)) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + r.Golden = string(b) + } + + return s, r +} diff --git a/pkg/scaffold/scaffold.go b/pkg/scaffold/internal/machinery/scaffold.go similarity index 86% rename from pkg/scaffold/scaffold.go rename to pkg/scaffold/internal/machinery/scaffold.go index 989baab513f..e6e33c2bfc4 100644 --- a/pkg/scaffold/scaffold.go +++ b/pkg/scaffold/internal/machinery/scaffold.go @@ -14,15 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package scaffold +package machinery import ( "bytes" "fmt" - "io" "io/ioutil" - "log" - "os" "path/filepath" "strings" "text/template" @@ -33,6 +30,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/filesystem" ) var options = imports.Options{ @@ -56,12 +54,11 @@ type Scaffold struct { // ConfigPath is the relative path to the project root ConfigPath string - GetWriter func(path string) (io.Writer, error) + // fs allows to mock the file system for tests + fs filesystem.FileSystem // Plugins is the list of plugins we should allow to transform our generated scaffolding - Plugins []Plugin - - FileExists func(path string) bool + Plugins []model.Plugin // BoilerplateOptional, if true, skips errors reading the Boilerplate file BoilerplateOptional bool @@ -70,13 +67,6 @@ type Scaffold struct { ConfigOptional bool } -// Plugin is the interface that a plugin must implement -// We will (later) have an ExecPlugin that implements this by exec-ing a binary -type Plugin interface { - // Pipe is the core plugin interface, that transforms a UniverseModel - Pipe(universe *model.Universe) error -} - func (s *Scaffold) setFields(t input.File) { // Inject project configuration into file templates if s.Config != nil { @@ -159,19 +149,9 @@ func (s *Scaffold) universeDefaults(universe *model.Universe, files int) { } // Execute executes scaffolding the for files -func (s *Scaffold) Execute( - universe *model.Universe, - options input.Options, - files ...input.File, -) error { - if s.GetWriter == nil { - s.GetWriter = (&FileWriter{}).WriteCloser - } - if s.FileExists == nil { - s.FileExists = func(path string) bool { - _, err := os.Stat(path) - return err == nil - } +func (s *Scaffold) Execute(universe *model.Universe, options input.Options, files ...input.File) error { + if s.fs == nil { + s.fs = filesystem.New() } if err := s.defaultOptions(&options); err != nil { @@ -237,27 +217,24 @@ func (s *Scaffold) buildFileModel(e input.File) (*model.File, error) { func (s *Scaffold) writeFile(file *model.File) error { // Check if the file to write already exists - if s.FileExists(file.Path) { + exists, err := s.fs.Exists(file.Path) + if err != nil { + return err + } + if exists { switch file.IfExistsAction { case input.Overwrite: case input.Skip: return nil case input.Error: - return fmt.Errorf("%s already exists", file.Path) + return fmt.Errorf("failed to create %s: file already exists", file.Path) } } - f, err := s.GetWriter(file.Path) + f, err := s.fs.Create(file.Path) if err != nil { return err } - if c, ok := f.(io.Closer); ok { - defer func() { - if err := c.Close(); err != nil { - log.Fatal(err) - } - }() - } _, err = f.Write([]byte(file.Contents)) diff --git a/pkg/scaffold/project/project_test.go b/pkg/scaffold/project/project_test.go index 0115b6bdd58..2f957182487 100644 --- a/pkg/scaffold/project/project_test.go +++ b/pkg/scaffold/project/project_test.go @@ -29,10 +29,9 @@ import ( . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/pkg/model" - "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" - "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" scaffoldv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/metricsauth" ) @@ -40,13 +39,13 @@ import ( const repo = "project" var _ = Describe("Project", func() { - var result *scaffoldtest.TestResult + var result *machinery.TestResult var writeToPath, goldenPath string - var s *scaffold.Scaffold + var s *machinery.Scaffold var year string JustBeforeEach(func() { - s, result = scaffoldtest.NewTestScaffold(writeToPath, goldenPath) + s, result = machinery.NewMockScaffold(writeToPath, goldenPath) s.BoilerplateOptional = true s.ConfigOptional = true year = strconv.Itoa(time.Now().Year()) diff --git a/pkg/scaffold/scaffoldtest/scaffoldtest.go b/pkg/scaffold/scaffoldtest/scaffoldtest.go deleted file mode 100644 index a16b8b12e51..00000000000 --- a/pkg/scaffold/scaffoldtest/scaffoldtest.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package scaffoldtest - -import ( - "bytes" - "go/build" - "io" - "io/ioutil" - "os" - "path" - "path/filepath" - - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/pkg/scaffold" - "sigs.k8s.io/kubebuilder/pkg/scaffold/input" -) - -// TestResult is the result of running the scaffolding. -type TestResult struct { - // Actual is the bytes written to a scaffolded file. - Actual bytes.Buffer - - // Golden is the golden file contents read from the controller-tools/testdata package - Golden string -} - -func getProjectRoot() string { - return path.Join(build.Default.GOPATH, "src", "sigs.k8s.io", "kubebuilder") -} - -// ProjectPath is the path to the controller-tools/testdata project file -func ProjectPath() string { - return filepath.Join(getProjectRoot(), "testdata", "gopath", "src", "project", "PROJECT") -} - -// BoilerplatePath is the path to the controller-tools/testdata boilerplate file -func BoilerplatePath() string { - return filepath.Join(getProjectRoot(), "testdata", "gopath", "src", "project", "hack", "boilerplate.go.txt") -} - -// Options are the options for scaffolding in the controller-tools/testdata directory -func Options() input.Options { - return input.Options{ - BoilerplatePath: BoilerplatePath(), - ProjectPath: ProjectPath(), - } -} - -// NewTestScaffold returns a new Scaffold and TestResult instance for testing -func NewTestScaffold(writeToPath, goldenPath string) (*scaffold.Scaffold, *TestResult) { - projRoot := getProjectRoot() - r := &TestResult{} - // Setup scaffold - s := &scaffold.Scaffold{ - GetWriter: func(path string) (io.Writer, error) { - defer ginkgo.GinkgoRecover() - gomega.Expect(path).To(gomega.Equal(writeToPath)) - return &r.Actual, nil - }, - FileExists: func(path string) bool { - return path != writeToPath - }, - ConfigPath: filepath.Join(projRoot, "testdata", "gopath", "src", "project"), - } - oldGoPath := build.Default.GOPATH - build.Default.GOPATH = filepath.Join(projRoot, "testdata", "gopath") - defer func() { build.Default.GOPATH = oldGoPath }() - if _, err := os.Stat(build.Default.GOPATH); err != nil { - panic(err) - } - - if len(goldenPath) > 0 { - b, err := ioutil.ReadFile(filepath.Join(projRoot, "testdata", "gopath", "src", "project", goldenPath)) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - r.Golden = string(b) - } - return s, r -} diff --git a/pkg/scaffold/update.go b/pkg/scaffold/update.go index e20fa4d4e32..6ed85d5003f 100644 --- a/pkg/scaffold/update.go +++ b/pkg/scaffold/update.go @@ -20,6 +20,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" ) @@ -42,7 +43,7 @@ func (s *updateScaffolder) Scaffold() error { return err } - return (&Scaffold{}).Execute( + return (&machinery.Scaffold{}).Execute( universe, input.Options{}, &project.GopkgToml{}, diff --git a/pkg/scaffold/v1/crd/suite_test.go b/pkg/scaffold/v1/crd/suite_test.go index 294a4173c7f..8ed711bafe6 100644 --- a/pkg/scaffold/v1/crd/suite_test.go +++ b/pkg/scaffold/v1/crd/suite_test.go @@ -27,8 +27,8 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" - "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" . "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/crd" ) @@ -95,8 +95,8 @@ var _ = Describe("Resource", func() { f := files[j] Context(f.file, func() { It(fmt.Sprintf("should write a file matching the golden file %s", f.file), func() { - s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) + s, result := machinery.NewMockScaffold(f.file, f.file) + Expect(s.Execute(&model.Universe{}, machinery.MockOptions(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) diff --git a/pkg/scaffold/v1/manager/manager_test.go b/pkg/scaffold/v1/manager/manager_test.go index b353dd4dbb2..014cf896332 100644 --- a/pkg/scaffold/v1/manager/manager_test.go +++ b/pkg/scaffold/v1/manager/manager_test.go @@ -25,7 +25,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" - "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" ) @@ -65,8 +65,8 @@ var _ = Describe("Manager", func() { f := files[j] Context(f.file, func() { It(fmt.Sprintf("should write a file matching the golden file %s", f.file), func() { - s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) + s, result := machinery.NewMockScaffold(f.file, f.file) + Expect(s.Execute(&model.Universe{}, machinery.MockOptions(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) @@ -77,9 +77,9 @@ var _ = Describe("Manager", func() { Context("APIs", func() { It("should return an error if the relative path cannot be calculated", func() { instance := &manager.APIs{} - s, _ := scaffoldtest.NewTestScaffold(filepath.Join("pkg", "apis", "apis.go"), "") + s, _ := machinery.NewMockScaffold(filepath.Join("pkg", "apis", "apis.go"), "") s.ConfigPath = "." - err := s.Execute(&model.Universe{}, scaffoldtest.Options(), instance) + err := s.Execute(&model.Universe{}, machinery.MockOptions(), instance) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Rel: can't make")) }) diff --git a/pkg/scaffold/v1/webhook/webhook_test.go b/pkg/scaffold/v1/webhook/webhook_test.go index a23cddef80e..2ee54c2faa9 100644 --- a/pkg/scaffold/v1/webhook/webhook_test.go +++ b/pkg/scaffold/v1/webhook/webhook_test.go @@ -26,8 +26,8 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" - "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" . "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/webhook" ) @@ -181,8 +181,8 @@ var _ = Describe("Webhook", func() { f := files[j] Context(f.file, func() { It(fmt.Sprintf("should write a file matching the golden file %s", f.file), func() { - s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) + s, result := machinery.NewMockScaffold(f.file, f.file) + Expect(s.Execute(&model.Universe{}, machinery.MockOptions(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) diff --git a/pkg/scaffold/webhook.go b/pkg/scaffold/webhook.go index fd1f8fb31cb..51050e85694 100644 --- a/pkg/scaffold/webhook.go +++ b/pkg/scaffold/webhook.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/internal/machinery" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" managerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" webhookv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/webhook" @@ -99,7 +100,7 @@ func (s *webhookScaffolder) scaffoldV1() error { webhookConfig := webhookv1.Config{Server: s.server, Type: s.webhookType, Operations: s.operations} - return (&Scaffold{}).Execute( + return (&machinery.Scaffold{}).Execute( universe, input.Options{}, &managerv1.Webhook{}, @@ -140,7 +141,7 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f Defaulting: s.defaulting, Validating: s.validation, } - if err := (&Scaffold{}).Execute( + if err := (&machinery.Scaffold{}).Execute( universe, input.Options{}, webhookScaffolder,