diff --git a/Makefile b/Makefile index 389384f1658..5f0173b0776 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ test-unit: ## Run the unit tests .PHONY: test-coverage test-coverage: ## Run unit tests creating the output to report coverage - rm -rf *.out # Remove all coverage files if exists - go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang,./pkg/plugins/internal/..." ./pkg/... + go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/machinery/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang" ./pkg/... .PHONY: test-integration test-integration: ## Run the integration tests diff --git a/cmd/main.go b/cmd/main.go index c3527528372..6cb55af7014 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/cli" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + declarativev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1" pluginv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2" pluginv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) @@ -30,13 +31,14 @@ func main() { c, err := cli.New( cli.WithCommandName("kubebuilder"), cli.WithVersion(versionString()), - cli.WithDefaultProjectVersion(cfgv3.Version), cli.WithPlugins( &pluginv2.Plugin{}, &pluginv3.Plugin{}, + &declarativev1.Plugin{}, ), cli.WithDefaultPlugins(cfgv2.Version, &pluginv2.Plugin{}), cli.WithDefaultPlugins(cfgv3.Version, &pluginv3.Plugin{}), + cli.WithDefaultProjectVersion(cfgv3.Version), cli.WithCompletion(), ) if err != nil { diff --git a/pkg/cli/api.go b/pkg/cli/api.go index 9cc48c49550..7241a22eb56 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -14,80 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const apiErrorMsg = "failed to create API" + func (c CLI) newCreateAPICmd() *cobra.Command { - ctx := c.newAPIContext() cmd := &cobra.Command{ - Use: "api", - Short: "Scaffold a Kubernetes API", - Long: ctx.Description, - Example: ctx.Examples, + Use: "api", + Short: "Scaffold a Kubernetes API", + Long: `Scaffold a Kubernetes API. +`, RunE: errCmdFunc( fmt.Errorf("api subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateAPI(ctx, cmd) - return cmd -} - -func (c CLI) newAPIContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a Kubernetes API. -`, - } -} - -// nolint:dupl -func (c CLI) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createAPIPlugin plugin.CreateAPI - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateAPI) - if isValid { - if createAPIPlugin != nil { - err := fmt.Errorf("duplicate API creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createAPIPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createAPIPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateAPI. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateAPI) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateAPI).GetCreateAPISubcommand() + }, + ) - if createAPIPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide an API creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(*subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"API creation"}) + return cmd } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, apiErrorMsg) - 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))) + return cmd } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 41d8726a64b..c7b4d7ac030 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -17,46 +17,30 @@ 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" + goPluginV2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2" ) const ( noticeColor = "\033[1;36m%s\033[0m" deprecationFmt = "[Deprecation Notice] %s\n\n" - projectVersionFlag = "project-version" pluginsFlag = "plugins" - - noPluginError = "invalid config file please verify that the version and layout fields are set and valid" + projectVersionFlag = "project-version" ) -// equalStringSlice checks if two string slices are equal. -func equalStringSlice(a, b []string) bool { - // Check lengths - if len(a) != len(b) { - return false - } - - // Check elements - for i, v := range a { - if v != b[i] { - return false - } - } - - return true -} - // CLI is the command line utility that is used to scaffold kubebuilder project files. type CLI struct { //nolint:maligned /* Fields set by Option */ @@ -65,12 +49,12 @@ type CLI struct { //nolint:maligned commandName string // CLI version string. version string - // Default project version in case none is provided and a config file can't be found. - defaultProjectVersion config.Version - // Default plugins in case none is provided and a config file can't be found. - defaultPlugins map[config.Version][]string // Plugins registered in the CLI. plugins map[string]plugin.Plugin + // Default plugins in case none is provided and a config file can't be found. + defaultPlugins map[config.Version][]string + // Default project version in case none is provided and a config file can't be found. + defaultProjectVersion config.Version // Commands injected by options. extraCommands []*cobra.Command // Whether to add a completion command to the CLI. @@ -78,16 +62,19 @@ type CLI struct { //nolint:maligned /* Internal fields */ - // Project version to scaffold. - projectVersion config.Version // Plugin keys to scaffold with. pluginKeys []string + // Project version to scaffold. + projectVersion config.Version // A filtered set of plugins that should be used by command constructors. resolvedPlugins []plugin.Plugin // Root command. cmd *cobra.Command + + // Underlying fs + fs afero.Fs } // New creates a new CLI instance. @@ -128,9 +115,10 @@ func newCLI(options ...Option) (*CLI, error) { // Default CLI options. c := &CLI{ commandName: "kubebuilder", - defaultProjectVersion: cfgv3.Version, - defaultPlugins: make(map[config.Version][]string), plugins: make(map[string]plugin.Plugin), + defaultPlugins: make(map[config.Version][]string), + defaultProjectVersion: cfgv3.Version, + fs: afero.NewOsFs(), } // Apply provided options. @@ -143,257 +131,248 @@ func newCLI(options ...Option) (*CLI, error) { return c, nil } -// getInfoFromFlags obtains the project version and plugin keys from flags. -func (c *CLI) getInfoFromFlags() (string, []string, error) { - // Partially parse the command line arguments - fs := pflag.NewFlagSet("base", pflag.ContinueOnError) +// buildCmd creates the underlying cobra command and stores it internally. +func (c *CLI) buildCmd() error { + c.cmd = c.newRootCmd() - // Load the base command global flags - fs.AddFlagSet(c.cmd.PersistentFlags()) + // Get project version and plugin keys. + if err := c.getInfo(); err != nil { + return err + } - // Omit unknown flags to avoid parsing errors - fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + // Resolve plugins for project version and plugin keys. + if err := c.resolvePlugins(); err != nil { + return err + } - // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default - // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command. - fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName)) + // Add the subcommands + c.addSubcommands() - // Parse the arguments - if err := fs.Parse(os.Args[1:]); err != nil { - return "", []string{}, err - } + return nil +} - // Define the flags needed for plugin resolution - var ( - projectVersion string - plugins []string - err error - ) - // GetXxxxx methods will not yield errors because we know for certain these flags exist and types match. - projectVersion, err = fs.GetString(projectVersionFlag) - if err != nil { - return "", []string{}, err - } - plugins, err = fs.GetStringSlice(pluginsFlag) - if err != nil { - return "", []string{}, err +// getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags. +func (c *CLI) getInfo() error { + // Get plugin keys and project version from project configuration file + // We discard the error if file doesn't exist 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 later. + hasConfigFile := true + if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) { + hasConfigFile = false + } else if err != nil { + return err } - // Remove leading and trailing spaces - for i, key := range plugins { - plugins[i] = strings.TrimSpace(key) + // We can't early return here in case a project configuration file was found because + // this command call may override the project plugins. + + // Get project version and plugin info from flags + if err := c.getInfoFromFlags(hasConfigFile); err != nil { + return err } - return projectVersion, plugins, nil + // Get project version and plugin info from defaults + c.getInfoFromDefaults() + + return nil } // getInfoFromConfigFile obtains the project version and plugin keys from the project config file. -func getInfoFromConfigFile() (config.Version, []string, error) { +func (c *CLI) getInfoFromConfigFile() error { // Read the project configuration file - projectConfig, err := internalconfig.Read() - switch { - case err == nil: - case os.IsNotExist(err): - return config.Version{}, nil, nil - default: - return config.Version{}, nil, err + cfg := yamlstore.New(c.fs) + if err := cfg.Load(); err != nil { + return err } - return getInfoFromConfig(projectConfig) + return c.getInfoFromConfig(cfg.Config()) } // getInfoFromConfig obtains the project version and plugin keys from the project config. // It is extracted from getInfoFromConfigFile for testing purposes. -func getInfoFromConfig(projectConfig config.Config) (config.Version, []string, error) { +func (c *CLI) getInfoFromConfig(projectConfig config.Config) error { // Split the comma-separated plugins var pluginSet []string if projectConfig.GetLayout() != "" { for _, p := range strings.Split(projectConfig.GetLayout(), ",") { pluginSet = append(pluginSet, strings.TrimSpace(p)) } + for _, pluginKey := range pluginSet { + if err := plugin.ValidateKey(pluginKey); err != nil { + return fmt.Errorf("invalid plugin key found in project configuration file: %w", err) + } + } + } else { + // Project version 2 did not store the layout field, but only the go.kubebuilder.io/v2 plugin was supported + pluginSet = append(pluginSet, plugin.KeyFor(goPluginV2.Plugin{})) } - return projectConfig.GetVersion(), pluginSet, nil + c.pluginKeys = append(c.pluginKeys, pluginSet...) + c.projectVersion = projectConfig.GetVersion() + return nil } -// resolveFlagsAndConfigFileConflicts checks if the provided combined input from flags and -// the config file is valid and uses default values in case some info was not provided. -func (c CLI) resolveFlagsAndConfigFileConflicts( - flagProjectVersionString string, - cfgProjectVersion config.Version, - flagPlugins, cfgPlugins []string, -) (config.Version, []string, error) { - // Parse project configuration version from flags - var flagProjectVersion config.Version - if flagProjectVersionString != "" { - if err := flagProjectVersion.Parse(flagProjectVersionString); err != nil { - return config.Version{}, nil, fmt.Errorf("unable to parse project version flag: %w", err) - } +// getInfoFromFlags obtains the project version and plugin keys from flags. +func (c *CLI) getInfoFromFlags(hasConfigFile bool) error { + // Partially parse the command line arguments + fs := pflag.NewFlagSet("base", pflag.ContinueOnError) + + // Load the base command global flags + fs.AddFlagSet(c.cmd.PersistentFlags()) + + // If we were unable to load the project configuration, we should also accept the project version flag + var projectVersionStr string + if !hasConfigFile { + fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version") } - // Resolve project version - var projectVersion config.Version - isFlagProjectVersionInvalid := flagProjectVersion.Validate() != nil - isCfgProjectVersionInvalid := cfgProjectVersion.Validate() != nil - switch { - // If they are both invalid (empty is invalid), use the default - case isFlagProjectVersionInvalid && isCfgProjectVersionInvalid: - projectVersion = c.defaultProjectVersion - // If any is invalid (empty is invalid), choose the other - case isCfgProjectVersionInvalid: - projectVersion = flagProjectVersion - case isFlagProjectVersionInvalid: - projectVersion = cfgProjectVersion - // If they are equal doesn't matter which we choose - case flagProjectVersion.Compare(cfgProjectVersion) == 0: - projectVersion = flagProjectVersion - // If both are valid (empty is invalid) and they are different error out - default: - return config.Version{}, nil, fmt.Errorf("project version conflict between command line args (%s) "+ - "and project configuration file (%s)", flagProjectVersionString, cfgProjectVersion) + // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default + // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command. + fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName)) + + // Omit unknown flags to avoid parsing errors + fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + + // Parse the arguments + if err := fs.Parse(os.Args[1:]); err != nil { + return err } - // Resolve plugins - var plugins []string - isFlagPluginsEmpty := len(flagPlugins) == 0 - isCfgPluginsEmpty := len(cfgPlugins) == 0 - switch { - // If they are both empty, use the default - case isFlagPluginsEmpty && isCfgPluginsEmpty: - if defaults, hasDefaults := c.defaultPlugins[projectVersion]; hasDefaults { - plugins = defaults + // If any plugin key was provided, replace those from the project configuration file + if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil { + return err + } else if len(pluginKeys) != 0 { + // Remove leading and trailing spaces and validate the plugin keys + for i, key := range pluginKeys { + pluginKeys[i] = strings.TrimSpace(key) + if err := plugin.ValidateKey(pluginKeys[i]); err != nil { + return fmt.Errorf("invalid plugin %q found in flags: %w", pluginKeys[i], err) + } } - // If any is empty, choose the other - case isCfgPluginsEmpty: - plugins = flagPlugins - case isFlagPluginsEmpty: - plugins = cfgPlugins - // If they are equal doesn't matter which we choose - case equalStringSlice(flagPlugins, cfgPlugins): - plugins = flagPlugins - // If none is empty and they are different error out - default: - return config.Version{}, nil, fmt.Errorf("plugins conflict between command line args (%v) "+ - "and project configuration file (%v)", flagPlugins, cfgPlugins) + + c.pluginKeys = pluginKeys } - // Validate the plugins - for _, p := range plugins { - if err := plugin.ValidateKey(p); err != nil { - return config.Version{}, nil, err + + // If the project version flag was accepted but not provided keep the empty version and try to resolve it later, + // else validate the provided project version + if projectVersionStr != "" { + if err := c.projectVersion.Parse(projectVersionStr); err != nil { + return fmt.Errorf("invalid project version flag: %w", err) } } - return projectVersion, plugins, nil + return nil } -// getInfo obtains the project version and plugin keys resolving conflicts among flags and the project config file. -func (c *CLI) getInfo() error { - // Get project version and plugin info from flags - flagProjectVersion, flagPlugins, err := c.getInfoFromFlags() - if err != nil { - return err +// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values +func (c *CLI) getInfoFromDefaults() { + // Should not use default values if a plugin was already set + // This checks includes the case where a project configuration file was found, + // as it will always have at least one plugin key set by now + if len(c.pluginKeys) != 0 { + // We don't assign a default value for project version here because we may be able to + // resolve the project version after resolving the plugins. + return + } + + // If the user provided a project version, use the default plugins for that project version + if c.projectVersion.Validate() == nil { + c.pluginKeys = c.defaultPlugins[c.projectVersion] + return + } + + // Else try to use the default plugins for the default project version + if c.defaultProjectVersion.Validate() == nil { + var found bool + if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found { + c.projectVersion = c.defaultProjectVersion + return + } + } + + // Else check if only default plugins for a project version were provided + if len(c.defaultPlugins) == 1 { + for projectVersion, defaultPlugins := range c.defaultPlugins { + c.pluginKeys = defaultPlugins + c.projectVersion = projectVersion + return + } } - // Get project version and plugin info from project configuration file - cfgProjectVersion, cfgPlugins, _ := 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. - - // Resolve project version and plugin keys - c.projectVersion, c.pluginKeys, err = c.resolveFlagsAndConfigFileConflicts( - flagProjectVersion, cfgProjectVersion, flagPlugins, cfgPlugins, - ) - return err } const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " + "https://kubebuilder.io/migration/plugin/plugins.html)" -// resolve selects from the available plugins those that match the project version and plugin keys provided. -func (c *CLI) resolve() error { - var plugins []plugin.Plugin +// resolvePlugins selects from the available plugins those that match the project version and plugin keys provided. +func (c *CLI) resolvePlugins() error { + knownProjectVersion := c.projectVersion.Validate() == nil + for _, pluginKey := range c.pluginKeys { - name, version := plugin.SplitKey(pluginKey) - shortName := plugin.GetShortName(name) + var extraErrMsg string + + plugins := make([]plugin.Plugin, 0, len(c.plugins)) + for _, p := range c.plugins { + plugins = append(plugins, p) + } + // We can omit the error because plugin keys have already been validated + plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey) + if knownProjectVersion { + plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion) + extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion) + } // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable". // This upgrade effectively removes a plugin, which is fine because unstable plugins are // under no support contract. However users should be notified _why_ their plugin cannot be found. - var extraErrMsg string - if version != "" { + if _, version := plugin.SplitKey(pluginKey); version != "" { var ver plugin.Version if err := ver.Parse(version); err != nil { return fmt.Errorf("error parsing input plugin version from key %q: %v", pluginKey, err) } if !ver.IsStable() { - extraErrMsg = unstablePluginMsg + extraErrMsg += unstablePluginMsg } } - var resolvedPlugins []plugin.Plugin - isFullName := shortName != name - hasVersion := version != "" - - switch { - // If it is fully qualified search it - case isFullName && hasVersion: - p, isKnown := c.plugins[pluginKey] - if !isKnown { - return fmt.Errorf("unknown fully qualified plugin %q%s", pluginKey, extraErrMsg) - } - if !plugin.SupportsVersion(p, c.projectVersion) { - return fmt.Errorf("plugin %q does not support project version %q", pluginKey, c.projectVersion) - } - plugins = append(plugins, p) - continue - // Shortname with version - case hasVersion: - for _, p := range c.plugins { - // Check that the shortname and version match - if plugin.GetShortName(p.Name()) == name && p.Version().String() == version { - resolvedPlugins = append(resolvedPlugins, p) - } - } - // Full name without version - case isFullName: - for _, p := range c.plugins { - // Check that the name matches - if p.Name() == name { - resolvedPlugins = append(resolvedPlugins, p) - } - } - // Shortname without version + // Only 1 plugin can match + switch len(plugins) { + case 1: + c.resolvedPlugins = append(c.resolvedPlugins, plugins[0]) + case 0: + return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg) default: - for _, p := range c.plugins { - // Check that the shortname matches - if plugin.GetShortName(p.Name()) == name { - resolvedPlugins = append(resolvedPlugins, p) - } - } + return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg) } + } - // Filter the ones that do not support the required project version - i := 0 - for _, resolvedPlugin := range resolvedPlugins { - if plugin.SupportsVersion(resolvedPlugin, c.projectVersion) { - resolvedPlugins[i] = resolvedPlugin - i++ - } - } - resolvedPlugins = resolvedPlugins[:i] + // Now we can try to resolve the project version if not known by this point + if !knownProjectVersion && len(c.resolvedPlugins) > 0 { + // Extract the common supported project versions + supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...) - // Only 1 plugin can match - switch len(resolvedPlugins) { - case 0: - return fmt.Errorf("no plugin could be resolved with key %q for project version %q%s", - pluginKey, c.projectVersion, extraErrMsg) + // If there is only one common supported project version, resolve to it + ProjectNumberVersionSwitch: + switch len(supportedProjectVersions) { case 1: - plugins = append(plugins, resolvedPlugins[0]) + c.projectVersion = supportedProjectVersions[0] + case 0: + return fmt.Errorf("no project version supported by all the resolved plugins") default: - return fmt.Errorf("ambiguous plugin %q for project version %q", pluginKey, c.projectVersion) + supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions)) + for _, supportedProjectVersion := range supportedProjectVersions { + // In case one of the multiple supported versions is the default one, choose that and exit the switch + if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 { + c.projectVersion = c.defaultProjectVersion + break ProjectNumberVersionSwitch + } + supportedProjectVersionStrings = append(supportedProjectVersionStrings, + fmt.Sprintf("%q", supportedProjectVersion)) + } + return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s", + strings.Join(supportedProjectVersionStrings, ", ")) } } - c.resolvedPlugins = plugins return nil } @@ -428,26 +407,6 @@ func (c *CLI) addSubcommands() { } } -// buildCmd creates the underlying cobra command and stores it internally. -func (c *CLI) buildCmd() error { - c.cmd = c.newRootCmd() - - // Get project version and plugin keys. - if err := c.getInfo(); err != nil { - return err - } - - // Resolve plugins for project version and plugin keys. - if err := c.resolve(); err != nil { - return err - } - - // Add the subcommands - c.addSubcommands() - - return nil -} - // addExtraCommands adds the additional commands. func (c *CLI) addExtraCommands() error { for _, cmd := range c.extraCommands { @@ -470,6 +429,13 @@ func (c CLI) printDeprecationWarnings() { } } +// metadata returns CLI's metadata. +func (c CLI) metadata() plugin.CLIMetadata { + return plugin.CLIMetadata{ + CommandName: c.commandName, + } +} + // Run executes the CLI utility. // // If an error is found, command help and examples will be printed. diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index d0e7a97c6c8..2ed5ef8b26a 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -20,15 +20,19 @@ import ( "fmt" "io/ioutil" "os" + "strings" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" + "github.com/spf13/afero" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v3/pkg/config" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) func makeMockPluginsFor(projectVersion config.Version, pluginKeys ...string) []plugin.Plugin { @@ -75,530 +79,226 @@ func hasSubCommand(c *CLI, name string) bool { } var _ = Describe("CLI", func() { + var ( + c *CLI + projectVersion = config.Version{Number: 3} + ) + + BeforeEach(func() { + c = &CLI{ + fs: afero.NewMemMapFs(), + } + }) - Context("getInfoFromFlags", func() { - var ( - projectVersion string - plugins []string - err error - c *CLI - ) - - // Save os.Args and restore it for every test - var args []string - BeforeEach(func() { - c = &CLI{} - c.cmd = c.newRootCmd() - args = os.Args - }) - AfterEach(func() { os.Args = args }) + // TODO: test CLI.getInfoFromConfigFile using a mock filesystem - When("no flag is set", func() { + Context("getInfoFromConfig", func() { + When("not having layout field", func() { It("should succeed", func() { - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(len(plugins)).To(Equal(0)) - }) - }) + projectConfig := cfgv2.New() - When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { - It("should succeed", func() { - setProjectVersionFlag("2") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(len(plugins)).To(Equal(0)) + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal([]string{"go.kubebuilder.io/v2"})) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { - It("should succeed using one plugin key", func() { - setPluginsFlag("go/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1"})) - }) - - It("should succeed using more than one plugin key", func() { - setPluginsFlag("go/v1,example/v2,test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) - }) + When("having a single plugin in the layout field", func() { + It("should succeed", func() { + projectConfig := cfgv3.New() + plugins := []string{"go.kubebuilder.io/v2"} + Expect(projectConfig.SetLayout(strings.Join(plugins, ","))).To(Succeed()) - It("should succeed using more than one plugin key with spaces", func() { - setPluginsFlag("go/v1 , example/v2 , test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(plugins)) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When(fmt.Sprintf("--%s and --%s flags are set", projectVersionFlag, pluginsFlag), func() { - It("should succeed using one plugin key", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1"})) - }) - - It("should succeed using more than one plugin keys", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1,example/v2,test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) - }) + When("having multiple plugins in the layout field", func() { + It("should succeed", func() { + projectConfig := cfgv3.New() + plugins := []string{"go.kubebuilder.io/v2", "declarative.kubebuilder.io/v1"} + Expect(projectConfig.SetLayout(strings.Join(plugins, ","))).To(Succeed()) - It("should succeed using more than one plugin keys with spaces", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1 , example/v2 , test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(plugins)) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When("additional flags are set", func() { - It("should succeed", func() { - setFlag("extra-flag", "extra-value") - _, _, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - }) + When("having invalid plugin keys in the layout field", func() { + It("should fail", func() { + projectConfig := cfgv3.New() + plugins := []string{"go_kubebuilder.io/v2"} + Expect(projectConfig.SetLayout(strings.Join(plugins, ","))).To(Succeed()) - // `--help` is not captured by the whitelist, so we need to special case it - It("should not fail for `--help`", func() { - setBoolFlag("help") - _, _, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) + Expect(c.getInfoFromConfig(projectConfig)).NotTo(Succeed()) }) }) }) - Context("getInfoFromConfig", func() { - var ( - projectConfig config.Config - projectVersion config.Version - plugins []string - err error - ) + Context("getInfoFromFlags", func() { + // Save os.Args and restore it for every test + var args []string + BeforeEach(func() { + c.cmd = c.newRootCmd() - When("not having layout field", func() { - It("should succeed", func() { - projectConfig = cfgv2.New() - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(len(plugins)).To(Equal(0)) - }) + args = os.Args + }) + AfterEach(func() { + os.Args = args }) - When("having layout field", func() { + When("no flag is set", func() { It("should succeed", func() { - projectConfig = cfgv3.New() - Expect(projectConfig.SetLayout("go.kubebuilder.io/v2")).To(Succeed()) - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(plugins).To(Equal([]string{projectConfig.GetLayout()})) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(BeEmpty()) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) }) - }) - Context("CLI.resolveFlagsAndConfigFileConflicts", func() { - const ( - pluginKey1 = "go.kubebuilder.io/v1" - pluginKey2 = "go.kubebuilder.io/v2" - pluginKey3 = "go.kubebuilder.io/v3" - ) - var ( - c *CLI + When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { + It("should succeed using one plugin key", func() { + pluginKeys := []string{"go/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) - projectVersion config.Version - plugins []string - err error + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) + }) - projectVersion1 = config.Version{Number: 1} - projectVersion2 = config.Version{Number: 2} - projectVersion3 = config.Version{Number: 3} - ) + It("should succeed using more than one plugin key", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) - When("having no project version set", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(config.Version{})).To(Equal(0)) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - }) - When("having one project version source", func() { - When("having default project version set", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) - }) + It("should succeed using more than one plugin key with spaces", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ", ")) - When("having project version set from flags", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - When("having project version set from config file", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - projectVersion1, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) + It("should fail for an invalid plugin key", func() { + setPluginsFlag("_/v1") + + Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) - When("having two project version source", func() { - When("having default project version set and from flags", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) - }) - }) + When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { + It("should succeed", func() { + setProjectVersionFlag(projectVersion.String()) - When("having default project version set and from config file", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - projectVersion2, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(BeEmpty()) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - When("having project version set from flags and config file", func() { - It("should succeed if they are the same", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - projectVersion1, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) - - It("should fail if they are different", func() { - c = &CLI{} - _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - projectVersion2, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) - }) + It("should fail for an invalid project version", func() { + setProjectVersionFlag("v_1") + + Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) - When("having three project version sources", func() { - It("should succeed if project version from flags and config file are the same", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - projectVersion2, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) + When(fmt.Sprintf("--%s and --%s flags are set", pluginsFlag, projectVersionFlag), func() { + It("should succeed using one plugin key", func() { + pluginKeys := []string{"go/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - It("should fail if project version from flags and config file are different", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - projectVersion3, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) + It("should succeed using more than one plugin key", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - }) - When("an invalid project version is set", func() { - It("should fail", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "0", - config.Version{}, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) + It("should succeed using more than one plugin key with spaces", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ", ")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) - When("having no plugin keys set", func() { + When("additional flags are set", func() { It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(0)) - }) - }) + setFlag("extra-flag", "extra-value") - When("having one plugin keys source", func() { - When("having default plugin keys set", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - defaultPlugins: map[config.Version][]string{ - projectVersion1: {pluginKey1}, - projectVersion2: {pluginKey2}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) }) - When("having plugin keys set from flags", func() { - It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) - }) + // `--help` is not captured by the allowlist, so we need to special case it + It("should not fail for `--help`", func() { + setBoolFlag("help") - When("having plugin keys set from config file", func() { - It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - []string{pluginKey1}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) }) }) + }) - When("having two plugin keys source", func() { - When("having default plugin keys set and from flags", func() { - It("should succeed", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) - }) + Context("getInfoFromDefaults", func() { + var ( + pluginKeys = []string{"go.kubebuilder.io/v2"} + ) - When("having default plugin keys set and from config file", func() { - It("should succeed", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - []string{pluginKey2}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) - }) + It("should be a no-op if already have plugin keys", func() { + c.pluginKeys = pluginKeys - When("having plugin keys set from flags and config file", func() { - It("should succeed if they are the same", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - []string{pluginKey1}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) - - It("should fail if they are different", func() { - c = &CLI{} - _, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - []string{pluginKey2}, - ) - Expect(err).To(HaveOccurred()) - }) - }) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - When("having three plugin keys sources", func() { - It("should succeed if plugin keys from flags and config file are the same", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - []string{pluginKey2}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) + It("should succeed if default plugins for project version are set", func() { + c.projectVersion = projectVersion + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} - It("should fail if plugin keys from flags and config file are different", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - []string{pluginKey3}, - ) - Expect(err).To(HaveOccurred()) - }) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - When("an invalid plugin key is set", func() { - It("should fail", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{"A"}, - nil, - ) - Expect(err).To(HaveOccurred()) - }) - }) - }) + It("should succeed if default plugins for default project version are set", func() { + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} + c.defaultProjectVersion = projectVersion - // NOTE: only flag info can be tested with CLI.getInfo as the config file doesn't exist, - // previous tests ensure that the info from config files is read properly and that - // conflicts are solved appropriately. - Context("CLI.getInfo", func() { - It("should set project version and plugin keys", func() { - projectVersion := config.Version{Number: 2} - pluginKeys := []string{"go.kubebuilder.io/v2"} - c := &CLI{ - defaultProjectVersion: projectVersion, - defaultPlugins: map[config.Version][]string{ - projectVersion: pluginKeys, - }, - } - c.cmd = c.newRootCmd() - Expect(c.getInfo()).To(Succeed()) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) + }) + + It("should succeed if default plugins for only a single project version are set", func() { + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} + + c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) - Context("CLI.resolve", func() { + Context("resolvePlugins", func() { var ( - c *CLI - - projectVersion = config.Version{Number: 2} - pluginKeys = []string{ "foo.example.com/v1", "bar.example.com/v1", @@ -611,48 +311,83 @@ var _ = Describe("CLI", func() { ) plugins := makeMockPluginsFor(projectVersion, pluginKeys...) - plugins = append(plugins, newMockPlugin("invalid.kubebuilder.io", "v1")) + plugins = append(plugins, + newMockPlugin("invalid.kubebuilder.io", "v1"), + newMockPlugin("only1.kubebuilder.io", "v1", + config.Version{Number: 1}), + newMockPlugin("only2.kubebuilder.io", "v1", + config.Version{Number: 2}), + newMockPlugin("1and2.kubebuilder.io", "v1", + config.Version{Number: 1}, config.Version{Number: 2}), + newMockPlugin("2and3.kubebuilder.io", "v1", + config.Version{Number: 2}, config.Version{Number: 3}), + newMockPlugin("1-2and3.kubebuilder.io", "v1", + config.Version{Number: 1}, config.Version{Number: 2}, config.Version{Number: 3}), + ) pluginMap := makeMapFor(plugins...) - for key, qualified := range map[string]string{ - "foo.example.com/v1": "foo.example.com/v1", - "foo.example.com": "foo.example.com/v1", - "baz": "baz.example.com/v1", - "foo/v2": "foo.kubebuilder.io/v2", - } { - key, qualified := key, qualified - It(fmt.Sprintf("should resolve %q", key), func() { - c = &CLI{ - plugins: pluginMap, - projectVersion: projectVersion, - pluginKeys: []string{key}, - } - Expect(c.resolve()).To(Succeed()) + BeforeEach(func() { + c.plugins = pluginMap + }) + + DescribeTable("should resolve", + func(key, qualified string) { + c.pluginKeys = []string{key} + c.projectVersion = projectVersion + + Expect(c.resolvePlugins()).To(Succeed()) Expect(len(c.resolvedPlugins)).To(Equal(1)) Expect(plugin.KeyFor(c.resolvedPlugins[0])).To(Equal(qualified)) - }) - } + }, + Entry("fully qualified plugin", "foo.example.com/v1", "foo.example.com/v1"), + Entry("plugin without version", "foo.example.com", "foo.example.com/v1"), + Entry("shortname without version", "baz", "baz.example.com/v1"), + Entry("shortname with version", "foo/v2", "foo.kubebuilder.io/v2"), + ) - for _, key := range []string{ - "foo.kubebuilder.io", - "foo/v1", - "foo", - "blah", - "foo.example.com/v2", - "foo/v3", - "foo.example.com/v3", - "invalid.kubebuilder.io/v1", - } { - key := key - It(fmt.Sprintf("should not resolve %q", key), func() { - c = &CLI{ - plugins: pluginMap, - projectVersion: projectVersion, - pluginKeys: []string{key}, - } - Expect(c.resolve()).NotTo(Succeed()) - }) - } + DescribeTable("should not resolve", + func(key string) { + c.pluginKeys = []string{key} + c.projectVersion = projectVersion + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }, + Entry("for an ambiguous version", "foo.kubebuilder.io"), + Entry("for an ambiguous name", "foo/v1"), + Entry("for an ambiguous name and version", "foo"), + Entry("for a non-existent name", "blah"), + Entry("for a non-existent version", "foo.example.com/v2"), + Entry("for a non-existent version", "foo/v3"), + Entry("for a non-existent version", "foo.example.com/v3"), + Entry("for a plugin that doesn't support the project version", "invalid.kubebuilder.io/v1"), + ) + + It("should succeed if only one common project version is found", func() { + c.pluginKeys = []string{"1and2", "2and3"} + + Expect(c.resolvePlugins()).To(Succeed()) + Expect(c.projectVersion.Compare(config.Version{Number: 2})).To(Equal(0)) + }) + + It("should fail if no common project version is found", func() { + c.pluginKeys = []string{"only1", "only2"} + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }) + + It("should fail if more than one common project versions are found", func() { + c.pluginKeys = []string{"1and2", "1-2and3"} + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }) + + It("should succeed if more than one common project versions are found and one is the default", func() { + c.pluginKeys = []string{"2and3", "1-2and3"} + c.defaultProjectVersion = projectVersion + + Expect(c.resolvePlugins()).To(Succeed()) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) + }) }) Context("New", func() { @@ -672,7 +407,11 @@ var _ = Describe("CLI", func() { When("providing a version string", func() { It("should create a valid CLI", func() { const version = "version string" - c, err = New(WithVersion(version)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithVersion(version), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c, "version")).To(BeTrue()) }) @@ -680,7 +419,11 @@ var _ = Describe("CLI", func() { When("enabling completion", func() { It("should create a valid CLI", func() { - c, err = New(WithCompletion()) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithCompletion(), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c, "completion")).To(BeTrue()) }) @@ -702,8 +445,19 @@ var _ = Describe("CLI", func() { It("should return a CLI that returns an error", func() { setPluginsFlag("foo") + c, err = New() Expect(err).NotTo(HaveOccurred()) + + // Overwrite stderr to read the output and reset it afterwards + _, w, _ := os.Pipe() + temp := os.Stderr + defer func() { + os.Stderr = temp + _ = w.Close() + }() + os.Stderr = w + Expect(c.Run()).NotTo(Succeed()) }) }) @@ -713,14 +467,22 @@ var _ = Describe("CLI", func() { It("should create a valid CLI for non-conflicting ones", func() { extraCommand = &cobra.Command{Use: "extra"} - c, err = New(WithExtraCommands(extraCommand)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraCommands(extraCommand), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c, extraCommand.Use)).To(BeTrue()) }) It("should return an error for conflicting ones", func() { extraCommand = &cobra.Command{Use: "init"} - _, err = New(WithExtraCommands(extraCommand)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraCommands(extraCommand), + ) Expect(err).To(HaveOccurred()) }) }) @@ -731,7 +493,6 @@ var _ = Describe("CLI", func() { deprecationWarning = "DEPRECATED" ) var ( - projectVersion = config.Version{Number: 2} deprecatedPlugin = newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) ) @@ -744,11 +505,13 @@ var _ = Describe("CLI", func() { os.Stdout = w c, err = New( - WithDefaultProjectVersion(projectVersion), - WithDefaultPlugins(projectVersion, deprecatedPlugin), WithPlugins(deprecatedPlugin), + WithDefaultPlugins(projectVersion, deprecatedPlugin), + WithDefaultProjectVersion(projectVersion), ) + _ = w.Close() + Expect(err).NotTo(HaveOccurred()) printed, _ := ioutil.ReadAll(r) Expect(string(printed)).To(Equal( diff --git a/pkg/cli/cmd_helpers.go b/pkg/cli/cmd_helpers.go index 505661c97f4..7945346b0d8 100644 --- a/pkg/cli/cmd_helpers.go +++ b/pkg/cli/cmd_helpers.go @@ -17,14 +17,37 @@ limitations under the License. package cli import ( + "errors" "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" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +// noResolvedPluginError is returned by subcommands that require a plugin when none was resolved. +type noResolvedPluginError struct{} + +// Error implements error interface. +func (e noResolvedPluginError) Error() string { + return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file" +} + +// noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found. +type noAvailablePluginError struct { + subcommand string +} + +// Error implements error interface. +func (e noAvailablePluginError) Error() string { + return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand) +} + // cmdErr updates a cobra command to output error information when executed // or used with the help flag. func cmdErr(cmd *cobra.Command, err error) { @@ -32,12 +55,6 @@ func cmdErr(cmd *cobra.Command, err error) { cmd.RunE = errCmdFunc(err) } -// cmdErrNoHelp calls cmdErr(cmd, err) then turns cmd's usage off. -func cmdErrNoHelp(cmd *cobra.Command, err error) { - cmdErr(cmd, err) - cmd.SilenceUsage = true -} - // errCmdFunc returns a cobra RunE function that returns the provided error func errCmdFunc(err error) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { @@ -45,17 +62,230 @@ 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, +// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins. +func (c CLI) filterSubcommands( + filter func(plugin.Plugin) bool, + extract func(plugin.Plugin) plugin.Subcommand, +) ([]string, *[]plugin.Subcommand) { + // Unbundle plugins + plugins := make([]plugin.Plugin, 0, len(c.resolvedPlugins)) + for _, p := range c.resolvedPlugins { + if bundle, isBundle := p.(plugin.Bundle); isBundle { + plugins = append(plugins, bundle.Plugins()...) + } else { + plugins = append(plugins, p) + } + } + + pluginKeys := make([]string, 0, len(plugins)) + subcommands := make([]plugin.Subcommand, 0, len(plugins)) + for _, p := range plugins { + if filter(p) { + pluginKeys = append(pluginKeys, plugin.KeyFor(p)) + subcommands = append(subcommands, extract(p)) + } + } + return pluginKeys, &subcommands +} + +// initializationMethods +func (c CLI) initializationMethods(cmd *cobra.Command, subcommands *[]plugin.Subcommand) *resourceOptions { + // Update metadata method. + meta := plugin.SubcommandMetadata{ + Description: cmd.Long, + Examples: cmd.Example, + } + for _, subcommand := range *subcommands { + if subcmd, updatesMetadata := subcommand.(plugin.UpdatesMetadata); updatesMetadata { + subcmd.UpdateMetadata(c.metadata(), &meta) + } + } + cmd.Long = meta.Description + cmd.Example = meta.Examples + + // Before binding specific plugin flags, bind common ones + requiresResource := false + for _, subcommand := range *subcommands { + if _, requiresResource = subcommand.(plugin.RequiresResource); requiresResource { + break + } + } + var options *resourceOptions + if requiresResource { + options = bindResourceFlags(cmd.Flags()) + } + + // Bind flags method. + for _, subcommand := range *subcommands { + if subcmd, hasFlags := subcommand.(plugin.HasFlags); hasFlags { + subcmd.BindFlags(cmd.Flags()) + } + } + + return options +} + +// executionMethodsFuncs returns cobra RunE functions for PreRunE, RunE, PostRunE cobra hooks. +func (c CLI) executionMethodsFuncs( + pluginKeys []string, + subcommands *[]plugin.Subcommand, + options *resourceOptions, + msg string, +) ( + func(*cobra.Command, []string) error, + func(*cobra.Command, []string) error, + func(*cobra.Command, []string) error, +) { + cfg := yamlstore.New(c.fs) + return executionMethodsPreRunEFunc(pluginKeys, subcommands, cfg, options, c.fs, msg), + executionMethodsRunEFunc(pluginKeys, subcommands, c.fs, msg), + executionMethodsPostRunEFunc(pluginKeys, subcommands, cfg, msg) +} + +// executionMethodsPreRunEFunc returns a cobra RunE function that loads the configuration +// and executes inject config, inject resource and pre-scaffold methods. +func executionMethodsPreRunEFunc( + pluginKeys []string, + subcommands *[]plugin.Subcommand, + cfg store.Store, + options *resourceOptions, + fs afero.Fs, msg string, ) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - if err := subcommand.Run(); err != nil { - return fmt.Errorf("%s: %v", msg, err) + if err := cfg.Load(); 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) + } + + var res *resource.Resource + if options != nil { + options.Domain = cfg.Config().GetDomain() // TODO: offer a flag instead of hard-coding project-wide domain + if err := options.validate(); err != nil { + return fmt.Errorf("%s: unable to create resource: %w", msg, err) + } + res = options.newResource() + } + + // Inject config method. + subcommandsCopy := make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { + if err := subcmd.InjectConfig(cfg.Config()); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the configuration to %q: %w", msg, pluginKeys[i], err) + } + } + } + } + + // Inject resource method. + if res != nil { + subcommandsCopy = make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, requiresResource := subcommand.(plugin.RequiresResource); requiresResource { + if err := subcmd.InjectResource(res); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the resource to %q: %w", msg, pluginKeys[i], err) + } + } + } + } + if err := res.Validate(); err != nil { + return fmt.Errorf("%s: created invalid resource: %w", msg, err) + } } - return c.Save() + + // Pre-scaffold method. + subcommandsCopy = make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { + if err := subcmd.PreScaffold(fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run pre-scaffold tasks of %q: %w", msg, pluginKeys[i], err) + } + } + } + } + + return nil + } +} + +// executionMethodsRunEFunc returns a cobra RunE function that executes the scaffold method. +func executionMethodsRunEFunc( + pluginKeys []string, + subcommands *[]plugin.Subcommand, + fs afero.Fs, + msg string, +) func(*cobra.Command, []string) error { + return func(*cobra.Command, []string) error { + // Scaffold method. + subcommandsCopy := make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if err := subcommand.Scaffold(fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to scaffold with %q: %v", msg, pluginKeys[i], err) + } + } + } + + return nil + } +} + +// executionMethodsPostRunEFunc returns a cobra RunE function that executes the post-scaffold method +// and saves the configuration. +func executionMethodsPostRunEFunc( + pluginKeys []string, + subcommands *[]plugin.Subcommand, + 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) + } + + // Post-scaffold method. + subcommandsCopy := make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold { + if err := subcmd.PostScaffold(); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run post-scaffold tasks of %q: %w", msg, pluginKeys[i], err) + } + } + } + } + + return nil } } diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index 05e20d6fe03..9c2679f9d89 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -14,80 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const editErrorMsg = "failed to edit project" + func (c CLI) newEditCmd() *cobra.Command { - ctx := c.newEditContext() cmd := &cobra.Command{ - Use: "edit", - Short: "This command will edit the project configuration", - Long: ctx.Description, - Example: ctx.Examples, + Use: "edit", + Short: "Update the project configuration", + Long: `Edit the project configuration. +`, RunE: errCmdFunc( fmt.Errorf("project must be initialized"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindEdit(ctx, cmd) - return cmd -} - -func (c CLI) newEditContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Edit the project configuration. -`, - } -} - -func (c CLI) bindEdit(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var editPlugin plugin.Edit - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Edit) - if isValid { - if editPlugin != nil { - err := fmt.Errorf( - "duplicate edit project plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(editPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - editPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Edit. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Edit) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Edit).GetEditSubcommand() + }, + ) - if editPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project edit plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(*subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"edit project"}) + return cmd } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, editErrorMsg) - 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))) + return cmd } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 575bb8f0a7c..d486627a829 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,47 +26,119 @@ import ( "github.com/spf13/cobra" - internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const initErrorMsg = "failed to initialize project" + func (c CLI) newInitCmd() *cobra.Command { - ctx := c.newInitContext() cmd := &cobra.Command{ - Use: "init", - Short: "Initialize a new project", - Long: ctx.Description, - Example: ctx.Examples, + Use: "init", + Short: "Initialize a new project", + Long: `Initialize a new project. + +For further help about a specific plugin, set --plugins. +`, + Example: c.getInitHelpExamples(), Run: func(cmd *cobra.Command, args []string) {}, } // Register --project-version on the dynamically created command // so that it shows up in help and does not cause a parse error. - cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), - fmt.Sprintf("project version, possible values: (%s)", strings.Join(c.getAvailableProjectVersions(), ", "))) - // The --plugins flag can only be called to init projects v2+. - if c.projectVersion.Compare(cfgv2.Version) == 1 { - cmd.Flags().StringSlice(pluginsFlag, nil, - "Name and optionally version of the plugin to initialize the project with. "+ - fmt.Sprintf("Available plugins: (%s)", strings.Join(c.getAvailablePlugins(), ", "))) + cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") + + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. + if len(c.resolvedPlugins) == 0 { + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindInit(ctx, cmd) - return cmd -} + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Init. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Init) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Init).GetInitSubcommand() + }, + ) + + // Verify that there is at least one remaining plugin. + if len(*subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"project initialization"}) + return cmd + } -func (c CLI) newInitContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Initialize a new project. + // Initialization methods. + _ = c.initializationMethods(cmd, subcommands) -For further help about a specific project version, set --project-version. -`, - Examples: c.getInitHelpExamples(), + // Execution methods. + cfg := yamlstore.New(c.fs) + 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", initErrorMsg) + } + + err := cfg.New(c.projectVersion) + if err != nil { + return fmt.Errorf("%s: error initializing project configuration: %w", initErrorMsg, err) + } + + // We extract the plugin keys again instead of using the ones obtained when filtering subcommands + // as there plugins are unbundled but we want to keep bundle names in the layout. + resolvedPluginKeys := make([]string, 0, len(c.resolvedPlugins)) + for _, p := range c.resolvedPlugins { + resolvedPluginKeys = append(resolvedPluginKeys, plugin.KeyFor(p)) + } + _ = cfg.Config().SetLayout(strings.Join(resolvedPluginKeys, ",")) + + // Inject config method. + subcommandsCopy := make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { + if err := subcmd.InjectConfig(cfg.Config()); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to inject the configuration to %q: %w", + initErrorMsg, pluginKeys[i], err) + } + } + } + } + + // Pre-scaffold method. + subcommandsCopy = make([]plugin.Subcommand, len(*subcommands)) + copy(subcommandsCopy, *subcommands) + for i, subcommand := range subcommandsCopy { + if subcmd, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { + if err := subcmd.PreScaffold(c.fs); err != nil { + var exitError plugin.ExitError + if errors.As(err, &exitError) { + fmt.Printf("skipping %q: %s\n", pluginKeys[i], exitError.Reason) + *subcommands = append((*subcommands)[:i], (*subcommands)[i+1:]...) + } else { + return fmt.Errorf("%s: unable to run pre-scaffold tasks of %q: %w", + initErrorMsg, pluginKeys[i], err) + } + } + } + } + + return nil } + cmd.RunE = executionMethodsRunEFunc(pluginKeys, subcommands, c.fs, initErrorMsg) + cmd.PostRunE = executionMethodsPostRunEFunc(pluginKeys, subcommands, cfg, initErrorMsg) + + return cmd } func (c CLI) getInitHelpExamples() string { @@ -109,55 +181,3 @@ func (c CLI) getAvailablePlugins() (pluginKeys []string) { sort.Strings(pluginKeys) return pluginKeys } - -func (c CLI) bindInit(ctx plugin.Context, cmd *cobra.Command) { - if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf("no resolved plugins, please specify plugins with --%s or/and --%s flags", - projectVersionFlag, pluginsFlag)) - return - } - - var initPlugin plugin.Init - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Init) - if isValid { - if initPlugin != nil { - err := fmt.Errorf("duplicate initialization plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(initPlugin), plugin.KeyFor(p)) - cmdErrNoHelp(cmd, err) - return - } - initPlugin = tmpPlugin - } - } - - if initPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project init plugin: %v", c.pluginKeys)) - 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") - } - if err := subcommand.Run(); err != nil { - return fmt.Errorf("failed to initialize project with %q: %v", plugin.KeyFor(initPlugin), err) - } - return cfg.Save() - } -} 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_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/options.go b/pkg/cli/options.go index fa4fd73a16b..99341605b66 100644 --- a/pkg/cli/options.go +++ b/pkg/cli/options.go @@ -44,15 +44,21 @@ func WithVersion(version string) Option { } } -// WithDefaultProjectVersion is an Option that sets the CLI's default project version. +// WithPlugins is an Option that sets the CLI's plugins. // -// Setting an invalid version results in an error. -func WithDefaultProjectVersion(version config.Version) Option { +// Specifying any invalid plugin results in an error. +func WithPlugins(plugins ...plugin.Plugin) Option { return func(c *CLI) error { - if err := version.Validate(); err != nil { - return fmt.Errorf("broken pre-set default project version %q: %v", version, err) + for _, p := range plugins { + key := plugin.KeyFor(p) + if _, isConflicting := c.plugins[key]; isConflicting { + return fmt.Errorf("two plugins have the same key: %q", key) + } + if err := plugin.Validate(p); err != nil { + return fmt.Errorf("broken pre-set plugin %q: %v", key, err) + } + c.plugins[key] = p } - c.defaultProjectVersion = version return nil } } @@ -63,7 +69,7 @@ func WithDefaultProjectVersion(version config.Version) Option { func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { return func(c *CLI) error { if err := projectVersion.Validate(); err != nil { - return fmt.Errorf("broken pre-set project version %q for default plugins: %v", projectVersion, err) + return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err) } if len(plugins) == 0 { return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion) @@ -81,21 +87,15 @@ func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) } } -// WithPlugins is an Option that sets the CLI's plugins. +// WithDefaultProjectVersion is an Option that sets the CLI's default project version. // -// Specifying any invalid plugin results in an error. -func WithPlugins(plugins ...plugin.Plugin) Option { +// Setting an invalid version results in an error. +func WithDefaultProjectVersion(version config.Version) Option { return func(c *CLI) error { - for _, p := range plugins { - key := plugin.KeyFor(p) - if _, isConflicting := c.plugins[key]; isConflicting { - return fmt.Errorf("two plugins have the same key: %q", key) - } - if err := plugin.Validate(p); err != nil { - return fmt.Errorf("broken pre-set plugin %q: %v", key, err) - } - c.plugins[key] = p + if err := version.Validate(); err != nil { + return fmt.Errorf("broken pre-set default project version %q: %v", version, err) } + c.defaultProjectVersion = version return nil } } diff --git a/pkg/cli/options_test.go b/pkg/cli/options_test.go index 9485c2c6f50..603f93f6582 100644 --- a/pkg/cli/options_test.go +++ b/pkg/cli/options_test.go @@ -17,9 +17,8 @@ limitations under the License. package cli import ( - "fmt" - . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "github.com/spf13/cobra" @@ -68,142 +67,136 @@ var _ = Describe("CLI options", func() { }) }) - Context("WithDefaultProjectVersion", func() { - It("should return a valid CLI", func() { - defaultProjectVersions := []config.Version{ - {Number: 1}, - {Number: 2}, - {Number: 3, Stage: stage.Alpha}, - } - for _, defaultProjectVersion := range defaultProjectVersions { - By(fmt.Sprintf("using %q", defaultProjectVersion)) - c, err = newCLI(WithDefaultProjectVersion(defaultProjectVersion)) - Expect(err).NotTo(HaveOccurred()) - Expect(c).NotTo(BeNil()) - Expect(c.defaultProjectVersion).To(Equal(defaultProjectVersion)) - } - }) - - It("should return an error", func() { - defaultProjectVersions := []config.Version{ - {}, // Empty default project version - {Number: 1, Stage: stage.Stage(27)}, // Invalid stage in default project version - } - for _, defaultProjectVersion := range defaultProjectVersions { - By(fmt.Sprintf("using %q", defaultProjectVersion)) - _, err = newCLI(WithDefaultProjectVersion(defaultProjectVersion)) - Expect(err).To(HaveOccurred()) - } - }) - }) - - Context("WithDefaultPlugins", func() { + Context("WithPlugins", func() { It("should return a valid CLI", func() { - c, err = newCLI(WithDefaultPlugins(projectVersion, p)) + c, err = newCLI(WithPlugins(p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) - Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) + Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p})) }) - When("providing an invalid project version", func() { + When("providing plugins with same keys", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) + _, err = newCLI(WithPlugins(p, p)) Expect(err).To(HaveOccurred()) }) }) - When("providing an empty set of plugins", func() { + When("providing plugins with same keys in two steps", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion)) + _, err = newCLI(WithPlugins(p), WithPlugins(p)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np1)) + _, err = newCLI(WithPlugins(np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np2)) + _, err = newCLI(WithPlugins(np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np3)) + _, err = newCLI(WithPlugins(np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np4)) - Expect(err).To(HaveOccurred()) - }) - }) - - When("providing a default plugin for an unsupported project version", func() { - It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) + _, err = newCLI(WithPlugins(np4)) Expect(err).To(HaveOccurred()) }) }) }) - Context("WithPlugins", func() { + Context("WithDefaultPlugins", func() { It("should return a valid CLI", func() { - c, err = newCLI(WithPlugins(p)) + c, err = newCLI(WithDefaultPlugins(projectVersion, p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) - Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p})) + Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) }) - When("providing plugins with same keys", func() { + When("providing an invalid project version", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(p, p)) + _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) Expect(err).To(HaveOccurred()) }) }) - When("providing plugins with same keys in two steps", func() { + When("providing an empty set of plugins", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(p), WithPlugins(p)) + _, err = newCLI(WithDefaultPlugins(projectVersion)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np1)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np2)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np3)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np4)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np4)) Expect(err).To(HaveOccurred()) }) }) + + When("providing a default plugin for an unsupported project version", func() { + It("should return an error", func() { + _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("WithDefaultProjectVersion", func() { + DescribeTable("should return a valid CLI", + func(projectVersion config.Version) { + c, err = newCLI(WithDefaultProjectVersion(projectVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(c).NotTo(BeNil()) + Expect(c.defaultProjectVersion).To(Equal(projectVersion)) + }, + Entry("for version `2`", config.Version{Number: 2}), + Entry("for version `3-alpha`", config.Version{Number: 3, Stage: stage.Alpha}), + Entry("for version `3`", config.Version{Number: 3}), + ) + + DescribeTable("should fail", + func(projectVersion config.Version) { + _, err = newCLI(WithDefaultProjectVersion(projectVersion)) + Expect(err).To(HaveOccurred()) + }, + Entry("for empty version", config.Version{}), + Entry("for invalid stage", config.Version{Number: 1, Stage: stage.Stage(27)}), + ) }) Context("WithExtraCommands", func() { diff --git a/pkg/cli/resource.go b/pkg/cli/resource.go new file mode 100644 index 00000000000..100bb32aa66 --- /dev/null +++ b/pkg/cli/resource.go @@ -0,0 +1,85 @@ +/* +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 cli + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + groupPresent = "group flag present but empty" + versionPresent = "version flag present but empty" + kindPresent = "kind flag present but empty" +) + +// resourceOptions contains the information required to build a new resource.Resource. +type resourceOptions struct { + resource.GVK +} + +func bindResourceFlags(fs *pflag.FlagSet) *resourceOptions { + options := &resourceOptions{} + + fs.StringVar(&options.Group, "group", "", "resource Group") + fs.StringVar(&options.Version, "version", "", "resource Version") + fs.StringVar(&options.Kind, "kind", "", "resource Kind") + + return options +} + +// validate verifies that all the fields have valid values. +func (opts resourceOptions) validate() error { + // Check that the required flags did not get a flag as their value. + // We can safely look for a '-' as the first char as none of the fields accepts it. + // NOTE: We must do this for all the required flags first or we may output the wrong + // error as flags may seem to be missing because Cobra assigned them to another flag. + if strings.HasPrefix(opts.Group, "-") { + return fmt.Errorf(groupPresent) + } + if strings.HasPrefix(opts.Version, "-") { + return fmt.Errorf(versionPresent) + } + if strings.HasPrefix(opts.Kind, "-") { + return fmt.Errorf(kindPresent) + } + + // We do not check here if the GVK values are empty because that would + // make them mandatory and some plugins may want to set default values. + // Instead, this is checked by resource.GVK.Validate() + + return nil +} + +// newResource creates a new resource from the options +func (opts resourceOptions) newResource() *resource.Resource { + return &resource.Resource{ + GVK: resource.GVK{ // Remove whitespaces to prevent values like " " pass validation + Group: strings.TrimSpace(opts.Group), + Domain: strings.TrimSpace(opts.Domain), + Version: strings.TrimSpace(opts.Version), + Kind: strings.TrimSpace(opts.Kind), + }, + Plural: resource.RegularPlural(opts.Kind), + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + } +} diff --git a/pkg/cli/resource_test.go b/pkg/cli/resource_test.go new file mode 100644 index 00000000000..6a4cd378180 --- /dev/null +++ b/pkg/cli/resource_test.go @@ -0,0 +1,110 @@ +/* +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 cli + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("resourceOptions", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" + ) + + var ( + fullGVK = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } + noDomainGVK = resource.GVK{ + Group: group, + Version: version, + Kind: kind, + } + noGroupGVK = resource.GVK{ + Domain: domain, + Version: version, + Kind: kind, + } + ) + + Context("validate", func() { + DescribeTable("should succeed for valid options", + func(options resourceOptions) { Expect(options.validate()).To(Succeed()) }, + Entry("full GVK", resourceOptions{GVK: fullGVK}), + Entry("missing domain", resourceOptions{GVK: noDomainGVK}), + Entry("missing group", resourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should fail for invalid options", + func(options resourceOptions) { Expect(options.validate()).NotTo(Succeed()) }, + Entry("group flag captured another flag", resourceOptions{GVK: resource.GVK{Group: "--version"}}), + Entry("version flag captured another flag", resourceOptions{GVK: resource.GVK{Version: "--kind"}}), + Entry("kind flag captured another flag", resourceOptions{GVK: resource.GVK{Kind: "--group"}}), + ) + }) + + Context("newResource", func() { + DescribeTable("should succeed if the Resource is valid", + func(options resourceOptions) { + Expect(options.validate()).To(Succeed()) + + resource := options.newResource() + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + // Plural is checked in the next test + Expect(resource.Path).To(Equal("")) + Expect(resource.API).NotTo(BeNil()) + Expect(resource.API.CRDVersion).To(Equal("")) + Expect(resource.API.Namespaced).To(BeFalse()) + Expect(resource.Controller).To(BeFalse()) + Expect(resource.Webhooks).NotTo(BeNil()) + Expect(resource.Webhooks.WebhookVersion).To(Equal("")) + Expect(resource.Webhooks.Defaulting).To(BeFalse()) + Expect(resource.Webhooks.Validation).To(BeFalse()) + Expect(resource.Webhooks.Conversion).To(BeFalse()) + }, + Entry("full GVK", resourceOptions{GVK: fullGVK}), + Entry("missing domain", resourceOptions{GVK: noDomainGVK}), + Entry("missing group", resourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should default the Plural by pluralizing the Kind", + func(kind, plural string) { + options := resourceOptions{GVK: resource.GVK{Group: group, Version: version, Kind: kind}} + Expect(options.validate()).To(Succeed()) + + resource := options.newResource() + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.Plural).To(Equal(plural)) + }, + Entry("for `FirstMate`", "FirstMate", "firstmates"), + Entry("for `Fish`", "Fish", "fish"), + Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), + ) + }) +}) diff --git a/pkg/cli/root.go b/pkg/cli/root.go index af4be6be247..93d0c0e863d 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -40,14 +40,11 @@ func (c CLI) newRootCmd() *cobra.Command { }, } - // Global flags for all subcommands - // NOTE: the current plugin resolution doesn't allow to provide values to this flag different to those configured - // for the project, so default values need to be empty and considered when these two sources are compared. - // Another approach would be to allow users to overwrite the project configuration values. In this case, flags - // would take precedence over project configuration, which would take precedence over CLI defaults. - fs := cmd.PersistentFlags() - fs.String(projectVersionFlag, "", "project version") - fs.StringSlice(pluginsFlag, nil, "plugin keys of the plugin to initialize the project with") + // Global flags for all subcommands. + cmd.PersistentFlags().StringSlice(pluginsFlag, nil, "plugin keys to be used for this subcommand execution") + + // Register --project-version on the root command so that it shows up in help. + cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") // As the root command will be used to shot the help message under some error conditions, // like during plugin resolving, we need to allow unknown flags to prevent parsing errors. @@ -56,10 +53,10 @@ func (c CLI) newRootCmd() *cobra.Command { return cmd } -// rootExamples builds the examples string for the root command +// rootExamples builds the examples string for the root command before resolving plugins func (c CLI) rootExamples() string { str := fmt.Sprintf(`The first step is to initialize your project: - %[1]s init --project-version= --plugins= + %[1]s init [--plugins= [--project-version=]] is a comma-separated list of plugin keys from the following table and a supported project version for these plugins. @@ -68,21 +65,19 @@ and a supported project version for these plugins. For more specific help for the init command of a certain plugins and project version configuration please run: - %[1]s init --help --project-version= --plugins= + %[1]s init --help --plugins= [--project-version=] `, c.commandName, c.getPluginTable()) - str += fmt.Sprintf("\nDefault project version: %s\n", c.defaultProjectVersion) - - if defaultPlugins, hasDefaultPlugins := c.defaultPlugins[c.defaultProjectVersion]; hasDefaultPlugins { - str += fmt.Sprintf("Default plugin keys: %q\n", strings.Join(defaultPlugins, ",")) + if len(c.defaultPlugins) != 0 { + if defaultPlugins, found := c.defaultPlugins[c.defaultProjectVersion]; found { + str += fmt.Sprintf("\nDefault plugin keys: %q\n", strings.Join(defaultPlugins, ",")) + } } - str += fmt.Sprintf(` -After the project has been initialized, run - %[1]s --help -to obtain further info about available commands.`, - c.commandName) + if c.defaultProjectVersion.Validate() == nil { + str += fmt.Sprintf("Default project version: %q\n", c.defaultProjectVersion) + } return str } diff --git a/pkg/cli/cli_suite_test.go b/pkg/cli/suite_test.go similarity index 100% rename from pkg/cli/cli_suite_test.go rename to pkg/cli/suite_test.go diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index f5b7d108a13..8f51d47a207 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -14,80 +14,58 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const webhookErrorMsg = "failed to create webhook" + func (c CLI) newCreateWebhookCmd() *cobra.Command { - ctx := c.newWebhookContext() cmd := &cobra.Command{ - Use: "webhook", - Short: "Scaffold a webhook for an API resource", - Long: ctx.Description, - Example: ctx.Examples, + Use: "webhook", + Short: "Scaffold a webhook for an API resource", + Long: `Scaffold a webhook for an API resource. +`, RunE: errCmdFunc( fmt.Errorf("webhook subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateWebhook(ctx, cmd) - return cmd -} - -func (c CLI) newWebhookContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a webhook for an API resource. -`, - } -} - -// nolint:dupl -func (c CLI) bindCreateWebhook(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createWebhookPlugin plugin.CreateWebhook - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateWebhook) - if isValid { - if createWebhookPlugin != nil { - err := fmt.Errorf("duplicate webhook creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createWebhookPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createWebhookPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateWebhook. + pluginKeys, subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateWebhook) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateWebhook).GetCreateWebhookSubcommand() + }, + ) - if createWebhookPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a webhook creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(*subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"webhook creation"}) + return cmd } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + // Initialization methods. + options := c.initializationMethods(cmd, subcommands) + + // Execution methods. + cmd.PreRunE, cmd.RunE, cmd.PostRunE = c.executionMethodsFuncs(pluginKeys, subcommands, options, webhookErrorMsg) - 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))) + return cmd } 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/machinery/errors.go b/pkg/machinery/errors.go new file mode 100644 index 00000000000..af015ac30e9 --- /dev/null +++ b/pkg/machinery/errors.go @@ -0,0 +1,145 @@ +/* +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 machinery + +import ( + "fmt" +) + +// This file contains the errors returned by the scaffolding machinery +// They are exported to be able to check which kind of error was returned + +// ValidateError is a wrapper error that will be used for errors returned by RequiresValidation.Validate +type ValidateError struct { + error +} + +// Unwrap implements Wrapper interface +func (e ValidateError) Unwrap() error { + return e.error +} + +// SetTemplateDefaultsError is a wrapper error that will be used for errors returned by Template.SetTemplateDefaults +type SetTemplateDefaultsError struct { + error +} + +// Unwrap implements Wrapper interface +func (e SetTemplateDefaultsError) Unwrap() error { + return e.error +} + +// ExistsFileError is a wrapper error that will be used for errors when checking for a file existence +type ExistsFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e ExistsFileError) Unwrap() error { + return e.error +} + +// OpenFileError is a wrapper error that will be used for errors when opening a file +type OpenFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e OpenFileError) Unwrap() error { + return e.error +} + +// CreateDirectoryError is a wrapper error that will be used for errors when creating a directory +type CreateDirectoryError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CreateDirectoryError) Unwrap() error { + return e.error +} + +// CreateFileError is a wrapper error that will be used for errors when creating a file +type CreateFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CreateFileError) Unwrap() error { + return e.error +} + +// ReadFileError is a wrapper error that will be used for errors when reading a file +type ReadFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e ReadFileError) Unwrap() error { + return e.error +} + +// WriteFileError is a wrapper error that will be used for errors when writing a file +type WriteFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e WriteFileError) Unwrap() error { + return e.error +} + +// CloseFileError is a wrapper error that will be used for errors when closing a file +type CloseFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CloseFileError) Unwrap() error { + return e.error +} + +// ModelAlreadyExistsError is returned if the file is expected not to exist but a previous model does +type ModelAlreadyExistsError struct { + path string +} + +// Error implements error interface +func (e ModelAlreadyExistsError) Error() string { + return fmt.Sprintf("failed to create %s: model already exists", e.path) +} + +// UnknownIfExistsActionError is returned if the if-exists-action is unknown +type UnknownIfExistsActionError struct { + path string + ifExistsAction IfExistsAction +} + +// Error implements error interface +func (e UnknownIfExistsActionError) Error() string { + return fmt.Sprintf("unknown behavior if file exists (%d) for %s", e.ifExistsAction, e.path) +} + +// FileAlreadyExistsError is returned if the file is expected not to exist but it does +type FileAlreadyExistsError struct { + path string +} + +// Error implements error interface +func (e FileAlreadyExistsError) Error() string { + return fmt.Sprintf("failed to create %s: file already exists", e.path) +} diff --git a/pkg/machinery/errors_test.go b/pkg/machinery/errors_test.go new file mode 100644 index 00000000000..2c4d682e957 --- /dev/null +++ b/pkg/machinery/errors_test.go @@ -0,0 +1,55 @@ +/* +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 machinery + +import ( + "errors" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Errors", func() { + var ( + path = filepath.Join("path", "to", "file") + testErr = errors.New("test error") + ) + + DescribeTable("should contain the wrapped error", + func(err error) { + Expect(errors.Is(err, testErr)).To(BeTrue()) + }, + Entry("for validate errors", ValidateError{testErr}), + Entry("for set template defaults errors", SetTemplateDefaultsError{testErr}), + Entry("for file existence errors", ExistsFileError{testErr}), + Entry("for file opening errors", OpenFileError{testErr}), + Entry("for directory creation errors", CreateDirectoryError{testErr}), + Entry("for file creation errors", CreateFileError{testErr}), + Entry("for file reading errors", ReadFileError{testErr}), + Entry("for file writing errors", WriteFileError{testErr}), + Entry("for file closing errors", CloseFileError{testErr}), + ) + + // NOTE: the following test increases coverage + It("should print a descriptive error message", func() { + Expect(ModelAlreadyExistsError{path}.Error()).To(ContainSubstring("model already exists")) + Expect(UnknownIfExistsActionError{path, -1}.Error()).To(ContainSubstring("unknown behavior if file exists")) + Expect(FileAlreadyExistsError{path}.Error()).To(ContainSubstring("file already exists")) + }) +}) diff --git a/pkg/model/file/file.go b/pkg/machinery/file.go similarity index 75% rename from pkg/model/file/file.go rename to pkg/machinery/file.go index 3b9db1cd341..bf053d14ce5 100644 --- a/pkg/model/file/file.go +++ b/pkg/machinery/file.go @@ -14,30 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery // IfExistsAction determines what to do if the scaffold file already exists type IfExistsAction int const ( - // Skip skips the file and moves to the next one - Skip IfExistsAction = iota + // SkipFile skips the file and moves to the next one + SkipFile IfExistsAction = iota // Error returns an error and stops processing Error - // Overwrite truncates and overwrites the existing file - Overwrite + // OverwriteFile truncates and overwrites the existing file + OverwriteFile ) // File describes a file that will be written type File struct { // Path is the file to write - Path string `json:"path,omitempty"` + Path string // Contents is the generated output - Contents string `json:"contents,omitempty"` + Contents string // IfExistsAction determines what to do if the file exists - IfExistsAction IfExistsAction `json:"ifExistsAction,omitempty"` + IfExistsAction IfExistsAction } diff --git a/pkg/model/file/funcmap.go b/pkg/machinery/funcmap.go similarity index 86% rename from pkg/model/file/funcmap.go rename to pkg/machinery/funcmap.go index a0a9432427b..ac25e272d70 100644 --- a/pkg/model/file/funcmap.go +++ b/pkg/machinery/funcmap.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "fmt" @@ -39,10 +39,9 @@ func isEmptyString(s string) bool { } // hashFNV will generate a random string useful for generating a unique string -func hashFNV(s string) (string, error) { +func hashFNV(s string) string { hasher := fnv.New32a() - if _, err := hasher.Write([]byte(s)); err != nil { - return "", err - } - return fmt.Sprintf("%x", hasher.Sum(nil)), nil + // Hash.Write never returns an error + _, _ = hasher.Write([]byte(s)) + return fmt.Sprintf("%x", hasher.Sum(nil)) } diff --git a/pkg/machinery/funcmap_test.go b/pkg/machinery/funcmap_test.go new file mode 100644 index 00000000000..7bb33df48af --- /dev/null +++ b/pkg/machinery/funcmap_test.go @@ -0,0 +1,45 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("funcmap functions", func() { + Context("isEmptyString", func() { + It("should return true for empty strings", func() { + Expect(isEmptyString("")).To(BeTrue()) + }) + + DescribeTable("should return false for any other string", + func(str string) { Expect(isEmptyString(str)).To(BeFalse()) }, + Entry(`for "a"`, "a"), + Entry(`for "1"`, "1"), + Entry(`for "-"`, "-"), + Entry(`for "."`, "."), + ) + }) + + Context("hashFNV", func() { + It("should hash the input", func() { + Expect(hashFNV("test")).To(Equal("afd071e5")) + }) + }) +}) diff --git a/pkg/machinery/injector.go b/pkg/machinery/injector.go new file mode 100644 index 00000000000..5675a8a9ec3 --- /dev/null +++ b/pkg/machinery/injector.go @@ -0,0 +1,66 @@ +/* +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 machinery + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// injector is used to inject certain fields to file templates. +type injector struct { + // config stores the project configuration. + config config.Config + + // boilerplate is the copyright comment added at the top of scaffolded files. + boilerplate string + + // resource contains the information of the API that is being scaffolded. + resource *resource.Resource +} + +// injectInto injects fields from the universe into the builder +func (i injector) injectInto(builder Builder) { + // Inject project configuration + if i.config != nil { + if builderWithDomain, hasDomain := builder.(HasDomain); hasDomain { + builderWithDomain.InjectDomain(i.config.GetDomain()) + } + if builderWithRepository, hasRepository := builder.(HasRepository); hasRepository { + builderWithRepository.InjectRepository(i.config.GetRepository()) + } + if builderWithProjectName, hasProjectName := builder.(HasProjectName); hasProjectName { + builderWithProjectName.InjectProjectName(i.config.GetProjectName()) + } + if builderWithMultiGroup, hasMultiGroup := builder.(HasMultiGroup); hasMultiGroup { + builderWithMultiGroup.InjectMultiGroup(i.config.IsMultiGroup()) + } + if builderWithComponentConfig, hasComponentConfig := builder.(HasComponentConfig); hasComponentConfig { + builderWithComponentConfig.InjectComponentConfig(i.config.IsComponentConfig()) + } + } + // Inject boilerplate + if builderWithBoilerplate, hasBoilerplate := builder.(HasBoilerplate); hasBoilerplate { + builderWithBoilerplate.InjectBoilerplate(i.boilerplate) + } + // Inject resource + if i.resource != nil { + if builderWithResource, hasResource := builder.(HasResource); hasResource { + builderWithResource.InjectResource(i.resource) + } + } +} diff --git a/pkg/machinery/injector_test.go b/pkg/machinery/injector_test.go new file mode 100644 index 00000000000..7c5c3038f4b --- /dev/null +++ b/pkg/machinery/injector_test.go @@ -0,0 +1,295 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +type templateBase struct { + path string + ifExistsAction IfExistsAction +} + +func (t templateBase) GetPath() string { + return t.path +} + +func (t templateBase) GetIfExistsAction() IfExistsAction { + return t.ifExistsAction +} + +type templateWithDomain struct { + templateBase + domain string +} + +func (t *templateWithDomain) InjectDomain(domain string) { + t.domain = domain +} + +type templateWithRepository struct { + templateBase + repository string +} + +func (t *templateWithRepository) InjectRepository(repository string) { + t.repository = repository +} + +type templateWithProjectName struct { + templateBase + projectName string +} + +func (t *templateWithProjectName) InjectProjectName(projectName string) { + t.projectName = projectName +} + +type templateWithMultiGroup struct { + templateBase + multiGroup bool +} + +func (t *templateWithMultiGroup) InjectMultiGroup(multiGroup bool) { + t.multiGroup = multiGroup +} + +type templateWithComponentConfig struct { + templateBase + componentConfig bool +} + +func (t *templateWithComponentConfig) InjectComponentConfig(componentConfig bool) { + t.componentConfig = componentConfig +} + +type templateWithBoilerplate struct { + templateBase + boilerplate string +} + +func (t *templateWithBoilerplate) InjectBoilerplate(boilerplate string) { + t.boilerplate = boilerplate +} + +type templateWithResource struct { + templateBase + resource *resource.Resource +} + +func (t *templateWithResource) InjectResource(res *resource.Resource) { + t.resource = res +} + +var _ = Describe("injector", func() { + var tmp = templateBase{ + path: "my/path/to/file", + ifExistsAction: Error, + } + + Context("injectInto", func() { + Context("Config", func() { + var c config.Config + + BeforeEach(func() { + c = cfgv3.New() + }) + + Context("Domain", func() { + var template *templateWithDomain + + BeforeEach(func() { + template = &templateWithDomain{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.domain).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a domain set", func() { + injector{config: c}.injectInto(template) + Expect(template.domain).To(Equal("")) + }) + + It("should inject if the config has a domain set", func() { + const domain = "my.domain" + Expect(c.SetDomain(domain)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.domain).To(Equal(domain)) + }) + }) + + Context("Repository", func() { + var template *templateWithRepository + + BeforeEach(func() { + template = &templateWithRepository{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.repository).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a repository set", func() { + injector{config: c}.injectInto(template) + Expect(template.repository).To(Equal("")) + }) + + It("should inject if the config has a repository set", func() { + const repo = "test" + Expect(c.SetRepository(repo)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.repository).To(Equal(repo)) + }) + }) + + Context("Project name", func() { + var template *templateWithProjectName + + BeforeEach(func() { + template = &templateWithProjectName{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.projectName).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a project name set", func() { + injector{config: c}.injectInto(template) + Expect(template.projectName).To(Equal("")) + }) + + It("should inject if the config has a project name set", func() { + const projectName = "my project" + Expect(c.SetProjectName(projectName)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.projectName).To(Equal(projectName)) + }) + }) + + Context("Multi-group", func() { + var template *templateWithMultiGroup + + BeforeEach(func() { + template = &templateWithMultiGroup{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.multiGroup).To(BeFalse()) + }) + + It("should not set the flag if the config doesn't have the multi-group flag set", func() { + injector{config: c}.injectInto(template) + Expect(template.multiGroup).To(BeFalse()) + }) + + It("should set the flag if the config has the multi-group flag set", func() { + Expect(c.SetMultiGroup()).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.multiGroup).To(BeTrue()) + }) + }) + + Context("Component config", func() { + var template *templateWithComponentConfig + + BeforeEach(func() { + template = &templateWithComponentConfig{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.componentConfig).To(BeFalse()) + }) + + It("should not set the flag if the config doesn't have the component config flag set", func() { + injector{config: c}.injectInto(template) + Expect(template.componentConfig).To(BeFalse()) + }) + + It("should set the flag if the config has the component config flag set", func() { + Expect(c.SetComponentConfig()).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.componentConfig).To(BeTrue()) + }) + }) + }) + + Context("Boilerplate", func() { + var template *templateWithBoilerplate + + BeforeEach(func() { + template = &templateWithBoilerplate{templateBase: tmp} + }) + + It("should not inject anything if no boilerplate was set", func() { + injector{}.injectInto(template) + Expect(template.boilerplate).To(Equal("")) + }) + + It("should inject if the a boilerplate was set", func() { + const boilerplate = `Copyright "The Kubernetes Authors"` + + injector{boilerplate: boilerplate}.injectInto(template) + Expect(template.boilerplate).To(Equal(boilerplate)) + }) + }) + + Context("Resource", func() { + var template *templateWithResource + + BeforeEach(func() { + template = &templateWithResource{templateBase: tmp} + }) + + It("should not inject anything if the resource is nil", func() { + injector{}.injectInto(template) + Expect(template.resource).To(BeNil()) + }) + + It("should inject if the config has a domain set", func() { + var res = &resource.Resource{ + GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }, + } + + injector{resource: res}.injectInto(template) + Expect(template.resource).To(Equal(res)) + }) + + }) + }) +}) diff --git a/pkg/model/file/interfaces.go b/pkg/machinery/interfaces.go similarity index 99% rename from pkg/model/file/interfaces.go rename to pkg/machinery/interfaces.go index 43470df4e60..73837dd8b30 100644 --- a/pkg/model/file/interfaces.go +++ b/pkg/machinery/interfaces.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "text/template" @@ -67,6 +67,12 @@ type HasRepository interface { InjectRepository(string) } +// HasProjectName allows a project name to be used on a template. +type HasProjectName interface { + // InjectProjectName sets the template project name. + InjectProjectName(string) +} + // HasMultiGroup allows the multi-group flag to be used on a template type HasMultiGroup interface { // InjectMultiGroup sets the template multi-group flag @@ -91,12 +97,6 @@ type HasResource interface { InjectResource(*resource.Resource) } -// HasProjectName allows a project name to be used on a template. -type HasProjectName interface { - // InjectProjectName sets the template project name. - InjectProjectName(string) -} - // UseCustomFuncMap allows a template to use a custom template.FuncMap instead of the default FuncMap. type UseCustomFuncMap interface { // GetFuncMap returns a custom FuncMap. diff --git a/pkg/plugins/internal/machinery/machinery_suite_test.go b/pkg/machinery/machinery_suite_test.go similarity index 100% rename from pkg/plugins/internal/machinery/machinery_suite_test.go rename to pkg/machinery/machinery_suite_test.go diff --git a/pkg/model/file/marker.go b/pkg/machinery/marker.go similarity index 87% rename from pkg/model/file/marker.go rename to pkg/machinery/marker.go index a5b22c65502..e048615c6fa 100644 --- a/pkg/model/file/marker.go +++ b/pkg/machinery/marker.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "fmt" @@ -45,7 +45,11 @@ func NewMarkerFor(path string, value string) Marker { return Marker{comment, value} } - panic(fmt.Errorf("unknown file extension: '%s', expected '.go', '.yaml' or '.yml'", ext)) + extensions := make([]string, 0, len(commentsByExt)) + for extension := range commentsByExt { + extensions = append(extensions, fmt.Sprintf("%q", extension)) + } + panic(fmt.Errorf("unknown file extension: '%s', expected one of: %s", ext, strings.Join(extensions, ", "))) } // String implements Stringer diff --git a/pkg/machinery/marker_test.go b/pkg/machinery/marker_test.go new file mode 100644 index 00000000000..2f4468ff042 --- /dev/null +++ b/pkg/machinery/marker_test.go @@ -0,0 +1,47 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("NerMarkerFor", func() { + DescribeTable("should create valid markers for known extensions", + func(path, comment string) { Expect(NewMarkerFor(path, "").comment).To(Equal(comment)) }, + Entry("for go files", "file.go", "//"), + Entry("for yaml files", "file.yaml", "#"), + Entry("for yaml files (short version)", "file.yml", "#"), + ) + + It("should panic for unknown extensions", func() { + // testing panics require to use a function with no arguments + Expect(func() { NewMarkerFor("file.unkownext", "") }).To(Panic()) + }) +}) + +var _ = Describe("Marker", func() { + Context("String", func() { + DescribeTable("should return the right string representation", + func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, + Entry("for go files", Marker{comment: "//", value: "test"}, "//+kubebuilder:scaffold:test"), + Entry("for yaml files", Marker{comment: "#", value: "test"}, "#+kubebuilder:scaffold:test"), + ) + }) +}) diff --git a/pkg/model/file/mixins.go b/pkg/machinery/mixins.go similarity index 99% rename from pkg/model/file/mixins.go rename to pkg/machinery/mixins.go index fcf1dbbbf58..9529e687bf6 100644 --- a/pkg/model/file/mixins.go +++ b/pkg/machinery/mixins.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" @@ -64,7 +64,7 @@ type InserterMixin struct { // GetIfExistsAction implements Builder func (t *InserterMixin) GetIfExistsAction() IfExistsAction { // Inserter builders always need to overwrite previous files - return Overwrite + return OverwriteFile } // DomainMixin provides templates with a injectable domain field @@ -93,6 +93,18 @@ func (m *RepositoryMixin) InjectRepository(repository string) { } } +// ProjectNameMixin provides templates with an injectable project name field. +type ProjectNameMixin struct { + ProjectName string +} + +// InjectProjectName implements HasProjectName. +func (m *ProjectNameMixin) InjectProjectName(projectName string) { + if m.ProjectName == "" { + m.ProjectName = projectName + } +} + // MultiGroupMixin provides templates with a injectable multi-group flag field type MultiGroupMixin struct { // MultiGroup is the multi-group flag @@ -139,15 +151,3 @@ func (m *ResourceMixin) InjectResource(res *resource.Resource) { m.Resource = res } } - -// ProjectNameMixin provides templates with an injectable project name field. -type ProjectNameMixin struct { - ProjectName string -} - -// InjectProjectName implements HasProjectName. -func (m *ProjectNameMixin) InjectProjectName(projectName string) { - if m.ProjectName == "" { - m.ProjectName = projectName - } -} diff --git a/pkg/machinery/mixins_test.go b/pkg/machinery/mixins_test.go new file mode 100644 index 00000000000..944e4c0c195 --- /dev/null +++ b/pkg/machinery/mixins_test.go @@ -0,0 +1,188 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +type mockTemplate struct { + TemplateMixin + DomainMixin + RepositoryMixin + ProjectNameMixin + MultiGroupMixin + ComponentConfigMixin + BoilerplateMixin + ResourceMixin +} + +type mockInserter struct { + // InserterMixin requires a different type because it collides with TemplateMixin + InserterMixin +} + +var _ = Describe("TemplateMixin", func() { + const ( + path = "path/to/file.go" + ifExistsAction = SkipFile + body = "content" + ) + + var tmp = mockTemplate{ + TemplateMixin: TemplateMixin{ + PathMixin: PathMixin{path}, + IfExistsActionMixin: IfExistsActionMixin{ifExistsAction}, + TemplateBody: body, + }, + } + + Context("GetPath", func() { + It("should return the path", func() { + Expect(tmp.GetPath()).To(Equal(path)) + }) + }) + + Context("GetIfExistsAction", func() { + It("should return the if-exists action", func() { + Expect(tmp.GetIfExistsAction()).To(Equal(ifExistsAction)) + }) + }) + + Context("GetBody", func() { + It("should return the body", func() { + Expect(tmp.GetBody()).To(Equal(body)) + }) + }) +}) + +var _ = Describe("InserterMixin", func() { + const path = "path/to/file.go" + + var tmp = mockInserter{ + InserterMixin: InserterMixin{ + PathMixin: PathMixin{path}, + }, + } + + Context("GetPath", func() { + It("should return the path", func() { + Expect(tmp.GetPath()).To(Equal(path)) + }) + }) + + Context("GetIfExistsAction", func() { + It("should return overwrite file always", func() { + Expect(tmp.GetIfExistsAction()).To(Equal(OverwriteFile)) + }) + }) +}) + +var _ = Describe("DomainMixin", func() { + const domain = "my.domain" + + var tmp = mockTemplate{} + + Context("InjectDomain", func() { + It("should inject the provided domain", func() { + tmp.InjectDomain(domain) + Expect(tmp.Domain).To(Equal(domain)) + }) + }) +}) + +var _ = Describe("RepositoryMixin", func() { + const repo = "test" + + var tmp = mockTemplate{} + + Context("InjectRepository", func() { + It("should inject the provided repository", func() { + tmp.InjectRepository(repo) + Expect(tmp.Repo).To(Equal(repo)) + }) + }) +}) + +var _ = Describe("ProjectNameMixin", func() { + const name = "my project" + + var tmp = mockTemplate{} + + Context("InjectProjectName", func() { + It("should inject the provided project name", func() { + tmp.InjectProjectName(name) + Expect(tmp.ProjectName).To(Equal(name)) + }) + }) +}) + +var _ = Describe("MultiGroupMixin", func() { + var tmp = mockTemplate{} + + Context("InjectMultiGroup", func() { + It("should inject the provided multi group flag", func() { + tmp.InjectMultiGroup(true) + Expect(tmp.MultiGroup).To(BeTrue()) + }) + }) +}) + +var _ = Describe("ComponentConfigMixin", func() { + var tmp = mockTemplate{} + + Context("InjectComponentConfig", func() { + It("should inject the provided component config flag", func() { + tmp.InjectComponentConfig(true) + Expect(tmp.ComponentConfig).To(BeTrue()) + }) + }) +}) + +var _ = Describe("BoilerplateMixin", func() { + const boilerplate = "Copyright" + + var tmp = mockTemplate{} + + Context("InjectBoilerplate", func() { + It("should inject the provided boilerplate", func() { + tmp.InjectBoilerplate(boilerplate) + Expect(tmp.Boilerplate).To(Equal(boilerplate)) + }) + }) +}) + +var _ = Describe("ResourceMixin", func() { + var res = &resource.Resource{GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }} + + var tmp = mockTemplate{} + + Context("InjectResource", func() { + It("should inject the provided resource", func() { + tmp.InjectResource(res) + Expect(tmp.Resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + }) + }) +}) diff --git a/pkg/plugins/internal/machinery/scaffold.go b/pkg/machinery/scaffold.go similarity index 52% rename from pkg/plugins/internal/machinery/scaffold.go rename to pkg/machinery/scaffold.go index 5391c45bb2e..ca3b4b36d93 100644 --- a/pkg/plugins/internal/machinery/scaffold.go +++ b/pkg/machinery/scaffold.go @@ -20,16 +20,23 @@ import ( "bufio" "bytes" "fmt" - "io/ioutil" + "os" "path/filepath" "strings" "text/template" + "github.com/spf13/afero" "golang.org/x/tools/imports" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/filesystem" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC + + defaultDirectoryPermission os.FileMode = 0700 + defaultFilePermission os.FileMode = 0600 ) var options = imports.Options{ @@ -42,71 +49,112 @@ var options = imports.Options{ // Scaffold uses templates to scaffold new files type Scaffold interface { // Execute writes to disk the provided files - Execute(*model.Universe, ...file.Builder) error + Execute(...Builder) error } // scaffold implements Scaffold interface type scaffold struct { - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin - // fs allows to mock the file system for tests - fs filesystem.FileSystem + fs afero.Fs + + // permissions for new directories and files + dirPerm os.FileMode + filePerm os.FileMode + + // injector is used to provide several fields to the templates + injector injector } +// ScaffoldOption allows to provide optional arguments to the Scaffold +type ScaffoldOption func(*scaffold) + // NewScaffold returns a new Scaffold with the provided plugins -func NewScaffold(plugins ...model.Plugin) Scaffold { - return &scaffold{ - plugins: plugins, - fs: filesystem.New(), +func NewScaffold(fs afero.Fs, options ...ScaffoldOption) Scaffold { + s := &scaffold{ + fs: fs, + dirPerm: defaultDirectoryPermission, + filePerm: defaultFilePermission, } + + for _, option := range options { + option(s) + } + + return s } -// Execute implements Scaffold.Execute -func (s *scaffold) Execute(universe *model.Universe, files ...file.Builder) error { - // Initialize the universe files - universe.Files = make(map[string]*file.File, len(files)) +// WithDirectoryPermissions sets the permissions for new directories +func WithDirectoryPermissions(dirPerm os.FileMode) ScaffoldOption { + return func(s *scaffold) { + s.dirPerm = dirPerm + } +} - // Set the repo as the local prefix so that it knows how to group imports - if universe.Config != nil { - imports.LocalPrefix = universe.Config.GetRepository() +// WithFilePermissions sets the permissions for new files +func WithFilePermissions(filePerm os.FileMode) ScaffoldOption { + return func(s *scaffold) { + s.filePerm = filePerm } +} - for _, f := range files { +// WithConfig provides the project configuration to the Scaffold +func WithConfig(cfg config.Config) ScaffoldOption { + return func(s *scaffold) { + s.injector.config = cfg + + if cfg != nil && cfg.GetRepository() != "" { + imports.LocalPrefix = cfg.GetRepository() + } + } +} + +// WithBoilerplate provides the boilerplate to the Scaffold +func WithBoilerplate(boilerplate string) ScaffoldOption { + return func(s *scaffold) { + s.injector.boilerplate = boilerplate + } +} + +// WithResource provides the resource to the Scaffold +func WithResource(resource *resource.Resource) ScaffoldOption { + return func(s *scaffold) { + s.injector.resource = resource + } +} + +// Execute implements Scaffold.Execute +func (s *scaffold) Execute(builders ...Builder) error { + // Initialize the files + files := make(map[string]*File, len(builders)) + + for _, builder := range builders { // Inject common fields - universe.InjectInto(f) + s.injector.injectInto(builder) // Validate file builders - if reqValFile, requiresValidation := f.(file.RequiresValidation); requiresValidation { - if err := reqValFile.Validate(); err != nil { - return file.NewValidateError(err) + if reqValBuilder, requiresValidation := builder.(RequiresValidation); requiresValidation { + if err := reqValBuilder.Validate(); err != nil { + return ValidateError{err} } } // Build models for Template builders - if t, isTemplate := f.(file.Template); isTemplate { - if err := s.buildFileModel(t, universe.Files); err != nil { + if t, isTemplate := builder.(Template); isTemplate { + if err := s.buildFileModel(t, files); err != nil { return err } } // Build models for Inserter builders - if i, isInserter := f.(file.Inserter); isInserter { - if err := s.updateFileModel(i, universe.Files); err != nil { + if i, isInserter := builder.(Inserter); isInserter { + if err := s.updateFileModel(i, files); err != nil { return err } } } - // Execute plugins - for _, plugin := range s.plugins { - if err := plugin.Pipe(universe); err != nil { - return model.NewPluginError(err) - } - } - // Persist the files to disk - for _, f := range universe.Files { + for _, f := range files { if err := s.writeFile(f); err != nil { return err } @@ -116,51 +164,60 @@ func (s *scaffold) Execute(universe *model.Universe, files ...file.Builder) erro } // buildFileModel scaffolds a single file -func (scaffold) buildFileModel(t file.Template, models map[string]*file.File) error { +func (scaffold) buildFileModel(t Template, models map[string]*File) error { // Set the template default values - err := t.SetTemplateDefaults() - if err != nil { - return file.NewSetTemplateDefaultsError(err) + if err := t.SetTemplateDefaults(); err != nil { + return SetTemplateDefaultsError{err} } + path := t.GetPath() + // Handle already existing models - if _, found := models[t.GetPath()]; found { + if _, found := models[path]; found { switch t.GetIfExistsAction() { - case file.Skip: + case SkipFile: return nil - case file.Error: - return modelAlreadyExistsError{t.GetPath()} - case file.Overwrite: + case Error: + return ModelAlreadyExistsError{path} + case OverwriteFile: default: - return unknownIfExistsActionError{t.GetPath(), t.GetIfExistsAction()} + return UnknownIfExistsActionError{path, t.GetIfExistsAction()} } } - m := &file.File{ - Path: t.GetPath(), - IfExistsAction: t.GetIfExistsAction(), - } - b, err := doTemplate(t) if err != nil { return err } - m.Contents = string(b) - models[m.Path] = m + models[path] = &File{ + Path: path, + Contents: string(b), + IfExistsAction: t.GetIfExistsAction(), + } return nil } // doTemplate executes the template for a file using the input -func doTemplate(t file.Template) ([]byte, error) { - temp, err := newTemplate(t).Parse(t.GetBody()) - if err != nil { +func doTemplate(t Template) ([]byte, error) { + // Create a new template.Template using the type of the Template as the name + temp := template.New(fmt.Sprintf("%T", t)) + + // Set the function map to be used + fm := DefaultFuncMap() + if templateWithFuncMap, hasCustomFuncMap := t.(UseCustomFuncMap); hasCustomFuncMap { + fm = templateWithFuncMap.GetFuncMap() + } + temp.Funcs(fm) + + // Set the template body + if _, err := temp.Parse(t.GetBody()); err != nil { return nil, err } + // Execute the template out := &bytes.Buffer{} - err = temp.Execute(out, t) - if err != nil { + if err := temp.Execute(out, t); err != nil { return nil, err } b := out.Bytes() @@ -168,8 +225,8 @@ func doTemplate(t file.Template) ([]byte, error) { // TODO(adirio): move go-formatting to write step // gofmt the imports if filepath.Ext(t.GetPath()) == ".go" { - b, err = imports.Process(t.GetPath(), b, &options) - if err != nil { + var err error + if b, err = imports.Process(t.GetPath(), b, &options); err != nil { return nil, err } } @@ -177,18 +234,8 @@ func doTemplate(t file.Template) ([]byte, error) { return b, nil } -// newTemplate a new template with common functions -func newTemplate(t file.Template) *template.Template { - fm := file.DefaultFuncMap() - useFM, ok := t.(file.UseCustomFuncMap) - if ok { - fm = useFM.GetFuncMap() - } - return template.New(fmt.Sprintf("%T", t)).Funcs(fm) -} - // updateFileModel updates a single file -func (s scaffold) updateFileModel(i file.Inserter, models map[string]*file.File) error { +func (s scaffold) updateFileModel(i Inserter, models map[string]*File) error { m, err := s.loadPreviousModel(i, models) if err != nil { return err @@ -223,19 +270,21 @@ func (s scaffold) updateFileModel(i file.Inserter, models map[string]*file.File) } m.Contents = string(formattedContent) - m.IfExistsAction = file.Overwrite + m.IfExistsAction = OverwriteFile models[m.Path] = m return nil } // loadPreviousModel gets the previous model from the models map or the actual file -func (s scaffold) loadPreviousModel(i file.Inserter, models map[string]*file.File) (*file.File, error) { +func (s scaffold) loadPreviousModel(i Inserter, models map[string]*File) (*File, error) { + path := i.GetPath() + // Lets see if we already have a model for this file - if m, found := models[i.GetPath()]; found { + if m, found := models[path]; found { // Check if there is already an scaffolded file - exists, err := s.fs.Exists(i.GetPath()) + exists, err := afero.Exists(s.fs, path) if err != nil { - return nil, err + return nil, ExistsFileError{err} } // If there is a model but no scaffolded file we return the model @@ -245,52 +294,50 @@ func (s scaffold) loadPreviousModel(i file.Inserter, models map[string]*file.Fil // If both a model and a file are found, check which has preference switch m.IfExistsAction { - case file.Skip: + case SkipFile: // File has preference - fromFile, err := s.loadModelFromFile(i.GetPath()) + fromFile, err := s.loadModelFromFile(path) if err != nil { return m, nil } return fromFile, nil - case file.Error: + case Error: // Writing will result in an error, so we can return error now - return nil, fileAlreadyExistsError{i.GetPath()} - case file.Overwrite: + return nil, FileAlreadyExistsError{path} + case OverwriteFile: // Model has preference return m, nil default: - return nil, unknownIfExistsActionError{i.GetPath(), m.IfExistsAction} + return nil, UnknownIfExistsActionError{path, m.IfExistsAction} } } // There was no model - return s.loadModelFromFile(i.GetPath()) + return s.loadModelFromFile(path) } // loadModelFromFile gets the previous model from the actual file -func (s scaffold) loadModelFromFile(path string) (f *file.File, err error) { +func (s scaffold) loadModelFromFile(path string) (f *File, err error) { reader, err := s.fs.Open(path) if err != nil { - return + return nil, OpenFileError{err} } defer func() { - closeErr := reader.Close() - if err == nil { - err = closeErr + if closeErr := reader.Close(); err == nil && closeErr != nil { + err = CloseFileError{closeErr} } }() - content, err := ioutil.ReadAll(reader) + content, err := afero.ReadAll(reader) if err != nil { - return + return nil, ReadFileError{err} } - f = &file.File{Path: path, Contents: string(content)} - return + return &File{Path: path, Contents: string(content)}, nil } // getValidCodeFragments obtains the code fragments from a file.Inserter -func getValidCodeFragments(i file.Inserter) file.CodeFragmentsMap { +func getValidCodeFragments(i Inserter) CodeFragmentsMap { // Get the code fragments codeFragments := i.GetCodeFragments() @@ -314,7 +361,7 @@ func getValidCodeFragments(i file.Inserter) file.CodeFragmentsMap { // filterExistingValues removes the single-line values that already exists // TODO: Add support for multi-line duplicate values -func filterExistingValues(content string, codeFragmentsMap file.CodeFragmentsMap) error { +func filterExistingValues(content string, codeFragmentsMap CodeFragmentsMap) error { scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := scanner.Text() @@ -335,7 +382,7 @@ func filterExistingValues(content string, codeFragmentsMap file.CodeFragmentsMap return nil } -func insertStrings(content string, codeFragmentsMap file.CodeFragmentsMap) ([]byte, error) { +func insertStrings(content string, codeFragmentsMap CodeFragmentsMap) ([]byte, error) { out := new(bytes.Buffer) scanner := bufio.NewScanner(strings.NewReader(content)) @@ -359,31 +406,44 @@ func insertStrings(content string, codeFragmentsMap file.CodeFragmentsMap) ([]by return out.Bytes(), nil } -func (s scaffold) writeFile(f *file.File) error { +func (s scaffold) writeFile(f *File) (err error) { // Check if the file to write already exists - exists, err := s.fs.Exists(f.Path) + exists, err := afero.Exists(s.fs, f.Path) if err != nil { - return err + return ExistsFileError{err} } if exists { switch f.IfExistsAction { - case file.Overwrite: + case OverwriteFile: // By not returning, the file is written as if it didn't exist - case file.Skip: + case SkipFile: // By returning nil, the file is not written but the process will carry on return nil - case file.Error: + case Error: // By returning an error, the file is not written and the process will fail - return fileAlreadyExistsError{f.Path} + return FileAlreadyExistsError{f.Path} } } - writer, err := s.fs.Create(f.Path) + // Create the directory if needed + if err := s.fs.MkdirAll(filepath.Dir(f.Path), s.dirPerm); err != nil { + return CreateDirectoryError{err} + } + + // Create or truncate the file + writer, err := s.fs.OpenFile(f.Path, createOrUpdate, s.filePerm) if err != nil { - return err + return CreateFileError{err} } + defer func() { + if closeErr := writer.Close(); err == nil && closeErr != nil { + err = CloseFileError{err} + } + }() - _, err = writer.Write([]byte(f.Contents)) + if _, err := writer.Write([]byte(f.Contents)); err != nil { + return WriteFileError{err} + } - return err + return nil } diff --git a/pkg/machinery/scaffold_test.go b/pkg/machinery/scaffold_test.go new file mode 100644 index 00000000000..0bb691c4297 --- /dev/null +++ b/pkg/machinery/scaffold_test.go @@ -0,0 +1,511 @@ +/* +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 ( + "errors" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("Scaffold", func() { + Describe("NewScaffold", func() { + It("should succeed for no option", func() { + s, ok := NewScaffold(afero.NewMemMapFs()).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) + }) + + It("should succeed with directory permissions option", func() { + const dirPermissions os.FileMode = 0755 + + s, ok := NewScaffold(afero.NewMemMapFs(), WithDirectoryPermissions(dirPermissions)).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(dirPermissions)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) + }) + + It("should succeed with file permissions option", func() { + const filePermissions os.FileMode = 0755 + + s, ok := NewScaffold(afero.NewMemMapFs(), WithFilePermissions(filePermissions)).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(filePermissions)) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) + }) + + It("should succeed with config option", func() { + cfg := cfgv3.New() + + s, ok := NewScaffold(afero.NewMemMapFs(), WithConfig(cfg)).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + Expect(s.injector.config).NotTo(BeNil()) + Expect(s.injector.config.GetVersion().Compare(cfgv3.Version)).To(Equal(0)) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) + }) + + It("should succeed with boilerplate option", func() { + const boilerplate = "Copyright" + + s, ok := NewScaffold(afero.NewMemMapFs(), WithBoilerplate(boilerplate)).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal(boilerplate)) + Expect(s.injector.resource).To(BeNil()) + }) + + It("should succeed with resource option", func() { + var res = &resource.Resource{GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }} + + s, ok := NewScaffold(afero.NewMemMapFs(), WithResource(res)).(*scaffold) + Expect(ok).To(BeTrue()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).NotTo(BeNil()) + Expect(s.injector.resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + }) + }) + + Describe("Scaffold.Execute", func() { + const ( + path = "filename" + pathGo = path + ".go" + pathYaml = path + ".yaml" + content = "Hello world!" + ) + + var ( + testErr = errors.New("error text") + + s *scaffold + ) + + BeforeEach(func() { + s = &scaffold{fs: afero.NewMemMapFs()} + }) + + DescribeTable("successes", + func(path, expected string, files ...Builder) { + Expect(s.Execute(files...)).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(expected)) + }, + Entry("should write the file", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, + ), + Entry("should skip optional models if already have one", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + ), + Entry("should overwrite required models if already have one", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content}, + ), + Entry("should format a go file", + pathGo, "package file\n", + fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: "package file"}, + ), + ) + + DescribeTable("file builders related errors", + func(errType error, files ...Builder) { + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &errType)).To(BeTrue()) + }, + Entry("should fail if unable to validate a file builder", + ValidateError{}, + fakeRequiresValidation{validateErr: testErr}, + ), + Entry("should fail if unable to set default values for a template", + SetTemplateDefaultsError{}, + fakeTemplate{err: testErr}, + ), + Entry("should fail if an unexpected previous model is found", + ModelAlreadyExistsError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}}, + ), + Entry("should fail if behavior if-exists-action is not defined", + UnknownIfExistsActionError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: -1}}, + ), + ) + + // Following errors are unwrapped, so we need to check for substrings + DescribeTable("template related errors", + func(errMsg string, files ...Builder) { + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errMsg)) + }, + Entry("should fail if a template is broken", + "template: ", + fakeTemplate{body: "{{ .Field }"}, + ), + Entry("should fail if a template params aren't provided", + "template: ", + fakeTemplate{body: "{{ .Field }}"}, + ), + Entry("should fail if unable to format a go file", + "expected 'package', found ", + fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: content}, + ), + ) + + DescribeTable("insert strings", + func(path, input, expected string, files ...Builder) { + Expect(afero.WriteFile(s.fs, path, []byte(input), 0666)).To(Succeed()) + + Expect(s.Execute(files...)).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(expected)) + }, + Entry("should insert lines for go files", + pathGo, + `package test + +//+kubebuilder:scaffold:- +`, + `package test + +var a int +var b int + +//+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathGo}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, + }, + }, + ), + Entry("should insert lines for yaml files", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use models if there is no file", + pathYaml, + "", + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` +#+kubebuilder:scaffold:- +`}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use required models over files", + pathYaml, + content, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` +#+kubebuilder:scaffold:- +`}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use files over optional models", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml}, body: content}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should filter invalid markers", + pathYaml, + ` +#+kubebuilder:scaffold:- +#+kubebuilder:scaffold:* +`, + ` +1 +2 +#+kubebuilder:scaffold:- +#+kubebuilder:scaffold:* +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + markers: []Marker{NewMarkerFor(pathYaml, "-")}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + }, + }, + ), + Entry("should filter already existing one-line code fragments", + pathYaml, + ` +1 +#+kubebuilder:scaffold:- +3 +4 +#+kubebuilder:scaffold:* +`, + ` +1 +2 +#+kubebuilder:scaffold:- +3 +4 +#+kubebuilder:scaffold:* +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + }, + }, + ), + Entry("should not insert anything if no code fragment", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +#+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {}, + }, + }, + ), + ) + + DescribeTable("insert strings related errors", + func(errType error, files ...Builder) { + Expect(afero.WriteFile(s.fs, path, []byte{}, 0666)).To(Succeed()) + + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &errType)).To(BeTrue()) + }, + Entry("should fail if inserting into a model that fails when a file exists and it does exist", + FileAlreadyExistsError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: Error}}, + fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, + ), + Entry("should fail if inserting into a model with unknown behavior if the file exists and it does exist", + UnknownIfExistsActionError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, + fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, + ), + ) + + Context("write when the file already exists", func() { + BeforeEach(func() { + _ = afero.WriteFile(s.fs, path, []byte{}, 0666) + }) + + It("should skip the file by default", func() { + Expect(s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path}, + body: content, + })).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(BeEmpty()) + }) + + It("should write the file if configured to do so", func() { + Expect(s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, + body: content, + })).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(content)) + }) + + It("should error if configured to do so", func() { + err := s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}, + body: content, + }) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &FileAlreadyExistsError{})).To(BeTrue()) + }) + }) + }) +}) + +var _ Builder = fakeBuilder{} + +// fakeBuilder is used to mock a file.Builder +type fakeBuilder struct { + path string + ifExistsAction IfExistsAction +} + +// GetPath implements file.Builder +func (f fakeBuilder) GetPath() string { + return f.path +} + +// GetIfExistsAction implements file.Builder +func (f fakeBuilder) GetIfExistsAction() IfExistsAction { + return f.ifExistsAction +} + +var _ RequiresValidation = fakeRequiresValidation{} + +// fakeRequiresValidation is used to mock a file.RequiresValidation in order to test Scaffold +type fakeRequiresValidation struct { + fakeBuilder + + validateErr error +} + +// Validate implements file.RequiresValidation +func (f fakeRequiresValidation) Validate() error { + return f.validateErr +} + +var _ Template = fakeTemplate{} + +// fakeTemplate is used to mock a file.File in order to test Scaffold +type fakeTemplate struct { + fakeBuilder + + body string + err error +} + +// GetBody implements file.Template +func (f fakeTemplate) GetBody() string { + return f.body +} + +// SetTemplateDefaults implements file.Template +func (f fakeTemplate) SetTemplateDefaults() error { + if f.err != nil { + return f.err + } + + return nil +} + +type fakeInserter struct { + fakeBuilder + + markers []Marker + codeFragments CodeFragmentsMap +} + +// GetMarkers implements file.UpdatableTemplate +func (f fakeInserter) GetMarkers() []Marker { + if f.markers != nil { + return f.markers + } + + markers := make([]Marker, 0, len(f.codeFragments)) + for marker := range f.codeFragments { + markers = append(markers, marker) + } + return markers +} + +// GetCodeFragments implements file.UpdatableTemplate +func (f fakeInserter) GetCodeFragments() CodeFragmentsMap { + return f.codeFragments +} diff --git a/pkg/model/file/errors.go b/pkg/model/file/errors.go deleted file mode 100644 index 20349f55844..00000000000 --- a/pkg/model/file/errors.go +++ /dev/null @@ -1,61 +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 file - -import ( - "errors" -) - -// validateError is a wrapper error that will be used for errors returned by RequiresValidation.Validate -type validateError struct { - error -} - -// NewValidateError wraps an error to specify it was returned by RequiresValidation.Validate -func NewValidateError(err error) error { - return validateError{err} -} - -// Unwrap implements Wrapper interface -func (e validateError) Unwrap() error { - return e.error -} - -// IsValidateError checks if the error was returned by RequiresValidation.Validate -func IsValidateError(err error) bool { - return errors.As(err, &validateError{}) -} - -// setTemplateDefaultsError is a wrapper error that will be used for errors returned by Template.SetTemplateDefaults -type setTemplateDefaultsError struct { - error -} - -// NewSetTemplateDefaultsError wraps an error to specify it was returned by Template.SetTemplateDefaults -func NewSetTemplateDefaultsError(err error) error { - return setTemplateDefaultsError{err} -} - -// Unwrap implements Wrapper interface -func (e setTemplateDefaultsError) Unwrap() error { - return e.error -} - -// IsSetTemplateDefaultsError checks if the error was returned by Template.SetTemplateDefaults -func IsSetTemplateDefaultsError(err error) bool { - return errors.As(err, &setTemplateDefaultsError{}) -} diff --git a/pkg/model/plugin.go b/pkg/model/plugin.go deleted file mode 100644 index 57d326fa378..00000000000 --- a/pkg/model/plugin.go +++ /dev/null @@ -1,48 +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 model - -import ( - "errors" -) - -// 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 -} - -// pluginError is a wrapper error that will be used for errors returned by Plugin.Pipe -type pluginError struct { - error -} - -// NewPluginError wraps an error to specify it was returned by Plugin.Pipe -func NewPluginError(err error) error { - return pluginError{err} -} - -// Unwrap implements Wrapper interface -func (e pluginError) Unwrap() error { - return e.error -} - -// IsPluginError checks if the error was returned by Plugin.Pipe -func IsPluginError(err error) bool { - return errors.As(err, &pluginError{}) -} diff --git a/pkg/model/resource/gvk.go b/pkg/model/resource/gvk.go index 46f1cb79ec8..c507a018ec4 100644 --- a/pkg/model/resource/gvk.go +++ b/pkg/model/resource/gvk.go @@ -26,6 +26,10 @@ import ( const ( versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$" + + groupRequired = "group cannot be empty if the domain is empty" + versionRequired = "version cannot be empty" + kindRequired = "kind cannot be empty" ) var ( @@ -44,17 +48,26 @@ type GVK struct { // Validate checks that the GVK is valid. func (gvk GVK) Validate() error { // Check if the qualified group has a valid DNS1123 subdomain value + if gvk.QualifiedGroup() == "" { + return fmt.Errorf(groupRequired) + } if err := validation.IsDNS1123Subdomain(gvk.QualifiedGroup()); err != nil { // NOTE: IsDNS1123Subdomain returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("either Group or Domain is invalid: %s", err) } // Check if the version follows the valid pattern + if gvk.Version == "" { + return fmt.Errorf(versionRequired) + } if !versionRegex.MatchString(gvk.Version) { return fmt.Errorf("Version must match %s (was %s)", versionPattern, gvk.Version) } // Check if kind has a valid DNS1035 label value + if gvk.Kind == "" { + return fmt.Errorf(kindRequired) + } if errors := validation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errors) != 0 { // NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("invalid Kind: %#v", errors) diff --git a/pkg/model/resource/resource_test.go b/pkg/model/resource/resource_test.go index 0d1151e8efc..fcd5364720c 100644 --- a/pkg/model/resource/resource_test.go +++ b/pkg/model/resource/resource_test.go @@ -66,6 +66,8 @@ var _ = Describe("Resource", func() { safeDomain = "testio" groupVersion = group + version domainVersion = safeDomain + version + safeGroup = "mygroup" + safeAlias = safeGroup + version ) var ( @@ -85,6 +87,22 @@ var _ = Describe("Resource", func() { Kind: kind, }, } + resHyphenGroup = Resource{ + GVK: GVK{ + Group: "my-group", + Domain: domain, + Version: version, + Kind: kind, + }, + } + resDotGroup = Resource{ + GVK: GVK{ + Group: "my.group", + Domain: domain, + Version: version, + Kind: kind, + }, + } ) DescribeTable("PackageName should return the correct string", @@ -92,6 +110,8 @@ var _ = Describe("Resource", func() { Entry("fully qualified resource", res, group), Entry("empty group name", resNoGroup, safeDomain), Entry("empty domain", resNoDomain, group), + Entry("hyphen-containing group", resHyphenGroup, safeGroup), + Entry("dot-containing group", resDotGroup, safeGroup), ) DescribeTable("ImportAlias", @@ -99,6 +119,8 @@ var _ = Describe("Resource", func() { Entry("fully qualified resource", res, groupVersion), Entry("empty group name", resNoGroup, domainVersion), Entry("empty domain", resNoDomain, groupVersion), + Entry("hyphen-containing group", resHyphenGroup, safeAlias), + Entry("dot-containing group", resDotGroup, safeAlias), ) }) @@ -284,6 +306,17 @@ var _ = Describe("Resource", func() { Expect(r.Update(other)).NotTo(Succeed()) }) + It("should work for a new path", func() { + const path = "api/v1" + r = Resource{GVK: gvk} + other = Resource{ + GVK: gvk, + Path: path, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Path).To(Equal(path)) + }) + It("should fail for different Paths", func() { r = Resource{ GVK: gvk, diff --git a/pkg/model/universe.go b/pkg/model/universe.go deleted file mode 100644 index 15d597c88f7..00000000000 --- a/pkg/model/universe.go +++ /dev/null @@ -1,111 +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 model - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -// Universe describes the entire state of file generation -type Universe struct { - // Config stores the project configuration - Config config.Config `json:"config,omitempty"` - - // Boilerplate is the copyright comment added at the top of scaffolded files - Boilerplate string `json:"boilerplate,omitempty"` - - // Resource contains the information of the API that is being scaffolded - Resource *resource.Resource `json:"resource,omitempty"` - - // Files contains the model of the files that are being scaffolded - Files map[string]*file.File `json:"files,omitempty"` -} - -// NewUniverse creates a new Universe -func NewUniverse(options ...UniverseOption) *Universe { - universe := &Universe{} - - // Apply options - for _, option := range options { - option(universe) - } - - return universe -} - -// UniverseOption configure Universe -type UniverseOption func(*Universe) - -// WithConfig stores the already loaded project configuration -func WithConfig(projectConfig config.Config) UniverseOption { - return func(universe *Universe) { - universe.Config = projectConfig - } -} - -// WithBoilerplate stores the already loaded project configuration -func WithBoilerplate(boilerplate string) UniverseOption { - return func(universe *Universe) { - universe.Boilerplate = boilerplate - } -} - -// WithoutBoilerplate is used for files that do not require a boilerplate -func WithoutBoilerplate(universe *Universe) { - universe.Boilerplate = "" -} - -// WithResource stores the provided resource -func WithResource(resource *resource.Resource) UniverseOption { - return func(universe *Universe) { - universe.Resource = resource - } -} - -// InjectInto injects fields from the universe into the builder -func (u Universe) InjectInto(builder file.Builder) { - // Inject project configuration - if u.Config != nil { - if builderWithDomain, hasDomain := builder.(file.HasDomain); hasDomain { - builderWithDomain.InjectDomain(u.Config.GetDomain()) - } - if builderWithRepository, hasRepository := builder.(file.HasRepository); hasRepository { - builderWithRepository.InjectRepository(u.Config.GetRepository()) - } - if builderWithProjectName, hasProjectName := builder.(file.HasProjectName); hasProjectName { - builderWithProjectName.InjectProjectName(u.Config.GetProjectName()) - } - if builderWithMultiGroup, hasMultiGroup := builder.(file.HasMultiGroup); hasMultiGroup { - builderWithMultiGroup.InjectMultiGroup(u.Config.IsMultiGroup()) - } - if builderWithComponentConfig, hasComponentConfig := builder.(file.HasComponentConfig); hasComponentConfig { - builderWithComponentConfig.InjectComponentConfig(u.Config.IsComponentConfig()) - } - } - // Inject boilerplate - if builderWithBoilerplate, hasBoilerplate := builder.(file.HasBoilerplate); hasBoilerplate { - builderWithBoilerplate.InjectBoilerplate(u.Boilerplate) - } - // Inject resource - if u.Resource != nil { - if builderWithResource, hasResource := builder.(file.HasResource); hasResource { - builderWithResource.InjectResource(u.Resource) - } - } -} diff --git a/pkg/plugin/bundle.go b/pkg/plugin/bundle.go new file mode 100644 index 00000000000..cc341739027 --- /dev/null +++ b/pkg/plugin/bundle.go @@ -0,0 +1,67 @@ +/* +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 plugin + +import ( + "fmt" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +type bundle struct { + name string + version Version + plugins []Plugin + + supportedProjectVersions []config.Version +} + +// NewBundle creates a new Bundle with the provided name and version, and that wraps the provided plugins. +// The list of supported project versions is computed from the provided plugins. +func NewBundle(name string, version Version, plugins ...Plugin) (Bundle, error) { + supportedProjectVersions := CommonSupportedProjectVersions(plugins...) + if len(supportedProjectVersions) == 0 { + return nil, fmt.Errorf("in order to bundle plugins, they must all support at least one common project version") + } + + return bundle{ + name: name, + version: version, + plugins: plugins, + supportedProjectVersions: supportedProjectVersions, + }, nil +} + +// Name implements Plugin +func (b bundle) Name() string { + return b.name +} + +// Version implements Plugin +func (b bundle) Version() Version { + return b.version +} + +// SupportedProjectVersions implements Plugin +func (b bundle) SupportedProjectVersions() []config.Version { + return b.supportedProjectVersions +} + +// Plugins implements Bundle +func (b bundle) Plugins() []Plugin { + return b.plugins +} diff --git a/pkg/plugin/bundle_test.go b/pkg/plugin/bundle_test.go new file mode 100644 index 00000000000..4592b66680d --- /dev/null +++ b/pkg/plugin/bundle_test.go @@ -0,0 +1,101 @@ +/* +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 plugin + +import ( + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +var _ = Describe("Bundle", func() { + const ( + name = "bundle.kubebuilder.io" + ) + + var ( + version = Version{Number: 1} + + p1 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3}, + }} + p2 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 3, Stage: stage.Alpha}, + }} + p3 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3, Stage: stage.Beta}, + }} + p4 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 2}, + {Number: 3}, + }} + ) + + Context("NewBundle", func() { + It("should succeed for plugins with common supported project versions", func() { + for _, plugins := range [][]Plugin{ + {p1, p2}, + {p1, p3}, + {p1, p4}, + {p2, p3}, + {p3, p4}, + + {p1, p2, p3}, + {p1, p3, p4}, + } { + b, err := NewBundle(name, version, plugins...) + Expect(err).NotTo(HaveOccurred()) + Expect(b.Name()).To(Equal(name)) + Expect(b.Version().Compare(version)).To(Equal(0)) + versions := b.SupportedProjectVersions() + sort.Slice(versions, func(i int, j int) bool { + return versions[i].Compare(versions[j]) == -1 + }) + expectedVersions := CommonSupportedProjectVersions(plugins...) + sort.Slice(expectedVersions, func(i int, j int) bool { + return expectedVersions[i].Compare(expectedVersions[j]) == -1 + }) + Expect(versions).To(Equal(expectedVersions)) + Expect(b.Plugins()).To(Equal(plugins)) + } + }) + + It("should fail for plugins with no common supported project version", func() { + for _, plugins := range [][]Plugin{ + {p2, p4}, + + {p1, p2, p4}, + {p2, p3, p4}, + + {p1, p2, p3, p4}, + } { + _, err := NewBundle(name, version, plugins...) + Expect(err).To(HaveOccurred()) + } + }) + }) +}) diff --git a/pkg/plugins/golang/v2/suite_test.go b/pkg/plugin/errors.go similarity index 64% rename from pkg/plugins/golang/v2/suite_test.go rename to pkg/plugin/errors.go index de06fb4cf53..4297ddca6ab 100644 --- a/pkg/plugins/golang/v2/suite_test.go +++ b/pkg/plugin/errors.go @@ -14,16 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package plugin import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "fmt" ) -func TestGoPluginV2(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Go Plugin v2 Suite") +// ExitError is a typed error that is returned by a plugin when no further steps should be executed for itself. +type ExitError struct { + Plugin string + Reason string +} + +// Error implements error +func (e ExitError) Error() string { + return fmt.Sprintf("plugin %q exit early: %s", e.Plugin, e.Reason) } diff --git a/pkg/plugins/internal/filesystem/filesystem_suite_test.go b/pkg/plugin/errors_test.go similarity index 64% rename from pkg/plugins/internal/filesystem/filesystem_suite_test.go rename to pkg/plugin/errors_test.go index 4d1750e2821..5d295b8212c 100644 --- a/pkg/plugins/internal/filesystem/filesystem_suite_test.go +++ b/pkg/plugin/errors_test.go @@ -14,16 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -package filesystem +package plugin import ( - "testing" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -func TestFilesystem(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Filesystem suite") -} +var _ = Describe("PluginKeyNotFoundError", func() { + var err = ExitError{ + Plugin: "go.kubebuilder.io/v1", + Reason: "skipping plugin", + } + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal("plugin \"go.kubebuilder.io/v1\" exit early: skipping plugin")) + }) + }) +}) diff --git a/pkg/plugin/filter.go b/pkg/plugin/filter.go new file mode 100644 index 00000000000..9a690263342 --- /dev/null +++ b/pkg/plugin/filter.go @@ -0,0 +1,61 @@ +/* +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 plugin + +import ( + "strings" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +// FilterPluginsByKey returns the set of plugins that match the provided key (may be not-fully qualified) +func FilterPluginsByKey(plugins []Plugin, key string) ([]Plugin, error) { + name, ver := SplitKey(key) + hasVersion := ver != "" + var version Version + if hasVersion { + if err := version.Parse(ver); err != nil { + return nil, err + } + } + + filtered := make([]Plugin, 0, len(plugins)) + for _, plugin := range plugins { + if !strings.HasPrefix(plugin.Name(), name) { + continue + } + if hasVersion && plugin.Version().Compare(version) != 0 { + continue + } + filtered = append(filtered, plugin) + } + return filtered, nil +} + +// FilterPluginsByProjectVersion returns the set of plugins that support the provided project version +func FilterPluginsByProjectVersion(plugins []Plugin, projectVersion config.Version) []Plugin { + filtered := make([]Plugin, 0, len(plugins)) + for _, plugin := range plugins { + for _, supportedVersion := range plugin.SupportedProjectVersions() { + if supportedVersion.Compare(projectVersion) == 0 { + filtered = append(filtered, plugin) + break + } + } + } + return filtered +} diff --git a/pkg/plugin/filter_test.go b/pkg/plugin/filter_test.go new file mode 100644 index 00000000000..2b45dcb6fed --- /dev/null +++ b/pkg/plugin/filter_test.go @@ -0,0 +1,84 @@ +/* +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 plugin + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +var ( + p1 = mockPlugin{ + name: "go.kubebuilder.io", + version: Version{Number: 2}, + supportedProjectVersions: []config.Version{{Number: 2}, {Number: 3}}, + } + p2 = mockPlugin{ + name: "go.kubebuilder.io", + version: Version{Number: 3}, + supportedProjectVersions: []config.Version{{Number: 3}}, + } + p3 = mockPlugin{ + name: "example.kubebuilder.io", + version: Version{Number: 1}, + supportedProjectVersions: []config.Version{{Number: 2}}, + } + p4 = mockPlugin{ + name: "test.kubebuilder.io", + version: Version{Number: 1}, + supportedProjectVersions: []config.Version{{Number: 3}}, + } + p5 = mockPlugin{ + name: "go.test.domain", + version: Version{Number: 2}, + supportedProjectVersions: []config.Version{{Number: 2}}, + } + + allPlugins = []Plugin{p1, p2, p3, p4, p5} +) + +var _ = Describe("FilterPluginsByKey", func() { + DescribeTable("should filter", + func(key string, plugins []Plugin) { + filtered, err := FilterPluginsByKey(allPlugins, key) + Expect(err).NotTo(HaveOccurred()) + Expect(filtered).To(Equal(plugins)) + }, + Entry("go plugins", "go", []Plugin{p1, p2, p5}), + Entry("go plugins (kubebuilder domain)", "go.kubebuilder", []Plugin{p1, p2}), + Entry("go v2 plugins", "go/v2", []Plugin{p1, p5}), + Entry("go v2 plugins (kubebuilder domain)", "go.kubebuilder/v2", []Plugin{p1}), + ) + + It("should fail for invalid versions", func() { + _, err := FilterPluginsByKey(allPlugins, "go/a") + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("FilterPluginsByKey", func() { + DescribeTable("should filter", + func(projectVersion config.Version, plugins []Plugin) { + Expect(FilterPluginsByProjectVersion(allPlugins, projectVersion)).To(Equal(plugins)) + }, + Entry("project v2 plugins", config.Version{Number: 2}, []Plugin{p1, p3, p5}), + Entry("project v3 plugins", config.Version{Number: 3}, []Plugin{p1, p2, p4}), + ) +}) diff --git a/pkg/plugin/helpers.go b/pkg/plugin/helpers.go index 3a2d43f4e4c..aa6dbbf30b1 100644 --- a/pkg/plugin/helpers.go +++ b/pkg/plugin/helpers.go @@ -25,17 +25,9 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" ) -// Key returns a unique identifying string for a plugin's name and version. -func Key(name, version string) string { - if version == "" { - return name - } - return path.Join(name, "v"+strings.TrimLeft(version, "v")) -} - // KeyFor returns a Plugin's unique identifying string. func KeyFor(p Plugin) string { - return Key(p.Name(), p.Version().String()) + return path.Join(p.Name(), p.Version().String()) } // SplitKey returns a name and version for a plugin key. @@ -49,6 +41,7 @@ func SplitKey(key string) (string, string) { // GetShortName returns plugin's short name (name before domain) if name // is fully qualified (has a domain suffix), otherwise GetShortName returns name. +// Deprecated func GetShortName(name string) string { return strings.SplitN(name, ".", 2)[0] } @@ -96,7 +89,7 @@ func validateName(name string) error { return nil } -// SupportsVersion checks if a plugins supports a project version. +// SupportsVersion checks if a plugin supports a project version. func SupportsVersion(p Plugin, projectVersion config.Version) bool { for _, version := range p.SupportedProjectVersions() { if projectVersion.Compare(version) == 0 { @@ -105,3 +98,29 @@ func SupportsVersion(p Plugin, projectVersion config.Version) bool { } return false } + +// CommonSupportedProjectVersions returns the projects versions that are supported by all the provided Plugins +func CommonSupportedProjectVersions(plugins ...Plugin) []config.Version { + // Count how many times each supported project version appears + supportedProjectVersionCounter := make(map[config.Version]int) + for _, plugin := range plugins { + for _, supportedProjectVersion := range plugin.SupportedProjectVersions() { + if _, exists := supportedProjectVersionCounter[supportedProjectVersion]; !exists { + supportedProjectVersionCounter[supportedProjectVersion] = 1 + } else { + supportedProjectVersionCounter[supportedProjectVersion]++ + } + } + } + + // Check which versions are present the expected number of times + supportedProjectVersions := make([]config.Version, 0, len(supportedProjectVersionCounter)) + expectedTimes := len(plugins) + for supportedProjectVersion, times := range supportedProjectVersionCounter { + if times == expectedTimes { + supportedProjectVersions = append(supportedProjectVersions, supportedProjectVersion) + } + } + + return supportedProjectVersions +} diff --git a/pkg/plugin/helpers_test.go b/pkg/plugin/helpers_test.go new file mode 100644 index 00000000000..60e311d335a --- /dev/null +++ b/pkg/plugin/helpers_test.go @@ -0,0 +1,190 @@ +/* +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 plugin + +import ( + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +const ( + short = "go" + name = "go.kubebuilder.io" + key = "go.kubebuilder.io/v1" +) + +var ( + version = Version{Number: 1} + supportedProjectVersions = []config.Version{ + {Number: 2}, + {Number: 3}, + } +) + +var _ = Describe("KeyFor", func() { + It("should join plugins name and version", func() { + plugin := mockPlugin{ + name: name, + version: version, + } + Expect(KeyFor(plugin)).To(Equal(key)) + }) +}) + +var _ = Describe("SplitKey", func() { + It("should split keys with versions", func() { + n, v := SplitKey(key) + Expect(n).To(Equal(name)) + Expect(v).To(Equal(version.String())) + }) + + It("should split keys without versions", func() { + n, v := SplitKey(name) + Expect(n).To(Equal(name)) + Expect(v).To(Equal("")) + }) +}) + +var _ = Describe("GetShortName", func() { + It("should extract base names from domains", func() { + Expect(GetShortName(name)).To(Equal(short)) + }) +}) + +var _ = Describe("Validate", func() { + It("should succeed for valid plugins", func() { + plugin := mockPlugin{ + name: name, + version: version, + supportedProjectVersions: supportedProjectVersions, + } + Expect(Validate(plugin)).To(Succeed()) + }) + + DescribeTable("should fail", + func(plugin Plugin) { + Expect(Validate(plugin)).NotTo(Succeed()) + }, + Entry("for invalid plugin names", mockPlugin{ + name: "go_kubebuilder.io", + version: version, + supportedProjectVersions: supportedProjectVersions, + }), + Entry("for invalid plugin versions", mockPlugin{ + name: name, + version: Version{Number: -1}, + supportedProjectVersions: supportedProjectVersions, + }), + Entry("for no supported project version", mockPlugin{ + name: name, + version: version, + supportedProjectVersions: nil, + }), + Entry("for invalid supported project version", mockPlugin{ + name: name, + version: version, + supportedProjectVersions: []config.Version{{Number: -1}}, + }), + ) +}) + +var _ = Describe("ValidateKey", func() { + It("should succeed for valid keys", func() { + Expect(ValidateKey(key)).To(Succeed()) + }) + + DescribeTable("should fail", + func(key string) { + Expect(ValidateKey(key)).NotTo(Succeed()) + }, + Entry("for invalid plugin names", "go_kubebuilder.io/v1"), + Entry("for invalid versions", "go.kubebuilder.io/a"), + ) +}) + +var _ = Describe("SupportsVersion", func() { + plugin := mockPlugin{ + supportedProjectVersions: supportedProjectVersions, + } + + It("should return true for supported versions", func() { + Expect(SupportsVersion(plugin, config.Version{Number: 2})).To(BeTrue()) + Expect(SupportsVersion(plugin, config.Version{Number: 3})).To(BeTrue()) + }) + + It("should return false for non-supported versions", func() { + Expect(SupportsVersion(plugin, config.Version{Number: 1})).To(BeFalse()) + Expect(SupportsVersion(plugin, config.Version{Number: 3, Stage: stage.Alpha})).To(BeFalse()) + }) +}) + +var _ = Describe("CommonSupportedProjectVersions", func() { + It("should return the common version", func() { + var ( + p1 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3}, + }} + p2 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 3, Stage: stage.Alpha}, + }} + p3 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3, Stage: stage.Beta}, + }} + p4 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 2}, + {Number: 3}, + }} + ) + + for _, tc := range []struct { + plugins []Plugin + versions []config.Version + }{ + {plugins: []Plugin{p1, p2}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p1, p3}, versions: []config.Version{{Number: 1}, {Number: 2}}}, + {plugins: []Plugin{p1, p4}, versions: []config.Version{{Number: 2}, {Number: 3}}}, + {plugins: []Plugin{p2, p3}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p2, p4}, versions: []config.Version{}}, + {plugins: []Plugin{p3, p4}, versions: []config.Version{{Number: 2}}}, + + {plugins: []Plugin{p1, p2, p3}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p1, p2, p4}, versions: []config.Version{}}, + {plugins: []Plugin{p1, p3, p4}, versions: []config.Version{{Number: 2}}}, + {plugins: []Plugin{p2, p3, p4}, versions: []config.Version{}}, + + {plugins: []Plugin{p1, p2, p3, p4}, versions: []config.Version{}}, + } { + versions := CommonSupportedProjectVersions(tc.plugins...) + sort.Slice(versions, func(i int, j int) bool { + return versions[i].Compare(versions[j]) == -1 + }) + Expect(versions).To(Equal(tc.versions)) + } + }) +}) diff --git a/plugins/addon/channel.go b/pkg/plugin/metadata.go similarity index 50% rename from plugins/addon/channel.go rename to pkg/plugin/metadata.go index f4804e5d528..5a83d8bdf2c 100644 --- a/plugins/addon/channel.go +++ b/pkg/plugin/metadata.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Kubernetes Authors. +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. @@ -14,28 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package addon +package plugin -import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -const exampleChannel = `# Versions for the stable channel -manifests: -- version: 0.0.1 -` - -// ExampleChannel adds a model file for the channel -func ExampleChannel(u *model.Universe) error { - m := &file.File{ - Path: filepath.Join("channels", "stable"), - Contents: exampleChannel, - IfExistsAction: file.Skip, - } +// CLIMetadata is the runtime meta-data of the CLI +type CLIMetadata struct { + // CommandName is the root command name. + CommandName string +} - _, err := AddFile(u, m) - return err +// SubcommandMetadata is the runtime meta-data for a subcommand +type SubcommandMetadata struct { + // Description is a description of what this command does. It is used to display help. + Description string + // Examples are one or more examples of the command-line usage of this command. It is used to display help. + Examples string } diff --git a/pkg/plugin/interfaces.go b/pkg/plugin/plugin.go similarity index 60% rename from pkg/plugin/interfaces.go rename to pkg/plugin/plugin.go index 1c1cf2da01a..131cd555aa4 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/plugin.go @@ -17,8 +17,6 @@ limitations under the License. package plugin import ( - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" ) @@ -43,32 +41,6 @@ type Deprecated interface { DeprecationWarning() string } -// Subcommand is an interface that defines the common base for subcommands returned by plugins -type Subcommand interface { - // UpdateContext updates a Context with subcommand-specific help text, like description and examples. It also serves - // to pass context from the CLI to the subcommand, such as the command name. - // Can be a no-op if default help text is desired. - UpdateContext(*Context) - // BindFlags binds the subcommand's flags to the CLI. This allows each subcommand to define its own - // command line flags. - 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) -} - -// Context is the runtime context for a subcommand. -type Context struct { - // CommandName sets the command name for a subcommand. - CommandName string - // Description is a description of what this subcommand does. It is used to display help. - Description string - // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. - Examples string -} - // Init is an interface for plugins that provide an `init` subcommand type Init interface { Plugin @@ -76,11 +48,6 @@ type Init interface { GetInitSubcommand() InitSubcommand } -// InitSubcommand is an interface that represents an `init` subcommand -type InitSubcommand interface { - Subcommand -} - // CreateAPI is an interface for plugins that provide a `create api` subcommand type CreateAPI interface { Plugin @@ -88,11 +55,6 @@ type CreateAPI interface { GetCreateAPISubcommand() CreateAPISubcommand } -// CreateAPISubcommand is an interface that represents a `create api` subcommand -type CreateAPISubcommand interface { - Subcommand -} - // CreateWebhook is an interface for plugins that provide a `create webhook` subcommand type CreateWebhook interface { Plugin @@ -100,11 +62,6 @@ type CreateWebhook interface { GetCreateWebhookSubcommand() CreateWebhookSubcommand } -// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand -type CreateWebhookSubcommand interface { - Subcommand -} - // Edit is an interface for plugins that provide a `edit` subcommand type Edit interface { Plugin @@ -112,11 +69,6 @@ type Edit interface { GetEditSubcommand() EditSubcommand } -// EditSubcommand is an interface that represents an `edit` subcommand -type EditSubcommand interface { - Subcommand -} - // Full is an interface for plugins that provide `init`, `create api`, `create webhook` and `edit` subcommands type Full interface { Init @@ -124,3 +76,10 @@ type Full interface { CreateWebhook Edit } + +// Bundle allows to group plugins under a single key +type Bundle interface { + Plugin + // Plugins returns a list of the bundled plugins + Plugins() []Plugin +} diff --git a/pkg/plugin/subcommand.go b/pkg/plugin/subcommand.go new file mode 100644 index 00000000000..483c018d3e0 --- /dev/null +++ b/pkg/plugin/subcommand.go @@ -0,0 +1,94 @@ +/* +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 plugin + +import ( + "github.com/spf13/afero" + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// UpdatesMetadata is an interface that implements the optional metadata update method. +type UpdatesMetadata interface { + // UpdateMetadata updates the subcommand metadata. + UpdateMetadata(CLIMetadata, *SubcommandMetadata) +} + +// HasFlags is an interface that implements the optional bind flags method. +type HasFlags interface { + // BindFlags binds flags to the CLI subcommand. + BindFlags(*pflag.FlagSet) +} + +// RequiresConfig is an interface that implements the optional inject config method. +type RequiresConfig interface { + // InjectConfig injects the configuration to a subcommand. + InjectConfig(config.Config) error +} + +// RequiresResource is an interface that implements the required inject resource method. +type RequiresResource interface { + // InjectResource injects the resource model to a subcommand. + InjectResource(*resource.Resource) error +} + +// HasPreScaffold is an interface that implements the optional pre-scaffold method. +type HasPreScaffold interface { + // PreScaffold executes tasks before the main scaffolding. + PreScaffold(afero.Fs) error +} + +// Scaffolder is an interface that implements the required scaffold method. +type Scaffolder interface { + // Scaffold implements the main scaffolding. + Scaffold(afero.Fs) error +} + +// HasPostScaffold is an interface that implements the optional post-scaffold method. +type HasPostScaffold interface { + // PostScaffold executes tasks after the main scaffolding. + PostScaffold() error +} + +// Subcommand is a base interface for all subcommands. +type Subcommand interface { + Scaffolder +} + +// InitSubcommand is an interface that represents an `init` subcommand. +type InitSubcommand interface { + Subcommand +} + +// CreateAPISubcommand is an interface that represents a `create api` subcommand. +type CreateAPISubcommand interface { + Subcommand + RequiresResource +} + +// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand. +type CreateWebhookSubcommand interface { + Subcommand + RequiresResource +} + +// EditSubcommand is an interface that represents an `edit` subcommand. +type EditSubcommand interface { + Subcommand +} diff --git a/pkg/plugin/suite_test.go b/pkg/plugin/suite_test.go new file mode 100644 index 00000000000..059ac751444 --- /dev/null +++ b/pkg/plugin/suite_test.go @@ -0,0 +1,41 @@ +/* +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 plugin + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Suite") +} + +type mockPlugin struct { + name string + version Version + supportedProjectVersions []config.Version +} + +func (p mockPlugin) Name() string { return p.name } +func (p mockPlugin) Version() Version { return p.version } +func (p mockPlugin) SupportedProjectVersions() []config.Version { return p.supportedProjectVersions } diff --git a/pkg/plugin/version_test.go b/pkg/plugin/version_test.go index f1512dbf7d8..610676906aa 100644 --- a/pkg/plugin/version_test.go +++ b/pkg/plugin/version_test.go @@ -18,22 +18,16 @@ package plugin import ( "sort" - "testing" - g "github.com/onsi/ginkgo" // An alias is required because Context is defined elsewhere in this package. + . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" ) -func TestPlugin(t *testing.T) { - RegisterFailHandler(g.Fail) - g.RunSpecs(t, "Plugin Suite") -} - -var _ = g.Describe("Version", func() { - g.Context("Parse", func() { +var _ = Describe("Version", func() { + Context("Parse", func() { DescribeTable("should be correctly parsed for valid version strings", func(str string, number int, s stage.Stage) { var v Version @@ -72,7 +66,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("String", func() { + Context("String", func() { DescribeTable("should return the correct string value", func(version Version, str string) { Expect(version.String()).To(Equal(str)) }, Entry("for version 0", Version{Number: 0}, "v0"), @@ -94,7 +88,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Validate", func() { + Context("Validate", func() { DescribeTable("should validate valid versions", func(version Version) { Expect(version.Validate()).To(Succeed()) }, Entry("for version 0", Version{Number: 0}), @@ -125,7 +119,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Compare", func() { + Context("Compare", func() { // Test Compare() by sorting a list. var ( versions = []Version{ @@ -155,7 +149,7 @@ var _ = g.Describe("Version", func() { } ) - g.It("sorts a valid list of versions correctly", func() { + It("sorts a valid list of versions correctly", func() { sort.Slice(versions, func(i int, j int) bool { return versions[i].Compare(versions[j]) == -1 }) @@ -164,7 +158,7 @@ var _ = g.Describe("Version", func() { }) - g.Context("IsStable", func() { + Context("IsStable", func() { DescribeTable("should return true for stable versions", func(version Version) { Expect(version.IsStable()).To(BeTrue()) }, Entry("for version 1", Version{Number: 1}), @@ -189,4 +183,5 @@ var _ = g.Describe("Version", func() { Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) }) + }) diff --git a/pkg/plugins/declarative/v1/api.go b/pkg/plugins/declarative/v1/api.go new file mode 100644 index 00000000000..e11c9439b70 --- /dev/null +++ b/pkg/plugins/declarative/v1/api.go @@ -0,0 +1,149 @@ +/* +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 v1 + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + + "github.com/spf13/afero" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1/internal/templates" + goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" +) + +const ( + // kbDeclarativePattern is the sigs.k8s.io/kubebuilder-declarative-pattern version + kbDeclarativePatternForV2 = "v0.0.0-20200522144838-848d48e5b073" + kbDeclarativePatternForV3 = "v0.0.0-20210113160450-b84d99da0217" + + exampleManifestVersion = "0.0.1" +) + +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + +type createAPISubcommand struct { + config config.Config + + resource *resource.Resource +} + +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = ` +Scaffold a Kubernetes API by writing a Resource definition and a Controller. + +After the scaffold is written, the dependencies will be updated and +make generate will be run. +` + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + %s create api --group ship --version v1beta1 --kind Frigate --resource --controller + + # Edit the API Scheme + nano api/v1beta1/frigate_types.go + + # Edit the Controller Test + nano controllers/frigate/frigate_controller_test.go + + # Install CRDs into the Kubernetes cluster using kubectl apply + make install + + # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config + make run +`, cliMeta.CommandName) +} + +func (p *createAPISubcommand) InjectConfig(c config.Config) error { + p.config = c + + return nil +} + +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if !p.resource.HasAPI() || !p.resource.HasController() { + return plugin.ExitError{ + Plugin: pluginName, + Reason: "declarative pattern is only supported when API and controller are scaffolded", + } + } + + return nil +} + +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { + fmt.Println("updating scaffold with declarative pattern...") + + // Load the boilerplate + bp, err := afero.ReadFile(fs, filepath.Join("hack", "boilerplate.go.txt")) + if err != nil { + return fmt.Errorf("error updating scaffold: unable to load boilerplate: %w", err) + } + boilerplate := string(bp) + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(fs, + machinery.WithConfig(p.config), + machinery.WithBoilerplate(boilerplate), + machinery.WithResource(p.resource), + ) + + if err := scaffold.Execute( + &templates.Types{}, + &templates.Controller{}, + &templates.Channel{ManifestVersion: exampleManifestVersion}, + &templates.Manifest{ManifestVersion: exampleManifestVersion}, + ); err != nil { + return fmt.Errorf("error updating scaffold: %w", err) + } + + // Track the resources following a declarative approach + cfg := pluginConfig{} + if err := p.config.DecodePluginConfig(pluginKey, &cfg); errors.As(err, &config.UnsupportedFieldError{}) { + // Config doesn't support per-plugin configuration, so we can't track them + } else { + // Fail unless they key wasn't found, which just means it is the first resource tracked + if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) { + return err + } + + cfg.Resources = append(cfg.Resources, p.resource.GVK) + if err := p.config.EncodePluginConfig(pluginKey, cfg); err != nil { + return err + } + } + + // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version + kbDeclarativePattern := kbDeclarativePatternForV2 + if strings.Split(p.config.GetLayout(), ",")[0] == plugin.KeyFor(goPluginV3.Plugin{}) { + kbDeclarativePattern = kbDeclarativePatternForV3 + } + err = util.RunCmd("Get declarative pattern", "go", "get", + "sigs.k8s.io/kubebuilder-declarative-pattern@"+kbDeclarativePattern) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/plugins/declarative/v1/internal/templates/channel.go b/pkg/plugins/declarative/v1/internal/templates/channel.go new file mode 100644 index 00000000000..9c72df227a7 --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/channel.go @@ -0,0 +1,52 @@ +/* +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 templates + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" +) + +var _ machinery.Template = &Channel{} + +// Channel scaffolds the file for the channel +type Channel struct { + machinery.TemplateMixin + + ManifestVersion string +} + +// SetTemplateDefaults implements file.Template +func (f *Channel) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("channels", "stable") + } + fmt.Println(f.Path) + + f.TemplateBody = channelTemplate + + f.IfExistsAction = machinery.SkipFile + + return nil +} + +const channelTemplate = `# Versions for the stable channel +manifests: +- version: {{ .ManifestVersion }} +` diff --git a/plugins/addon/controller.go b/pkg/plugins/declarative/v1/internal/templates/controller.go similarity index 66% rename from plugins/addon/controller.go rename to pkg/plugins/declarative/v1/internal/templates/controller.go index d84615feafb..31e4f11bbfa 100644 --- a/plugins/addon/controller.go +++ b/pkg/plugins/declarative/v1/internal/templates/controller.go @@ -1,30 +1,52 @@ -package addon +/* +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 templates import ( "path/filepath" - "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -// ReplaceController replaces the controller with a modified version -func ReplaceController(u *model.Universe) error { - templateBody := controllerTemplate +var _ machinery.Template = &Controller{} - funcs := DefaultTemplateFunctions() - contents, err := RunTemplate("controller", templateBody, u, funcs) - if err != nil { - return err - } +// Controller scaffolds the file that defines the controller for a CRD or a builtin resource +// nolint:maligned +type Controller struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin +} - m := &file.File{ - Path: filepath.Join("controllers", strings.ToLower(u.Resource.Kind)+"_controller.go"), - Contents: contents, - IfExistsAction: file.Error, +// SetTemplateDefaults implements file.Template +func (f *Controller) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup { + f.Path = filepath.Join("controllers", "%[group]", "%[kind]_controller.go") + } else { + f.Path = filepath.Join("controllers", "%[kind]_controller.go") + } } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = controllerTemplate - ReplaceFileIfExists(u, m) + f.IfExistsAction = machinery.OverwriteFile return nil } diff --git a/pkg/plugins/declarative/v1/internal/templates/manifest.go b/pkg/plugins/declarative/v1/internal/templates/manifest.go new file mode 100644 index 00000000000..252af3bf888 --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/manifest.go @@ -0,0 +1,52 @@ +/* +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 templates + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" +) + +var _ machinery.Template = &Manifest{} + +// Manifest scaffolds the file that acts as a placeholder for the manifest +type Manifest struct { + machinery.TemplateMixin + machinery.ResourceMixin + + ManifestVersion string +} + +// SetTemplateDefaults implements file.Template +func (f *Manifest) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("channels", "packages", "%[kind]", f.ManifestVersion, "manifest.yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + fmt.Println(f.Path) + + f.TemplateBody = manifestTemplate + + f.IfExistsAction = machinery.SkipFile + + return nil +} + +const manifestTemplate = `# Placeholder manifest - replace with the manifest for your addon +` diff --git a/plugins/addon/type.go b/pkg/plugins/declarative/v1/internal/templates/types.go similarity index 63% rename from plugins/addon/type.go rename to pkg/plugins/declarative/v1/internal/templates/types.go index ebcbeab56b7..3a0c2b946c7 100644 --- a/plugins/addon/type.go +++ b/pkg/plugins/declarative/v1/internal/templates/types.go @@ -1,50 +1,67 @@ -package addon +/* +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 templates import ( "fmt" "path/filepath" - "strings" + "text/template" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -// ReplaceTypes replaces the API types with a modified version -func ReplaceTypes(u *model.Universe) error { - funcs := DefaultTemplateFunctions() - funcs["JSONTag"] = JSONTag +var _ machinery.Template = &Types{} - contents, err := RunTemplate("types", typesTemplate, u, funcs) - if err != nil { - return err - } +// Types scaffolds the file that defines the schema for a CRD +// nolint:maligned +type Types struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin +} - var path string - if u.Config.IsMultiGroup() { - path = filepath.Join("apis", u.Resource.Version, strings.ToLower(u.Resource.Kind)+"_types.go") - } else { - path = filepath.Join("api", u.Resource.Version, strings.ToLower(u.Resource.Kind)+"_types.go") +// SetTemplateDefaults implements file.Template +func (f *Types) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup { + f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_types.go") + } else { + f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go") + } } + f.Path = f.Resource.Replacer().Replace(f.Path) - m := &file.File{ - Path: path, - Contents: contents, - IfExistsAction: file.Error, - } + f.TemplateBody = typesTemplate - ReplaceFileIfExists(u, m) + f.IfExistsAction = machinery.OverwriteFile return nil } -// JSONTag is a helper to build the json tag for a struct -// It works around escaping problems for the json tag syntax -func JSONTag(tag string) string { - return fmt.Sprintf("`json:\"%s\"`", tag) +// GetFuncMap implements file.UseCustomFuncMap +func (f Types) GetFuncMap() template.FuncMap { + funcMap := machinery.DefaultFuncMap() + funcMap["JSONTag"] = func(tag string) string { + return fmt.Sprintf("`json:%q`", tag) + } + return funcMap } -// Resource.Resource - const typesTemplate = `{{ .Boilerplate }} package {{ .Resource.Version }} diff --git a/pkg/plugins/declarative/v1/plugin.go b/pkg/plugins/declarative/v1/plugin.go new file mode 100644 index 00000000000..fdd291aca80 --- /dev/null +++ b/pkg/plugins/declarative/v1/plugin.go @@ -0,0 +1,57 @@ +/* +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 v1 + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" +) + +const pluginName = "declarative" + plugins.DefaultNameQualifier + +var ( + pluginVersion = plugin.Version{Number: 1} + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} + pluginKey = plugin.KeyFor(Plugin{}) +) + +var _ plugin.CreateAPI = Plugin{} + +// Plugin implements the plugin.Full interface +type Plugin struct { + createAPISubcommand +} + +// Name returns the name of the plugin +func (Plugin) Name() string { return pluginName } + +// Version returns the version of the plugin +func (Plugin) Version() plugin.Version { return pluginVersion } + +// SupportedProjectVersions returns an array with all project versions supported by the plugin +func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } + +// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis +func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } + +type pluginConfig struct { + Resources []resource.GVK `json:"resources,omitempty"` +} diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index 892217da7ca..230dc1c3383 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -17,23 +17,13 @@ limitations under the License. package golang import ( - "fmt" "path" - "strings" - 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" ) -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty if the domain is empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - var ( coreGroups = map[string]string{ "admission": "k8s.io", @@ -64,17 +54,7 @@ var ( // Options contains the information required to build a new resource.Resource. type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - // Plural is the resource's kind plural form. - // Optional Plural string // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. @@ -93,82 +73,36 @@ type Options struct { DoConversion bool } -// Validate verifies that all the fields have valid values -func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 && len(opts.Domain) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - return nil -} - -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - -// NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Controller: opts.DoController, - } - +// UpdateResource updates the provided resource with the options +func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { if opts.Plural != "" { res.Plural = opts.Plural - } else { - // If not provided, compute a plural for Kind - res.Plural = resource.RegularPlural(opts.Kind) } if opts.DoAPI { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) + res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup()) res.API = &resource.API{ CRDVersion: opts.CRDVersion, Namespaced: opts.Namespaced, } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} + } + + if opts.DoController { + res.Controller = true } if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, + res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup()) + res.Webhooks.WebhookVersion = opts.WebhookVersion + if opts.DoDefaulting { + res.Webhooks.Defaulting = true + } + if opts.DoValidation { + res.Webhooks.Validation = true + } + if opts.DoConversion { + res.Webhooks.Conversion = true } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} } // domain and path may need to be changed in case we are referring to a builtin core resource: @@ -178,15 +112,18 @@ func (opts Options) NewResource(c newconfig.Config) resource.Resource { // - In any other case, default to => project resource // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath if !opts.DoAPI { - loadedRes, err := c.GetResource(opts.GVK()) - alreadyHasAPI := err == nil && loadedRes.HasAPI() + var alreadyHasAPI bool + if c.GetVersion().Compare(cfgv2.Version) == 0 { + alreadyHasAPI = c.HasResource(res.GVK) + } else { + loadedRes, err := c.GetResource(res.GVK) + alreadyHasAPI = err == nil && loadedRes.HasAPI() + } if !alreadyHasAPI { - if domain, found := coreGroups[opts.Group]; found { + if domain, found := coreGroups[res.Group]; found { res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) + res.Path = path.Join("k8s.io", "api", res.Group, res.Version) } } } - - return res } diff --git a/pkg/plugins/golang/options_test.go b/pkg/plugins/golang/options_test.go index 8faa101d975..7df8d828907 100644 --- a/pkg/plugins/golang/options_test.go +++ b/pkg/plugins/golang/options_test.go @@ -24,41 +24,37 @@ import ( . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) var _ = Describe("Options", func() { - Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Context("UpdateResource", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" ) + var ( + gvk = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group and domain", Options{Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), + cfg config.Config ) - }) - - Context("NewResource", func() { - var cfg config.Config BeforeEach(func() { cfg = cfgv3.New() _ = cfg.SetRepository("test") }) - DescribeTable("should succeed if the Resource is valid", + DescribeTable("should succeed", func(options Options) { - Expect(options.Validate()).To(Succeed()) - for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -66,86 +62,65 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.API).NotTo(BeNil()) + res := resource.Resource{ + GVK: gvk, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + } + + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + Expect(res.GVK.IsEqualTo(gvk)).To(BeTrue()) + if options.Plural != "" { + Expect(res.Plural).To(Equal(options.Plural)) + } if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion { if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) + Expect(res.Path).To(Equal( + path.Join(cfg.GetRepository(), "apis", gvk.Group, gvk.Version))) } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + Expect(res.Path).To(Equal(path.Join(cfg.GetRepository(), "api", gvk.Version))) } } else { // Core-resources have a path despite not having an API/Webhook but they are not tested here - Expect(resource.Path).To(Equal("")) + Expect(res.Path).To(Equal("")) } + Expect(res.API).NotTo(BeNil()) if options.DoAPI { - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.API.IsEmpty()).To(BeFalse()) + Expect(res.API.CRDVersion).To(Equal(options.CRDVersion)) + Expect(res.API.Namespaced).To(Equal(options.Namespaced)) + Expect(res.API.IsEmpty()).To(BeFalse()) } else { - Expect(resource.API.IsEmpty()).To(BeTrue()) + Expect(res.API.IsEmpty()).To(BeTrue()) } - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks).NotTo(BeNil()) + Expect(res.Controller).To(Equal(options.DoController)) + Expect(res.Webhooks).NotTo(BeNil()) if options.DoDefaulting || options.DoValidation || options.DoConversion { - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.Webhooks.IsEmpty()).To(BeFalse()) + Expect(res.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) + Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) + Expect(res.Webhooks.Validation).To(Equal(options.DoValidation)) + Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion)) + Expect(res.Webhooks.IsEmpty()).To(BeFalse()) } else { - Expect(resource.Webhooks.IsEmpty()).To(BeTrue()) + Expect(res.Webhooks.IsEmpty()).To(BeTrue()) } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) + Expect(res.QualifiedGroup()).To(Equal(gvk.Group + "." + gvk.Domain)) + Expect(res.PackageName()).To(Equal(gvk.Group)) + Expect(res.ImportAlias()).To(Equal(gvk.Group + gvk.Version)) } }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - WebhookVersion: "v1", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - }), + Entry("when updating nothing", Options{}), + Entry("when updating the plural", Options{Plural: "mates"}), + Entry("when updating the API", Options{DoAPI: true, CRDVersion: "v1", Namespaced: true}), + Entry("when updating the Controller", Options{DoController: true}), + Entry("when updating Webhooks", + Options{WebhookVersion: "v1", DoDefaulting: true, DoValidation: true, DoConversion: true}), ) - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - + DescribeTable("should use core apis", + func(group, qualified string) { + options := Options{} for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -153,48 +128,39 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) + res := resource.Resource{ + GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + + Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version))) + Expect(res.HasAPI()).To(BeFalse()) + Expect(res.QualifiedGroup()).To(Equal(qualified)) } }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), + Entry("for `apps`", "apps", "apps"), + Entry("for `authentication`", "authentication", "authentication.k8s.io"), ) - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) + DescribeTable("should use core apis with project version 2", + // This needs a separate test because project version 2 didn't store API and therefore + // the `HasAPI` method of the resource obtained with `GetResource` will always return false. + // Instead, the existence of a resource in the list means the API was scaffolded. + func(group, qualified string) { + cfg = cfgv2.New() + _ = cfg.SetRepository("test") + options := Options{} for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -202,100 +168,28 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + res := resource.Resource{ + GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), - ) - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) - DescribeTable("should use core apis", - func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API).NotTo(BeNil()) - Expect(resource.API.IsEmpty()).To(BeTrue()) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) + Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version))) + Expect(res.HasAPI()).To(BeFalse()) + Expect(res.QualifiedGroup()).To(Equal(qualified)) } }, Entry("for `apps`", "apps", "apps"), Entry("for `authentication`", "authentication", "authentication.k8s.io"), ) - - It("should use domain if the group is empty", func() { - safeDomain := "testio" - - options := Options{ - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal("")) - if multiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "apis", options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Domain)) - Expect(resource.PackageName()).To(Equal(safeDomain)) - Expect(resource.ImportAlias()).To(Equal(safeDomain + options.Version)) - } - }) }) }) diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index 8a249067e56..1c467f6a7e6 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -20,32 +20,27 @@ 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" - "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config - // pattern indicates that we should use a plugin to build according to a pattern - pattern string - - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -58,21 +53,16 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller. -create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only -scaffold a Controller for an existing Resource, select "n" for Resource. To only define -the schema for a Resource without writing a Controller, select "n" for Controller. +If information about whether the resource and controller should be scaffolded +was not explicitly provided, it will prompt the user if they should be. -After the scaffold is written, api will run make on the project. +After the scaffold is written, the dependencies will be updated and +make generate will be run. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate %s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme @@ -89,32 +79,20 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run - `, - ctx.CommandName) +`, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") - if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { - fs.StringVar(&p.pattern, "pattern", "", - "generates an API following an extension pattern (addon)") - } - fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - 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 + p.options = &goPlugin.Options{CRDVersion: "v1beta1"} fs.BoolVar(&p.options.DoAPI, "resource", true, "if set, generate the resource without prompting the user") p.resourceFlag = fs.Lookup("resource") - p.options.CRDVersion = "v1beta1" fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") fs.BoolVar(&p.options.DoController, "controller", true, @@ -122,11 +100,19 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c + + return nil } -func (p *createAPISubcommand) Run() error { +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") + } + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { @@ -138,16 +124,7 @@ func (p *createAPISubcommand) Run() error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p) -} - -func (p *createAPISubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err @@ -170,50 +147,24 @@ func (p *createAPISubcommand) Validate() error { return nil } -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) { - case "": - // Default pattern - case "addon": - plugins = append(plugins, &addon.Plugin{}) - default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) - } - - return scaffolds.NewAPIScaffolder(p.config, string(bp), p.resource, p.force, plugins), nil +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *createAPISubcommand) PostScaffold() error { - // Load the requested plugins - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version - err := util.RunCmd("Get controller runtime", "go", "get", - "sigs.k8s.io/kubebuilder-declarative-pattern@"+scaffolds.KbDeclarativePattern) - if err != nil { - return err - } - default: - return fmt.Errorf("unknown pattern %q", p.pattern) - } - err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } - if p.runMake { // TODO: check if API was scaffolded - return util.RunCmd("Running make", "make", "generate") + if p.runMake && p.resource.HasAPI() { + err = util.RunCmd("Running make", "make", "generate") + if err != nil { + return err + } } + return nil } diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go index 7c232a44bec..974502609a2 100644 --- a/pkg/plugins/golang/v2/edit.go +++ b/pkg/plugins/golang/v2/edit.go @@ -19,56 +19,47 @@ package v2 import ( "fmt" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %s edit --multigroup - - # Disable the multigroup layout - %s edit --multigroup=false - `, ctx.CommandName, ctx.CommandName) +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout + %[1]s edit --multigroup + + # Disable the multigroup layout + %[1]s edit --multigroup=false +`, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run() error { - return cmdutil.Run(p) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index 3ab7042c0c6..254ff8167c4 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" @@ -31,9 +32,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config @@ -54,30 +56,22 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. - -Writes the following files: -- a boilerplate license file -- a PROJECT file with the domain and repo -- a Makefile to build the project -- a go.mod with project dependencies -- a Kustomization.yaml for customizating manifests -- a Patch file for customizing image for manager manifests -- a Patch file for enabling prometheus metrics -- a main.go to run + subcmdMeta.Description = `Initialize a new project including the following files: + - a "go.mod" with project dependencies + - a "PROJECT" file that stores project configuration + - a "Makefile" with several useful make targets for the project + - several YAML files for project deployment under the "config" directory + - a "main.go" file that creates the manager that will run the project controllers ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, - ctx.CommandName) + subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright + %[1]s init --plugins go/v2 --domain example.org --owner "Your name" - p.commandName = ctx.CommandName + # Initialize a new project defining an specific project version + %[1]s init --plugins go/v2 --project-version 2 +`, cliMeta.CommandName) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -96,30 +90,26 @@ 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) { - // v2+ project configs get a 'layout' value. - if c.GetVersion().Compare(cfgv2.Version) > 0 { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) - } - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run() error { - return cmdutil.Run(p) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := golang.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := golang.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath + } + if err := p.config.SetRepository(p.repo); err != nil { + return err } if p.config.GetVersion().Compare(cfgv2.Version) > 0 { @@ -135,37 +125,33 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } - } - - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := golang.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if err := p.config.SetProjectName(p.name); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if p.config.GetVersion().Compare(cfgv2.Version) > 0 { - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(afero.Fs) error { + // Validate the supported go versions + if !p.skipGoVersionCheck { + if err := golang.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + return nil } -func (p *initSubcommand) PostScaffold() error { +func (p *initSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + err := scaffolder.Scaffold() + if err != nil { + return err + } + if !p.fetchDeps { fmt.Println("Skipping fetching dependencies.") return nil @@ -173,13 +159,17 @@ func (p *initSubcommand) PostScaffold() error { // Ensure that we are pinning controller-runtime version // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 - err := util.RunCmd("Get controller runtime", "go", "get", + err = util.RunCmd("Get controller runtime", "go", "get", "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion) if err != nil { return err } - err = util.RunCmd("Update dependencies", "go", "mod", "tidy") + return nil +} + +func (p *initSubcommand) PostScaffold() error { + err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } diff --git a/pkg/plugins/golang/v2/options.go b/pkg/plugins/golang/v2/options.go deleted file mode 100644 index 52f4410b838..00000000000 --- a/pkg/plugins/golang/v2/options.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -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 v2 - -import ( - "fmt" - "path" - "strings" - - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - -var ( - coreGroups = map[string]string{ - "admission": "k8s.io", - "admissionregistration": "k8s.io", - "apps": "", - "auditregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "authorization": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "coordination": "k8s.io", - "core": "", - "events": "k8s.io", - "extensions": "", - "imagepolicy": "k8s.io", - "networking": "k8s.io", - "node": "k8s.io", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "scheduling": "k8s.io", - "setting": "k8s.io", - "storage": "k8s.io", - } -) - -// Options contains the information required to build a new resource.Resource. -type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - - // Plural is the resource's kind plural form. - // Optional - Plural string - - // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. - CRDVersion string - // WebhookVersion is the {Validating,Mutating}WebhookConfiguration API version that will be used for the resource. - WebhookVersion string - - // Namespaced is true if the resource should be namespaced. - Namespaced bool - - // Flags that define which parts should be scaffolded - DoAPI bool - DoController bool - DoDefaulting bool - DoValidation bool - DoConversion bool -} - -// Validate verifies that all the fields have valid values -func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - return nil -} - -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - -// NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Controller: opts.DoController, - } - - if opts.Plural != "" { - res.Plural = opts.Plural - } else { - // If not provided, compute a plural for for Kind - res.Plural = resource.RegularPlural(opts.Kind) - } - - if opts.DoAPI { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.API = &resource.API{ - CRDVersion: opts.CRDVersion, - Namespaced: opts.Namespaced, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} - } - - if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} - } - - // domain and path may need to be changed in case we are referring to a builtin core resource: - // - Check if we are scaffolding the resource now => project resource - // - Check if we already scaffolded the resource => project resource - // - Check if the resource group is a well-known core group => builtin core resource - // - In any other case, default to => project resource - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - if !opts.DoAPI { - var alreadyHasAPI bool - if c.GetVersion().Compare(cfgv2.Version) == 0 { - alreadyHasAPI = c.HasResource(opts.GVK()) - } else { - loadedRes, err := c.GetResource(opts.GVK()) - alreadyHasAPI = err == nil && loadedRes.HasAPI() - } - if !alreadyHasAPI { - if domain, found := coreGroups[opts.Group]; found { - res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) - } - } - } - - return res -} diff --git a/pkg/plugins/golang/v2/options_test.go b/pkg/plugins/golang/v2/options_test.go deleted file mode 100644 index 692a262e5ca..00000000000 --- a/pkg/plugins/golang/v2/options_test.go +++ /dev/null @@ -1,268 +0,0 @@ -/* -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 v2 - -import ( - "path" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" -) - -var _ = Describe("Options", func() { - Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - ) - - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), - ) - }) - - Context("NewResource", func() { - var cfg config.Config - - BeforeEach(func() { - cfg = cfgv2.New() - _ = cfg.SetRepository("test") - }) - - DescribeTable("should succeed if the Resource is valid", - func(options Options) { - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.API).NotTo(BeNil()) - if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion { - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - } else { - // Core-resources have a path despite not having an API/Webhook but they are not tested here - Expect(resource.Path).To(Equal("")) - } - if options.DoAPI { - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.API.IsEmpty()).To(BeFalse()) - } else { - Expect(resource.API.IsEmpty()).To(BeTrue()) - } - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks).NotTo(BeNil()) - if options.DoDefaulting || options.DoValidation || options.DoConversion { - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.Webhooks.IsEmpty()).To(BeFalse()) - } else { - Expect(resource.Webhooks.IsEmpty()).To(BeTrue()) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) - } - }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1beta1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - WebhookVersion: "v1beta1", - }), - ) - - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), - ) - - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), - ) - - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) - - DescribeTable("should use core apis", - func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API).NotTo(BeNil()) - Expect(resource.API.IsEmpty()).To(BeTrue()) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) - } - }, - Entry("for `apps`", "apps", "apps"), - Entry("for `authentication`", "authentication", "authentication.k8s.io"), - ) - }) -}) diff --git a/pkg/plugins/golang/v2/plugin.go b/pkg/plugins/golang/v2/plugin.go index e04d35701af..dd38b95ddda 100644 --- a/pkg/plugins/golang/v2/plugin.go +++ b/pkg/plugins/golang/v2/plugin.go @@ -27,8 +27,8 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} pluginVersion = plugin.Version{Number: 2} + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} ) var _ plugin.Full = Plugin{} diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go index 353aac0e5f0..4ddaf7f5fc8 100644 --- a/pkg/plugins/golang/v2/scaffolds/api.go +++ b/pkg/plugins/golang/v2/scaffolds/api.go @@ -19,10 +19,13 @@ 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" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "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/config/crd" @@ -30,25 +33,19 @@ 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/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" ) -// KbDeclarativePattern is the sigs.k8s.io/kubebuilder-declarative-pattern version -// (used only to gen api with --pattern=addon) -const KbDeclarativePattern = "v0.0.0-20200522144838-848d48e5b073" - -var _ cmdutil.Scaffolder = &apiScaffolder{} +var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. type apiScaffolder struct { - config config.Config - boilerplate string - resource resource.Resource + config config.Config + resource resource.Resource - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin + // 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 @@ -57,35 +54,38 @@ 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 { +) plugins.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - force: force, + config: config, + resource: res, + force: force, } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *apiScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *apiScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -func (s *apiScaffolder) newUniverse() *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(s.boilerplate), - model.WithResource(&s.resource), + // Load the boilerplate + boilerplate, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err) + } + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + machinery.WithResource(&s.resource), ) -} -func (s *apiScaffolder) scaffold() error { // Keep track of these values before the update doAPI := s.resource.HasAPI() doController := s.resource.HasController() @@ -103,9 +103,7 @@ func (s *apiScaffolder) scaffold() error { } if doAPI { - - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &api.Types{Force: s.force}, &api.Group{}, &samples.CRDSample{Force: s.force}, @@ -117,19 +115,16 @@ func (s *apiScaffolder) scaffold() error { return fmt.Errorf("error scaffolding APIs: %w", err) } - if err := machinery.NewScaffold().Execute( - s.newUniverse(), + if err := scaffold.Execute( &crd.Kustomization{}, &crd.KustomizeConfig{}, ); err != nil { return fmt.Errorf("error scaffolding kustomization: %v", err) } - } if doController { - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &controllers.SuiteTest{Force: s.force}, &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force}, ); err != nil { @@ -137,8 +132,7 @@ func (s *apiScaffolder) scaffold() error { } } - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &templates.MainUpdater{WireResource: doAPI, WireController: doController}, ); err != nil { return fmt.Errorf("error updating main.go: %v", err) diff --git a/pkg/plugins/golang/v2/scaffolds/edit.go b/pkg/plugins/golang/v2/scaffolds/edit.go index 4c5def9d576..fe7c9564ce7 100644 --- a/pkg/plugins/golang/v2/scaffolds/edit.go +++ b/pkg/plugins/golang/v2/scaffolds/edit.go @@ -18,32 +18,41 @@ 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" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) -var _ cmdutil.Scaffolder = &editScaffolder{} +var _ plugins.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 -func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder { +func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, } } -// 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..57febe08ad3 100644 --- a/pkg/plugins/golang/v2/scaffolds/init.go +++ b/pkg/plugins/golang/v2/scaffolds/init.go @@ -18,11 +18,13 @@ 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/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault" @@ -31,8 +33,6 @@ 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/webhook" "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" ) const ( @@ -46,17 +46,20 @@ const ( imageName = "controller:latest" ) -var _ cmdutil.Scaffolder = &initScaffolder{} +var _ plugins.Scaffolder = &initScaffolder{} type initScaffolder struct { config config.Config 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 -func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaffolder { +func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: filepath.Join("hack", "boilerplate.go.txt"), @@ -65,47 +68,56 @@ func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaf } } -func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(boilerplate), - ) +// InjectFS implements cmdutil.Scaffolder +func (s *initScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } -// 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{} + // Initialize the machinery.Scaffold that will write the boilerplate file to disk + // The boilerplate file needs to be scaffolded as a separate step as it is going to + // be used by the rest of the files, even those scaffolded in this command call. + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + ) + + bpFile := &hack.Boilerplate{ + License: s.license, + Owner: s.owner, + } bpFile.Path = s.boilerplatePath - bpFile.License = s.license - bpFile.Owner = s.owner - if err := machinery.NewScaffold().Execute( - s.newUniverse(""), - bpFile, - ); err != nil { + if err := scaffold.Execute(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( - s.newUniverse(string(boilerplate)), - &templates.GitIgnore{}, + // Initialize the machinery.Scaffold that will write the files to disk + scaffold = machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + ) + + return scaffold.Execute( + &rbac.Kustomization{}, &rbac.AuthProxyRole{}, &rbac.AuthProxyRoleBinding{}, - &kdefault.ManagerAuthProxyPatch{}, &rbac.AuthProxyService{}, &rbac.AuthProxyClientRole{}, + &rbac.RoleBinding{}, + &rbac.LeaderElectionRole{}, + &rbac.LeaderElectionRoleBinding{}, + &manager.Kustomization{}, &manager.Config{Image: imageName}, &templates.Main{}, &templates.GoMod{ControllerRuntimeVersion: ControllerRuntimeVersion}, + &templates.GitIgnore{}, &templates.Makefile{ Image: imageName, BoilerplatePath: s.boilerplatePath, @@ -114,16 +126,12 @@ func (s *initScaffolder) scaffold() error { }, &templates.Dockerfile{}, &kdefault.Kustomization{}, + &kdefault.ManagerAuthProxyPatch{}, &kdefault.ManagerWebhookPatch{}, - &rbac.RoleBinding{}, - &rbac.LeaderElectionRole{}, - &rbac.LeaderElectionRoleBinding{}, - &rbac.Kustomization{}, - &manager.Kustomization{}, + &kdefault.WebhookCAInjectionPatch{}, &webhook.Kustomization{}, &webhook.KustomizeConfig{}, &webhook.Service{}, - &kdefault.WebhookCAInjectionPatch{}, &prometheus.Kustomization{}, &prometheus.Monitor{}, &certmanager.Certificate{}, diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go index bccba858d49..ba9f21f6acc 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go @@ -19,17 +19,17 @@ package api import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Group{} +var _ machinery.Template = &Group{} // Group scaffolds the file that defines the registration methods for a certain group and version type Group struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go index cd0907121cf..d80e5f33b93 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Types{} +var _ machinery.Template = &Types{} // Types scaffolds the file that defines the schema for a CRD // nolint:maligned type Types struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin Force bool } @@ -51,9 +51,9 @@ func (f *Types) SetTemplateDefaults() error { f.TemplateBody = typesTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go index 806a37d5524..5def3986d32 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go @@ -21,17 +21,17 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Webhook{} +var _ machinery.Template = &Webhook{} // Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource type Webhook struct { // nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' QualifiedGroupWithDash string @@ -58,7 +58,7 @@ func (f *Webhook) SetTemplateDefaults() error { } f.TemplateBody = webhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go index c8ced013fc8..4d17b8dff60 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Certificate{} +var _ machinery.Template = &Certificate{} // Certificate scaffolds a file that defines the issuer CR and the certificate CR type Certificate struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go index 7437ab18bf1..78b2c5e9484 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go index 8eada694a10..e7dbcf8986d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go index fd06bdf7848..37ec7556cb1 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go @@ -20,16 +20,16 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} -var _ file.Inserter = &Kustomization{} +var _ machinery.Template = &Kustomization{} +var _ machinery.Inserter = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the crd folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -40,9 +40,9 @@ func (f *Kustomization) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(kustomizationTemplate, - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), ) return nil @@ -55,11 +55,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *Kustomization) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), +func (f *Kustomization) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), } } @@ -73,8 +73,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate resource code fragments res := make([]string, 0) @@ -90,13 +90,13 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(res) != 0 { - fragments[file.NewMarkerFor(f.Path, resourceMarker)] = res + fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res } if len(webhookPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch + fragments[machinery.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch } if len(caInjectionPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch + fragments[machinery.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go index 84025c65e3a..0013026f81c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go @@ -19,14 +19,14 @@ package crd import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go index 00d03f090d1..376a058a01c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableCAInjectionPatch{} +var _ machinery.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go index 8e3fc5d051a..1472028b627 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableWebhookPatch{} +var _ machinery.Template = &EnableWebhookPatch{} // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD type EnableWebhookPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go index c51474d4cb5..3cee6b8595d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookCAInjectionPatch{} +var _ machinery.Template = &WebhookCAInjectionPatch{} // WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks type WebhookCAInjectionPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error { f.TemplateBody = injectCAPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go index 1250bcfcdaf..68ecc351cd7 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go @@ -21,15 +21,15 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder type Kustomization struct { - file.TemplateMixin - file.ProjectNameMixin + machinery.TemplateMixin + machinery.ProjectNameMixin } // SetTemplateDefaults implements file.Template @@ -40,7 +40,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.ProjectName == "" { // Use directory name as project name, which will be empty if the project version is < v3. diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go index 8f8be890a7e..7d49171c16f 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerAuthProxyPatch{} +var _ machinery.Template = &ManagerAuthProxyPatch{} // ManagerAuthProxyPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager type ManagerAuthProxyPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *ManagerAuthProxyPatch) SetTemplateDefaults() error { f.TemplateBody = kustomizeAuthProxyPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go index 65e34c7d793..a9e0844bf18 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerWebhookPatch{} +var _ machinery.Template = &ManagerWebhookPatch{} // ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager type ManagerWebhookPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go index 0a1bc1cdeb7..1e08e134923 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Config{} +var _ machinery.Template = &Config{} // Config scaffolds a file that defines the namespace and the manager deployment type Config struct { - file.TemplateMixin + machinery.TemplateMixin // Image is controller manager image name Image string diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go index 1832137b95b..f8d5ecd7ec9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the manager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeManagerTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go index 16b9bb4b563..c271a6a3dbb 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go index 4ee7d32c1c2..41870f6850a 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Monitor{} +var _ machinery.Template = &Monitor{} // Monitor scaffolds a file that defines the prometheus service monitor type Monitor struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go index 2b6d77e596f..1eee0af2031 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyClientRole{} +var _ machinery.Template = &AuthProxyClientRole{} // AuthProxyClientRole scaffolds a file that defines the role for the metrics reader type AuthProxyClientRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go index 01b8413b855..df22ef8dc39 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRole{} +var _ machinery.Template = &AuthProxyRole{} // AuthProxyRole scaffolds a file that defines the role for the auth proxy type AuthProxyRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go index 1aaf9ebbf85..eafc45f6ee9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRoleBinding{} +var _ machinery.Template = &AuthProxyRoleBinding{} // AuthProxyRoleBinding scaffolds a file that defines the role binding for the auth proxy type AuthProxyRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go index e84ec0e322c..6287d360ebb 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyService{} +var _ machinery.Template = &AuthProxyService{} // AuthProxyService scaffolds a file that defines the service for the auth proxy type AuthProxyService struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go index 3de358308bd..7024549629d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDEditorRole{} +var _ machinery.Template = &CRDEditorRole{} // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals type CRDEditorRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go index 5898a8fe334..74177476661 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDViewerRole{} +var _ machinery.Template = &CRDViewerRole{} // CRDViewerRole scaffolds a file that defines the role that allows to view plurals type CRDViewerRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go index d8d9b41b180..f5b164e5b79 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeRBACTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go index b787f441f59..6de4d48b784 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRole{} +var _ machinery.Template = &LeaderElectionRole{} // LeaderElectionRole scaffolds a file that defines the role that allows leader election type LeaderElectionRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go index f196868a3f8..9dd75b7ff6b 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRoleBinding{} +var _ machinery.Template = &LeaderElectionRoleBinding{} // LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election type LeaderElectionRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go index 6786223d648..0cc6687e8c3 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &RoleBinding{} +var _ machinery.Template = &RoleBinding{} // RoleBinding scaffolds a file that defines the role binding for the manager type RoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go index b9b349ff336..af9e29f6f6c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go @@ -19,15 +19,15 @@ package samples import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDSample{} +var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -40,9 +40,9 @@ func (f *CRDSample) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.TemplateBody = crdSampleTemplate diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go index c16ee89b9f6..7157dd8380c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeWebhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go index 1b43d4ee825..ac2c92cc89e 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the webhook folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *KustomizeConfig) SetTemplateDefaults() error { f.TemplateBody = kustomizeConfigWebhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go index 3a2689c4d42..7783aa136c4 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Service{} +var _ machinery.Template = &Service{} // Service scaffolds a file that defines the webhook service type Service struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Service) SetTemplateDefaults() error { f.TemplateBody = serviceTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go index ee059001455..3d400d399e1 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Controller{} +var _ machinery.Template = &Controller{} // Controller scaffolds the file that defines the controller for a CRD or a builtin resource // nolint:maligned type Controller struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin ControllerRuntimeVersion string @@ -53,9 +53,9 @@ func (f *Controller) SetTemplateDefaults() error { f.TemplateBody = controllerTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go index 61bd1d584d7..31cef7a8650 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &SuiteTest{} -var _ file.Inserter = &SuiteTest{} +var _ machinery.Template = &SuiteTest{} +var _ machinery.Inserter = &SuiteTest{} // SuiteTest scaffolds the file that sets up the controller tests // nolint:maligned type SuiteTest struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string @@ -52,8 +52,8 @@ func (f *SuiteTest) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), ) // If is multigroup the path needs to be ../../ since it has @@ -64,7 +64,7 @@ func (f *SuiteTest) SetTemplateDefaults() error { } if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } return nil @@ -76,10 +76,10 @@ const ( ) // GetMarkers implements file.Inserter -func (f *SuiteTest) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), +func (f *SuiteTest) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), } } @@ -93,8 +93,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 2) +func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 2) // Generate import code fragments imports := make([]string, 0) @@ -110,10 +110,10 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go index 1d20cafef78..38e4c47167c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Dockerfile{} +var _ machinery.Template = &Dockerfile{} // Dockerfile scaffolds a file that defines the containerized build process type Dockerfile struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go index 4505799177b..a60f46d6760 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GitIgnore{} +var _ machinery.Template = &GitIgnore{} // GitIgnore scaffolds a file that defines which files should be ignored by git type GitIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go index f40e8eda54d..e369bc98567 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GoMod{} +var _ machinery.Template = &GoMod{} // GoMod scaffolds a file that defines the project dependencies type GoMod struct { - file.TemplateMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.RepositoryMixin ControllerRuntimeVersion string } @@ -38,7 +38,7 @@ func (f *GoMod) SetTemplateDefaults() error { f.TemplateBody = goModTemplate - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile return nil } 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..1d07c79b45a 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go @@ -21,15 +21,18 @@ import ( "path/filepath" "time" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Boilerplate{} +// DefaultBoilerplatePath is the default path to the boilerplate file +var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") + +var _ machinery.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files type Boilerplate struct { - file.TemplateMixin - file.BoilerplateMixin + machinery.TemplateMixin + machinery.BoilerplateMixin // License is the License type to write License string @@ -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/internal/templates/main.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go index f234d80970a..c2bd6928d85 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) const defaultMainPath = "main.go" -var _ file.Template = &Main{} +var _ machinery.Template = &Main{} // Main scaffolds a file that defines the controller manager entry point type Main struct { - file.TemplateMixin - file.BoilerplateMixin - file.DomainMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.BoilerplateMixin + machinery.DomainMixin + machinery.RepositoryMixin } // SetTemplateDefaults implements file.Template @@ -42,21 +42,21 @@ func (f *Main) SetTemplateDefaults() error { } f.TemplateBody = fmt.Sprintf(mainTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, setupMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, setupMarker), ) return nil } -var _ file.Inserter = &MainUpdater{} +var _ machinery.Inserter = &MainUpdater{} // MainUpdater updates main.go to run Controllers type MainUpdater struct { //nolint:maligned - file.RepositoryMixin - file.MultiGroupMixin - file.ResourceMixin + machinery.RepositoryMixin + machinery.MultiGroupMixin + machinery.ResourceMixin // Flags to indicate which parts need to be included when updating the file WireResource, WireController, WireWebhook bool @@ -68,8 +68,8 @@ func (*MainUpdater) GetPath() string { } // GetIfExistsAction implements file.Builder -func (*MainUpdater) GetIfExistsAction() file.IfExistsAction { - return file.Overwrite +func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction { + return machinery.OverwriteFile } const ( @@ -79,11 +79,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *MainUpdater) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(defaultMainPath, importMarker), - file.NewMarkerFor(defaultMainPath, addSchemeMarker), - file.NewMarkerFor(defaultMainPath, setupMarker), +func (f *MainUpdater) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(defaultMainPath, importMarker), + machinery.NewMarkerFor(defaultMainPath, addSchemeMarker), + machinery.NewMarkerFor(defaultMainPath, setupMarker), } } @@ -122,8 +122,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // If resource is not being provided we are creating the file, not updating it if f.Resource == nil { @@ -169,13 +169,13 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, importMarker)] = imports + fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme } if len(setup) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, setupMarker)] = setup + fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go index cea72da1141..e799bd05cc9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Makefile{} +var _ machinery.Template = &Makefile{} // Makefile scaffolds a file that defines project management CLI commands type Makefile struct { - file.TemplateMixin + machinery.TemplateMixin // Image is controller manager image name Image string @@ -44,7 +44,7 @@ func (f *Makefile) SetTemplateDefaults() error { f.TemplateBody = makefileTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.Image == "" { f.Image = "controller:latest" diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go index d60175f8c13..c7ac711a127 100644 --- a/pkg/plugins/golang/v2/scaffolds/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/webhook.go @@ -19,57 +19,62 @@ 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/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "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/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" ) -var _ cmdutil.Scaffolder = &webhookScaffolder{} +var _ plugins.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { - config config.Config - boilerplate string - resource resource.Resource + config config.Config + 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) plugins.Scaffolder { return &webhookScaffolder{ - config: config, - boilerplate: boilerplate, - resource: resource, + config: config, + resource: resource, } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *webhookScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *webhookScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -func (s *webhookScaffolder) newUniverse() *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(s.boilerplate), - model.WithResource(&s.resource), + // Load the boilerplate + boilerplate, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err) + } + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + machinery.WithResource(&s.resource), ) -} -func (s *webhookScaffolder) scaffold() error { if err := s.config.UpdateResource(s.resource); err != nil { return fmt.Errorf("error updating resource: %w", err) } - if err := machinery.NewScaffold().Execute( - s.newUniverse(), + if err := scaffold.Execute( &api.Webhook{}, &templates.MainUpdater{WireWebhook: true}, ); err != nil { diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index 735c48c68df..d31b8a400b5 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -18,59 +18,51 @@ 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" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { - config newconfig.Config + config config.Config // For help text. commandName string - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, -validating and (or) conversion webhooks. + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, +validating and/or conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 - # and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation - - # Create conversion webhook for CRD of group shio, version v1beta1 and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, - ctx.CommandName, ctx.CommandName) - - p.commandName = ctx.CommandName + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1 + # and Kind: Frigate + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation + + # Create conversion webhook for Group: ship, Version: v1beta1 + # and Kind: Frigate + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion +`, cliMeta.CommandName) } 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") + p.options = &goPlugin.Options{WebhookVersion: "v1beta1"} + fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form") - p.options.WebhookVersion = "v1beta1" fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, @@ -79,22 +71,21 @@ 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) error { p.config = c -} -func (p *createWebhookSubcommand) Run() error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p) + return nil } -func (p *createWebhookSubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } @@ -120,16 +111,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -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 -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index a17edf2f515..a3482dd07dc 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -20,30 +20,20 @@ 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" - "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) const ( - // KbDeclarativePatternVersion is the sigs.k8s.io/kubebuilder-declarative-pattern version - // (used only to gen api with --pattern=addon) - // TODO: remove this when a better solution for using addons is implemented. - KbDeclarativePatternVersion = "b84d99da021778217217885dd9582ed3cc879ebe" - // defaultCRDVersion is the default CRD API version to scaffold. defaultCRDVersion = "v1" ) @@ -51,15 +41,14 @@ const ( // DefaultMainPath is default file path of main.go const DefaultMainPath = "main.go" +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config - // pattern indicates that we should use a plugin to build according to a pattern - pattern string - options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -72,21 +61,16 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller. -create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only -scaffold a Controller for an existing Resource, select "n" for Resource. To only define -the schema for a Resource without writing a Controller, select "n" for Controller. +If information about whether the resource and controller should be scaffolded +was not explicitly provided, it will prompt the user if they should be. -After the scaffold is written, api will run make on the project. +After the scaffold is written, the dependencies will be updated and +make generate will be run. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate %s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme @@ -103,27 +87,17 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run - `, - ctx.CommandName) +`, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") - // TODO: remove this when a better solution for using addons is implemented. - if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { - fs.StringVar(&p.pattern, "pattern", "", - "generates an API following an extension pattern (addon)") - } - fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") 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") fs.BoolVar(&p.options.DoAPI, "resource", true, @@ -138,13 +112,18 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c + + return nil } -func (p *createAPISubcommand) Run() error { +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + // TODO: re-evaluate whether y/n input still makes sense. We should probably always // scaffold the resource and controller. + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") @@ -155,30 +134,16 @@ func (p *createAPISubcommand) Run() error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p) -} - -func (p *createAPISubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err } - // check if main.go is present in the root directory - if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { - return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) - } - // In case we want to scaffold a resource API we need to do some checks if p.options.DoAPI { // Check that resource doesn't have the API scaffolded or flag force was set - if res, err := p.config.GetResource(p.resource.GVK); err == nil && res.HasAPI() && !p.force { + if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force { return errors.New("API resource already exists") } @@ -198,51 +163,32 @@ func (p *createAPISubcommand) Validate() error { return nil } -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) +func (p *createAPISubcommand) PreScaffold(afero.Fs) error { + // check if main.go is present in the root directory + if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { + return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) } - // Load the requested plugins - plugins := make([]model.Plugin, 0) - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - plugins = append(plugins, &addon.Plugin{}) - default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) - } + return nil +} - return scaffolds.NewAPIScaffolder(p.config, string(bp), p.resource, p.force, plugins), nil +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *createAPISubcommand) PostScaffold() error { - // Load the requested plugins - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version - // TODO: either find a better way to inject this version (ex. tools.go). - err := util.RunCmd("Get kubebuilder-declarative-pattern dependency", "go", "get", - "sigs.k8s.io/kubebuilder-declarative-pattern@"+KbDeclarativePatternVersion) - if err != nil { - return err - } - default: - return fmt.Errorf("unknown pattern %q", p.pattern) - } - err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } - - if p.runMake { // TODO: check if API was scaffolded - return util.RunCmd("Running make", "make", "generate") + if p.runMake && p.resource.HasAPI() { + err = util.RunCmd("Running make", "make", "generate") + if err != nil { + return err + } } + return nil } diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go index 503ed2fafe3..b514edf89b7 100644 --- a/pkg/plugins/golang/v3/edit.go +++ b/pkg/plugins/golang/v3/edit.go @@ -19,56 +19,47 @@ package v3 import ( "fmt" + "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %s edit --multigroup - - # Disable the multigroup layout - %s edit --multigroup=false - `, ctx.CommandName, ctx.CommandName) +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout + %[1]s edit --multigroup + + # Disable the multigroup layout + %[1]s edit --multigroup=false +`, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run() error { - return cmdutil.Run(p) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index c32a1da01ff..fa97e3e6509 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" @@ -30,9 +31,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config // For help text. @@ -53,30 +55,22 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. - -Writes the following files: -- a boilerplate license file -- a PROJECT file with the domain and repo -- a Makefile to build the project -- a go.mod with project dependencies -- a Kustomization.yaml for customizating manifests -- a Patch file for customizing image for manager manifests -- a Patch file for enabling prometheus metrics -- a main.go to run + subcmdMeta.Description = `Initialize a new project including the following files: + - a "go.mod" with project dependencies + - a "PROJECT" file that stores project configuration + - a "Makefile" with several useful make targets for the project + - several YAML files for project deployment under the "config" directory + - a "main.go" file that creates the manager that will run the project controllers ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, - ctx.CommandName) + subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright + %[1]s init --plugins go/v3 --domain example.org --owner "Your name" - p.commandName = ctx.CommandName + # Initialize a new project defining an specific project version + %[1]s init --plugins go/v3 --project-version 3 +`, cliMeta.CommandName) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -100,26 +94,22 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { "create a versioned ComponentConfig file, may be 'true' or 'false'") } -func (p *initSubcommand) InjectConfig(c config.Config) { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run() error { - return cmdutil.Run(p) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := golang.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := golang.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath } - - // Check if the current directory has not files or directories which does not allow to init the project - if err := checkDir(); err != nil { + if err := p.config.SetRepository(p.repo); err != nil { return err } @@ -135,39 +125,43 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } + if err := p.config.SetProjectName(p.name); err != nil { + return err + } - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := golang.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if p.componentConfig { + if err := p.config.SetComponentConfig(); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err - } - if p.componentConfig { - if err := p.config.SetComponentConfig(); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(afero.Fs) error { + // Requires go1.11+ + if !p.skipGoVersionCheck { + if err := golang.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + // Check if the current directory has not files or directories which does not allow to init the project + if err := checkDir(); err != nil { + return err + } + + return nil } -func (p *initSubcommand) PostScaffold() error { +func (p *initSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + err := scaffolder.Scaffold() + if err != nil { + return err + } + if !p.fetchDeps { fmt.Println("Skipping fetching dependencies.") return nil @@ -175,13 +169,17 @@ func (p *initSubcommand) PostScaffold() error { // Ensure that we are pinning controller-runtime version // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 - err := util.RunCmd("Get controller runtime", "go", "get", + err = util.RunCmd("Get controller runtime", "go", "get", "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion) if err != nil { return err } - err = util.RunCmd("Update dependencies", "go", "mod", "tidy") + return nil +} + +func (p *initSubcommand) PostScaffold() error { + err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } diff --git a/pkg/plugins/golang/v3/plugin.go b/pkg/plugins/golang/v3/plugin.go index f109b5d1223..1d6a2c381cb 100644 --- a/pkg/plugins/golang/v3/plugin.go +++ b/pkg/plugins/golang/v3/plugin.go @@ -26,8 +26,8 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []config.Version{cfgv3.Version} pluginVersion = plugin.Version{Number: 3} + supportedProjectVersions = []config.Version{cfgv3.Version} ) var _ plugin.Full = Plugin{} diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go index 1d9698cafc3..f2797f6c524 100644 --- a/pkg/plugins/golang/v3/scaffolds/api.go +++ b/pkg/plugins/golang/v3/scaffolds/api.go @@ -19,9 +19,12 @@ 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/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "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/crd" @@ -29,21 +32,19 @@ 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/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" ) -var _ cmdutil.Scaffolder = &apiScaffolder{} +var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. type apiScaffolder struct { - config config.Config - boilerplate string - resource resource.Resource + config config.Config + resource resource.Resource - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin + // 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 @@ -52,36 +53,38 @@ 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 { +) plugins.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - force: force, + config: config, + resource: res, + force: force, } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *apiScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *apiScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -func (s *apiScaffolder) newUniverse() *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(s.boilerplate), - model.WithResource(&s.resource), + // Load the boilerplate + boilerplate, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err) + } + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + machinery.WithResource(&s.resource), ) -} -// TODO: re-use universe created by s.newUniverse() if possible. -func (s *apiScaffolder) scaffold() error { // Keep track of these values before the update doAPI := s.resource.HasAPI() doController := s.resource.HasController() @@ -92,8 +95,7 @@ func (s *apiScaffolder) scaffold() error { if doAPI { - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &api.Types{Force: s.force}, &api.Group{}, &samples.CRDSample{Force: s.force}, @@ -105,8 +107,7 @@ func (s *apiScaffolder) scaffold() error { return fmt.Errorf("error scaffolding APIs: %v", err) } - if err := machinery.NewScaffold().Execute( - s.newUniverse(), + if err := scaffold.Execute( &crd.Kustomization{}, &crd.KustomizeConfig{}, ); err != nil { @@ -116,8 +117,7 @@ func (s *apiScaffolder) scaffold() error { } if doController { - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &controllers.SuiteTest{Force: s.force}, &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force}, ); err != nil { @@ -125,8 +125,7 @@ func (s *apiScaffolder) scaffold() error { } } - if err := machinery.NewScaffold(s.plugins...).Execute( - s.newUniverse(), + if err := scaffold.Execute( &templates.MainUpdater{WireResource: doAPI, WireController: doController}, ); err != nil { return fmt.Errorf("error updating main.go: %v", err) diff --git a/pkg/plugins/golang/v3/scaffolds/edit.go b/pkg/plugins/golang/v3/scaffolds/edit.go index f4f92785359..e62d95c6deb 100644 --- a/pkg/plugins/golang/v3/scaffolds/edit.go +++ b/pkg/plugins/golang/v3/scaffolds/edit.go @@ -18,32 +18,41 @@ 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" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) -var _ cmdutil.Scaffolder = &editScaffolder{} +var _ plugins.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 -func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder { +func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, } } -// 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..78f20db3f57 100644 --- a/pkg/plugins/golang/v3/scaffolds/init.go +++ b/pkg/plugins/golang/v3/scaffolds/init.go @@ -18,11 +18,13 @@ 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/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault" @@ -30,8 +32,6 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus" "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/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" ) const ( @@ -45,17 +45,20 @@ const ( imageName = "controller:latest" ) -var _ cmdutil.Scaffolder = &initScaffolder{} +var _ plugins.Scaffolder = &initScaffolder{} type initScaffolder struct { config config.Config 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 -func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaffolder { +func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: filepath.Join("hack", "boilerplate.go.txt"), @@ -64,39 +67,43 @@ func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaf } } -func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(boilerplate), - ) +// InjectFS implements cmdutil.Scaffolder +func (s *initScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs } -// 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{} + // Initialize the machinery.Scaffold that will write the boilerplate file to disk + // The boilerplate file needs to be scaffolded as a separate step as it is going to + // be used by the rest of the files, even those scaffolded in this command call. + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + ) + + bpFile := &hack.Boilerplate{ + License: s.license, + Owner: s.owner, + } bpFile.Path = s.boilerplatePath - bpFile.License = s.license - bpFile.Owner = s.owner - if err := machinery.NewScaffold().Execute( - s.newUniverse(""), - bpFile, - ); err != nil { + if err := scaffold.Execute(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( - s.newUniverse(string(boilerplate)), + // Initialize the machinery.Scaffold that will write the files to disk + scaffold = machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + ) + + return scaffold.Execute( &rbac.Kustomization{}, &rbac.AuthProxyRole{}, &rbac.AuthProxyRoleBinding{}, diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go index f8c1faa7e2b..8c93af689a0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go @@ -19,17 +19,17 @@ package api import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Group{} +var _ machinery.Template = &Group{} // Group scaffolds the file that defines the registration methods for a certain group and version type Group struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go index 4c253344db0..a76e3cb1391 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Types{} +var _ machinery.Template = &Types{} // Types scaffolds the file that defines the schema for a CRD // nolint:maligned type Types struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin Force bool } @@ -55,9 +55,9 @@ func (f *Types) SetTemplateDefaults() error { f.TemplateBody = typesTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go index f3f9c6ccd76..f51ef49bda0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go @@ -21,17 +21,17 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Webhook{} +var _ machinery.Template = &Webhook{} // Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource type Webhook struct { // nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' QualifiedGroupWithDash string @@ -65,9 +65,9 @@ func (f *Webhook) SetTemplateDefaults() error { f.TemplateBody = webhookTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go index f708a8afbb8..7f30dd89f37 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go @@ -4,18 +4,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookSuite{} -var _ file.Inserter = &WebhookSuite{} +var _ machinery.Template = &WebhookSuite{} +var _ machinery.Inserter = &WebhookSuite{} // WebhookSuite scaffolds the file that sets up the webhook tests type WebhookSuite struct { //nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // todo: currently is not possible to know if an API was or not scaffolded. We can fix it when #1826 be addressed WireResource bool @@ -40,9 +40,9 @@ func (f *WebhookSuite) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, addWebhookManagerMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), "%s", "%d", ) @@ -66,11 +66,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *WebhookSuite) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, addWebhookManagerMarker), +func (f *WebhookSuite) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), } } @@ -88,8 +88,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *WebhookSuite) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate import code fragments imports := make([]string, 0) @@ -105,13 +105,13 @@ func (f *WebhookSuite) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(addWebhookManager) != 0 { - fragments[file.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager + fragments[machinery.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager } if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go index a654f82951f..76c2b186f65 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Certificate{} +var _ machinery.Template = &Certificate{} // Certificate scaffolds a file that defines the issuer CR and the certificate CR type Certificate struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go index 4210f6f133b..522bdd9b67f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go index 8ade67534fc..573a7e8f602 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go index 9a87af34668..23690588e9f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go @@ -20,16 +20,16 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} -var _ file.Inserter = &Kustomization{} +var _ machinery.Template = &Kustomization{} +var _ machinery.Inserter = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the crd folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -40,9 +40,9 @@ func (f *Kustomization) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(kustomizationTemplate, - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), ) return nil @@ -55,11 +55,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *Kustomization) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), +func (f *Kustomization) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), } } @@ -73,8 +73,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate resource code fragments res := make([]string, 0) @@ -90,13 +90,13 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(res) != 0 { - fragments[file.NewMarkerFor(f.Path, resourceMarker)] = res + fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res } if len(webhookPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch + fragments[machinery.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch } if len(caInjectionPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch + fragments[machinery.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go index 7b56a21c9df..428bfde8b88 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go @@ -19,15 +19,15 @@ package crd import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go index c954670d1db..cc688e50f63 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableCAInjectionPatch{} +var _ machinery.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go index 7cc1da1d65e..1bf0e7ac071 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableWebhookPatch{} +var _ machinery.Template = &EnableWebhookPatch{} // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD type EnableWebhookPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go index a3ba80da2b9..d93780f7dfa 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go @@ -19,15 +19,15 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookCAInjectionPatch{} +var _ machinery.Template = &WebhookCAInjectionPatch{} // WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks type WebhookCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -39,7 +39,7 @@ func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error { f.TemplateBody = injectCAPatchTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go index c7aea4259fb..b6860c307d0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go @@ -19,16 +19,16 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder type Kustomization struct { - file.TemplateMixin - file.ProjectNameMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ProjectNameMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -39,7 +39,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go index 8cf97588bc9..c47ab74426f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go @@ -19,15 +19,15 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerAuthProxyPatch{} +var _ machinery.Template = &ManagerAuthProxyPatch{} // ManagerAuthProxyPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager type ManagerAuthProxyPatch struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *ManagerAuthProxyPatch) SetTemplateDefaults() error { f.TemplateBody = kustomizeAuthProxyPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go index 8b32276fe7b..fb620573bbe 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerConfigPatch{} +var _ machinery.Template = &ManagerConfigPatch{} // ManagerConfigPatch scaffolds a ManagerConfigPatch for a Resource type ManagerConfigPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements input.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go index 3d86dd4f091..7f993dbd31c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerWebhookPatch{} +var _ machinery.Template = &ManagerWebhookPatch{} // ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager type ManagerWebhookPatch struct { - file.TemplateMixin + machinery.TemplateMixin Force bool } @@ -40,10 +40,10 @@ func (f *ManagerWebhookPatch) SetTemplateDefaults() error { f.TemplateBody = managerWebhookPatchTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go index f8657a4401d..5527d331be4 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go @@ -19,15 +19,15 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Config{} +var _ machinery.Template = &Config{} // Config scaffolds a file that defines the namespace and the manager deployment type Config struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin // Image is controller manager image name Image string diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go index ea18f2145a0..fa977d90ecb 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go @@ -19,16 +19,16 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ControllerManagerConfig{} +var _ machinery.Template = &ControllerManagerConfig{} // ControllerManagerConfig scaffolds the config file in config/manager folder. type ControllerManagerConfig struct { - file.TemplateMixin - file.DomainMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.DomainMixin + machinery.RepositoryMixin } // SetTemplateDefaults implements input.Template @@ -39,7 +39,7 @@ func (f *ControllerManagerConfig) SetTemplateDefaults() error { f.TemplateBody = controllerManagerConfigTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go index 5b3f307acfb..eec807da3d1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the manager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeManagerTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go index e2cd771253a..76bf6e1c5e1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go index 4ee7d32c1c2..41870f6850a 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Monitor{} +var _ machinery.Template = &Monitor{} // Monitor scaffolds a file that defines the prometheus service monitor type Monitor struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go index d7bfee31882..e41eeceb55e 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyClientRole{} +var _ machinery.Template = &AuthProxyClientRole{} // AuthProxyClientRole scaffolds a file that defines the role for the metrics reader type AuthProxyClientRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go index f5900ec2b8e..0f359d78e4c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRole{} +var _ machinery.Template = &AuthProxyRole{} // AuthProxyRole scaffolds a file that defines the role for the auth proxy type AuthProxyRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go index 5003d1e4d8d..9bbc09da9a2 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRoleBinding{} +var _ machinery.Template = &AuthProxyRoleBinding{} // AuthProxyRoleBinding scaffolds a file that defines the role binding for the auth proxy type AuthProxyRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go index ffd9cd2ab19..f9c7249c829 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyService{} +var _ machinery.Template = &AuthProxyService{} // AuthProxyService scaffolds a file that defines the service for the auth proxy type AuthProxyService struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go index a099b595e9b..a8832864277 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDEditorRole{} +var _ machinery.Template = &CRDEditorRole{} // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals type CRDEditorRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go index 0b3311650b7..d83c3295f53 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDViewerRole{} +var _ machinery.Template = &CRDViewerRole{} // CRDViewerRole scaffolds a file that defines the role that allows to view plurals type CRDViewerRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go index c87e412896c..d3ea9b22fd9 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeRBACTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go index 3f85432601f..1008bf2c387 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRole{} +var _ machinery.Template = &LeaderElectionRole{} // LeaderElectionRole scaffolds a file that defines the role that allows leader election type LeaderElectionRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go index c523bed20f6..8148f9ab393 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRoleBinding{} +var _ machinery.Template = &LeaderElectionRoleBinding{} // LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election type LeaderElectionRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go index 5407455f47d..9473660e03c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &RoleBinding{} +var _ machinery.Template = &RoleBinding{} // RoleBinding scaffolds a file that defines the role binding for the manager type RoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go index cc33daf3e41..14227777734 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ServiceAccount{} +var _ machinery.Template = &ServiceAccount{} // ServiceAccount scaffolds a file that defines the service account the manager is deployed in. type ServiceAccount struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go index ab68ba16e4c..f813a2353b7 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go @@ -19,15 +19,15 @@ package samples import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDSample{} +var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -40,9 +40,9 @@ func (f *CRDSample) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.TemplateBody = crdSampleTemplate diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go index 74546ab10ae..3f55f70a12f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go @@ -19,15 +19,15 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -41,10 +41,10 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeWebhookTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go index a719ae63c46..524f11e71c1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the webhook folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *KustomizeConfig) SetTemplateDefaults() error { f.TemplateBody = kustomizeConfigWebhookTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go index f05e6bc719a..a7052dbb3bc 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Service{} +var _ machinery.Template = &Service{} // Service scaffolds a file that defines the webhook service type Service struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *Service) SetTemplateDefaults() error { f.TemplateBody = serviceTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go index 779edb04b45..32d64bcf006 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Controller{} +var _ machinery.Template = &Controller{} // Controller scaffolds the file that defines the controller for a CRD or a builtin resource // nolint:maligned type Controller struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin ControllerRuntimeVersion string @@ -53,9 +53,9 @@ func (f *Controller) SetTemplateDefaults() error { f.TemplateBody = controllerTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go index d6e6ac95af5..1dbb649a40c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &SuiteTest{} -var _ file.Inserter = &SuiteTest{} +var _ machinery.Template = &SuiteTest{} +var _ machinery.Inserter = &SuiteTest{} // SuiteTest scaffolds the file that sets up the controller tests // nolint:maligned type SuiteTest struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string @@ -52,8 +52,8 @@ func (f *SuiteTest) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), ) // If is multigroup the path needs to be ../../ since it has @@ -64,7 +64,7 @@ func (f *SuiteTest) SetTemplateDefaults() error { } if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } return nil @@ -76,10 +76,10 @@ const ( ) // GetMarkers implements file.Inserter -func (f *SuiteTest) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), +func (f *SuiteTest) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), } } @@ -93,8 +93,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 2) +func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 2) // Generate import code fragments imports := make([]string, 0) @@ -110,10 +110,10 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go index 359a0a2ebc5..7794ab4e08c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Dockerfile{} +var _ machinery.Template = &Dockerfile{} // Dockerfile scaffolds a file that defines the containerized build process type Dockerfile struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go index 7aa0150303d..2051adc956c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &DockerIgnore{} +var _ machinery.Template = &DockerIgnore{} // DockerIgnore scaffolds a file that defines which files should be ignored by the containerized build process type DockerIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go index bbf1e92bf53..2b5ef060f98 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GitIgnore{} +var _ machinery.Template = &GitIgnore{} // GitIgnore scaffolds a file that defines which files should be ignored by git type GitIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go index fdb3ce26764..e79e17bf47a 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GoMod{} +var _ machinery.Template = &GoMod{} // GoMod scaffolds a file that defines the project dependencies type GoMod struct { - file.TemplateMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.RepositoryMixin ControllerRuntimeVersion string } @@ -38,7 +38,7 @@ func (f *GoMod) SetTemplateDefaults() error { f.TemplateBody = goModTemplate - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile return nil } 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..3efbe592a54 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go @@ -21,15 +21,18 @@ import ( "path/filepath" "time" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Boilerplate{} +// DefaultBoilerplatePath is the default path to the boilerplate file +var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") + +var _ machinery.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files type Boilerplate struct { - file.TemplateMixin - file.BoilerplateMixin + machinery.TemplateMixin + machinery.BoilerplateMixin // License is the License type to write License string @@ -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/internal/templates/main.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go index 65863974cb7..df7abb4751f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go @@ -20,20 +20,20 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) const defaultMainPath = "main.go" -var _ file.Template = &Main{} +var _ machinery.Template = &Main{} // Main scaffolds a file that defines the controller manager entry point type Main struct { - file.TemplateMixin - file.BoilerplateMixin - file.DomainMixin - file.RepositoryMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.BoilerplateMixin + machinery.DomainMixin + machinery.RepositoryMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -43,21 +43,21 @@ func (f *Main) SetTemplateDefaults() error { } f.TemplateBody = fmt.Sprintf(mainTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, setupMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, setupMarker), ) return nil } -var _ file.Inserter = &MainUpdater{} +var _ machinery.Inserter = &MainUpdater{} // MainUpdater updates main.go to run Controllers type MainUpdater struct { //nolint:maligned - file.RepositoryMixin - file.MultiGroupMixin - file.ResourceMixin + machinery.RepositoryMixin + machinery.MultiGroupMixin + machinery.ResourceMixin // Flags to indicate which parts need to be included when updating the file WireResource, WireController, WireWebhook bool @@ -69,8 +69,8 @@ func (*MainUpdater) GetPath() string { } // GetIfExistsAction implements file.Builder -func (*MainUpdater) GetIfExistsAction() file.IfExistsAction { - return file.Overwrite +func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction { + return machinery.OverwriteFile } const ( @@ -80,11 +80,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *MainUpdater) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(defaultMainPath, importMarker), - file.NewMarkerFor(defaultMainPath, addSchemeMarker), - file.NewMarkerFor(defaultMainPath, setupMarker), +func (f *MainUpdater) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(defaultMainPath, importMarker), + machinery.NewMarkerFor(defaultMainPath, addSchemeMarker), + machinery.NewMarkerFor(defaultMainPath, setupMarker), } } @@ -123,8 +123,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // If resource is not being provided we are creating the file, not updating it if f.Resource == nil { @@ -170,13 +170,13 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, importMarker)] = imports + fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme } if len(setup) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, setupMarker)] = setup + fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go index 09b983942fa..787c1ef34f2 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Makefile{} +var _ machinery.Template = &Makefile{} // Makefile scaffolds a file that defines project management CLI commands type Makefile struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin // Image is controller manager image name Image string @@ -47,7 +47,7 @@ func (f *Makefile) SetTemplateDefaults() error { f.TemplateBody = makefileTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.Image == "" { f.Image = "controller:latest" diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go index b195ee7eef7..3403411628b 100644 --- a/pkg/plugins/golang/v3/scaffolds/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/webhook.go @@ -19,58 +19,63 @@ 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/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "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/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" ) -var _ cmdutil.Scaffolder = &webhookScaffolder{} +var _ plugins.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { - config config.Config - boilerplate string - resource resource.Resource + config config.Config + 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) plugins.Scaffolder { return &webhookScaffolder{ - config: config, - boilerplate: boilerplate, - resource: resource, - force: force, + config: config, + resource: resource, + force: force, } } -// Scaffold implements Scaffolder +// InjectFS implements cmdutil.Scaffolder +func (s *webhookScaffolder) InjectFS(fs afero.Fs) { + s.fs = fs +} + +// Scaffold implements cmdutil.Scaffolder func (s *webhookScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - return s.scaffold() -} -func (s *webhookScaffolder) newUniverse() *model.Universe { - return model.NewUniverse( - model.WithConfig(s.config), - model.WithBoilerplate(s.boilerplate), - model.WithResource(&s.resource), + // Load the boilerplate + boilerplate, err := afero.ReadFile(s.fs, hack.DefaultBoilerplatePath) + if err != nil { + return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err) + } + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(s.fs, + machinery.WithConfig(s.config), + machinery.WithBoilerplate(string(boilerplate)), + machinery.WithResource(&s.resource), ) -} -func (s *webhookScaffolder) scaffold() error { // Keep track of these values before the update doDefaulting := s.resource.HasDefaultingWebhook() doValidation := s.resource.HasValidationWebhook() @@ -80,8 +85,7 @@ func (s *webhookScaffolder) scaffold() error { return fmt.Errorf("error updating resource: %w", err) } - if err := machinery.NewScaffold().Execute( - s.newUniverse(), + if err := scaffold.Execute( &api.Webhook{Force: s.force}, &templates.MainUpdater{WireWebhook: true}, &kdefault.WebhookCAInjectionPatch{}, @@ -100,8 +104,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( - s.newUniverse(), + if err := scaffold.Execute( &api.WebhookSuite{}, ); err != nil { return err diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 6e12fa4c4be..1c76e703431 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" @@ -29,12 +28,13 @@ import ( pluginutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) // defaultWebhookVersion is the default mutating/validating webhook config API version to scaffold. const defaultWebhookVersion = "v1" +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { config config.Config // For help text. @@ -42,39 +42,31 @@ type createWebhookSubcommand struct { options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // force indicates that the resource should be created even if it already exists force bool } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, -validating and (or) conversion webhooks. + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, +validating and/or conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 - # and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation - - # Create conversion webhook for CRD of group ship, version v1beta1 and kind Frigate. - %s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, - ctx.CommandName, ctx.CommandName) - - p.commandName = ctx.CommandName + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1 + # and Kind: Frigate + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation + + # Create conversion webhook for Group: ship, Version: v1beta1 + # and Kind: Frigate + %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion +`, cliMeta.CommandName) } 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") fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion, @@ -90,21 +82,16 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "attempt to create resource even if it already exists") } -func (p *createWebhookSubcommand) InjectConfig(c config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *createWebhookSubcommand) Run() error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p) + return nil } -func (p *createWebhookSubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err @@ -130,16 +117,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -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 -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs afero.Fs) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/internal/cmdutil/cmdutil.go b/pkg/plugins/internal/cmdutil/cmdutil.go deleted file mode 100644 index aa3e7f4a9e9..00000000000 --- a/pkg/plugins/internal/cmdutil/cmdutil.go +++ /dev/null @@ -1,60 +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 cmdutil - -// Scaffolder interface creates files to set up a controller manager -type Scaffolder interface { - // 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, ...) - Validate() error - // - 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 - PostScaffold() error -} - -// Run executes a command -func Run(options RunOptions) error { - // Step 1: validate - if err := options.Validate(); err != nil { - return err - } - - // Step 2: get scaffolder - scaffolder, err := options.GetScaffolder() - if err != nil { - return err - } - // Step 3: scaffold - if scaffolder != nil { - if err := scaffolder.Scaffold(); err != nil { - return err - } - } - // Step 4: finish - if err := options.PostScaffold(); err != nil { - return err - } - - return nil -} diff --git a/pkg/plugins/internal/filesystem/errors.go b/pkg/plugins/internal/filesystem/errors.go deleted file mode 100644 index 7f605d3241a..00000000000 --- a/pkg/plugins/internal/filesystem/errors.go +++ /dev/null @@ -1,173 +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 filesystem - -import ( - "errors" - "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 - -// fileExistsError is returned if it could not be checked if the file exists -type fileExistsError struct { - path string - err error -} - -// Error implements error interface -func (e fileExistsError) Error() string { - return fmt.Sprintf("failed to check if %s exists: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e fileExistsError) Unwrap() error { - return e.err -} - -// IsFileExistsError checks if the returned error is because the file could not be checked for existence -func IsFileExistsError(err error) bool { - return errors.As(err, &fileExistsError{}) -} - -// openFileError is returned if the file could not be opened -type openFileError struct { - path string - err error -} - -// Error implements error interface -func (e openFileError) Error() string { - return fmt.Sprintf("failed to open %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e openFileError) Unwrap() error { - return e.err -} - -// IsOpenFileError checks if the returned error is because the file could not be opened -func IsOpenFileError(err error) bool { - return errors.As(err, &openFileError{}) -} - -// createDirectoryError is returned if the directory could not be created -type createDirectoryError struct { - path string - err error -} - -// Error implements error interface -func (e createDirectoryError) Error() string { - return fmt.Sprintf("failed to create directory for %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e createDirectoryError) Unwrap() error { - return e.err -} - -// IsCreateDirectoryError checks if the returned error is because the directory could not be created -func IsCreateDirectoryError(err error) bool { - return errors.As(err, &createDirectoryError{}) -} - -// createFileError is returned if the file could not be created -type createFileError struct { - path string - err error -} - -// Error implements error interface -func (e createFileError) Error() string { - return fmt.Sprintf("failed to create %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e createFileError) Unwrap() error { - return e.err -} - -// IsCreateFileError checks if the returned error is because the file could not be created -func IsCreateFileError(err error) bool { - return errors.As(err, &createFileError{}) -} - -// readFileError is returned if the file could not be read -type readFileError struct { - path string - err error -} - -// Error implements error interface -func (e readFileError) Error() string { - return fmt.Sprintf("failed to read from %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e readFileError) Unwrap() error { - return e.err -} - -// IsReadFileError checks if the returned error is because the file could not be read -func IsReadFileError(err error) bool { - return errors.As(err, &readFileError{}) -} - -// writeFileError is returned if the file could not be written -type writeFileError struct { - path string - err error -} - -// Error implements error interface -func (e writeFileError) Error() string { - return fmt.Sprintf("failed to write to %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e writeFileError) Unwrap() error { - return e.err -} - -// IsWriteFileError checks if the returned error is because the file could not be written to -func IsWriteFileError(err error) bool { - return errors.As(err, &writeFileError{}) -} - -// closeFileError is returned if the file could not be created -type closeFileError struct { - path string - err error -} - -// Error implements error interface -func (e closeFileError) Error() string { - return fmt.Sprintf("failed to close %s: %v", e.path, e.err) -} - -// Unwrap implements Wrapper interface -func (e closeFileError) Unwrap() error { - return e.err -} - -// IsCloseFileError checks if the returned error is because the file could not be closed -func IsCloseFileError(err error) bool { - return errors.As(err, &closeFileError{}) -} diff --git a/pkg/plugins/internal/filesystem/errors_test.go b/pkg/plugins/internal/filesystem/errors_test.go deleted file mode 100644 index 090101fa4e4..00000000000 --- a/pkg/plugins/internal/filesystem/errors_test.go +++ /dev/null @@ -1,77 +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 filesystem - -import ( - "errors" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("Errors", func() { - var ( - path = filepath.Join("path", "to", "file") - err = errors.New("test error") - fileExistsErr = fileExistsError{path, err} - openFileErr = openFileError{path, err} - createDirectoryErr = createDirectoryError{path, err} - createFileErr = createFileError{path, err} - readFileErr = readFileError{path, err} - writeFileErr = writeFileError{path, err} - closeFileErr = closeFileError{path, err} - ) - - DescribeTable("IsXxxxError should return true for themselves and false for the rest", - func(f func(error) bool, itself error, rest ...error) { - Expect(f(itself)).To(BeTrue()) - for _, err := range rest { - Expect(f(err)).To(BeFalse()) - } - }, - Entry("file exists", IsFileExistsError, fileExistsErr, - openFileErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr, closeFileErr), - Entry("open file", IsOpenFileError, openFileErr, - fileExistsErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr, closeFileErr), - Entry("create directory", IsCreateDirectoryError, createDirectoryErr, - fileExistsErr, openFileErr, createFileErr, readFileErr, writeFileErr, closeFileErr), - Entry("create file", IsCreateFileError, createFileErr, - fileExistsErr, openFileErr, createDirectoryErr, readFileErr, writeFileErr, closeFileErr), - Entry("read file", IsReadFileError, readFileErr, - fileExistsErr, openFileErr, createDirectoryErr, createFileErr, writeFileErr, closeFileErr), - Entry("write file", IsWriteFileError, writeFileErr, - fileExistsErr, openFileErr, createDirectoryErr, createFileErr, readFileErr, closeFileErr), - Entry("close file", IsCloseFileError, closeFileErr, - fileExistsErr, openFileErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr), - ) - - DescribeTable("should contain the wrapped error and error message", - func(err error) { - Expect(err).To(MatchError(err)) - Expect(err.Error()).To(ContainSubstring(err.Error())) - }, - Entry("file exists", fileExistsErr), - Entry("open file", openFileErr), - Entry("create directory", createDirectoryErr), - Entry("create file", createFileErr), - Entry("read file", readFileErr), - Entry("write file", writeFileErr), - Entry("close file", closeFileErr), - ) -}) diff --git a/pkg/plugins/internal/filesystem/filesystem.go b/pkg/plugins/internal/filesystem/filesystem.go deleted file mode 100644 index e7e362c5c44..00000000000 --- a/pkg/plugins/internal/filesystem/filesystem.go +++ /dev/null @@ -1,181 +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 filesystem - -import ( - "io" - "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) - - // Open opens the file and returns a self-closing io.Reader. - Open(path string) (io.ReadCloser, 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) { - exists, err := afero.Exists(fs.fs, path) - if err != nil { - return exists, fileExistsError{path, err} - } - - return exists, nil -} - -// Open implements FileSystem.Open -func (fs fileSystem) Open(path string) (io.ReadCloser, error) { - rc, err := fs.fs.Open(path) - if err != nil { - return nil, openFileError{path, err} - } - - return &readFile{path, rc}, nil -} - -// 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 &writeFile{path, wc}, nil -} - -var _ io.ReadCloser = &readFile{} - -// readFile implements io.Reader -type readFile struct { - path string - io.ReadCloser -} - -// Read implements io.Reader.ReadCloser -func (f *readFile) Read(content []byte) (n int, err error) { - // Read the content - n, err = f.ReadCloser.Read(content) - // EOF is a special case error that we can't wrap - if err == io.EOF { - return - } - if err != nil { - return n, readFileError{f.path, err} - } - - return n, nil -} - -// Close implements io.Reader.ReadCloser -func (f *readFile) Close() error { - if err := f.ReadCloser.Close(); err != nil { - return closeFileError{f.path, err} - } - - return nil -} - -// writeFile implements io.Writer -type writeFile struct { - path string - io.WriteCloser -} - -// Write implements io.Writer.Write -func (f *writeFile) Write(content []byte) (n int, err error) { - // Close the file when we end writing - defer func() { - if closeErr := f.Close(); err == nil && closeErr != nil { - err = 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/plugins/internal/filesystem/filesystem_test.go b/pkg/plugins/internal/filesystem/filesystem_test.go deleted file mode 100644 index 6a3d216f112..00000000000 --- a/pkg/plugins/internal/filesystem/filesystem_test.go +++ /dev/null @@ -1,150 +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 filesystem - -import ( - "os" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -var _ = Describe("FileSystem", func() { - Describe("New", func() { - const ( - dirPerm os.FileMode = 0777 - filePerm os.FileMode = 0666 - ) - - var ( - fsi FileSystem - fs fileSystem - ok bool - ) - - Context("when using no options", func() { - BeforeEach(func() { - fsi = New() - fs, ok = fsi.(fileSystem) - }) - - It("should be a fileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(fs.fs).NotTo(BeNil()) - }) - - It("should use default directory permission", func() { - Expect(fs.dirPerm).To(Equal(defaultDirectoryPermission)) - }) - - It("should use default file permission", func() { - Expect(fs.filePerm).To(Equal(defaultFilePermission)) - }) - - It("should use default file mode", func() { - Expect(fs.fileMode).To(Equal(createOrUpdate)) - }) - }) - - Context("when using directory permission option", func() { - BeforeEach(func() { - fsi = New(DirectoryPermissions(dirPerm)) - fs, ok = fsi.(fileSystem) - }) - - It("should be a fileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(fs.fs).NotTo(BeNil()) - }) - - It("should use provided directory permission", func() { - Expect(fs.dirPerm).To(Equal(dirPerm)) - }) - - It("should use default file permission", func() { - Expect(fs.filePerm).To(Equal(defaultFilePermission)) - }) - - It("should use default file mode", func() { - Expect(fs.fileMode).To(Equal(createOrUpdate)) - }) - }) - - Context("when using file permission option", func() { - BeforeEach(func() { - fsi = New(FilePermissions(filePerm)) - fs, ok = fsi.(fileSystem) - }) - - It("should be a fileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(fs.fs).NotTo(BeNil()) - }) - - It("should use default directory permission", func() { - Expect(fs.dirPerm).To(Equal(defaultDirectoryPermission)) - }) - - It("should use provided file permission", func() { - Expect(fs.filePerm).To(Equal(filePerm)) - }) - - It("should use default file mode", func() { - Expect(fs.fileMode).To(Equal(createOrUpdate)) - }) - }) - - Context("when using both directory and file permission options", func() { - BeforeEach(func() { - fsi = New(DirectoryPermissions(dirPerm), FilePermissions(filePerm)) - fs, ok = fsi.(fileSystem) - }) - - It("should be a fileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(fs.fs).NotTo(BeNil()) - }) - - It("should use provided directory permission", func() { - Expect(fs.dirPerm).To(Equal(dirPerm)) - }) - - It("should use provided file permission", func() { - Expect(fs.filePerm).To(Equal(filePerm)) - }) - - It("should use default file mode", func() { - Expect(fs.fileMode).To(Equal(createOrUpdate)) - }) - }) - }) - - // NOTE: FileSystem.Exists, FileSystem.Open, FileSystem.Open().Read, FileSystem.Create and FileSystem.Create().Write - // are hard to test in unitary tests as they deal with actual files -}) diff --git a/pkg/plugins/internal/filesystem/mock.go b/pkg/plugins/internal/filesystem/mock.go deleted file mode 100644 index b7d213c1fc3..00000000000 --- a/pkg/plugins/internal/filesystem/mock.go +++ /dev/null @@ -1,217 +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 filesystem - -import ( - "bytes" - "io" -) - -// mockFileSystem implements FileSystem -type mockFileSystem struct { - path string - exists func(path string) bool - existsError error - openFileError error - createDirError error - createFileError error - input *bytes.Buffer - readFileError 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 - } -} - -// MockOpenFileError makes FileSystem.Open return err -func MockOpenFileError(err error) MockOptions { - return func(fs *mockFileSystem) { - fs.openFileError = 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 - } -} - -// MockInput provides a buffer where the content will be read from -func MockInput(input *bytes.Buffer) MockOptions { - return func(fs *mockFileSystem) { - fs.input = input - } -} - -// MockReadFileError makes the Read method (of the io.Reader returned by FileSystem.Open) return err -func MockReadFileError(err error) MockOptions { - return func(fs *mockFileSystem) { - fs.readFileError = 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, fileExistsError{path, fs.existsError} - } - - return fs.exists(path), nil -} - -// Open implements FileSystem.Open -func (fs mockFileSystem) Open(path string) (io.ReadCloser, error) { - if fs.openFileError != nil { - return nil, openFileError{path, fs.openFileError} - } - - if fs.input == nil { - fs.input = bytes.NewBufferString("Hello world!") - } - - return &mockReadFile{path, fs.input, fs.readFileError, fs.closeFileError}, nil -} - -// Create implements FileSystem.Create -func (fs mockFileSystem) Create(path string) (io.Writer, error) { - if fs.createDirError != nil { - return nil, createDirectoryError{path, fs.createDirError} - } - - if fs.createFileError != nil { - return nil, createFileError{path, fs.createFileError} - } - - return &mockWriteFile{path, fs.output, fs.writeFileError, fs.closeFileError}, nil -} - -// mockReadFile implements io.Reader mocking a readFile for tests -type mockReadFile struct { - path string - input *bytes.Buffer - readFileError error - closeFileError error -} - -// Read implements io.Reader.ReadCloser -func (f *mockReadFile) Read(content []byte) (n int, err error) { - if f.readFileError != nil { - return 0, readFileError{path: f.path, err: f.readFileError} - } - - return f.input.Read(content) -} - -// Read implements io.Reader.ReadCloser -func (f *mockReadFile) Close() error { - if f.closeFileError != nil { - return closeFileError{path: f.path, err: f.closeFileError} - } - - return nil -} - -// mockWriteFile implements io.Writer mocking a writeFile for tests -type mockWriteFile struct { - path string - content *bytes.Buffer - writeFileError error - closeFileError error -} - -// Write implements io.Writer.Write -func (f *mockWriteFile) Write(content []byte) (n int, err error) { - defer func() { - if err == nil && f.closeFileError != nil { - err = closeFileError{f.path, f.closeFileError} - } - }() - - if f.writeFileError != nil { - return 0, writeFileError{f.path, f.writeFileError} - } - - return f.content.Write(content) -} diff --git a/pkg/plugins/internal/filesystem/mock_test.go b/pkg/plugins/internal/filesystem/mock_test.go deleted file mode 100644 index e9e7d58ccbf..00000000000 --- a/pkg/plugins/internal/filesystem/mock_test.go +++ /dev/null @@ -1,448 +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 filesystem - -import ( - "bytes" - "errors" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -//nolint:dupl -var _ = Describe("MockFileSystem", func() { - var ( - fsi FileSystem - fs mockFileSystem - ok bool - options []MockOptions - testErr = errors.New("test error") - ) - - JustBeforeEach(func() { - fsi = NewMock(options...) - fs, ok = fsi.(mockFileSystem) - }) - - Context("when using no options", func() { - BeforeEach(func() { - options = make([]MockOptions, 0) - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockPath", func() { - var filePath = filepath.Join("path", "to", "file") - - BeforeEach(func() { - options = []MockOptions{MockPath(filePath)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should save the provided path", func() { - Expect(fs.path).To(Equal(filePath)) - }) - }) - - Context("when using MockExists", func() { - BeforeEach(func() { - options = []MockOptions{MockExists(func(_ string) bool { return true })} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeTrue()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockExistsError", func() { - BeforeEach(func() { - options = []MockOptions{MockExistsError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should error when calling Exists", func() { - _, err := fsi.Exists("") - Expect(err).To(MatchError(testErr)) - Expect(IsFileExistsError(err)).To(BeTrue()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockOpenFileError", func() { - BeforeEach(func() { - options = []MockOptions{MockOpenFileError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should error when calling Open", func() { - _, err := fsi.Open("") - Expect(err).To(MatchError(testErr)) - Expect(IsOpenFileError(err)).To(BeTrue()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockCreateDirError", func() { - BeforeEach(func() { - options = []MockOptions{MockCreateDirError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should error when calling Create", func() { - _, err := fsi.Create("") - Expect(err).To(MatchError(testErr)) - Expect(IsCreateDirectoryError(err)).To(BeTrue()) - }) - }) - - Context("when using MockCreateFileError", func() { - BeforeEach(func() { - options = []MockOptions{MockCreateFileError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should error when calling Create", func() { - _, err := fsi.Create("") - Expect(err).To(MatchError(testErr)) - Expect(IsCreateFileError(err)).To(BeTrue()) - }) - }) - - Context("when using MockInput", func() { - var ( - input *bytes.Buffer - fileContent = []byte("Hello world!") - ) - - BeforeEach(func() { - input = bytes.NewBufferString("Hello world!") - options = []MockOptions{MockInput(input)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files and the content to be accessible", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - output := make([]byte, len(fileContent)) - n, err := f.Read(output) - Expect(err).NotTo(HaveOccurred()) - Expect(n).To(Equal(len(fileContent))) - Expect(output).To(Equal(fileContent)) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockReadFileError", func() { - BeforeEach(func() { - options = []MockOptions{MockReadFileError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should error when calling Open().Read", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - output := make([]byte, 0) - _, err = f.Read(output) - Expect(err).To(MatchError(testErr)) - Expect(IsReadFileError(err)).To(BeTrue()) - }) - - It("should create writable files", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - }) - - Context("when using MockOutput", func() { - var ( - output bytes.Buffer - fileContent = []byte("Hello world!") - ) - - BeforeEach(func() { - options = []MockOptions{MockOutput(&output)} - output.Reset() - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should create writable files and the content should be accesible", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - n, err := f.Write(fileContent) - Expect(err).NotTo(HaveOccurred()) - Expect(n).To(Equal(len(fileContent))) - Expect(output.Bytes()).To(Equal(fileContent)) - }) - }) - - Context("when using MockWriteFileError", func() { - BeforeEach(func() { - options = []MockOptions{MockWriteFileError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should open readable files", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Read([]byte("")) - Expect(err).NotTo(HaveOccurred()) - }) - - It("should error when calling Create().Write", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).To(MatchError(testErr)) - Expect(IsWriteFileError(err)).To(BeTrue()) - }) - }) - - Context("when using MockCloseFileError", func() { - BeforeEach(func() { - options = []MockOptions{MockCloseFileError(testErr)} - }) - - It("should be a mockFileSystem instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should claim that files don't exist", func() { - exists, err := fsi.Exists("") - Expect(err).NotTo(HaveOccurred()) - Expect(exists).To(BeFalse()) - }) - - It("should error when calling Open().Close", func() { - f, err := fsi.Open("") - Expect(err).NotTo(HaveOccurred()) - - err = f.Close() - Expect(err).To(MatchError(testErr)) - Expect(IsCloseFileError(err)).To(BeTrue()) - }) - - It("should error when calling Create().Write", func() { - f, err := fsi.Create("") - Expect(err).NotTo(HaveOccurred()) - - _, err = f.Write([]byte("")) - Expect(err).To(MatchError(testErr)) - Expect(IsCloseFileError(err)).To(BeTrue()) - }) - }) -}) diff --git a/pkg/plugins/internal/machinery/errors.go b/pkg/plugins/internal/machinery/errors.go deleted file mode 100644 index faba57a1d05..00000000000 --- a/pkg/plugins/internal/machinery/errors.go +++ /dev/null @@ -1,74 +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 machinery - -import ( - "errors" - "fmt" - - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -// This file contains the errors returned by the scaffolding machinery -// 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 - -// fileAlreadyExistsError is returned if the file is expected not to exist but it does -type fileAlreadyExistsError struct { - path string -} - -// Error implements error interface -func (e fileAlreadyExistsError) Error() string { - return fmt.Sprintf("failed to create %s: file already exists", e.path) -} - -// IsFileAlreadyExistsError checks if the returned error is because the file already existed when expected not to -func IsFileAlreadyExistsError(err error) bool { - return errors.As(err, &fileAlreadyExistsError{}) -} - -// modelAlreadyExistsError is returned if the file is expected not to exist but a previous model does -type modelAlreadyExistsError struct { - path string -} - -// Error implements error interface -func (e modelAlreadyExistsError) Error() string { - return fmt.Sprintf("failed to create %s: model already exists", e.path) -} - -// IsModelAlreadyExistsError checks if the returned error is because the model already existed when expected not to -func IsModelAlreadyExistsError(err error) bool { - return errors.As(err, &modelAlreadyExistsError{}) -} - -// unknownIfExistsActionError is returned if the if-exists-action is unknown -type unknownIfExistsActionError struct { - path string - ifExistsAction file.IfExistsAction -} - -// Error implements error interface -func (e unknownIfExistsActionError) Error() string { - return fmt.Sprintf("unknown behavior if file exists (%d) for %s", e.ifExistsAction, e.path) -} - -// IsUnknownIfExistsActionError checks if the returned error is because the if-exists-action is unknown -func IsUnknownIfExistsActionError(err error) bool { - return errors.As(err, &unknownIfExistsActionError{}) -} diff --git a/pkg/plugins/internal/machinery/errors_test.go b/pkg/plugins/internal/machinery/errors_test.go deleted file mode 100644 index 3d6b8ce14e4..00000000000 --- a/pkg/plugins/internal/machinery/errors_test.go +++ /dev/null @@ -1,65 +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 machinery - -import ( - "errors" - "path/filepath" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" -) - -var _ = Describe("Errors", func() { - var ( - path = filepath.Join("path", "to", "file") - err = errors.New("test error") - fileAlreadyExistsErr = fileAlreadyExistsError{path} - modelAlreadyExistsErr = modelAlreadyExistsError{path} - unknownIfExistsActionErr = unknownIfExistsActionError{path, -1} - ) - - DescribeTable("IsXxxxError should return true for themselves and false for the rest", - func(f func(error) bool, itself error, rest ...error) { - Expect(f(itself)).To(BeTrue()) - for _, err := range rest { - Expect(f(err)).To(BeFalse()) - } - }, - Entry("file exists", IsFileAlreadyExistsError, fileAlreadyExistsErr, - err, modelAlreadyExistsErr, unknownIfExistsActionErr), - Entry("model exists", IsModelAlreadyExistsError, modelAlreadyExistsErr, - err, fileAlreadyExistsErr, unknownIfExistsActionErr), - Entry("unknown if exists action", IsUnknownIfExistsActionError, unknownIfExistsActionErr, - err, fileAlreadyExistsErr, modelAlreadyExistsErr), - ) - - DescribeTable("should contain the wrapped error and error message", - func(err error) { - Expect(err).To(MatchError(err)) - Expect(err.Error()).To(ContainSubstring(err.Error())) - }, - ) - - // NOTE: the following test increases coverage - It("should print a descriptive error message", func() { - Expect(fileAlreadyExistsErr.Error()).To(ContainSubstring("file already exists")) - Expect(modelAlreadyExistsErr.Error()).To(ContainSubstring("model already exists")) - Expect(unknownIfExistsActionErr.Error()).To(ContainSubstring("unknown behavior if file exists")) - }) -}) diff --git a/pkg/plugins/internal/machinery/scaffold_test.go b/pkg/plugins/internal/machinery/scaffold_test.go deleted file mode 100644 index ebefadc948c..00000000000 --- a/pkg/plugins/internal/machinery/scaffold_test.go +++ /dev/null @@ -1,554 +0,0 @@ -/* -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" - "errors" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/filesystem" -) - -var _ = Describe("Scaffold", func() { - Describe("NewScaffold", func() { - var ( - si Scaffold - s *scaffold - ok bool - ) - - Context("when using no plugins", func() { - BeforeEach(func() { - si = NewScaffold() - s, ok = si.(*scaffold) - }) - - It("should be a scaffold instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(s.fs).NotTo(BeNil()) - }) - - It("should not have any plugin", func() { - Expect(len(s.plugins)).To(Equal(0)) - }) - }) - - Context("when using one plugin", func() { - BeforeEach(func() { - si = NewScaffold(fakePlugin{}) - s, ok = si.(*scaffold) - }) - - It("should be a scaffold instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(s.fs).NotTo(BeNil()) - }) - - It("should have one plugin", func() { - Expect(len(s.plugins)).To(Equal(1)) - }) - }) - - Context("when using several plugins", func() { - BeforeEach(func() { - si = NewScaffold(fakePlugin{}, fakePlugin{}, fakePlugin{}) - s, ok = si.(*scaffold) - }) - - It("should be a scaffold instance", func() { - Expect(ok).To(BeTrue()) - }) - - It("should not have a nil fs", func() { - Expect(s.fs).NotTo(BeNil()) - }) - - It("should have several plugins", func() { - Expect(len(s.plugins)).To(Equal(3)) - }) - }) - }) - - Describe("Scaffold.Execute", func() { - const fileContent = "Hello world!" - - var ( - output bytes.Buffer - testErr = errors.New("error text") - ) - - BeforeEach(func() { - output.Reset() - }) - - DescribeTable("successes", - func(expected string, files ...file.Builder) { - s := &scaffold{ - fs: filesystem.NewMock( - filesystem.MockOutput(&output), - ), - } - - Expect(s.Execute(model.NewUniverse(), files...)).To(Succeed()) - Expect(output.String()).To(Equal(expected)) - }, - Entry("should write the file", - fileContent, - fakeTemplate{body: fileContent}, - ), - Entry("should skip optional models if already have one", - fileContent, - fakeTemplate{body: fileContent}, - fakeTemplate{}, - ), - Entry("should overwrite required models if already have one", - fileContent, - fakeTemplate{}, - fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: fileContent}, - ), - Entry("should format a go file", - "package file\n", - fakeTemplate{fakeBuilder: fakeBuilder{path: "file.go"}, body: "package file"}, - ), - ) - - DescribeTable("file builders related errors", - func(f func(error) bool, files ...file.Builder) { - s := &scaffold{fs: filesystem.NewMock()} - - Expect(f(s.Execute(model.NewUniverse(), files...))).To(BeTrue()) - }, - Entry("should fail if unable to validate a file builder", - file.IsValidateError, - fakeRequiresValidation{validateErr: testErr}, - ), - Entry("should fail if unable to set default values for a template", - file.IsSetTemplateDefaultsError, - fakeTemplate{err: testErr}, - ), - Entry("should fail if an unexpected previous model is found", - IsModelAlreadyExistsError, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename"}}, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}}, - ), - Entry("should fail if behavior if file exists is not defined", - IsUnknownIfExistsActionError, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename"}}, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, - ), - ) - - // Following errors are unwrapped, so we need to check for substrings - DescribeTable("template related errors", - func(errMsg string, files ...file.Builder) { - s := &scaffold{fs: filesystem.NewMock()} - - err := s.Execute(model.NewUniverse(), files...) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring(errMsg)) - }, - Entry("should fail if a template is broken", - "template: ", - fakeTemplate{body: "{{ .Field }"}, - ), - Entry("should fail if a template params aren't provided", - "template: ", - fakeTemplate{body: "{{ .Field }}"}, - ), - Entry("should fail if unable to format a go file", - "expected 'package', found ", - fakeTemplate{fakeBuilder: fakeBuilder{path: "file.go"}, body: fileContent}, - ), - ) - - DescribeTable("insert strings", - func(input, expected string, files ...file.Builder) { - s := &scaffold{ - fs: filesystem.NewMock( - filesystem.MockInput(bytes.NewBufferString(input)), - filesystem.MockOutput(&output), - filesystem.MockExists(func(_ string) bool { return len(input) != 0 }), - ), - } - - Expect(s.Execute(model.NewUniverse(), files...)).To(Succeed()) - Expect(output.String()).To(Equal(expected)) - }, - Entry("should insert lines for go files", - ` -//+kubebuilder:scaffold:- -`, - ` -1 -2 -//+kubebuilder:scaffold:- -`, - fakeInserter{codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}}, - }, - ), - Entry("should insert lines for yaml files", - ` -#+kubebuilder:scaffold:- -`, - ` -1 -2 -#+kubebuilder:scaffold:- -`, - fakeInserter{codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.yaml", "-"): {"1\n", "2\n"}}, - }, - ), - Entry("should use models if there is no file", - "", - ` -1 -2 -//+kubebuilder:scaffold:- -`, - fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: ` -//+kubebuilder:scaffold:- -`}, - fakeInserter{codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}}, - }, - ), - Entry("should use required models over files", - fileContent, - ` -1 -2 -//+kubebuilder:scaffold:- -`, - fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: ` -//+kubebuilder:scaffold:- -`}, - fakeInserter{codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}}, - }, - ), - Entry("should use files over optional models", - ` -//+kubebuilder:scaffold:- -`, - ` -1 -2 -//+kubebuilder:scaffold:- -`, - fakeTemplate{body: fileContent}, - fakeInserter{ - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}, - }, - }, - ), - Entry("should filter invalid markers", - ` -//+kubebuilder:scaffold:- -//+kubebuilder:scaffold:* -`, - ` -1 -2 -//+kubebuilder:scaffold:- -//+kubebuilder:scaffold:* -`, - fakeInserter{ - markers: []file.Marker{file.NewMarkerFor("file.go", "-")}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}, - file.NewMarkerFor("file.go", "*"): {"3\n", "4\n"}, - }, - }, - ), - Entry("should filter already existing one-line code fragments", - ` -1 -//+kubebuilder:scaffold:- -3 -4 -//+kubebuilder:scaffold:* -`, - ` -1 -2 -//+kubebuilder:scaffold:- -3 -4 -//+kubebuilder:scaffold:* -`, - fakeInserter{ - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}, - file.NewMarkerFor("file.go", "*"): {"3\n", "4\n"}, - }, - }, - ), - Entry("should not insert anything if no code fragment", - "", // input is provided through a template as mock fs doesn't copy it to the output buffer if no-op - ` -//+kubebuilder:scaffold:- -`, - fakeTemplate{body: ` -//+kubebuilder:scaffold:- -`}, - fakeInserter{ - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor("file.go", "-"): {}, - }, - }, - ), - ) - - DescribeTable("insert strings related errors", - func(f func(error) bool, files ...file.Builder) { - s := &scaffold{ - fs: filesystem.NewMock( - filesystem.MockExists(func(_ string) bool { return true }), - ), - } - - err := s.Execute(model.NewUniverse(), files...) - Expect(err).To(HaveOccurred()) - Expect(f(err)).To(BeTrue()) - }, - Entry("should fail if inserting into a model that fails when a file exists and it does exist", - IsFileAlreadyExistsError, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}}, - fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, - ), - Entry("should fail if inserting into a model with unknown behavior if the file exists and it does exist", - IsUnknownIfExistsActionError, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, - fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, - ), - ) - - It("should fail if a plugin fails", func() { - s := &scaffold{ - fs: filesystem.NewMock(), - plugins: []model.Plugin{fakePlugin{err: testErr}}, - } - - err := s.Execute( - model.NewUniverse(), - fakeTemplate{}, - ) - Expect(err).To(MatchError(testErr)) - Expect(model.IsPluginError(err)).To(BeTrue()) - }) - - Context("write when the file already exists", func() { - var s Scaffold - - BeforeEach(func() { - s = &scaffold{ - fs: filesystem.NewMock( - filesystem.MockExists(func(_ string) bool { return true }), - filesystem.MockOutput(&output), - ), - } - }) - - It("should skip the file by default", func() { - Expect(s.Execute( - model.NewUniverse(), - fakeTemplate{body: fileContent}, - )).To(Succeed()) - Expect(output.String()).To(BeEmpty()) - }) - - It("should write the file if configured to do so", func() { - Expect(s.Execute( - model.NewUniverse(), - fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: fileContent}, - )).To(Succeed()) - Expect(output.String()).To(Equal(fileContent)) - }) - - It("should error if configured to do so", func() { - err := s.Execute( - model.NewUniverse(), - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}, body: fileContent}, - ) - Expect(err).To(HaveOccurred()) - Expect(IsFileAlreadyExistsError(err)).To(BeTrue()) - Expect(output.String()).To(BeEmpty()) - }) - }) - - DescribeTable("filesystem errors", - func( - mockErrorF func(error) filesystem.MockOptions, - checkErrorF func(error) bool, - files ...file.Builder, - ) { - s := &scaffold{ - fs: filesystem.NewMock( - mockErrorF(testErr), - ), - } - - err := s.Execute(model.NewUniverse(), files...) - Expect(err).To(HaveOccurred()) - Expect(checkErrorF(err)).To(BeTrue()) - }, - Entry("should fail if fs.Exists failed (at file writing)", - filesystem.MockExistsError, filesystem.IsFileExistsError, - fakeTemplate{}, - ), - Entry("should fail if fs.Exists failed (at model updating)", - filesystem.MockExistsError, filesystem.IsFileExistsError, - fakeTemplate{}, - fakeInserter{}, - ), - Entry("should fail if fs.Open was unable to open the file", - filesystem.MockOpenFileError, filesystem.IsOpenFileError, - fakeInserter{}, - ), - Entry("should fail if fs.Open().Read was unable to read the file", - filesystem.MockReadFileError, filesystem.IsReadFileError, - fakeInserter{}, - ), - Entry("should fail if fs.Open().Close was unable to close the file", - filesystem.MockCloseFileError, filesystem.IsCloseFileError, - fakeInserter{}, - ), - Entry("should fail if fs.Create was unable to create the directory", - filesystem.MockCreateDirError, filesystem.IsCreateDirectoryError, - fakeTemplate{}, - ), - Entry("should fail if fs.Create was unable to create the file", - filesystem.MockCreateFileError, filesystem.IsCreateFileError, - fakeTemplate{}, - ), - Entry("should fail if fs.Create().Write was unable to write the file", - filesystem.MockWriteFileError, filesystem.IsWriteFileError, - fakeTemplate{}, - ), - Entry("should fail if fs.Create().Write was unable to close the file", - filesystem.MockCloseFileError, filesystem.IsCloseFileError, - fakeTemplate{}, - ), - ) - }) -}) - -var _ model.Plugin = fakePlugin{} - -// fakePlugin is used to mock a model.Plugin in order to test Scaffold -type fakePlugin struct { - err error -} - -// Pipe implements model.Plugin -func (f fakePlugin) Pipe(_ *model.Universe) error { - return f.err -} - -var _ file.Builder = fakeBuilder{} - -// fakeBuilder is used to mock a file.Builder -type fakeBuilder struct { - path string - ifExistsAction file.IfExistsAction -} - -// GetPath implements file.Builder -func (f fakeBuilder) GetPath() string { - return f.path -} - -// GetIfExistsAction implements file.Builder -func (f fakeBuilder) GetIfExistsAction() file.IfExistsAction { - return f.ifExistsAction -} - -var _ file.RequiresValidation = fakeRequiresValidation{} - -// fakeRequiresValidation is used to mock a file.RequiresValidation in order to test Scaffold -type fakeRequiresValidation struct { - fakeBuilder - - validateErr error -} - -// Validate implements file.RequiresValidation -func (f fakeRequiresValidation) Validate() error { - return f.validateErr -} - -var _ file.Template = fakeTemplate{} - -// fakeTemplate is used to mock a file.File in order to test Scaffold -type fakeTemplate struct { - fakeBuilder - - body string - err error -} - -// GetBody implements file.Template -func (f fakeTemplate) GetBody() string { - return f.body -} - -// SetTemplateDefaults implements file.Template -func (f fakeTemplate) SetTemplateDefaults() error { - if f.err != nil { - return f.err - } - - return nil -} - -type fakeInserter struct { - fakeBuilder - - markers []file.Marker - codeFragments file.CodeFragmentsMap -} - -// GetMarkers implements file.UpdatableTemplate -func (f fakeInserter) GetMarkers() []file.Marker { - if f.markers != nil { - return f.markers - } - - markers := make([]file.Marker, 0, len(f.codeFragments)) - for marker := range f.codeFragments { - markers = append(markers, marker) - } - return markers -} - -// GetCodeFragments implements file.UpdatableTemplate -func (f fakeInserter) GetCodeFragments() file.CodeFragmentsMap { - return f.codeFragments -} diff --git a/pkg/cli/internal/config/config_suite_test.go b/pkg/plugins/scaffolder.go similarity index 73% rename from pkg/cli/internal/config/config_suite_test.go rename to pkg/plugins/scaffolder.go index fffe2f8bb2f..f487d0caa5a 100644 --- a/pkg/cli/internal/config/config_suite_test.go +++ b/pkg/plugins/scaffolder.go @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package config +package plugins import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "github.com/spf13/afero" ) -func TestCLI(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Config Suite") +// Scaffolder interface creates files to set up a controller manager +type Scaffolder interface { + InjectFS(afero.Fs) + // Scaffold performs the scaffolding + Scaffold() error } diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 4cab5423bc2..00000000000 --- a/plugins/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Kubebuilder plugins - -**Status: Experimental** - -We are developing a plugin system to kubebuilder, so that we can generate -operators that follow other patterns. - -While plugins remain experimental, you must pass the `KUBEBUILDER_ENABLE_PLUGINS=1` -environment variable to enable plugin functionality. (Any non-empty -value will work!) - -When you specify `KUBEBUILDER_ENABLE_PLUGINS=1`, a flag `--pattern` will become -available for resource generation. Specifying `--pattern=addon` will change -resource code generation to generate code that follows the addon pattern, as -being developed in the -[cluster-addons](https://github.com/kubernetes-sigs/cluster-addons) -subproject. - -The `pattern=addon` plugin is intended to serve both as an example of a plugin, -and as a real-world use case for driving development of the plugin system. We -don't intend for the plugin system to become an emacs competitor, but it must be -sufficiently flexible to support the various patterns of operators that -kubebuilder will generate. - -## Plugin model - -We intend for plugins to be packaged in a separate binary, which will be -executed by the `kubebuilder` main binary. Data will be piped to the binary via -stdin, and returned over stdout. The serialization format will likely either be -yaml or json (to be determined!). - -While we are developing this functionality though, we are developing it using an -in-process golang interface named `Plugin`, defined in -[pkg/plugin/scaffold/scaffold.go](../pkg/plugin/scaffold/scaffold.go). The interface is a -simple single-method interface that is intended to mirror the data-in / data-out -approach that will be used when executing a plugin in a separate binary. When -we have more stability of the plugin, we intend to replace the in-process -implementation with a implementation that `exec`s a plugin in a separate binary. - -The approach being prototyped is that we pass a model of the full state of the -generation world to the Plugin, which returns the full state of the generation -world after making appropriate changes. We are starting to define a `model` -package which includes a `Universe` comprising the various `File`s that are -being generated, along with the inputs like the `Boilerplate` and the `Resource` -we are currently generating. A plugin can change the `Contents` of `File`s, or -add/remove `File`s entirely. diff --git a/plugins/addon/helpers.go b/plugins/addon/helpers.go deleted file mode 100644 index 48f91e89d8d..00000000000 --- a/plugins/addon/helpers.go +++ /dev/null @@ -1,88 +0,0 @@ -package addon - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/gobuffalo/flect" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -// This file gathers functions that are likely to be useful to other -// plugins. Once we have validated they are used in more than one -// place, we can promote them to a shared location. - -// PluginFunc executes a step of Plugin -type PluginFunc func(u *model.Universe) error - -// AddFile adds the specified file to the model. -// If the file exists the function returns false and does not modify the Universe -// If the file does not exist, the function returns true and adds the file to the Universe -// If there is a problem with the file the function returns an error -func AddFile(u *model.Universe, add *file.File) (bool, error) { - p := add.Path - if p == "" { - return false, fmt.Errorf("path must be set") - } - - if _, found := u.Files[p]; found { - return false, nil - } - - u.Files[p] = add - return true, nil -} - -// ReplaceFileIfExists replaces the specified file in the model by path -// Returns true if the file was replaced. -func ReplaceFileIfExists(u *model.Universe, add *file.File) bool { - p := add.Path - if p == "" { - panic("path must be set") - } - - if _, found := u.Files[p]; found { - u.Files[p] = add - return true - } - - return false -} - -// ReplaceFile replaces the specified file in the model by path -// If the file does not exist, it returns an error -func ReplaceFile(u *model.Universe, add *file.File) error { - found := ReplaceFileIfExists(u, add) - if !found { - return fmt.Errorf("file not found %q", add.Path) - } - return nil -} - -// DefaultTemplateFunctions returns a map of template helpers -func DefaultTemplateFunctions() template.FuncMap { - return template.FuncMap{ - "title": strings.Title, - "lower": strings.ToLower, - "plural": flect.Pluralize, - } -} - -// RunTemplate parses a template -func RunTemplate(templateName, templateValue string, data interface{}, funcMap template.FuncMap) (string, error) { - t, err := template.New(templateName).Funcs(funcMap).Parse(templateValue) - if err != nil { - return "", fmt.Errorf("error building template %s: %v", templateName, err) - } - - var b bytes.Buffer - if err := t.Execute(&b, data); err != nil { - return "", fmt.Errorf("error rending template %s: %v", templateName, err) - } - - return b.String(), nil -} diff --git a/plugins/addon/manifest.go b/plugins/addon/manifest.go deleted file mode 100644 index 227f1300ef1..00000000000 --- a/plugins/addon/manifest.go +++ /dev/null @@ -1,50 +0,0 @@ -/* -Copyright 2019 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 addon - -import ( - "path/filepath" - "strings" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -const exampleManifestVersion = "0.0.1" - -const exampleManifestContents = `# Placeholder manifest - replace with the manifest for your addon -` - -// ExampleManifest adds a model file for the manifest placeholder -func ExampleManifest(u *model.Universe) error { - packageName := getPackageName(u) - - m := &file.File{ - Path: filepath.Join("channels", "packages", packageName, exampleManifestVersion, "manifest.yaml"), - Contents: exampleManifestContents, - IfExistsAction: file.Skip, - } - - _, err := AddFile(u, m) - - return err -} - -// getPackageName returns the (default) name of the declarative package -func getPackageName(u *model.Universe) string { - return strings.ToLower(u.Resource.Kind) -} diff --git a/plugins/addon/plugin.go b/plugins/addon/plugin.go deleted file mode 100644 index 3da0363b929..00000000000 --- a/plugins/addon/plugin.go +++ /dev/null @@ -1,28 +0,0 @@ -package addon - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model" -) - -// Plugin implements model.Plugin -type Plugin struct { -} - -// Pipe implements model.Plugin -func (p *Plugin) Pipe(u *model.Universe) error { - functions := []PluginFunc{ - ExampleManifest, - ExampleChannel, - ReplaceController, - ReplaceTypes, - } - - for _, fn := range functions { - if err := fn(u); err != nil { - return err - } - - } - - return nil -} diff --git a/test/e2e/v2/plugin_cluster_test.go b/test/e2e/v2/plugin_cluster_test.go index 6832259788a..e38527b9e39 100644 --- a/test/e2e/v2/plugin_cluster_test.go +++ b/test/e2e/v2/plugin_cluster_test.go @@ -66,6 +66,7 @@ var _ = Describe("kubebuilder", func() { var controllerPodName string By("init v2 project") err := kbc.Init( + "--plugins", "go/v2", "--project-version", "2", "--domain", kbc.Domain, "--fetch-deps=false") diff --git a/test/e2e/v3/generate_test.go b/test/e2e/v3/generate_test.go index e754973743f..ed4f6117097 100644 --- a/test/e2e/v3/generate_test.go +++ b/test/e2e/v3/generate_test.go @@ -34,8 +34,8 @@ func GenerateV2(kbc *utils.TestContext) { By("initializing a project") err = kbc.Init( - "--project-version", "3", "--plugins", "go/v2", + "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) @@ -129,8 +129,8 @@ func GenerateV3(kbc *utils.TestContext, crdAndWebhookVersion string) { By("initializing a project") err = kbc.Init( - "--project-version", "3", "--plugins", "go/v3", + "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 608bc757ce0..01dc515a14f 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -54,7 +54,11 @@ function scaffold_test_project { $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --force fi - $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + if [ $project == "project-v2" ]; then + $kb create api --plugins="go/v2,declarative" --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + else + $kb create api --plugins="go/v3,declarative" --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + fi $kb create webhook --group crew --version v1 --kind FirstMate --conversion if [ $project == "project-v3" ]; then @@ -96,13 +100,10 @@ function scaffold_test_project { $kb create webhook --version v1 --kind Lakers --defaulting --programmatic-validation fi elif [[ $project =~ addon ]]; then - header_text 'enabling --pattern flag ...' - export KUBEBUILDER_ENABLE_PLUGINS=1 header_text 'Creating APIs ...' - $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false --pattern=addon - $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false --pattern=addon - $kb create api --group crew --version v1 --kind Admiral --controller=true --resource=true --namespaced=false --make=false --pattern=addon - unset KUBEBUILDER_ENABLE_PLUGINS + $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false + $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + $kb create api --group crew --version v1 --kind Admiral --controller=true --resource=true --namespaced=false --make=false fi make generate manifests @@ -116,9 +117,9 @@ build_kb # Project version 2 uses plugin go/v2 (default). scaffold_test_project project-v2 --project-version=2 scaffold_test_project project-v2-multigroup --project-version=2 -scaffold_test_project project-v2-addon --project-version=2 +scaffold_test_project project-v2-addon --project-version=3 --plugins="go/v2,declarative" # Project version 3 (default) uses plugin go/v3 (default). scaffold_test_project project-v3 scaffold_test_project project-v3-multigroup -scaffold_test_project project-v3-addon +scaffold_test_project project-v3-addon --plugins="go/v3,declarative" scaffold_test_project project-v3-config --component-config diff --git a/testdata/project-v2-addon/PROJECT b/testdata/project-v2-addon/PROJECT index 77f34e78733..236cb6a4aa7 100644 --- a/testdata/project-v2-addon/PROJECT +++ b/testdata/project-v2-addon/PROJECT @@ -1,13 +1,47 @@ domain: testproject.org +layout: go.kubebuilder.io/v2,declarative.kubebuilder.io/v1 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: Captain + version: v1 + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 + - domain: testproject.org + group: crew + kind: Admiral + version: v1 +projectName: project-v2-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v2-addon resources: -- group: crew +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: testproject.org + group: crew kind: Captain + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -- group: crew +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: testproject.org + group: crew kind: FirstMate + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -- group: crew +- api: + crdVersion: v1beta1 + controller: true + domain: testproject.org + group: crew kind: Admiral + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -version: "2" +version: "3" diff --git a/testdata/project-v2/api/v1/firstmate_types.go b/testdata/project-v2/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v2/api/v1/firstmate_types.go +++ b/testdata/project-v2/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v2/api/v1/zz_generated.deepcopy.go b/testdata/project-v2/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v2/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v2/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v2/channels/stable b/testdata/project-v2/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v2/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml index 499d4131a73..c2f3d91c436 100644 --- a/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,30 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + required: + - healthy type: object type: object version: v1 diff --git a/testdata/project-v2/controllers/firstmate_controller.go b/testdata/project-v2/controllers/firstmate_controller.go index 00a899419b8..fb4111d9517 100644 --- a/testdata/project-v2/controllers/firstmate_controller.go +++ b/testdata/project-v2/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v2/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.6.4/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} - // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + declarative.WithObjectTransform(declarative.AddLabels(labels)), + declarative.WithOwner(declarative.SourceAsOwner), + declarative.WithLabels(watchLabels), + declarative.WithStatus(status.NewBasic(mgr.GetClient())), + // TODO: add an application to your manifest: declarative.WithObjectTransform(addon.TransformApplicationFromStatus), + // TODO: add an application to your manifest: declarative.WithManagedApplication(watchLabels), + declarative.WithObjectTransform(addon.ApplyPatches), + ); err != nil { + return err + } + + c, err := controller.New("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to deployed objects + _, err = declarative.WatchAll(mgr.GetConfig(), c, r, watchLabels) + if err != nil { + return err + } + + return nil } diff --git a/testdata/project-v2/go.mod b/testdata/project-v2/go.mod index d12efdcca14..0d462758f6c 100644 --- a/testdata/project-v2/go.mod +++ b/testdata/project-v2/go.mod @@ -9,4 +9,5 @@ require ( k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 sigs.k8s.io/controller-runtime v0.6.4 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20200522144838-848d48e5b073 ) diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT index f5b6f774bc0..3d9c4c25425 100644 --- a/testdata/project-v3-addon/PROJECT +++ b/testdata/project-v3-addon/PROJECT @@ -1,5 +1,20 @@ domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: go.kubebuilder.io/v3,declarative.kubebuilder.io/v1 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: Captain + version: v1 + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 + - domain: testproject.org + group: crew + kind: Admiral + version: v1 projectName: project-v3-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v3-addon resources: diff --git a/testdata/project-v3-config/PROJECT b/testdata/project-v3-config/PROJECT index 804ab898289..150f89d6e0f 100644 --- a/testdata/project-v3-config/PROJECT +++ b/testdata/project-v3-config/PROJECT @@ -1,6 +1,13 @@ componentConfig: true domain: testproject.org layout: go.kubebuilder.io/v3 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 projectName: project-v3-config repo: sigs.k8s.io/kubebuilder/testdata/project-v3-config resources: diff --git a/testdata/project-v3-config/api/v1/firstmate_types.go b/testdata/project-v3-config/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v3-config/api/v1/firstmate_types.go +++ b/testdata/project-v3-config/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go b/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v3-config/channels/stable b/testdata/project-v3-config/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v3-config/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml index be577063e51..dac655c8885 100644 --- a/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,32 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + phase: + type: string + required: + - healthy type: object type: object served: true diff --git a/testdata/project-v3-config/config/rbac/role.yaml b/testdata/project-v3-config/config/rbac/role.yaml index 1d105256811..e6f9fed07ad 100644 --- a/testdata/project-v3-config/config/rbac/role.yaml +++ b/testdata/project-v3-config/config/rbac/role.yaml @@ -70,12 +70,6 @@ rules: - patch - update - watch -- apiGroups: - - crew.testproject.org - resources: - - firstmates/finalizers - verbs: - - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v3-config/controllers/firstmate_controller.go b/testdata/project-v3-config/controllers/firstmate_controller.go index 0263db8d5df..2f822ecd193 100644 --- a/testdata/project-v3-config/controllers/firstmate_controller.go +++ b/testdata/project-v3-config/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-config/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + declarative.WithObjectTransform(declarative.AddLabels(labels)), + declarative.WithOwner(declarative.SourceAsOwner), + declarative.WithLabels(watchLabels), + declarative.WithStatus(status.NewBasic(mgr.GetClient())), + // TODO: add an application to your manifest: declarative.WithObjectTransform(addon.TransformApplicationFromStatus), + // TODO: add an application to your manifest: declarative.WithManagedApplication(watchLabels), + declarative.WithObjectTransform(addon.ApplyPatches), + ); err != nil { + return err + } + + c, err := controller.New("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to deployed objects + _, err = declarative.WatchAll(mgr.GetConfig(), c, r, watchLabels) + if err != nil { + return err + } + + return nil } diff --git a/testdata/project-v3-config/go.mod b/testdata/project-v3-config/go.mod index 13552682304..073e09a9498 100644 --- a/testdata/project-v3-config/go.mod +++ b/testdata/project-v3-config/go.mod @@ -10,4 +10,5 @@ require ( k8s.io/apimachinery v0.19.2 k8s.io/client-go v0.19.2 sigs.k8s.io/controller-runtime v0.7.2 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210113160450-b84d99da0217 ) diff --git a/testdata/project-v3/PROJECT b/testdata/project-v3/PROJECT index 8dde5b76ffe..b7e4f55d9db 100644 --- a/testdata/project-v3/PROJECT +++ b/testdata/project-v3/PROJECT @@ -1,5 +1,12 @@ domain: testproject.org layout: go.kubebuilder.io/v3 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 projectName: project-v3 repo: sigs.k8s.io/kubebuilder/testdata/project-v3 resources: diff --git a/testdata/project-v3/api/v1/firstmate_types.go b/testdata/project-v3/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v3/api/v1/firstmate_types.go +++ b/testdata/project-v3/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v3/api/v1/zz_generated.deepcopy.go b/testdata/project-v3/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v3/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v3/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v3/channels/stable b/testdata/project-v3/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v3/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml index be577063e51..dac655c8885 100644 --- a/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,32 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + phase: + type: string + required: + - healthy type: object type: object served: true diff --git a/testdata/project-v3/config/rbac/role.yaml b/testdata/project-v3/config/rbac/role.yaml index a02d192e82e..780572f0f90 100644 --- a/testdata/project-v3/config/rbac/role.yaml +++ b/testdata/project-v3/config/rbac/role.yaml @@ -70,12 +70,6 @@ rules: - patch - update - watch -- apiGroups: - - crew.testproject.org - resources: - - firstmates/finalizers - verbs: - - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v3/controllers/firstmate_controller.go b/testdata/project-v3/controllers/firstmate_controller.go index 51a1224eb57..e2ea33adb18 100644 --- a/testdata/project-v3/controllers/firstmate_controller.go +++ b/testdata/project-v3/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v3/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + declarative.WithObjectTransform(declarative.AddLabels(labels)), + declarative.WithOwner(declarative.SourceAsOwner), + declarative.WithLabels(watchLabels), + declarative.WithStatus(status.NewBasic(mgr.GetClient())), + // TODO: add an application to your manifest: declarative.WithObjectTransform(addon.TransformApplicationFromStatus), + // TODO: add an application to your manifest: declarative.WithManagedApplication(watchLabels), + declarative.WithObjectTransform(addon.ApplyPatches), + ); err != nil { + return err + } + + c, err := controller.New("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to deployed objects + _, err = declarative.WatchAll(mgr.GetConfig(), c, r, watchLabels) + if err != nil { + return err + } + + return nil } diff --git a/testdata/project-v3/go.mod b/testdata/project-v3/go.mod index 92e85f54740..705bc66c458 100644 --- a/testdata/project-v3/go.mod +++ b/testdata/project-v3/go.mod @@ -10,4 +10,5 @@ require ( k8s.io/apimachinery v0.19.2 k8s.io/client-go v0.19.2 sigs.k8s.io/controller-runtime v0.7.2 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210113160450-b84d99da0217 )