From 450aa15a226ef1e61045824268188043d3f30a6e Mon Sep 17 00:00:00 2001 From: Adrian Orive Date: Thu, 4 Feb 2021 12:50:55 +0100 Subject: [PATCH] Store abstraction for persisting Config - Use PreRunE and PostRunE functions to handle configuration file loading and saving - Use a filesystem provided by the cli to persist the project config and to scaffold files Signed-off-by: Adrian Orive --- pkg/cli/api.go | 17 +- pkg/cli/cli.go | 19 +- pkg/cli/cli_test.go | 2 + pkg/cli/cmd_helpers.go | 44 +++- pkg/cli/edit.go | 17 +- pkg/cli/init.go | 36 +-- pkg/cli/internal/config/config.go | 193 -------------- pkg/cli/internal/config/config_suite_test.go | 29 --- pkg/cli/internal/config/config_test.go | 71 ----- pkg/cli/webhook.go | 17 +- pkg/config/store/errors.go | 51 ++++ pkg/config/store/errors_test.go | 68 +++++ pkg/config/store/interface.go | 38 +++ pkg/config/store/yaml/store.go | 147 +++++++++++ pkg/config/store/yaml/store_test.go | 245 ++++++++++++++++++ pkg/config/v3/config.go | 7 +- pkg/config/v3/config_test.go | 17 +- pkg/plugin/interfaces.go | 6 +- pkg/plugins/golang/v2/api.go | 18 +- pkg/plugins/golang/v2/edit.go | 5 +- pkg/plugins/golang/v2/init.go | 9 +- pkg/plugins/golang/v2/scaffolds/api.go | 43 +-- pkg/plugins/golang/v2/scaffolds/edit.go | 20 +- pkg/plugins/golang/v2/scaffolds/init.go | 22 +- .../internal/templates/hack/boilerplate.go | 5 +- pkg/plugins/golang/v2/scaffolds/webhook.go | 38 ++- pkg/plugins/golang/v2/webhook.go | 24 +- pkg/plugins/golang/v3/api.go | 19 +- pkg/plugins/golang/v3/edit.go | 5 +- pkg/plugins/golang/v3/init.go | 5 +- pkg/plugins/golang/v3/scaffolds/api.go | 44 ++-- pkg/plugins/golang/v3/scaffolds/edit.go | 20 +- pkg/plugins/golang/v3/scaffolds/init.go | 23 +- .../internal/templates/hack/boilerplate.go | 5 +- pkg/plugins/golang/v3/scaffolds/webhook.go | 43 +-- pkg/plugins/golang/v3/webhook.go | 19 +- pkg/plugins/internal/cmdutil/cmdutil.go | 22 +- pkg/plugins/internal/filesystem/filesystem.go | 4 +- .../internal/filesystem/filesystem_test.go | 9 +- pkg/plugins/internal/machinery/scaffold.go | 5 +- .../internal/machinery/scaffold_test.go | 7 +- 41 files changed, 897 insertions(+), 541 deletions(-) delete mode 100644 pkg/cli/internal/config/config.go delete mode 100644 pkg/cli/internal/config/config_suite_test.go delete mode 100644 pkg/cli/internal/config/config_test.go create mode 100644 pkg/config/store/errors.go create mode 100644 pkg/config/store/errors_test.go create mode 100644 pkg/config/store/interface.go create mode 100644 pkg/config/store/yaml/store.go create mode 100644 pkg/config/store/yaml/store_test.go diff --git a/pkg/cli/api.go b/pkg/cli/api.go index 9cc48c49550..60cccb81198 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -76,18 +76,15 @@ func (c CLI) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) { return } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } - subcommand := createAPIPlugin.GetCreateAPISubcommand() - subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin))) + + cfg := yamlstore.New(c.fs) + msg := fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin)) + cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) + cmd.RunE = runECmdFunc(c.fs, subcommand, msg) + cmd.PostRunE = postRunECmdFunc(cfg, msg) } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 41d8726a64b..e3547d9fcc4 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -17,15 +17,17 @@ limitations under the License. package cli import ( + "errors" "fmt" "os" "strings" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" - internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/config" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -88,6 +90,9 @@ type CLI struct { //nolint:maligned // Root command. cmd *cobra.Command + + // Underlying fs + fs afero.Fs } // New creates a new CLI instance. @@ -131,6 +136,7 @@ func newCLI(options ...Option) (*CLI, error) { defaultProjectVersion: cfgv3.Version, defaultPlugins: make(map[config.Version][]string), plugins: make(map[string]plugin.Plugin), + fs: afero.NewOsFs(), } // Apply provided options. @@ -188,18 +194,19 @@ func (c *CLI) getInfoFromFlags() (string, []string, error) { } // getInfoFromConfigFile obtains the project version and plugin keys from the project config file. -func getInfoFromConfigFile() (config.Version, []string, error) { +func (c cli) getInfoFromConfigFile() (config.Version, []string, error) { // Read the project configuration file - projectConfig, err := internalconfig.Read() + cfg := yamlstore.New(c.fs) + err := cfg.Load() switch { case err == nil: - case os.IsNotExist(err): + case errors.Is(err, os.ErrNotExist): return config.Version{}, nil, nil default: return config.Version{}, nil, err } - return getInfoFromConfig(projectConfig) + return getInfoFromConfig(cfg.Config()) } // getInfoFromConfig obtains the project version and plugin keys from the project config. @@ -294,7 +301,7 @@ func (c *CLI) getInfo() error { return err } // Get project version and plugin info from project configuration file - cfgProjectVersion, cfgPlugins, _ := getInfoFromConfigFile() + cfgProjectVersion, cfgPlugins, _ := c.getInfoFromConfigFile() // We discard the error because not being able to read a project configuration file // is not fatal for some commands. The ones that require it need to check its existence. diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index d0e7a97c6c8..9e52c54330f 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -23,6 +23,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/spf13/afero" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -585,6 +586,7 @@ var _ = Describe("CLI", func() { defaultPlugins: map[config.Version][]string{ projectVersion: pluginKeys, }, + fs: afero.NewMemMapFs(), } c.cmd = c.newRootCmd() Expect(c.getInfo()).To(Succeed()) diff --git a/pkg/cli/cmd_helpers.go b/pkg/cli/cmd_helpers.go index 505661c97f4..15ee41751aa 100644 --- a/pkg/cli/cmd_helpers.go +++ b/pkg/cli/cmd_helpers.go @@ -18,10 +18,12 @@ package cli import ( "fmt" + "os" + "github.com/spf13/afero" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config/store" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -45,17 +47,39 @@ func errCmdFunc(err error) func(*cobra.Command, []string) error { } } -// runECmdFunc returns a cobra RunE function that runs subcommand and saves the -// config, which may have been modified by subcommand. -func runECmdFunc( - c *config.Config, - subcommand plugin.Subcommand, - msg string, -) func(*cobra.Command, []string) error { +// preRunECmdFunc returns a cobra PreRunE function that loads the configuration file +// and injects it into the subcommand +func preRunECmdFunc(subcmd plugin.Subcommand, cfg store.Store, msg string) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - if err := subcommand.Run(); err != nil { + err := cfg.Load() + if os.IsNotExist(err) { + return fmt.Errorf("%s: unable to find configuration file, project must be initialized", msg) + } else if err != nil { + return fmt.Errorf("%s: unable to load configuration file: %w", msg, err) + } + + subcmd.InjectConfig(cfg.Config()) + return nil + } +} + +// runECmdFunc returns a cobra RunE function that runs subcommand +func runECmdFunc(fs afero.Fs, subcommand plugin.Subcommand, msg string) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + if err := subcommand.Run(fs); err != nil { return fmt.Errorf("%s: %v", msg, err) } - return c.Save() + return nil + } +} + +// postRunECmdFunc returns a cobra PostRunE function that saves the configuration file +func postRunECmdFunc(cfg store.Store, msg string) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + err := cfg.Save() + if err != nil { + return fmt.Errorf("%s: unable to save configuration file: %w", msg, err) + } + return nil } } diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index 05e20d6fe03..07ff17a3c83 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -76,18 +76,15 @@ func (c CLI) bindEdit(ctx plugin.Context, cmd *cobra.Command) { return } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } - subcommand := editPlugin.GetEditSubcommand() - subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to edit project with %q", plugin.KeyFor(editPlugin))) + + cfg := yamlstore.New(c.fs) + msg := fmt.Sprintf("failed to edit project with %q", plugin.KeyFor(editPlugin)) + cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) + cmd.RunE = runECmdFunc(c.fs, subcommand, msg) + cmd.PostRunE = postRunECmdFunc(cfg, msg) } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 575bb8f0a7c..04a38bdbd80 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -17,8 +17,8 @@ limitations under the License. package cli import ( + "errors" "fmt" - "log" "os" "sort" "strconv" @@ -26,8 +26,8 @@ import ( "github.com/spf13/cobra" - internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/config" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -136,28 +136,28 @@ func (c CLI) bindInit(ctx plugin.Context, cmd *cobra.Command) { return } - cfg, err := internalconfig.New(c.projectVersion, internalconfig.DefaultPath) - if err != nil { - cmdErr(cmd, fmt.Errorf("unable to initialize the project configuration: %w", err)) - return - } - subcommand := initPlugin.GetInitSubcommand() - subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description cmd.Example = ctx.Examples - cmd.RunE = func(*cobra.Command, []string) error { - // Check if a config is initialized in the command runner so the check - // doesn't erroneously fail other commands used in initialized projects. - _, err := internalconfig.Read() - if err == nil || os.IsExist(err) { - log.Fatal("config already initialized") + + cfg := yamlstore.New(c.fs) + msg := fmt.Sprintf("failed to initialize project with %q", plugin.KeyFor(initPlugin)) + cmd.PreRunE = func(*cobra.Command, []string) error { + // Check if a config is initialized. + if err := cfg.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("%s: already initialized", msg) } - if err := subcommand.Run(); err != nil { - return fmt.Errorf("failed to initialize project with %q: %v", plugin.KeyFor(initPlugin), err) + + err := cfg.New(c.projectVersion) + if err != nil { + return fmt.Errorf("%s: error initializing project configuration: %w", msg, err) } - return cfg.Save() + + subcommand.InjectConfig(cfg.Config()) + return nil } + cmd.RunE = runECmdFunc(c.fs, subcommand, msg) + cmd.PostRunE = postRunECmdFunc(cfg, msg) } diff --git a/pkg/cli/internal/config/config.go b/pkg/cli/internal/config/config.go deleted file mode 100644 index 0bcbd4ba014..00000000000 --- a/pkg/cli/internal/config/config.go +++ /dev/null @@ -1,193 +0,0 @@ -/* -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 config - -import ( - "errors" - "fmt" - "os" - - "github.com/spf13/afero" - "sigs.k8s.io/yaml" - - "sigs.k8s.io/kubebuilder/v3/pkg/config" -) - -const ( - // DefaultPath is the default path for the configuration file - DefaultPath = "PROJECT" -) - -func exists(fs afero.Fs, path string) (bool, error) { - // Look up the file - _, err := fs.Stat(path) - - // If we could find it the file exists - if err == nil || os.IsExist(err) { - return true, nil - } - - // Not existing and different errors are differentiated - if os.IsNotExist(err) { - err = nil - } - return false, err -} - -type versionedConfig struct { - Version config.Version -} - -func readFrom(fs afero.Fs, path string) (config.Config, error) { - // Read the file - in, err := afero.ReadFile(fs, path) //nolint:gosec - if err != nil { - return nil, err - } - - // Check the file version - var versioned versionedConfig - if err := yaml.Unmarshal(in, &versioned); err != nil { - return nil, err - } - - // Create the config object - var c config.Config - c, err = config.New(versioned.Version) - if err != nil { - return nil, err - } - - // Unmarshal the file content - if err := c.Unmarshal(in); err != nil { - return nil, err - } - - return c, nil -} - -// Read obtains the configuration from the default path but doesn't allow to persist changes -func Read() (config.Config, error) { - return ReadFrom(DefaultPath) -} - -// ReadFrom obtains the configuration from the provided path but doesn't allow to persist changes -func ReadFrom(path string) (config.Config, error) { - return readFrom(afero.NewOsFs(), path) -} - -// Config extends model/config.Config allowing to persist changes -// NOTE: the existence of Config structs in both model and internal packages is to guarantee that kubebuilder -// is the only project that can modify the file, while plugins can still receive the configuration -type Config struct { - config.Config - - // path stores where the config should be saved to - path string - // mustNotExist requires the file not to exist when saving it - mustNotExist bool - // fs is for testing. - fs afero.Fs -} - -// New creates a new configuration that will be stored at the provided path -func New(version config.Version, path string) (*Config, error) { - cfg, err := config.New(version) - if err != nil { - return nil, err - } - - return &Config{ - Config: cfg, - path: path, - mustNotExist: true, - fs: afero.NewOsFs(), - }, nil -} - -// Load obtains the configuration from the default path allowing to persist changes (Save method) -func Load() (*Config, error) { - return LoadFrom(DefaultPath) -} - -// LoadInitialized calls Load() but returns helpful error messages if the config -// does not exist. -func LoadInitialized() (*Config, error) { - c, err := Load() - if os.IsNotExist(err) { - return nil, errors.New("unable to find configuration file, project must be initialized") - } - return c, err -} - -// LoadFrom obtains the configuration from the provided path allowing to persist changes (Save method) -func LoadFrom(path string) (*Config, error) { - fs := afero.NewOsFs() - c, err := readFrom(fs, path) - return &Config{Config: c, path: path, fs: fs}, err -} - -// Save saves the configuration information -func (c Config) Save() error { - if c.fs == nil { - c.fs = afero.NewOsFs() - } - // If path is unset, it was created directly with `Config{}` - if c.path == "" { - return saveError{errors.New("no information where it should be stored, " + - "use one of the constructors (`New`, `Load` or `LoadFrom`) to create Config instances")} - } - - // If it is a new configuration, the path should not exist yet - if c.mustNotExist { - // Lets check that the file doesn't exist - alreadyExists, err := exists(c.fs, c.path) - if err != nil { - return saveError{err} - } - if alreadyExists { - return saveError{errors.New("configuration already exists in the provided path")} - } - } - - // Marshall into YAML - content, err := c.Marshal() - if err != nil { - return saveError{err} - } - - // Write the marshalled configuration - err = afero.WriteFile(c.fs, c.path, content, 0600) - if err != nil { - return saveError{fmt.Errorf("failed to save configuration to %s: %v", c.path, err)} - } - - return nil -} - -// Path returns the path for configuration file -func (c Config) Path() string { - return c.path -} - -type saveError struct { - err error -} - -func (e saveError) Error() string { - return fmt.Sprintf("unable to save the configuration: %v", e.err) -} diff --git a/pkg/cli/internal/config/config_suite_test.go b/pkg/cli/internal/config/config_suite_test.go deleted file mode 100644 index fffe2f8bb2f..00000000000 --- a/pkg/cli/internal/config/config_suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -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 config - -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestCLI(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Config Suite") -} diff --git a/pkg/cli/internal/config/config_test.go b/pkg/cli/internal/config/config_test.go deleted file mode 100644 index fe7d7ee4932..00000000000 --- a/pkg/cli/internal/config/config_test.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -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 config - -import ( - "os" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/afero" - - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" -) - -var _ = Describe("Config", func() { - Context("Save", func() { - It("should success for valid configs", func() { - cfg := Config{ - Config: cfgv2.New(), - fs: afero.NewMemMapFs(), - path: DefaultPath, - } - Expect(cfg.Save()).To(Succeed()) - - cfgBytes, err := afero.ReadFile(cfg.fs, DefaultPath) - Expect(err).NotTo(HaveOccurred()) - Expect(string(cfgBytes)).To(Equal(`version: "2" -`)) - }) - - It("should fail if path is not provided", func() { - cfg := Config{ - Config: cfgv2.New(), - fs: afero.NewMemMapFs(), - } - Expect(cfg.Save()).NotTo(Succeed()) - }) - }) - - Context("readFrom", func() { - It("should success for valid configs", func() { - configStr := `domain: example.com -repo: github.com/example/project -version: "2"` - expectedConfig := cfgv2.New() - _ = expectedConfig.SetDomain("example.com") - _ = expectedConfig.SetRepository("github.com/example/project") - - fs := afero.NewMemMapFs() - Expect(afero.WriteFile(fs, DefaultPath, []byte(configStr), os.ModePerm)).To(Succeed()) - - cfg, err := readFrom(fs, DefaultPath) - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).To(Equal(expectedConfig)) - }) - }) -}) diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index f5b7d108a13..80d102d1bc8 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -76,18 +76,15 @@ func (c CLI) bindCreateWebhook(ctx plugin.Context, cmd *cobra.Command) { return } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } - subcommand := createWebhookPlugin.GetCreateWebhookSubcommand() - subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to create webhook with %q", plugin.KeyFor(createWebhookPlugin))) + + cfg := yamlstore.New(c.fs) + msg := fmt.Sprintf("failed to create webhook with %q", plugin.KeyFor(createWebhookPlugin)) + cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) + cmd.RunE = runECmdFunc(c.fs, subcommand, msg) + cmd.PostRunE = postRunECmdFunc(cfg, msg) } diff --git a/pkg/config/store/errors.go b/pkg/config/store/errors.go new file mode 100644 index 00000000000..fd57aee0a9a --- /dev/null +++ b/pkg/config/store/errors.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 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 store + +import ( + "fmt" +) + +// LoadError wraps errors yielded by Store.Load and Store.LoadFrom methods +type LoadError struct { + Err error +} + +// Error implements error interface +func (e LoadError) Error() string { + return fmt.Sprintf("unable to load the configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e LoadError) Unwrap() error { + return e.Err +} + +// SaveError wraps errors yielded by Store.Save and Store.SaveTo methods +type SaveError struct { + Err error +} + +// Error implements error interface +func (e SaveError) Error() string { + return fmt.Sprintf("unable to save the configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e SaveError) Unwrap() error { + return e.Err +} diff --git a/pkg/config/store/errors_test.go b/pkg/config/store/errors_test.go new file mode 100644 index 00000000000..acc9b445b1c --- /dev/null +++ b/pkg/config/store/errors_test.go @@ -0,0 +1,68 @@ +/* +Copyright 2021 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 store + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfigStore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Store Suite") +} + +var _ = Describe("LoadError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = LoadError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("unable to load the configuration: %v", wrapped))) + }) + }) + + Context("Unwrap", func() { + It("should unwrap to the wrapped error", func() { + Expect(err.Unwrap()).To(Equal(wrapped)) + }) + }) +}) + +var _ = Describe("SaveError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = SaveError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("unable to save the configuration: %v", wrapped))) + }) + }) + + Context("Unwrap", func() { + It("should unwrap to the wrapped error", func() { + Expect(err.Unwrap()).To(Equal(wrapped)) + }) + }) +}) diff --git a/pkg/config/store/interface.go b/pkg/config/store/interface.go new file mode 100644 index 00000000000..2bb1a21cb01 --- /dev/null +++ b/pkg/config/store/interface.go @@ -0,0 +1,38 @@ +/* +Copyright 2021 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 store + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +// Store represents a persistence backend for config.Config +type Store interface { + // New creates a new config.Config to store + New(config.Version) error + // Load retrieves the config.Config from the persistence backend + Load() error + // LoadFrom retrieves the config.Config from the persistence backend at the specified key + LoadFrom(string) error + // Save stores the config.Config into the persistence backend + Save() error + // SaveTo stores the config.Config into the persistence backend at the specified key + SaveTo(string) error + + // Config returns the stored config.Config + Config() config.Config +} diff --git a/pkg/config/store/yaml/store.go b/pkg/config/store/yaml/store.go new file mode 100644 index 00000000000..7ca80c2a018 --- /dev/null +++ b/pkg/config/store/yaml/store.go @@ -0,0 +1,147 @@ +/* +Copyright 2021 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 yaml + +import ( + "fmt" + "os" + + "github.com/spf13/afero" + "sigs.k8s.io/yaml" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config/store" +) + +const ( + // DefaultPath is the default path for the configuration file + DefaultPath = "PROJECT" +) + +// yamlStore implements store.Store using a YAML file as the storage backend +// The key is translated into the YAML file path +type yamlStore struct { + // fs is the filesystem that will be used to store the config.Config + fs afero.Fs + // mustNotExist requires the file not to exist when saving it + mustNotExist bool + + cfg config.Config +} + +// New creates a new configuration that will be stored at the provided path +func New(fs afero.Fs) store.Store { + return &yamlStore{fs: fs} +} + +// New implements store.Store interface +func (s *yamlStore) New(version config.Version) error { + cfg, err := config.New(version) + if err != nil { + return err + } + + s.cfg = cfg + s.mustNotExist = true + return nil +} + +// Load implements store.Store interface +func (s *yamlStore) Load() error { + return s.LoadFrom(DefaultPath) +} + +type versionedConfig struct { + Version config.Version `json:"version"` +} + +// LoadFrom implements store.Store interface +func (s *yamlStore) LoadFrom(path string) error { + s.mustNotExist = false + + // Read the file + in, err := afero.ReadFile(s.fs, path) + if err != nil { + return store.LoadError{Err: fmt.Errorf("unable to read %q file: %w", path, err)} + } + + // Check the file version + var versioned versionedConfig + if err := yaml.Unmarshal(in, &versioned); err != nil { + return store.LoadError{Err: fmt.Errorf("unable to determine config version: %w", err)} + } + + // Create the config object + var cfg config.Config + cfg, err = config.New(versioned.Version) + if err != nil { + return store.LoadError{Err: fmt.Errorf("unable to create config for version %q: %w", versioned.Version, err)} + } + + // Unmarshal the file content + if err := cfg.Unmarshal(in); err != nil { + return store.LoadError{Err: fmt.Errorf("unable to unmarshal config at %q: %w", path, err)} + } + + s.cfg = cfg + return nil +} + +// Save implements store.Store interface +func (s yamlStore) Save() error { + return s.SaveTo(DefaultPath) +} + +// SaveTo implements store.Store interface +func (s yamlStore) SaveTo(path string) error { + // If yamlStore is unset, none of New, Load, or LoadFrom were called successfully + if s.cfg == nil { + return store.SaveError{Err: fmt.Errorf("undefined config, use one of the initializers: New, Load, LoadFrom")} + } + + // If it is a new configuration, the path should not exist yet + if s.mustNotExist { + // Lets check that the file doesn't exist + _, err := s.fs.Stat(path) + if os.IsNotExist(err) { + // This is exactly what we want + } else if err == nil || os.IsExist(err) { + return store.SaveError{Err: fmt.Errorf("configuration already exists in %q", path)} + } else { + return store.SaveError{Err: fmt.Errorf("unable to check for file prior existence: %w", err)} + } + } + + // Marshall into YAML + content, err := s.cfg.Marshal() + if err != nil { + return store.SaveError{Err: fmt.Errorf("unable to marshal to YAML: %w", err)} + } + + // Write the marshalled configuration + err = afero.WriteFile(s.fs, path, content, 0600) + if err != nil { + return store.SaveError{Err: fmt.Errorf("failed to save configuration to %q: %w", path, err)} + } + + return nil +} + +// Config implements store.Store interface +func (s yamlStore) Config() config.Config { + return s.cfg +} diff --git a/pkg/config/store/yaml/store_test.go b/pkg/config/store/yaml/store_test.go new file mode 100644 index 00000000000..5d3521f13f5 --- /dev/null +++ b/pkg/config/store/yaml/store_test.go @@ -0,0 +1,245 @@ +/* +Copyright 2021 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 yaml + +import ( + "errors" + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config/store" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" +) + +func TestConfigStoreYaml(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Store YAML Suite") +} + +var _ = Describe("New", func() { + It("should return a new empty store", func() { + s := New(afero.NewMemMapFs()) + Expect(s.Config()).To(BeNil()) + + ys, ok := s.(*yamlStore) + Expect(ok).To(BeTrue()) + Expect(ys.fs).NotTo(BeNil()) + }) +}) + +var _ = Describe("yamlStore", func() { + const ( + v2File = `version: "2" +` + unversionedFile = `version: +` + nonexistentVersionFile = `version: 1-alpha +` // v1-alpha never existed + wrongFile = `version: "2" +layout: "" +` // layout field does not exist in v2 + ) + + var ( + s *yamlStore + + path = DefaultPath + "2" + ) + + BeforeEach(func() { + s = New(afero.NewMemMapFs()).(*yamlStore) + }) + + Context("New", func() { + It("should initialize a new Config backend for the provided version", func() { + Expect(s.New(cfgv2.Version)).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeTrue()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail for an unregistered config version", func() { + Expect(s.New(config.Version{})).NotTo(Succeed()) + }) + }) + + Context("Load", func() { + It("should load the Config from an existing file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(v2File), os.ModePerm)).To(Succeed()) + + Expect(s.Load()).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeFalse()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail if no file exists at the default path", func() { + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to identify the version of the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(unversionedFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to create a Config for the version of the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(nonexistentVersionFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to unmarshal the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(wrongFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + }) + + Context("LoadFrom", func() { + It("should load the Config from an existing file from the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(v2File), os.ModePerm)).To(Succeed()) + + Expect(s.LoadFrom(path)).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeFalse()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail if no file exists at the specified path", func() { + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to identify the version of the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(unversionedFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to create a Config for the version of the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(nonexistentVersionFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to unmarshal the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(wrongFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + }) + + Context("Save", func() { + + It("should succeed for a valid config", func() { + s.cfg = cfgv2.New() + Expect(s.Save()).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should succeed for a valid config that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(s.Save()).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should fail for an empty config", func() { + err := s.Save() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + + It("should fail for a pre-existent file that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(v2File), os.ModePerm)).To(Succeed()) + + err := s.Save() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + }) + + Context("SaveTo", func() { + It("should success for valid configs", func() { + s.cfg = cfgv2.New() + Expect(s.SaveTo(path)).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(`version: "2" +`)) + }) + + It("should succeed for a valid config that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(s.SaveTo(path)).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should fail for an empty config", func() { + err := s.SaveTo(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + + It("should fail for a pre-existent file that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(afero.WriteFile(s.fs, path, []byte(v2File), os.ModePerm)).To(Succeed()) + + err := s.SaveTo(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + }) +}) diff --git a/pkg/config/v3/config.go b/pkg/config/v3/config.go index a462bbd5249..b2d2de713a8 100644 --- a/pkg/config/v3/config.go +++ b/pkg/config/v3/config.go @@ -47,12 +47,11 @@ type cfg struct { Resources []resource.Resource `json:"resources,omitempty"` // Plugins - Plugins PluginConfigs `json:"plugins,omitempty"` + Plugins pluginConfigs `json:"plugins,omitempty"` } -// PluginConfigs holds a set of arbitrary plugin configuration objects mapped by plugin key. -// TODO: do not export this once internalconfig has merged with config -type PluginConfigs map[string]pluginConfig +// pluginConfigs holds a set of arbitrary plugin configuration objects mapped by plugin key. +type pluginConfigs map[string]pluginConfig // pluginConfig is an arbitrary plugin configuration object. type pluginConfig interface{} diff --git a/pkg/config/v3/config_test.go b/pkg/config/v3/config_test.go index cbcb5890f99..a8eb4af5939 100644 --- a/pkg/config/v3/config_test.go +++ b/pkg/config/v3/config_test.go @@ -386,8 +386,8 @@ var _ = Describe("cfg", func() { Repository: repo, Name: name, Layout: layout, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ + Plugins: pluginConfigs{ + key: map[string]interface{}{ "data-1": "", }, }, @@ -398,8 +398,8 @@ var _ = Describe("cfg", func() { Repository: repo, Name: name, Layout: layout, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ + Plugins: pluginConfigs{ + key: map[string]interface{}{ "data-1": "plugin value 1", "data-2": "plugin value 2", }, @@ -418,6 +418,13 @@ var _ = Describe("cfg", func() { Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue()) }) + It("DecodePluginConfig should fail to retrieve data from a non-existent plugin", func() { + var pluginConfig PluginConfig + err := c1.DecodePluginConfig("plugin-y", &pluginConfig) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue()) + }) + DescribeTable("DecodePluginConfig should retrieve the plugin data correctly", func(inputConfig cfg, expectedPluginConfig PluginConfig) { var pluginConfig PluginConfig @@ -507,7 +514,7 @@ var _ = Describe("cfg", func() { }, }, }, - Plugins: PluginConfigs{ + Plugins: pluginConfigs{ "plugin-x": map[string]interface{}{ "data-1": "single plugin datum", }, diff --git a/pkg/plugin/interfaces.go b/pkg/plugin/interfaces.go index 1c1cf2da01a..6957c104c1e 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/interfaces.go @@ -17,6 +17,7 @@ limitations under the License. package plugin import ( + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -51,12 +52,13 @@ type Subcommand interface { UpdateContext(*Context) // BindFlags binds the subcommand's flags to the CLI. This allows each subcommand to define its own // command line flags. + // NOTE(Adirio): by the time we bind flags, the config hasn't been injected, trying to use it panics BindFlags(*pflag.FlagSet) - // Run runs the subcommand. - Run() error // InjectConfig passes a config to a plugin. The plugin may modify the config. // Initializing, loading, and saving the config is managed by the cli package. InjectConfig(config.Config) + // Run runs the subcommand. + Run(fs afero.Fs) error } // Context is the runtime context for a subcommand. diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index 8a249067e56..28db64803d7 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -20,11 +20,10 @@ import ( "bufio" "errors" "fmt" - "io/ioutil" "os" - "path/filepath" "strings" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -106,7 +105,6 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &Options{} fs.StringVar(&p.options.Group, "group", "", "resource Group") - p.options.Domain = p.config.GetDomain() fs.StringVar(&p.options.Version, "version", "", "resource Version") fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") // p.options.Plural can be set to specify an irregular plural form @@ -124,9 +122,11 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { func (p *createAPISubcommand) InjectConfig(c config.Config) { p.config = c + + p.options.Domain = c.GetDomain() } -func (p *createAPISubcommand) Run() error { +func (p *createAPISubcommand) Run(fs afero.Fs) error { // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { @@ -141,7 +141,7 @@ func (p *createAPISubcommand) Run() error { // Create the resource from the options p.resource = p.options.NewResource(p.config) - return cmdutil.Run(p) + return cmdutil.Run(p, fs) } func (p *createAPISubcommand) Validate() error { @@ -171,12 +171,6 @@ func (p *createAPISubcommand) Validate() error { } func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the boilerplate - bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec - if err != nil { - return nil, fmt.Errorf("unable to load boilerplate: %v", err) - } - // Load the requested plugins plugins := make([]model.Plugin, 0) switch strings.ToLower(p.pattern) { @@ -188,7 +182,7 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unknown pattern %q", p.pattern) } - return scaffolds.NewAPIScaffolder(p.config, string(bp), p.resource, p.force, plugins), nil + return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go index 7c232a44bec..505abc2497b 100644 --- a/pkg/plugins/golang/v2/edit.go +++ b/pkg/plugins/golang/v2/edit.go @@ -19,6 +19,7 @@ package v2 import ( "fmt" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -57,8 +58,8 @@ func (p *editSubcommand) InjectConfig(c config.Config) { p.config = c } -func (p *editSubcommand) Run() error { - return cmdutil.Run(p) +func (p *editSubcommand) Run(fs afero.Fs) error { + return cmdutil.Run(p, fs) } func (p *editSubcommand) Validate() error { diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index 3ab7042c0c6..d4c421f4b67 100644 --- a/pkg/plugins/golang/v2/init.go +++ b/pkg/plugins/golang/v2/init.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -96,9 +97,7 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.domain, "domain", "my.domain", "domain for groups") fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+ "defaults to the go package of the current working directory.") - if p.config.GetVersion().Compare(cfgv2.Version) > 0 { - fs.StringVar(&p.name, "project-name", "", "name of this project") - } + fs.StringVar(&p.name, "project-name", "", "name of this project") } func (p *initSubcommand) InjectConfig(c config.Config) { @@ -110,8 +109,8 @@ func (p *initSubcommand) InjectConfig(c config.Config) { p.config = c } -func (p *initSubcommand) Run() error { - return cmdutil.Run(p) +func (p *initSubcommand) Run(fs afero.Fs) error { + return cmdutil.Run(p, fs) } func (p *initSubcommand) Validate() error { diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go index 353aac0e5f0..030cbb2747e 100644 --- a/pkg/plugins/golang/v2/scaffolds/api.go +++ b/pkg/plugins/golang/v2/scaffolds/api.go @@ -19,6 +19,8 @@ package scaffolds import ( "fmt" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/model" @@ -30,6 +32,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" ) @@ -47,6 +50,9 @@ type apiScaffolder struct { boilerplate string resource resource.Resource + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs + // plugins is the list of plugins we should allow to transform our generated scaffolding plugins []model.Plugin @@ -57,24 +63,21 @@ type apiScaffolder struct { // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations func NewAPIScaffolder( config config.Config, - boilerplate string, res resource.Resource, force bool, plugins []model.Plugin, ) cmdutil.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - force: force, + config: config, + resource: res, + plugins: plugins, + force: force, } } -// Scaffold implements Scaffolder -func (s *apiScaffolder) Scaffold() error { - fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() +// InjectFS implements cmdutil.Scaffolder +func (s *apiScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } func (s *apiScaffolder) newUniverse() *model.Universe { @@ -85,7 +88,17 @@ func (s *apiScaffolder) newUniverse() *model.Universe { ) } -func (s *apiScaffolder) scaffold() error { +// Scaffold implements cmdutil.Scaffolder +func (s *apiScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + // Load the boilerplate + bp, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err) + } + s.boilerplate = string(bp) + // Keep track of these values before the update doAPI := s.resource.HasAPI() doController := s.resource.HasController() @@ -104,7 +117,7 @@ func (s *apiScaffolder) scaffold() error { if doAPI { - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &api.Types{Force: s.force}, &api.Group{}, @@ -117,7 +130,7 @@ func (s *apiScaffolder) scaffold() error { return fmt.Errorf("error scaffolding APIs: %w", err) } - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(), &crd.Kustomization{}, &crd.KustomizeConfig{}, @@ -128,7 +141,7 @@ func (s *apiScaffolder) scaffold() error { } if doController { - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &controllers.SuiteTest{Force: s.force}, &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force}, @@ -137,7 +150,7 @@ func (s *apiScaffolder) scaffold() error { } } - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &templates.MainUpdater{WireResource: doAPI, WireController: doController}, ); err != nil { diff --git a/pkg/plugins/golang/v2/scaffolds/edit.go b/pkg/plugins/golang/v2/scaffolds/edit.go index 4c5def9d576..e3096729381 100644 --- a/pkg/plugins/golang/v2/scaffolds/edit.go +++ b/pkg/plugins/golang/v2/scaffolds/edit.go @@ -18,9 +18,10 @@ package scaffolds import ( "fmt" - "io/ioutil" "strings" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) @@ -30,6 +31,9 @@ var _ cmdutil.Scaffolder = &editScaffolder{} type editScaffolder struct { config config.Config multigroup bool + + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs } // NewEditScaffolder returns a new Scaffolder for configuration edit operations @@ -40,10 +44,15 @@ func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *editScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *editScaffolder) Scaffold() error { filename := "Dockerfile" - bs, err := ioutil.ReadFile(filename) + bs, err := afero.ReadFile(s.fs, filename) if err != nil { return err } @@ -76,9 +85,8 @@ func (s *editScaffolder) Scaffold() error { // Check if the str is not empty, because when the file is already in desired format it will return empty string // because there is nothing to replace. if str != "" { - // false positive - // nolint:gosec - return ioutil.WriteFile(filename, []byte(str), 0644) + // TODO: instead of writing it directly, we should use the scaffolding machinery for consistency + return afero.WriteFile(s.fs, filename, []byte(str), 0644) } return nil diff --git a/pkg/plugins/golang/v2/scaffolds/init.go b/pkg/plugins/golang/v2/scaffolds/init.go index d0674f28395..e938ffeebab 100644 --- a/pkg/plugins/golang/v2/scaffolds/init.go +++ b/pkg/plugins/golang/v2/scaffolds/init.go @@ -18,9 +18,10 @@ package scaffolds import ( "fmt" - "io/ioutil" "path/filepath" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" @@ -53,6 +54,9 @@ type initScaffolder struct { boilerplatePath string license string owner string + + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs } // NewInitScaffolder returns a new Scaffolder for project initialization operations @@ -65,6 +69,11 @@ func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaf } } +// InjectFS implements cmdutil.Scaffolder +func (s *initScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { return model.NewUniverse( model.WithConfig(s.config), @@ -72,30 +81,27 @@ func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { ) } -// Scaffold implements Scaffolder +// Scaffold implements cmdutil.Scaffolder func (s *initScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -func (s *initScaffolder) scaffold() error { bpFile := &hack.Boilerplate{} bpFile.Path = s.boilerplatePath bpFile.License = s.license bpFile.Owner = s.owner - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(""), bpFile, ); err != nil { return err } - boilerplate, err := ioutil.ReadFile(s.boilerplatePath) //nolint:gosec + boilerplate, err := afero.ReadFile(s.fs, s.boilerplatePath) if err != nil { return err } - return machinery.NewScaffold().Execute( + return machinery.NewScaffold(s.fs).Execute( s.newUniverse(string(boilerplate)), &templates.GitIgnore{}, &rbac.AuthProxyRole{}, diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go index 561d4befbdc..a7b6dac612e 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go @@ -24,6 +24,9 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/file" ) +// DefaultBoilerplatePath is the default path to the boilerplate file +var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") + var _ file.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files @@ -62,7 +65,7 @@ func (f Boilerplate) Validate() error { // SetTemplateDefaults implements file.Template func (f *Boilerplate) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("hack", "boilerplate.go.txt") + f.Path = DefaultBoilerplatePath } if f.License == "" { diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go index d60175f8c13..f2d973ce4ce 100644 --- a/pkg/plugins/golang/v2/scaffolds/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/webhook.go @@ -19,11 +19,14 @@ package scaffolds import ( "fmt" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" ) @@ -34,25 +37,22 @@ type webhookScaffolder struct { config config.Config boilerplate string resource resource.Resource + + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations -func NewWebhookScaffolder( - config config.Config, - boilerplate string, - resource resource.Resource, -) cmdutil.Scaffolder { +func NewWebhookScaffolder(config config.Config, resource resource.Resource) cmdutil.Scaffolder { return &webhookScaffolder{ - config: config, - boilerplate: boilerplate, - resource: resource, + config: config, + resource: resource, } } -// Scaffold implements Scaffolder -func (s *webhookScaffolder) Scaffold() error { - fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() +// InjectFS implements cmdutil.Scaffolder +func (s *webhookScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } func (s *webhookScaffolder) newUniverse() *model.Universe { @@ -63,12 +63,22 @@ func (s *webhookScaffolder) newUniverse() *model.Universe { ) } -func (s *webhookScaffolder) scaffold() error { +// Scaffold implements cmdutil.Scaffolder +func (s *webhookScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + // Load the boilerplate + bp, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err) + } + s.boilerplate = string(bp) + if err := s.config.UpdateResource(s.resource); err != nil { return fmt.Errorf("error updating resource: %w", err) } - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(), &api.Webhook{}, &templates.MainUpdater{WireWebhook: true}, diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index 735c48c68df..d472db1f99f 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -18,12 +18,11 @@ package v2 import ( "fmt" - "io/ioutil" - "path/filepath" + "github.com/spf13/afero" "github.com/spf13/pflag" - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" @@ -32,7 +31,7 @@ import ( ) type createWebhookSubcommand struct { - config newconfig.Config + config config.Config // For help text. commandName string @@ -65,7 +64,6 @@ validating and (or) conversion webhooks. func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &Options{} fs.StringVar(&p.options.Group, "group", "", "resource Group") - p.options.Domain = p.config.GetDomain() fs.StringVar(&p.options.Version, "version", "", "resource Version") fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form") @@ -79,15 +77,17 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "if set, scaffold the conversion webhook") } -func (p *createWebhookSubcommand) InjectConfig(c newconfig.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) { p.config = c + + p.options.Domain = c.GetDomain() } -func (p *createWebhookSubcommand) Run() error { +func (p *createWebhookSubcommand) Run(fs afero.Fs) error { // Create the resource from the options p.resource = p.options.NewResource(p.config) - return cmdutil.Run(p) + return cmdutil.Run(p, fs) } func (p *createWebhookSubcommand) Validate() error { @@ -121,13 +121,7 @@ func (p *createWebhookSubcommand) Validate() error { } func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the boilerplate - bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec - if err != nil { - return nil, fmt.Errorf("unable to load boilerplate: %v", err) - } - - return scaffolds.NewWebhookScaffolder(p.config, string(bp), p.resource), nil + return scaffolds.NewWebhookScaffolder(p.config, p.resource), nil } func (p *createWebhookSubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index a17edf2f515..d36ed5a6250 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -20,11 +20,10 @@ import ( "bufio" "errors" "fmt" - "io/ioutil" "os" - "path/filepath" "strings" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -121,7 +120,6 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &goPlugin.Options{} fs.StringVar(&p.options.Group, "group", "", "resource Group") - p.options.Domain = p.config.GetDomain() fs.StringVar(&p.options.Version, "version", "", "resource Version") fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") @@ -140,9 +138,12 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { func (p *createAPISubcommand) InjectConfig(c config.Config) { p.config = c + + // TODO: offer a flag instead of hard-coding the project-wide domain + p.options.Domain = c.GetDomain() } -func (p *createAPISubcommand) Run() error { +func (p *createAPISubcommand) Run(fs afero.Fs) error { // TODO: re-evaluate whether y/n input still makes sense. We should probably always // scaffold the resource and controller. reader := bufio.NewReader(os.Stdin) @@ -158,7 +159,7 @@ func (p *createAPISubcommand) Run() error { // Create the resource from the options p.resource = p.options.NewResource(p.config) - return cmdutil.Run(p) + return cmdutil.Run(p, fs) } func (p *createAPISubcommand) Validate() error { @@ -199,12 +200,6 @@ func (p *createAPISubcommand) Validate() error { } func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the boilerplate - bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec - if err != nil { - return nil, fmt.Errorf("unable to load boilerplate: %v", err) - } - // Load the requested plugins plugins := make([]model.Plugin, 0) switch strings.ToLower(p.pattern) { @@ -216,7 +211,7 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unknown pattern %q", p.pattern) } - return scaffolds.NewAPIScaffolder(p.config, string(bp), p.resource, p.force, plugins), nil + return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go index 503ed2fafe3..99aa054e568 100644 --- a/pkg/plugins/golang/v3/edit.go +++ b/pkg/plugins/golang/v3/edit.go @@ -19,6 +19,7 @@ package v3 import ( "fmt" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -57,8 +58,8 @@ func (p *editSubcommand) InjectConfig(c config.Config) { p.config = c } -func (p *editSubcommand) Run() error { - return cmdutil.Run(p) +func (p *editSubcommand) Run(fs afero.Fs) error { + return cmdutil.Run(p, fs) } func (p *editSubcommand) Validate() error { diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index c32a1da01ff..c8d3fe05e8f 100644 --- a/pkg/plugins/golang/v3/init.go +++ b/pkg/plugins/golang/v3/init.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -106,8 +107,8 @@ func (p *initSubcommand) InjectConfig(c config.Config) { p.config = c } -func (p *initSubcommand) Run() error { - return cmdutil.Run(p) +func (p *initSubcommand) Run(fs afero.Fs) error { + return cmdutil.Run(p, fs) } func (p *initSubcommand) Validate() error { diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go index 1d9698cafc3..5c200d00067 100644 --- a/pkg/plugins/golang/v3/scaffolds/api.go +++ b/pkg/plugins/golang/v3/scaffolds/api.go @@ -19,6 +19,8 @@ package scaffolds import ( "fmt" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" @@ -29,6 +31,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" ) @@ -42,6 +45,9 @@ type apiScaffolder struct { boilerplate string resource resource.Resource + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs + // plugins is the list of plugins we should allow to transform our generated scaffolding plugins []model.Plugin @@ -52,24 +58,21 @@ type apiScaffolder struct { // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations func NewAPIScaffolder( config config.Config, - boilerplate string, res resource.Resource, force bool, plugins []model.Plugin, ) cmdutil.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - force: force, + config: config, + resource: res, + plugins: plugins, + force: force, } } -// Scaffold implements Scaffolder -func (s *apiScaffolder) Scaffold() error { - fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() +// InjectFS implements cmdutil.Scaffolder +func (s *apiScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } func (s *apiScaffolder) newUniverse() *model.Universe { @@ -80,8 +83,17 @@ func (s *apiScaffolder) newUniverse() *model.Universe { ) } -// TODO: re-use universe created by s.newUniverse() if possible. -func (s *apiScaffolder) scaffold() error { +// Scaffold implements cmdutil.Scaffolder +func (s *apiScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + // Load the boilerplate + bp, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err) + } + s.boilerplate = string(bp) + // Keep track of these values before the update doAPI := s.resource.HasAPI() doController := s.resource.HasController() @@ -92,7 +104,7 @@ func (s *apiScaffolder) scaffold() error { if doAPI { - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &api.Types{Force: s.force}, &api.Group{}, @@ -105,7 +117,7 @@ func (s *apiScaffolder) scaffold() error { return fmt.Errorf("error scaffolding APIs: %v", err) } - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(), &crd.Kustomization{}, &crd.KustomizeConfig{}, @@ -116,7 +128,7 @@ func (s *apiScaffolder) scaffold() error { } if doController { - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &controllers.SuiteTest{Force: s.force}, &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force}, @@ -125,7 +137,7 @@ func (s *apiScaffolder) scaffold() error { } } - if err := machinery.NewScaffold(s.plugins...).Execute( + if err := machinery.NewScaffold(s.fs, s.plugins...).Execute( s.newUniverse(), &templates.MainUpdater{WireResource: doAPI, WireController: doController}, ); err != nil { diff --git a/pkg/plugins/golang/v3/scaffolds/edit.go b/pkg/plugins/golang/v3/scaffolds/edit.go index f4f92785359..2b15a83339e 100644 --- a/pkg/plugins/golang/v3/scaffolds/edit.go +++ b/pkg/plugins/golang/v3/scaffolds/edit.go @@ -18,9 +18,10 @@ package scaffolds import ( "fmt" - "io/ioutil" "strings" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) @@ -30,6 +31,9 @@ var _ cmdutil.Scaffolder = &editScaffolder{} type editScaffolder struct { config config.Config multigroup bool + + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs } // NewEditScaffolder returns a new Scaffolder for configuration edit operations @@ -40,10 +44,15 @@ func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *editScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *editScaffolder) Scaffold() error { filename := "Dockerfile" - bs, err := ioutil.ReadFile(filename) + bs, err := afero.ReadFile(s.fs, filename) if err != nil { return err } @@ -77,9 +86,8 @@ func (s *editScaffolder) Scaffold() error { // Check if the str is not empty, because when the file is already in desired format it will return empty string // because there is nothing to replace. if str != "" { - // false positive - // nolint:gosec - return ioutil.WriteFile(filename, []byte(str), 0644) + // TODO: instead of writing it directly, we should use the scaffolding machinery for consistency + return afero.WriteFile(s.fs, filename, []byte(str), 0644) } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/init.go b/pkg/plugins/golang/v3/scaffolds/init.go index c9f9d8fb471..47955943c61 100644 --- a/pkg/plugins/golang/v3/scaffolds/init.go +++ b/pkg/plugins/golang/v3/scaffolds/init.go @@ -18,9 +18,10 @@ package scaffolds import ( "fmt" - "io/ioutil" "path/filepath" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" @@ -52,6 +53,9 @@ type initScaffolder struct { boilerplatePath string license string owner string + + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs } // NewInitScaffolder returns a new Scaffolder for project initialization operations @@ -64,6 +68,11 @@ func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaf } } +// InjectFS implements cmdutil.Scaffolder +func (s *initScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { return model.NewUniverse( model.WithConfig(s.config), @@ -71,31 +80,27 @@ func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { ) } -// Scaffold implements Scaffolder +// Scaffold implements cmdutil.Scaffolder func (s *initScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -// TODO: re-use universe created by s.newUniverse() if possible. -func (s *initScaffolder) scaffold() error { bpFile := &hack.Boilerplate{} bpFile.Path = s.boilerplatePath bpFile.License = s.license bpFile.Owner = s.owner - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(""), bpFile, ); err != nil { return err } - boilerplate, err := ioutil.ReadFile(s.boilerplatePath) //nolint:gosec + boilerplate, err := afero.ReadFile(s.fs, s.boilerplatePath) if err != nil { return err } - return machinery.NewScaffold().Execute( + return machinery.NewScaffold(s.fs).Execute( s.newUniverse(string(boilerplate)), &rbac.Kustomization{}, &rbac.AuthProxyRole{}, diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go index e8a21cbe6ce..8603a6cd699 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go @@ -24,6 +24,9 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/file" ) +// DefaultBoilerplatePath is the default path to the boilerplate file +var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") + var _ file.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files @@ -62,7 +65,7 @@ func (f Boilerplate) Validate() error { // SetTemplateDefaults implements file.Template func (f *Boilerplate) SetTemplateDefaults() error { if f.Path == "" { - f.Path = filepath.Join("hack", "boilerplate.go.txt") + f.Path = DefaultBoilerplatePath } if f.License == "" { diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go index b195ee7eef7..0e2545faa5b 100644 --- a/pkg/plugins/golang/v3/scaffolds/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/webhook.go @@ -19,6 +19,8 @@ package scaffolds import ( "fmt" + "github.com/spf13/afero" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" @@ -26,6 +28,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" ) @@ -37,29 +40,25 @@ type webhookScaffolder struct { boilerplate string resource resource.Resource + // fs is the filesystem that will be used by the scaffolder + fs afero.Fs + // force indicates whether to scaffold controller files even if it exists or not force bool } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations -func NewWebhookScaffolder( - config config.Config, - boilerplate string, - resource resource.Resource, - force bool, -) cmdutil.Scaffolder { +func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) cmdutil.Scaffolder { return &webhookScaffolder{ - config: config, - boilerplate: boilerplate, - resource: resource, - force: force, + config: config, + resource: resource, + force: force, } } -// Scaffold implements Scaffolder -func (s *webhookScaffolder) Scaffold() error { - fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() +// InjectFS implements cmdutil.Scaffolder +func (s *webhookScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } func (s *webhookScaffolder) newUniverse() *model.Universe { @@ -70,7 +69,17 @@ func (s *webhookScaffolder) newUniverse() *model.Universe { ) } -func (s *webhookScaffolder) scaffold() error { +// Scaffold implements cmdutil.Scaffolder +func (s *webhookScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + // Load the boilerplate + bp, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err) + } + s.boilerplate = string(bp) + // Keep track of these values before the update doDefaulting := s.resource.HasDefaultingWebhook() doValidation := s.resource.HasValidationWebhook() @@ -80,7 +89,7 @@ func (s *webhookScaffolder) scaffold() error { return fmt.Errorf("error updating resource: %w", err) } - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(), &api.Webhook{Force: s.force}, &templates.MainUpdater{WireWebhook: true}, @@ -100,7 +109,7 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest. if doDefaulting || doValidation { - if err := machinery.NewScaffold().Execute( + if err := machinery.NewScaffold(s.fs).Execute( s.newUniverse(), &api.WebhookSuite{}, ); err != nil { diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 6e12fa4c4be..3f66641811e 100644 --- a/pkg/plugins/golang/v3/webhook.go +++ b/pkg/plugins/golang/v3/webhook.go @@ -18,9 +18,8 @@ package v3 import ( "fmt" - "io/ioutil" - "path/filepath" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" @@ -72,7 +71,6 @@ validating and (or) conversion webhooks. func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &goPlugin.Options{} fs.StringVar(&p.options.Group, "group", "", "resource Group") - p.options.Domain = p.config.GetDomain() fs.StringVar(&p.options.Version, "version", "", "resource Version") fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") @@ -92,13 +90,16 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { func (p *createWebhookSubcommand) InjectConfig(c config.Config) { p.config = c + + // TODO: offer a flag instead of hard-coding the project-wide domain + p.options.Domain = c.GetDomain() } -func (p *createWebhookSubcommand) Run() error { +func (p *createWebhookSubcommand) Run(fs afero.Fs) error { // Create the resource from the options p.resource = p.options.NewResource(p.config) - return cmdutil.Run(p) + return cmdutil.Run(p, fs) } func (p *createWebhookSubcommand) Validate() error { @@ -131,13 +132,7 @@ func (p *createWebhookSubcommand) Validate() error { } func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the boilerplate - bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec - if err != nil { - return nil, fmt.Errorf("unable to load boilerplate: %v", err) - } - - return scaffolds.NewWebhookScaffolder(p.config, string(bp), p.resource, p.force), nil + return scaffolds.NewWebhookScaffolder(p.config, p.resource, p.force), nil } func (p *createWebhookSubcommand) PostScaffold() error { diff --git a/pkg/plugins/internal/cmdutil/cmdutil.go b/pkg/plugins/internal/cmdutil/cmdutil.go index aa3e7f4a9e9..8c591cedc98 100644 --- a/pkg/plugins/internal/cmdutil/cmdutil.go +++ b/pkg/plugins/internal/cmdutil/cmdutil.go @@ -16,25 +16,31 @@ limitations under the License. package cmdutil +import ( + "github.com/spf13/afero" +) + // Scaffolder interface creates files to set up a controller manager type Scaffolder interface { + InjectFS(afero.Fs) // Scaffold performs the scaffolding Scaffold() error } // RunOptions represent the types used to implement the different commands type RunOptions interface { - // - Step 1: verify that the command can be run (e.g., go version, project version, arguments, ...) + // - Step 1: verify that the command can be run (e.g., go version, project version, arguments, ...). Validate() error - // - Step 2: create the Scaffolder instance + // - Step 2: create the Scaffolder instance. GetScaffolder() (Scaffolder, error) - // - Step 3: call the Scaffold method of the Scaffolder instance. Doesn't need any method - // - Step 4: finish the command execution + // - Step 3: inject the filesystem into the Scaffolder instance. Doesn't need any method. + // - Step 4: call the Scaffold method of the Scaffolder instance. Doesn't need any method. + // - Step 5: finish the command execution. PostScaffold() error } // Run executes a command -func Run(options RunOptions) error { +func Run(options RunOptions, fs afero.Fs) error { // Step 1: validate if err := options.Validate(); err != nil { return err @@ -45,13 +51,15 @@ func Run(options RunOptions) error { if err != nil { return err } - // Step 3: scaffold + // Step 3: inject filesystem + scaffolder.InjectFS(fs) + // Step 4: scaffold if scaffolder != nil { if err := scaffolder.Scaffold(); err != nil { return err } } - // Step 4: finish + // Step 5: finish if err := options.PostScaffold(); err != nil { return err } diff --git a/pkg/plugins/internal/filesystem/filesystem.go b/pkg/plugins/internal/filesystem/filesystem.go index e7e362c5c44..26cf4ba31ef 100644 --- a/pkg/plugins/internal/filesystem/filesystem.go +++ b/pkg/plugins/internal/filesystem/filesystem.go @@ -53,10 +53,10 @@ type fileSystem struct { } // New returns a new FileSystem -func New(options ...Options) FileSystem { +func New(underlying afero.Fs, options ...Options) FileSystem { // Default values fs := fileSystem{ - fs: afero.NewOsFs(), + fs: underlying, dirPerm: defaultDirectoryPermission, filePerm: defaultFilePermission, fileMode: createOrUpdate, diff --git a/pkg/plugins/internal/filesystem/filesystem_test.go b/pkg/plugins/internal/filesystem/filesystem_test.go index 6a3d216f112..c86b08426b2 100644 --- a/pkg/plugins/internal/filesystem/filesystem_test.go +++ b/pkg/plugins/internal/filesystem/filesystem_test.go @@ -21,6 +21,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/spf13/afero" ) var _ = Describe("FileSystem", func() { @@ -38,7 +39,7 @@ var _ = Describe("FileSystem", func() { Context("when using no options", func() { BeforeEach(func() { - fsi = New() + fsi = New(afero.NewMemMapFs()) fs, ok = fsi.(fileSystem) }) @@ -65,7 +66,7 @@ var _ = Describe("FileSystem", func() { Context("when using directory permission option", func() { BeforeEach(func() { - fsi = New(DirectoryPermissions(dirPerm)) + fsi = New(afero.NewMemMapFs(), DirectoryPermissions(dirPerm)) fs, ok = fsi.(fileSystem) }) @@ -92,7 +93,7 @@ var _ = Describe("FileSystem", func() { Context("when using file permission option", func() { BeforeEach(func() { - fsi = New(FilePermissions(filePerm)) + fsi = New(afero.NewMemMapFs(), FilePermissions(filePerm)) fs, ok = fsi.(fileSystem) }) @@ -119,7 +120,7 @@ var _ = Describe("FileSystem", func() { Context("when using both directory and file permission options", func() { BeforeEach(func() { - fsi = New(DirectoryPermissions(dirPerm), FilePermissions(filePerm)) + fsi = New(afero.NewMemMapFs(), DirectoryPermissions(dirPerm), FilePermissions(filePerm)) fs, ok = fsi.(fileSystem) }) diff --git a/pkg/plugins/internal/machinery/scaffold.go b/pkg/plugins/internal/machinery/scaffold.go index 5391c45bb2e..90dcee43a9b 100644 --- a/pkg/plugins/internal/machinery/scaffold.go +++ b/pkg/plugins/internal/machinery/scaffold.go @@ -25,6 +25,7 @@ import ( "strings" "text/template" + "github.com/spf13/afero" "golang.org/x/tools/imports" "sigs.k8s.io/kubebuilder/v3/pkg/model" @@ -55,10 +56,10 @@ type scaffold struct { } // NewScaffold returns a new Scaffold with the provided plugins -func NewScaffold(plugins ...model.Plugin) Scaffold { +func NewScaffold(fs afero.Fs, plugins ...model.Plugin) Scaffold { return &scaffold{ plugins: plugins, - fs: filesystem.New(), + fs: filesystem.New(fs), } } diff --git a/pkg/plugins/internal/machinery/scaffold_test.go b/pkg/plugins/internal/machinery/scaffold_test.go index ebefadc948c..8e48c28d018 100644 --- a/pkg/plugins/internal/machinery/scaffold_test.go +++ b/pkg/plugins/internal/machinery/scaffold_test.go @@ -21,6 +21,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" + "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/file" @@ -37,7 +38,7 @@ var _ = Describe("Scaffold", func() { Context("when using no plugins", func() { BeforeEach(func() { - si = NewScaffold() + si = NewScaffold(afero.NewMemMapFs()) s, ok = si.(*scaffold) }) @@ -56,7 +57,7 @@ var _ = Describe("Scaffold", func() { Context("when using one plugin", func() { BeforeEach(func() { - si = NewScaffold(fakePlugin{}) + si = NewScaffold(afero.NewMemMapFs(), fakePlugin{}) s, ok = si.(*scaffold) }) @@ -75,7 +76,7 @@ var _ = Describe("Scaffold", func() { Context("when using several plugins", func() { BeforeEach(func() { - si = NewScaffold(fakePlugin{}, fakePlugin{}, fakePlugin{}) + si = NewScaffold(afero.NewMemMapFs(), fakePlugin{}, fakePlugin{}, fakePlugin{}) s, ok = si.(*scaffold) })