diff --git a/cmd/main.go b/cmd/main.go index e95a455f76b..6f13054b4a4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -20,7 +20,8 @@ import ( "log" "sigs.k8s.io/kubebuilder/v3/pkg/cli" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" pluginv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2" pluginv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) @@ -29,13 +30,13 @@ func main() { c, err := cli.New( cli.WithCommandName("kubebuilder"), cli.WithVersion(versionString()), - cli.WithDefaultProjectVersion(config.Version3Alpha), + cli.WithDefaultProjectVersion(cfgv3alpha.Version), cli.WithPlugins( &pluginv2.Plugin{}, &pluginv3.Plugin{}, ), - cli.WithDefaultPlugins(config.Version2, &pluginv2.Plugin{}), - cli.WithDefaultPlugins(config.Version3Alpha, &pluginv3.Plugin{}), + cli.WithDefaultPlugins(cfgv2.Version, &pluginv2.Plugin{}), + cli.WithDefaultPlugins(cfgv3alpha.Version, &pluginv3.Plugin{}), cli.WithCompletion, ) if err != nil { diff --git a/pkg/cli/api.go b/pkg/cli/api.go index d40680af112..c858784962f 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -83,7 +83,7 @@ func (c cli) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) { } subcommand := createAPIPlugin.GetCreateAPISubcommand() - subcommand.InjectConfig(&cfg.Config) + subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index f4437f4baff..cc67383a4b9 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -25,8 +25,8 @@ import ( "github.com/spf13/pflag" internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" - "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -74,9 +74,9 @@ type cli struct { //nolint:maligned // CLI version string. version string // Default project version in case none is provided and a config file can't be found. - defaultProjectVersion string + defaultProjectVersion config.Version // Default plugins in case none is provided and a config file can't be found. - defaultPlugins map[string][]string + defaultPlugins map[config.Version][]string // Plugins registered in the cli. plugins map[string]plugin.Plugin // Commands injected by options. @@ -87,7 +87,7 @@ type cli struct { //nolint:maligned /* Internal fields */ // Project version to scaffold. - projectVersion string + projectVersion config.Version // Plugin keys to scaffold with. pluginKeys []string @@ -131,8 +131,8 @@ func newCLI(opts ...Option) (*cli, error) { // Default cli options. c := &cli{ commandName: "kubebuilder", - defaultProjectVersion: internalconfig.DefaultVersion, - defaultPlugins: make(map[string][]string), + defaultProjectVersion: cfgv3alpha.Version, + defaultPlugins: make(map[config.Version][]string), plugins: make(map[string]plugin.Plugin), } @@ -191,15 +191,15 @@ func (c *cli) getInfoFromFlags() (string, []string, error) { } // getInfoFromConfigFile obtains the project version and plugin keys from the project config file. -func getInfoFromConfigFile() (string, []string, error) { +func getInfoFromConfigFile() (config.Version, []string, error) { // Read the project configuration file projectConfig, err := internalconfig.Read() switch { case err == nil: case os.IsNotExist(err): - return "", nil, nil + return config.Version{}, nil, nil default: - return "", nil, err + return config.Version{}, nil, err } return getInfoFromConfig(projectConfig) @@ -207,74 +207,82 @@ func getInfoFromConfigFile() (string, []string, error) { // 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) (string, []string, error) { +func getInfoFromConfig(projectConfig config.Config) (config.Version, []string, error) { // Split the comma-separated plugins var pluginSet []string - if projectConfig.Layout != "" { - for _, p := range strings.Split(projectConfig.Layout, ",") { + if projectConfig.GetLayout() != "" { + for _, p := range strings.Split(projectConfig.GetLayout(), ",") { pluginSet = append(pluginSet, strings.TrimSpace(p)) } } - return projectConfig.Version, pluginSet, nil + return projectConfig.GetVersion(), pluginSet, 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( - flagProjectVersion, cfgProjectVersion string, + flagProjectVersionString string, + cfgProjectVersion config.Version, flagPlugins, cfgPlugins []string, -) (string, []string, error) { +) (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) + } + } + // Resolve project version - var projectVersion string + var projectVersion config.Version + isFlagProjectVersionInvalid := flagProjectVersion.Validate() != nil + isCfgProjectVersionInvalid := cfgProjectVersion.Validate() != nil switch { - // If they are both blank, use the default - case flagProjectVersion == "" && cfgProjectVersion == "": + // If they are both invalid (empty is invalid), use the default + case isFlagProjectVersionInvalid && isCfgProjectVersionInvalid: projectVersion = c.defaultProjectVersion - // If they are equal doesn't matter which we choose - case flagProjectVersion == cfgProjectVersion: + // If any is invalid (empty is invalid), choose the other + case isCfgProjectVersionInvalid: projectVersion = flagProjectVersion - // If any is blank, choose the other - case cfgProjectVersion == "": - projectVersion = flagProjectVersion - case flagProjectVersion == "": + case isFlagProjectVersionInvalid: projectVersion = cfgProjectVersion - // If none is blank and they are different error out + // 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 "", nil, fmt.Errorf("project version conflict between command line args (%s) "+ - "and project configuration file (%s)", flagProjectVersion, cfgProjectVersion) - } - // It still may be empty if default, flag and config project versions are empty - if projectVersion != "" { - // Validate the project version - if err := validation.ValidateProjectVersion(projectVersion); err != nil { - return "", nil, err - } + return config.Version{}, nil, fmt.Errorf("project version conflict between command line args (%s) "+ + "and project configuration file (%s)", flagProjectVersionString, cfgProjectVersion) } // Resolve plugins var plugins []string + isFlagPluginsEmpty := len(flagPlugins) == 0 + isCfgPluginsEmpty := len(cfgPlugins) == 0 switch { - // If they are both blank, use the default - case len(flagPlugins) == 0 && len(cfgPlugins) == 0: - plugins = c.defaultPlugins[projectVersion] + // If they are both empty, use the default + case isFlagPluginsEmpty && isCfgPluginsEmpty: + if defaults, hasDefaults := c.defaultPlugins[projectVersion]; hasDefaults { + plugins = defaults + } + // 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 any is blank, choose the other - case len(cfgPlugins) == 0: - plugins = flagPlugins - case len(flagPlugins) == 0: - plugins = cfgPlugins - // If none is blank and they are different error out + // If none is empty and they are different error out default: - return "", nil, fmt.Errorf("plugins conflict between command line args (%v) "+ + return config.Version{}, nil, fmt.Errorf("plugins conflict between command line args (%v) "+ "and project configuration file (%v)", flagPlugins, cfgPlugins) } // Validate the plugins for _, p := range plugins { if err := plugin.ValidateKey(p); err != nil { - return "", nil, err + return config.Version{}, nil, err } } @@ -315,8 +323,8 @@ func (c *cli) resolve() error { // under no support contract. However users should be notified _why_ their plugin cannot be found. var extraErrMsg string if version != "" { - ver, err := plugin.ParseVersion(version) - if err != nil { + 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() { diff --git a/pkg/cli/cli_suite_test.go b/pkg/cli/cli_suite_test.go index 563a47fcb9b..72df7f64d9c 100644 --- a/pkg/cli/cli_suite_test.go +++ b/pkg/cli/cli_suite_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -39,27 +40,27 @@ var ( type mockPlugin struct { //nolint:maligned name string version plugin.Version - projectVersions []string + projectVersions []config.Version } -func newMockPlugin(name, version string, projVers ...string) plugin.Plugin { - v, err := plugin.ParseVersion(version) - if err != nil { +func newMockPlugin(name, version string, projVers ...config.Version) plugin.Plugin { + var v plugin.Version + if err := v.Parse(version); err != nil { panic(err) } return mockPlugin{name, v, projVers} } -func (p mockPlugin) Name() string { return p.name } -func (p mockPlugin) Version() plugin.Version { return p.version } -func (p mockPlugin) SupportedProjectVersions() []string { return p.projectVersions } +func (p mockPlugin) Name() string { return p.name } +func (p mockPlugin) Version() plugin.Version { return p.version } +func (p mockPlugin) SupportedProjectVersions() []config.Version { return p.projectVersions } type mockDeprecatedPlugin struct { //nolint:maligned mockPlugin deprecation string } -func newMockDeprecatedPlugin(name, version, deprecation string, projVers ...string) plugin.Plugin { +func newMockDeprecatedPlugin(name, version, deprecation string, projVers ...config.Version) plugin.Plugin { return mockDeprecatedPlugin{ mockPlugin: newMockPlugin(name, version, projVers...).(mockPlugin), deprecation: deprecation, diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index 6f01e97bdc5..61706d893cb 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -25,11 +25,13 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -func makeMockPluginsFor(projectVersion string, pluginKeys ...string) []plugin.Plugin { +func makeMockPluginsFor(projectVersion config.Version, pluginKeys ...string) []plugin.Plugin { plugins := make([]plugin.Plugin, 0, len(pluginKeys)) for _, key := range pluginKeys { n, v := plugin.SplitKey(key) @@ -92,7 +94,7 @@ var _ = Describe("CLI", func() { AfterEach(func() { os.Args = args }) When("no flag is set", func() { - It("should success", func() { + It("should succeed", func() { projectVersion, plugins, err = c.getInfoFromFlags() Expect(err).NotTo(HaveOccurred()) Expect(projectVersion).To(Equal("")) @@ -101,7 +103,7 @@ var _ = Describe("CLI", func() { }) When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { - It("should success", func() { + It("should succeed", func() { setProjectVersionFlag("2") projectVersion, plugins, err = c.getInfoFromFlags() Expect(err).NotTo(HaveOccurred()) @@ -111,7 +113,7 @@ var _ = Describe("CLI", func() { }) When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { - It("should success using one plugin key", func() { + It("should succeed using one plugin key", func() { setPluginsFlag("go/v1") projectVersion, plugins, err = c.getInfoFromFlags() Expect(err).NotTo(HaveOccurred()) @@ -119,7 +121,7 @@ var _ = Describe("CLI", func() { Expect(plugins).To(Equal([]string{"go/v1"})) }) - It("should success using more than one plugin key", func() { + 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()) @@ -127,7 +129,7 @@ var _ = Describe("CLI", func() { Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) }) - It("should success using more than one plugin key with spaces", func() { + 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()) @@ -137,7 +139,7 @@ var _ = Describe("CLI", func() { }) When(fmt.Sprintf("--%s and --%s flags are set", projectVersionFlag, pluginsFlag), func() { - It("should success using one plugin key", func() { + It("should succeed using one plugin key", func() { setProjectVersionFlag("2") setPluginsFlag("go/v1") projectVersion, plugins, err = c.getInfoFromFlags() @@ -146,7 +148,7 @@ var _ = Describe("CLI", func() { Expect(plugins).To(Equal([]string{"go/v1"})) }) - It("should success using more than one plugin keys", func() { + It("should succeed using more than one plugin keys", func() { setProjectVersionFlag("2") setPluginsFlag("go/v1,example/v2,test/v1") projectVersion, plugins, err = c.getInfoFromFlags() @@ -155,7 +157,7 @@ var _ = Describe("CLI", func() { Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) }) - It("should success using more than one plugin keys with spaces", func() { + 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() @@ -166,7 +168,7 @@ var _ = Describe("CLI", func() { }) When("additional flags are set", func() { - It("should not fail", func() { + It("should succeed", func() { setFlag("extra-flag", "extra-value") _, _, err = c.getInfoFromFlags() Expect(err).NotTo(HaveOccurred()) @@ -183,66 +185,36 @@ var _ = Describe("CLI", func() { Context("getInfoFromConfig", func() { var ( - projectConfig *config.Config - projectVersion string + projectConfig config.Config + projectVersion config.Version plugins []string err error ) - When("having version field", func() { - It("should success", func() { - projectConfig = &config.Config{ - Version: "2", - } + When("not having layout field", func() { + It("should succeed", func() { + projectConfig = cfgv2.New() projectVersion, plugins, err = getInfoFromConfig(projectConfig) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectConfig.Version)) + Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) Expect(len(plugins)).To(Equal(0)) }) }) When("having layout field", func() { - It("should success", func() { - projectConfig = &config.Config{ - Layout: "go.kubebuilder.io/v2", - } - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{projectConfig.Layout})) - }) - }) - - When("having both version and layout fields", func() { - It("should success", func() { - projectConfig = &config.Config{ - Version: "3-alpha", - Layout: "go.kubebuilder.io/v2", - } - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectConfig.Version)) - Expect(plugins).To(Equal([]string{projectConfig.Layout})) - }) - }) - - When("not having neither version nor layout fields set", func() { - It("should success", func() { - projectConfig = &config.Config{} + It("should succeed", func() { + projectConfig = cfgv3alpha.New() + Expect(projectConfig.SetLayout("go.kubebuilder.io/v2")).To(Succeed()) projectVersion, plugins, err = getInfoFromConfig(projectConfig) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(len(plugins)).To(Equal(0)) + Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) + Expect(plugins).To(Equal([]string{projectConfig.GetLayout()})) }) }) }) Context("cli.resolveFlagsAndConfigFileConflicts", func() { const ( - projectVersion1 = "1" - projectVersion2 = "2" - projectVersion3 = "3" - pluginKey1 = "go.kubebuilder.io/v1" pluginKey2 = "go.kubebuilder.io/v2" pluginKey3 = "go.kubebuilder.io/v3" @@ -250,58 +222,62 @@ var _ = Describe("CLI", func() { var ( c *cli - projectVersion string + projectVersion config.Version plugins []string err error + + projectVersion1 = config.Version{Number: 1} + projectVersion2 = config.Version{Number: 2} + projectVersion3 = config.Version{Number: 3} ) When("having no project version set", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) + Expect(projectVersion.Compare(config.Version{})).To(Equal(0)) }) }) When("having one project version source", func() { When("having default project version set", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ defaultProjectVersion: projectVersion1, } projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion1)) + Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) }) }) When("having project version set from flags", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1, - "", + projectVersion1.String(), + config.Version{}, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion1)) + Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) }) }) When("having project version set from config file", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( "", @@ -310,30 +286,30 @@ var _ = Describe("CLI", func() { nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion1)) + Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) }) }) }) When("having two project version source", func() { When("having default project version set and from flags", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ defaultProjectVersion: projectVersion1, } projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2, - "", + projectVersion2.String(), + config.Version{}, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion2)) + Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) }) }) When("having default project version set and from config file", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ defaultProjectVersion: projectVersion1, } @@ -344,27 +320,27 @@ var _ = Describe("CLI", func() { nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion2)) + Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) }) }) When("having project version set from flags and config file", func() { - It("should success if they are the same", func() { + It("should succeed if they are the same", func() { c = &cli{} projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1, + projectVersion1.String(), projectVersion1, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion1)) + Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) }) It("should fail if they are different", func() { c = &cli{} _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1, + projectVersion1.String(), projectVersion2, nil, nil, @@ -375,18 +351,18 @@ var _ = Describe("CLI", func() { }) When("having three project version sources", func() { - It("should success if project version from flags and config file are the same", 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, + projectVersion2.String(), projectVersion2, nil, nil, ) Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal(projectVersion2)) + Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) }) It("should fail if project version from flags and config file are different", func() { @@ -394,7 +370,7 @@ var _ = Describe("CLI", func() { defaultProjectVersion: projectVersion1, } _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2, + projectVersion2.String(), projectVersion3, nil, nil, @@ -405,12 +381,10 @@ var _ = Describe("CLI", func() { When("an invalid project version is set", func() { It("should fail", func() { - c = &cli{ - defaultProjectVersion: "v1", - } + c = &cli{} projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - "", + "0", + config.Version{}, nil, nil, ) @@ -419,11 +393,11 @@ var _ = Describe("CLI", func() { }) When("having no plugin keys set", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, nil, ) @@ -434,17 +408,17 @@ var _ = Describe("CLI", func() { When("having one plugin keys source", func() { When("having default plugin keys set", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ defaultProjectVersion: projectVersion1, - defaultPlugins: map[string][]string{ + defaultPlugins: map[config.Version][]string{ projectVersion1: {pluginKey1}, projectVersion2: {pluginKey2}, }, } _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, nil, ) @@ -455,11 +429,11 @@ var _ = Describe("CLI", func() { }) When("having plugin keys set from flags", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey1}, nil, ) @@ -470,11 +444,11 @@ var _ = Describe("CLI", func() { }) When("having plugin keys set from config file", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{} _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, []string{pluginKey1}, ) @@ -487,15 +461,15 @@ var _ = Describe("CLI", func() { When("having two plugin keys source", func() { When("having default plugin keys set and from flags", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ - defaultPlugins: map[string][]string{ - "": {pluginKey1}, + defaultPlugins: map[config.Version][]string{ + {}: {pluginKey1}, }, } _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey2}, nil, ) @@ -506,15 +480,15 @@ var _ = Describe("CLI", func() { }) When("having default plugin keys set and from config file", func() { - It("should success", func() { + It("should succeed", func() { c = &cli{ - defaultPlugins: map[string][]string{ - "": {pluginKey1}, + defaultPlugins: map[config.Version][]string{ + {}: {pluginKey1}, }, } _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, nil, []string{pluginKey2}, ) @@ -525,11 +499,11 @@ var _ = Describe("CLI", func() { }) When("having plugin keys set from flags and config file", func() { - It("should success if they are the same", func() { + It("should succeed if they are the same", func() { c = &cli{} _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey1}, []string{pluginKey1}, ) @@ -542,7 +516,7 @@ var _ = Describe("CLI", func() { c = &cli{} _, _, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey1}, []string{pluginKey2}, ) @@ -552,15 +526,15 @@ var _ = Describe("CLI", func() { }) When("having three plugin keys sources", func() { - It("should success if plugin keys from flags and config file are the same", func() { + It("should succeed if plugin keys from flags and config file are the same", func() { c = &cli{ - defaultPlugins: map[string][]string{ - "": {pluginKey1}, + defaultPlugins: map[config.Version][]string{ + {}: {pluginKey1}, }, } _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey2}, []string{pluginKey2}, ) @@ -571,13 +545,13 @@ var _ = Describe("CLI", func() { It("should fail if plugin keys from flags and config file are different", func() { c = &cli{ - defaultPlugins: map[string][]string{ - "": {pluginKey1}, + defaultPlugins: map[config.Version][]string{ + {}: {pluginKey1}, }, } _, _, err = c.resolveFlagsAndConfigFileConflicts( "", - "", + config.Version{}, []string{pluginKey2}, []string{pluginKey3}, ) @@ -587,16 +561,11 @@ var _ = Describe("CLI", func() { When("an invalid plugin key is set", func() { It("should fail", func() { - c = &cli{ - defaultProjectVersion: projectVersion1, - defaultPlugins: map[string][]string{ - projectVersion1: {"invalid_plugin/v1"}, - }, - } + c = &cli{} _, plugins, err = c.resolveFlagsAndConfigFileConflicts( "", - "", - nil, + config.Version{}, + []string{"A"}, nil, ) Expect(err).To(HaveOccurred()) @@ -609,26 +578,27 @@ var _ = Describe("CLI", func() { // conflicts are solved appropriately. Context("cli.getInfo", func() { It("should set project version and plugin keys", func() { - projectVersion := "2" + projectVersion := config.Version{Number: 2} pluginKeys := []string{"go.kubebuilder.io/v2"} c := &cli{ defaultProjectVersion: projectVersion, - defaultPlugins: map[string][]string{ + defaultPlugins: map[config.Version][]string{ projectVersion: pluginKeys, }, } c.cmd = c.newRootCmd() Expect(c.getInfo()).To(Succeed()) - Expect(c.projectVersion).To(Equal(projectVersion)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) Expect(c.pluginKeys).To(Equal(pluginKeys)) }) }) Context("cli.resolve", func() { - const projectVersion = "2" var ( c *cli + projectVersion = config.Version{Number: 2} + pluginKeys = []string{ "foo.example.com/v1", "bar.example.com/v1", @@ -719,7 +689,7 @@ var _ = Describe("CLI", func() { When("providing an invalid option", func() { It("should return an error", func() { // An empty project version is not valid - _, err = New(WithDefaultProjectVersion("")) + _, err = New(WithDefaultProjectVersion(config.Version{})) Expect(err).To(HaveOccurred()) }) }) @@ -756,12 +726,14 @@ var _ = Describe("CLI", func() { }) When("providing deprecated plugins", func() { - It("should success and print the deprecation notice", func() { + It("should succeed and print the deprecation notice", func() { const ( - projectVersion = "2" deprecationWarning = "DEPRECATED" ) - var deprecatedPlugin = newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) + var ( + projectVersion = config.Version{Number: 2} + deprecatedPlugin = newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) + ) // Overwrite stdout to read the output and reset it afterwards r, w, _ := os.Pipe() diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index ae6df00d1c2..a5908a08364 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -83,7 +83,7 @@ func (c cli) bindEdit(ctx plugin.Context, cmd *cobra.Command) { } subcommand := editPlugin.GetEditSubcommand() - subcommand.InjectConfig(&cfg.Config) + subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 35e0f436469..92997491bc2 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -27,7 +27,8 @@ import ( "github.com/spf13/cobra" internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -43,10 +44,10 @@ func (c cli) newInitCmd() *cobra.Command { // 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, + 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 != config.Version2 { + 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(), ", "))) @@ -82,7 +83,7 @@ func (c cli) getInitHelpExamples() string { } func (c cli) getAvailableProjectVersions() (projectVersions []string) { - versionSet := make(map[string]struct{}) + versionSet := make(map[config.Version]struct{}) for _, p := range c.plugins { // Only return versions of non-deprecated plugins. if _, isDeprecated := p.(plugin.Deprecated); !isDeprecated { @@ -92,7 +93,7 @@ func (c cli) getAvailableProjectVersions() (projectVersions []string) { } } for version := range versionSet { - projectVersions = append(projectVersions, strconv.Quote(version)) + projectVersions = append(projectVersions, strconv.Quote(version.String())) } sort.Strings(projectVersions) return projectVersions @@ -135,11 +136,14 @@ func (c cli) bindInit(ctx plugin.Context, cmd *cobra.Command) { return } - cfg := internalconfig.New(internalconfig.DefaultPath) - cfg.Version = c.projectVersion + 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.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description diff --git a/pkg/cli/internal/config/config.go b/pkg/cli/internal/config/config.go index 5e556138167..0bcbd4ba014 100644 --- a/pkg/cli/internal/config/config.go +++ b/pkg/cli/internal/config/config.go @@ -22,16 +22,14 @@ import ( "os" "github.com/spf13/afero" + "sigs.k8s.io/yaml" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" ) const ( // DefaultPath is the default path for the configuration file DefaultPath = "PROJECT" - - // DefaultVersion is the version which will be used when the version flag is not provided - DefaultVersion = config.Version3Alpha ) func exists(fs afero.Fs, path string) (bool, error) { @@ -50,35 +48,46 @@ func exists(fs afero.Fs, path string) (bool, error) { return false, err } -func readFrom(fs afero.Fs, path string) (c config.Config, err error) { +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 + return nil, err } - // Unmarshal the file content - if err = c.Unmarshal(in); err != nil { - return + // 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 } - // kubebuilder v1 omitted version and it is not supported, so return an error - if c.Version == "" { - return config.Config{}, fmt.Errorf("project version key `version` is empty or does not exist in %s", path) + // Unmarshal the file content + if err := c.Unmarshal(in); err != nil { + return nil, err } - return + return c, nil } // Read obtains the configuration from the default path but doesn't allow to persist changes -func Read() (*config.Config, error) { +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) { - c, err := readFrom(afero.NewOsFs(), path) - return &c, err +func ReadFrom(path string) (config.Config, error) { + return readFrom(afero.NewOsFs(), path) } // Config extends model/config.Config allowing to persist changes @@ -96,15 +105,18 @@ type Config struct { } // New creates a new configuration that will be stored at the provided path -func New(path string) *Config { +func New(version config.Version, path string) (*Config, error) { + cfg, err := config.New(version) + if err != nil { + return nil, err + } + return &Config{ - Config: config.Config{ - Version: DefaultVersion, - }, + Config: cfg, path: path, mustNotExist: true, fs: afero.NewOsFs(), - } + }, nil } // Load obtains the configuration from the default path allowing to persist changes (Save method) diff --git a/pkg/cli/internal/config/config_test.go b/pkg/cli/internal/config/config_test.go index cdfa9caa51d..fe7d7ee4932 100644 --- a/pkg/cli/internal/config/config_test.go +++ b/pkg/cli/internal/config/config_test.go @@ -23,179 +23,49 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/afero" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" ) var _ = Describe("Config", func() { - var ( - cfg Config - expectedConfigStr string - ) - - Context("with valid keys", func() { - It("should save correctly", func() { - - By("saving empty config") - Expect(cfg.Save()).NotTo(Succeed()) - - By("saving empty config with path") - cfg = Config{ - fs: afero.NewMemMapFs(), - path: DefaultPath, + 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(expectedConfigStr)) - By("saving config version 2") - cfg = Config{ - Config: config.Config{ - Version: config.Version2, - Repo: "github.com/example/project", - Domain: "example.com", - }, - fs: afero.NewMemMapFs(), - path: DefaultPath, - } - expectedConfigStr = `domain: example.com -repo: github.com/example/project -version: "2" -` - Expect(cfg.Save()).To(Succeed()) - cfgBytes, err = afero.ReadFile(cfg.fs, DefaultPath) + cfgBytes, err := afero.ReadFile(cfg.fs, DefaultPath) Expect(err).NotTo(HaveOccurred()) - Expect(string(cfgBytes)).To(Equal(expectedConfigStr)) + Expect(string(cfgBytes)).To(Equal(`version: "2" +`)) + }) - By("saving config version 3-alpha with plugin config") - cfg = Config{ - Config: config.Config{ - Version: config.Version3Alpha, - Repo: "github.com/example/project", - Domain: "example.com", - Plugins: config.PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "single plugin datum", - }, - "plugin-y/v1": map[string]interface{}{ - "data-1": "plugin value 1", - "data-2": "plugin value 2", - "data-3": []string{"plugin value 3", "plugin value 4"}, - }, - }, - }, - fs: afero.NewMemMapFs(), - path: DefaultPath, + It("should fail if path is not provided", func() { + cfg := Config{ + Config: cfgv2.New(), + fs: afero.NewMemMapFs(), } - expectedConfigStr = `domain: example.com -repo: github.com/example/project -version: 3-alpha -plugins: - plugin-x: - data-1: single plugin datum - plugin-y/v1: - data-1: plugin value 1 - data-2: plugin value 2 - data-3: - - plugin value 3 - - plugin value 4 -` - Expect(cfg.Save()).To(Succeed()) - cfgBytes, err = afero.ReadFile(cfg.fs, DefaultPath) - Expect(err).NotTo(HaveOccurred()) - Expect(string(cfgBytes)).To(Equal(expectedConfigStr)) + Expect(cfg.Save()).NotTo(Succeed()) }) + }) - It("should load correctly", func() { - var ( - fs afero.Fs - configStr string - expectedConfig config.Config - ) - - By("loading config version 2") - fs = afero.NewMemMapFs() - configStr = `domain: example.com + Context("readFrom", func() { + It("should success for valid configs", func() { + configStr := `domain: example.com repo: github.com/example/project version: "2"` - expectedConfig = config.Config{ - Version: config.Version2, - Repo: "github.com/example/project", - Domain: "example.com", - } - 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)) + expectedConfig := cfgv2.New() + _ = expectedConfig.SetDomain("example.com") + _ = expectedConfig.SetRepository("github.com/example/project") - By("loading config version 3-alpha with plugin config") - fs = afero.NewMemMapFs() - configStr = `domain: example.com -repo: github.com/example/project -version: 3-alpha -plugins: - plugin-x: - data-1: single plugin datum - plugin-y/v1: - data-1: plugin value 1 - data-2: plugin value 2 - data-3: - - "plugin value 3" - - "plugin value 4"` - expectedConfig = config.Config{ - Version: config.Version3Alpha, - Repo: "github.com/example/project", - Domain: "example.com", - Plugins: config.PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "single plugin datum", - }, - "plugin-y/v1": map[string]interface{}{ - "data-1": "plugin value 1", - "data-2": "plugin value 2", - "data-3": []interface{}{"plugin value 3", "plugin value 4"}, - }, - }, - } + fs := afero.NewMemMapFs() Expect(afero.WriteFile(fs, DefaultPath, []byte(configStr), os.ModePerm)).To(Succeed()) - cfg, err = readFrom(fs, DefaultPath) + + cfg, err := readFrom(fs, DefaultPath) Expect(err).NotTo(HaveOccurred()) Expect(cfg).To(Equal(expectedConfig)) }) }) - - Context("with invalid keys", func() { - It("should return a save error", func() { - By("saving config version 2 with plugin config") - cfg = Config{ - Config: config.Config{ - Version: config.Version2, - Repo: "github.com/example/project", - Domain: "example.com", - Plugins: config.PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "single plugin datum", - }, - }, - }, - fs: afero.NewMemMapFs(), - path: DefaultPath, - } - Expect(cfg.Save()).NotTo(Succeed()) - }) - - It("should return a load error", func() { - By("loading config version 2 with plugin config") - fs := afero.NewMemMapFs() - configStr := `domain: example.com -repo: github.com/example/project -version: "2" -plugins: - plugin-x: - data-1: single plugin datum` - Expect(afero.WriteFile(fs, DefaultPath, []byte(configStr), os.ModePerm)).To(Succeed()) - _, err := readFrom(fs, DefaultPath) - Expect(err).To(HaveOccurred()) - }) - }) }) diff --git a/pkg/cli/options.go b/pkg/cli/options.go index 6187579f100..df89dd5c649 100644 --- a/pkg/cli/options.go +++ b/pkg/cli/options.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -46,9 +46,9 @@ func WithVersion(version string) Option { // WithDefaultProjectVersion is an Option that sets the cli's default project version. // Setting an unknown version will result in an error. -func WithDefaultProjectVersion(version string) Option { +func WithDefaultProjectVersion(version config.Version) Option { return func(c *cli) error { - if err := validation.ValidateProjectVersion(version); err != nil { + if err := version.Validate(); err != nil { return fmt.Errorf("broken pre-set default project version %q: %v", version, err) } c.defaultProjectVersion = version @@ -57,9 +57,9 @@ func WithDefaultProjectVersion(version string) Option { } // WithDefaultPlugins is an Option that sets the cli's default plugins. -func WithDefaultPlugins(projectVersion string, plugins ...plugin.Plugin) Option { +func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { return func(c *cli) error { - if err := validation.ValidateProjectVersion(projectVersion); err != nil { + if err := projectVersion.Validate(); err != nil { return fmt.Errorf("broken pre-set project version %q for default plugins: %v", projectVersion, err) } if len(plugins) == 0 { diff --git a/pkg/cli/options_test.go b/pkg/cli/options_test.go index d24f8ab8118..88ebe1b4b2c 100644 --- a/pkg/cli/options_test.go +++ b/pkg/cli/options_test.go @@ -23,26 +23,29 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/cobra" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) var _ = Describe("CLI options", func() { const ( - pluginName = "plugin" - pluginVersion = "v1" - projectVersion = "1" + pluginName = "plugin" + pluginVersion = "v1" ) var ( c *cli err error + projectVersion = config.Version{Number: 1} + p = newMockPlugin(pluginName, pluginVersion, projectVersion) np1 = newMockPlugin("Plugin", pluginVersion, projectVersion) - np2 = mockPlugin{pluginName, plugin.Version{Number: -1, Stage: plugin.StableStage}, []string{projectVersion}} + np2 = mockPlugin{pluginName, plugin.Version{Number: -1}, []config.Version{projectVersion}} np3 = newMockPlugin(pluginName, pluginVersion) - np4 = newMockPlugin(pluginName, pluginVersion, "a") + np4 = newMockPlugin(pluginName, pluginVersion, config.Version{}) ) Context("WithCommandName", func() { @@ -67,10 +70,10 @@ var _ = Describe("CLI options", func() { Context("WithDefaultProjectVersion", func() { It("should return a valid CLI", func() { - defaultProjectVersions := []string{ - "1", - "2", - "3-alpha", + defaultProjectVersions := []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3, Stage: stage.Alpha}, } for _, defaultProjectVersion := range defaultProjectVersions { By(fmt.Sprintf("using %q", defaultProjectVersion)) @@ -82,12 +85,9 @@ var _ = Describe("CLI options", func() { }) It("should return an error", func() { - defaultProjectVersions := []string{ - "", // Empty default project version - "v1", // 'v' prefix for project version - "1alpha", // non-delimited non-stable suffix - "1.alpha", // non-stable version delimited by '.' - "1-alpha1", // number-suffixed non-stable version + 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)) @@ -102,12 +102,12 @@ var _ = Describe("CLI options", func() { c, err = newCLI(WithDefaultPlugins(projectVersion, p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) - Expect(c.defaultPlugins).To(Equal(map[string][]string{projectVersion: {plugin.KeyFor(p)}})) + Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) }) When("providing an invalid project version", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins("", p)) + _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) Expect(err).To(HaveOccurred()) }) }) @@ -149,7 +149,7 @@ var _ = Describe("CLI options", func() { When("providing a default plugin for an unsupported project version", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins("2", p)) + _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 6a30cc43646..1203700a2aa 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -67,12 +67,10 @@ configuration please run: `, c.commandName, c.getPluginTable()) - if c.defaultProjectVersion != "" { - str += fmt.Sprintf("\nDefault project version: %s\n", c.defaultProjectVersion) + 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 defaultPlugins, hasDefaultPlugins := c.defaultPlugins[c.defaultProjectVersion]; hasDefaultPlugins { + str += fmt.Sprintf("Default plugin keys: %q\n", strings.Join(defaultPlugins, ",")) } str += fmt.Sprintf(` @@ -98,11 +96,16 @@ func (c cli) getPluginTable() string { maxPluginKeyLength = len(pluginKey) } pluginKeys = append(pluginKeys, pluginKey) - supportedProjectVersions := strings.Join(plugin.SupportedProjectVersions(), ", ") - if len(supportedProjectVersions) > maxProjectVersionLength { - maxProjectVersionLength = len(supportedProjectVersions) + supportedProjectVersions := plugin.SupportedProjectVersions() + supportedProjectVersionStrs := make([]string, 0, len(supportedProjectVersions)) + for _, version := range supportedProjectVersions { + supportedProjectVersionStrs = append(supportedProjectVersionStrs, version.String()) + } + supportedProjectVersionsStr := strings.Join(supportedProjectVersionStrs, ", ") + if len(supportedProjectVersionsStr) > maxProjectVersionLength { + maxProjectVersionLength = len(supportedProjectVersionsStr) } - projectVersions = append(projectVersions, supportedProjectVersions) + projectVersions = append(projectVersions, supportedProjectVersionsStr) } lines := make([]string, 0, len(c.plugins)+2) diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index 9a61a6c4579..fb2e4511a66 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -83,7 +83,7 @@ func (c cli) bindCreateWebhook(ctx plugin.Context, cmd *cobra.Command) { } subcommand := createWebhookPlugin.GetCreateWebhookSubcommand() - subcommand.InjectConfig(&cfg.Config) + subcommand.InjectConfig(cfg.Config) subcommand.BindFlags(cmd.Flags()) subcommand.UpdateContext(&ctx) cmd.Long = ctx.Description diff --git a/pkg/config/errors.go b/pkg/config/errors.go new file mode 100644 index 00000000000..fed0b421882 --- /dev/null +++ b/pkg/config/errors.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 config + +import ( + "fmt" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// UnsupportedVersionError is returned by New when a project configuration version is not supported. +type UnsupportedVersionError struct { + Version Version +} + +// Error implements error interface +func (e UnsupportedVersionError) Error() string { + return fmt.Sprintf("version %s is not supported", e.Version) +} + +// UnsupportedField is returned when a project configuration version does not support +// one of the fields as interface must be common for all the versions +type UnsupportedField struct { + Version Version + Field string +} + +// Error implements error interface +func (e UnsupportedField) Error() string { + return fmt.Sprintf("version %s does not support the %s field", e.Version, e.Field) +} + +// UnknownResource is returned by Config.GetResource when the provided GVK cannot be found +type UnknownResource struct { + GVK resource.GVK +} + +// Error implements error interface +func (e UnknownResource) Error() string { + return fmt.Sprintf("resource %v could not be found", e.GVK) +} + +// MarshalError is returned by Config.Marshal when something went wrong while marshalling to YAML +type MarshalError struct { + Err error +} + +// Error implements error interface +func (e MarshalError) Error() string { + return fmt.Sprintf("error marshalling project configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e MarshalError) Unwrap() error { + return e.Err +} + +// UnmarshalError is returned by Config.Unmarshal when something went wrong while unmarshalling from YAML +type UnmarshalError struct { + Err error +} + +// Error implements error interface +func (e UnmarshalError) Error() string { + return fmt.Sprintf("error unmarshalling project configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e UnmarshalError) Unwrap() error { + return e.Err +} diff --git a/pkg/config/errors_test.go b/pkg/config/errors_test.go new file mode 100644 index 00000000000..b998ea949e4 --- /dev/null +++ b/pkg/config/errors_test.go @@ -0,0 +1,106 @@ +/* +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 config + +import ( + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("UnsupportedVersionError", func() { + var err = UnsupportedVersionError{ + Version: Version{Number: 1}, + } + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal("version 1 is not supported")) + }) + }) +}) + +var _ = Describe("UnsupportedField", func() { + var err = UnsupportedField{ + Version: Version{Number: 1}, + Field: "name", + } + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal("version 1 does not support the name field")) + }) + }) +}) + +var _ = Describe("UnknownResource", func() { + var err = UnknownResource{ + GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }, + } + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal("resource {group my.domain v1 Kind} could not be found")) + }) + }) +}) + +var _ = Describe("MarshalError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = MarshalError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("error marshalling project configuration: %v", wrapped))) + }) + }) + + Context("Unwrap", func() { + It("should unwrap to the wrapped error", func() { + Expect(err.Unwrap()).To(Equal(wrapped)) + }) + }) +}) + +var _ = Describe("UnmarshalError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = UnmarshalError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("error unmarshalling project 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/interface.go b/pkg/config/interface.go new file mode 100644 index 00000000000..752e7e40853 --- /dev/null +++ b/pkg/config/interface.go @@ -0,0 +1,114 @@ +/* +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 config + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// Config defines the interface that project configuration types must follow +type Config interface { + /* Version */ + + // GetVersion returns the current project version + GetVersion() Version + + /* String fields */ + + // GetDomain returns the project domain + GetDomain() string + // SetDomain sets the project domain + SetDomain(domain string) error + + // GetRepository returns the project repository. + GetRepository() string + // SetRepository sets the project repository + SetRepository(repository string) error + + // GetProjectName returns the project name + // This method was introduced in project version 3-alpha. + GetProjectName() string + // SetName sets the project name + // This method was introduced in project version 3-alpha. + SetName(name string) error + + // GetLayout returns the config layout + // This method was introduced in project version 3-alpha. + GetLayout() string + // SetLayout sets the Config layout + // This method was introduced in project version 3-alpha. + SetLayout(layout string) error + + /* Boolean fields */ + + // IsMultiGroup checks if multi-group is enabled + IsMultiGroup() bool + // SetMultiGroup enables multi-group + SetMultiGroup() error + // ClearMultiGroup disables multi-group + ClearMultiGroup() error + + // IsComponentConfig checks if component config is enabled + // This method was introduced in project version 3-alpha. + IsComponentConfig() bool + // SetComponentConfig enables component config + // This method was introduced in project version 3-alpha. + SetComponentConfig() error + // ClearComponentConfig disables component config + // This method was introduced in project version 3-alpha. + ClearComponentConfig() error + + /* Resources */ + + // ResourcesLength returns the number of tracked resources + ResourcesLength() int + // HasResource checks if the provided GVK is stored in the Config + HasResource(gvk resource.GVK) bool + // GetResource returns the stored resource matching the provided GVK + GetResource(gvk resource.GVK) (resource.Resource, error) + // GetResources returns all the stored resources + GetResources() ([]resource.Resource, error) + // AddResource adds the provided resource if it was not present, no-op if it was already present + AddResource(res resource.Resource) error + // UpdateResource adds the provided resource if it was not present, modifies it if it was already present + UpdateResource(res resource.Resource) error + + // HasGroup checks if the provided group is the same as any of the tracked resources + HasGroup(group string) bool + // IsCRDVersionCompatible returns true if crdVersion can be added to the existing set of CRD versions. + IsCRDVersionCompatible(crdVersion string) bool + // IsWebhookVersionCompatible returns true if webhookVersion can be added to the existing set of Webhook versions. + IsWebhookVersionCompatible(webhookVersion string) bool + + /* Plugins */ + + // DecodePluginConfig decodes a plugin config stored in Config into configObj, which must be a pointer. + // This method is intended to be used for custom configuration objects, which were introduced in project version + // 3-alpha. + DecodePluginConfig(key string, configObj interface{}) error + // EncodePluginConfig encodes a config object into Config by overwriting the existing object stored under key. + // This method is intended to be used for custom configuration objects, which were introduced in project version + // 3-alpha. + EncodePluginConfig(key string, configObj interface{}) error + + /* Persistence */ + + // Marshal returns the YAML representation of the Config + Marshal() ([]byte, error) + // Unmarshal loads the Config fields from its YAML representation + Unmarshal([]byte) error +} diff --git a/pkg/config/registry.go b/pkg/config/registry.go new file mode 100644 index 00000000000..b0c03f754fe --- /dev/null +++ b/pkg/config/registry.go @@ -0,0 +1,37 @@ +/* +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 config + +type constructorFunc func() Config + +var ( + registry = make(map[Version]constructorFunc) +) + +// Register allows implementations of Config to register themselves so that they can be created with New +func Register(version Version, constructor constructorFunc) { + registry[version] = constructor +} + +// New creates Config instances from the previously registered implementations through Register +func New(version Version) (Config, error) { + if constructor, exists := registry[version]; exists { + return constructor(), nil + } + + return nil, UnsupportedVersionError{Version: version} +} diff --git a/pkg/config/resgistry_test.go b/pkg/config/resgistry_test.go new file mode 100644 index 00000000000..c10fd839526 --- /dev/null +++ b/pkg/config/resgistry_test.go @@ -0,0 +1,56 @@ +/* +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 config + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("registry", func() { + var ( + version = Version{} + f = func() Config { return nil } + ) + + AfterEach(func() { + registry = make(map[Version]constructorFunc) + }) + + Context("Register", func() { + It("should register new constructors", func() { + Register(version, f) + Expect(registry).To(HaveKey(version)) + Expect(registry[version]()).To(BeNil()) + }) + }) + + Context("New", func() { + It("should use the registered constructors", func() { + registry[version] = f + result, err := New(version) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(BeNil()) + }) + + It("should fail for unregistered constructors", func() { + _, err := New(version) + Expect(err).To(HaveOccurred()) + }) + }) + +}) diff --git a/pkg/model/config/config_suite_test.go b/pkg/config/suite_test.go similarity index 90% rename from pkg/model/config/config_suite_test.go rename to pkg/config/suite_test.go index fffe2f8bb2f..c2a73ef1566 100644 --- a/pkg/model/config/config_suite_test.go +++ b/pkg/config/suite_test.go @@ -1,5 +1,5 @@ /* -Copyright 2020 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. @@ -23,7 +23,7 @@ import ( . "github.com/onsi/gomega" ) -func TestCLI(t *testing.T) { +func TestConfig(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Config Suite") } diff --git a/pkg/config/v2/config.go b/pkg/config/v2/config.go new file mode 100644 index 00000000000..49c069c17e1 --- /dev/null +++ b/pkg/config/v2/config.go @@ -0,0 +1,268 @@ +/* +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 ( + "strings" + + "sigs.k8s.io/yaml" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// Version is the config.Version for project configuration 2 +var Version = config.Version{Number: 2} + +const apiVersion = "v1beta1" + +type cfg struct { + // Version + Version config.Version `json:"version"` + + // String fields + Domain string `json:"domain,omitempty"` + Repository string `json:"repo,omitempty"` + + // Boolean fields + MultiGroup bool `json:"multigroup,omitempty"` + + // Resources + Gvks []resource.GVK `json:"resources,omitempty"` +} + +// New returns a new config.Config +func New() config.Config { + return &cfg{Version: Version} +} + +func init() { + config.Register(Version, New) +} + +// GetVersion implements config.Config +func (c cfg) GetVersion() config.Version { + return c.Version +} + +// GetDomain implements config.Config +func (c cfg) GetDomain() string { + return c.Domain +} + +// SetDomain implements config.Config +func (c *cfg) SetDomain(domain string) error { + c.Domain = domain + return nil +} + +// GetRepository implements config.Config +func (c cfg) GetRepository() string { + return c.Repository +} + +// SetRepository implements config.Config +func (c *cfg) SetRepository(repository string) error { + c.Repository = repository + return nil +} + +// GetProjectName implements config.Config +func (c cfg) GetProjectName() string { + return "" +} + +// SetName implements config.Config +func (c *cfg) SetName(string) error { + return config.UnsupportedField{ + Version: Version, + Field: "project name", + } +} + +// GetLayout implements config.Config +func (c cfg) GetLayout() string { + return "" +} + +// SetLayout implements config.Config +func (c *cfg) SetLayout(string) error { + return config.UnsupportedField{ + Version: Version, + Field: "layout", + } +} + +// IsMultiGroup implements config.Config +func (c cfg) IsMultiGroup() bool { + return c.MultiGroup +} + +// SetMultiGroup implements config.Config +func (c *cfg) SetMultiGroup() error { + c.MultiGroup = true + return nil +} + +// ClearMultiGroup implements config.Config +func (c *cfg) ClearMultiGroup() error { + c.MultiGroup = false + return nil +} + +// IsComponentConfig implements config.Config +func (c cfg) IsComponentConfig() bool { + return false +} + +// SetComponentConfig implements config.Config +func (c *cfg) SetComponentConfig() error { + return config.UnsupportedField{ + Version: Version, + Field: "component config", + } +} + +// ClearComponentConfig implements config.Config +func (c *cfg) ClearComponentConfig() error { + return config.UnsupportedField{ + Version: Version, + Field: "component config", + } +} + +// ResourcesLength implements config.Config +func (c cfg) ResourcesLength() int { + return len(c.Gvks) +} + +// HasResource implements config.Config +func (c cfg) HasResource(gvk resource.GVK) bool { + gvk.Domain = "" // Version 2 does not include domain per resource + + for _, trackedGVK := range c.Gvks { + if gvk.IsEqualTo(trackedGVK) { + return true + } + } + + return false +} + +// GetResource implements config.Config +func (c cfg) GetResource(gvk resource.GVK) (resource.Resource, error) { + gvk.Domain = "" // Version 2 does not include domain per resource + + for _, trackedGVK := range c.Gvks { + if gvk.IsEqualTo(trackedGVK) { + return resource.Resource{ + GVK: trackedGVK, + }, nil + } + } + + return resource.Resource{}, config.UnknownResource{GVK: gvk} +} + +// GetResources implements config.Config +func (c cfg) GetResources() ([]resource.Resource, error) { + resources := make([]resource.Resource, 0, len(c.Gvks)) + for _, gvk := range c.Gvks { + resources = append(resources, resource.Resource{ + GVK: gvk, + }) + } + + return resources, nil +} + +// AddResource implements config.Config +func (c *cfg) AddResource(res resource.Resource) error { + // As res is passed by value it is already a shallow copy, and we are only using + // fields that do not require a deep copy, so no need to make a deep copy + + res.Domain = "" // Version 2 does not include domain per resource + + if !c.HasResource(res.GVK) { + c.Gvks = append(c.Gvks, res.GVK) + } + + return nil +} + +// UpdateResource implements config.Config +func (c *cfg) UpdateResource(res resource.Resource) error { + return c.AddResource(res) +} + +// HasGroup implements config.Config +func (c cfg) HasGroup(group string) bool { + // Return true if the target group is found in the tracked resources + for _, r := range c.Gvks { + if strings.EqualFold(group, r.Group) { + return true + } + } + + // Return false otherwise + return false +} + +// IsCRDVersionCompatible implements config.Config +func (c cfg) IsCRDVersionCompatible(crdVersion string) bool { + return crdVersion == apiVersion +} + +// IsWebhookVersionCompatible implements config.Config +func (c cfg) IsWebhookVersionCompatible(webhookVersion string) bool { + return webhookVersion == apiVersion +} + +// DecodePluginConfig implements config.Config +func (c cfg) DecodePluginConfig(string, interface{}) error { + return config.UnsupportedField{ + Version: Version, + Field: "plugins", + } +} + +// EncodePluginConfig implements config.Config +func (c cfg) EncodePluginConfig(string, interface{}) error { + return config.UnsupportedField{ + Version: Version, + Field: "plugins", + } +} + +// Marshal implements config.Config +func (c cfg) Marshal() ([]byte, error) { + content, err := yaml.Marshal(c) + if err != nil { + return nil, config.MarshalError{Err: err} + } + + return content, nil +} + +// Unmarshal implements config.Config +func (c *cfg) Unmarshal(b []byte) error { + if err := yaml.UnmarshalStrict(b, c); err != nil { + return config.UnmarshalError{Err: err} + } + + return nil +} diff --git a/pkg/config/v2/config_test.go b/pkg/config/v2/config_test.go new file mode 100644 index 00000000000..8f9479758f5 --- /dev/null +++ b/pkg/config/v2/config_test.go @@ -0,0 +1,348 @@ +/* +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 ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +func TestConfigV2(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config V2 Suite") +} + +var _ = Describe("cfg", func() { + const ( + domain = "my.domain" + repo = "myrepo" + + otherDomain = "other.domain" + otherRepo = "otherrepo" + ) + + var c cfg + + BeforeEach(func() { + c = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + } + }) + + Context("Version", func() { + It("GetVersion should return version 2", func() { + Expect(c.GetVersion().Compare(Version)).To(Equal(0)) + }) + }) + + Context("Domain", func() { + It("GetDomain should return the domain", func() { + Expect(c.GetDomain()).To(Equal(domain)) + }) + + It("SetDomain should set the domain", func() { + Expect(c.SetDomain(otherDomain)).To(Succeed()) + Expect(c.Domain).To(Equal(otherDomain)) + }) + }) + + Context("Repository", func() { + It("GetRepository should return the repository", func() { + Expect(c.GetRepository()).To(Equal(repo)) + }) + + It("SetRepository should set the repository", func() { + Expect(c.SetRepository(otherRepo)).To(Succeed()) + Expect(c.Repository).To(Equal(otherRepo)) + }) + }) + + Context("Name", func() { + It("GetProjectName should return an empty name", func() { + Expect(c.GetProjectName()).To(Equal("")) + }) + + It("SetName should fail to set the name", func() { + Expect(c.SetName("name")).NotTo(Succeed()) + }) + }) + + Context("Layout", func() { + It("GetLayout should return an empty layout", func() { + Expect(c.GetLayout()).To(Equal("")) + }) + + It("SetLayout should fail to set the layout", func() { + Expect(c.SetLayout("layout")).NotTo(Succeed()) + }) + }) + + Context("Multi group", func() { + It("IsMultiGroup should return false if not set", func() { + Expect(c.IsMultiGroup()).To(BeFalse()) + }) + + It("IsMultiGroup should return true if set", func() { + c.MultiGroup = true + Expect(c.IsMultiGroup()).To(BeTrue()) + }) + + It("SetMultiGroup should enable multi-group support", func() { + Expect(c.SetMultiGroup()).To(Succeed()) + Expect(c.MultiGroup).To(BeTrue()) + }) + + It("ClearMultiGroup should disable multi-group support", func() { + c.MultiGroup = true + Expect(c.ClearMultiGroup()).To(Succeed()) + Expect(c.MultiGroup).To(BeFalse()) + }) + }) + + Context("Component config", func() { + It("IsComponentConfig should return false", func() { + Expect(c.IsComponentConfig()).To(BeFalse()) + }) + + It("SetComponentConfig should fail to enable component config support", func() { + Expect(c.SetComponentConfig()).NotTo(Succeed()) + }) + + It("ClearComponentConfig should fail to disable component config support", func() { + Expect(c.ClearComponentConfig()).NotTo(Succeed()) + }) + }) + + Context("Resources", func() { + var res = resource.Resource{ + GVK: resource.GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + }, + } + + DescribeTable("ResourcesLength should return the number of resources", + func(n int) { + for i := 0; i < n; i++ { + c.Gvks = append(c.Gvks, res.GVK) + } + Expect(c.ResourcesLength()).To(Equal(n)) + }, + Entry("for no resources", 0), + Entry("for one resource", 1), + Entry("for several resources", 3), + ) + + It("HasResource should return false for a non-existent resource", func() { + Expect(c.HasResource(res.GVK)).To(BeFalse()) + }) + + It("HasResource should return true for an existent resource", func() { + c.Gvks = append(c.Gvks, res.GVK) + Expect(c.HasResource(res.GVK)).To(BeTrue()) + }) + + It("GetResource should fail for a non-existent resource", func() { + _, err := c.GetResource(res.GVK) + Expect(err).To(HaveOccurred()) + }) + + It("GetResource should return an existent resource", func() { + c.Gvks = append(c.Gvks, res.GVK) + r, err := c.GetResource(res.GVK) + Expect(err).NotTo(HaveOccurred()) + Expect(r.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + }) + + It("GetResources should return a slice of the tracked resources", func() { + c.Gvks = append(c.Gvks, res.GVK, res.GVK, res.GVK) + resources, err := c.GetResources() + Expect(err).NotTo(HaveOccurred()) + Expect(resources).To(Equal([]resource.Resource{res, res, res})) + }) + + It("AddResource should add the provided resource if non-existent", func() { + l := len(c.Gvks) + Expect(c.AddResource(res)).To(Succeed()) + Expect(len(c.Gvks)).To(Equal(l + 1)) + Expect(c.Gvks[0].IsEqualTo(res.GVK)).To(BeTrue()) + }) + + It("AddResource should do nothing if the resource already exists", func() { + c.Gvks = append(c.Gvks, res.GVK) + l := len(c.Gvks) + Expect(c.AddResource(res)).To(Succeed()) + Expect(len(c.Gvks)).To(Equal(l)) + }) + + It("UpdateResource should add the provided resource if non-existent", func() { + l := len(c.Gvks) + Expect(c.UpdateResource(res)).To(Succeed()) + Expect(len(c.Gvks)).To(Equal(l + 1)) + Expect(c.Gvks[0].IsEqualTo(res.GVK)).To(BeTrue()) + }) + + It("UpdateResource should do nothing if the resource already exists", func() { + c.Gvks = append(c.Gvks, res.GVK) + l := len(c.Gvks) + Expect(c.UpdateResource(res)).To(Succeed()) + Expect(len(c.Gvks)).To(Equal(l)) + }) + + It("HasGroup should return false with no tracked resources", func() { + Expect(c.HasGroup(res.Group)).To(BeFalse()) + }) + + It("HasGroup should return true with tracked resources in the same group", func() { + c.Gvks = append(c.Gvks, res.GVK) + Expect(c.HasGroup(res.Group)).To(BeTrue()) + }) + + It("HasGroup should return false with tracked resources in other group", func() { + c.Gvks = append(c.Gvks, res.GVK) + Expect(c.HasGroup("other-group")).To(BeFalse()) + }) + + It("IsCRDVersionCompatible should return true for `v1beta1`", func() { + Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) + }) + + It("IsCRDVersionCompatible should return false for any other than `v1beta1`", func() { + Expect(c.IsCRDVersionCompatible("v1")).To(BeFalse()) + Expect(c.IsCRDVersionCompatible("v2")).To(BeFalse()) + }) + + It("IsWebhookVersionCompatible should return true for `v1beta1`", func() { + Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) + }) + + It("IsWebhookVersionCompatible should return false for any other than `v1beta1`", func() { + Expect(c.IsWebhookVersionCompatible("v1")).To(BeFalse()) + Expect(c.IsWebhookVersionCompatible("v2")).To(BeFalse()) + }) + }) + + Context("Plugins", func() { + It("DecodePluginConfig should fail", func() { + Expect(c.DecodePluginConfig("", nil)).NotTo(Succeed()) + }) + + It("EncodePluginConfig should fail", func() { + Expect(c.EncodePluginConfig("", nil)).NotTo(Succeed()) + }) + }) + + Context("Persistence", func() { + var ( + // BeforeEach is called after the entries are evaluated, and therefore, c is not available + c1 = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + } + c2 = cfg{ + Version: Version, + Domain: otherDomain, + Repository: otherRepo, + MultiGroup: true, + Gvks: []resource.GVK{ + {Group: "group", Version: "v1", Kind: "Kind"}, + {Group: "group", Version: "v1", Kind: "Kind2"}, + {Group: "group", Version: "v1-beta", Kind: "Kind"}, + {Group: "group2", Version: "v1", Kind: "Kind"}, + }, + } + s1 = `domain: my.domain +repo: myrepo +version: "2" +` + s2 = `domain: other.domain +multigroup: true +repo: otherrepo +resources: +- group: group + kind: Kind + version: v1 +- group: group + kind: Kind2 + version: v1 +- group: group + kind: Kind + version: v1-beta +- group: group2 + kind: Kind + version: v1 +version: "2" +` + ) + + DescribeTable("Marshal should succeed", + func(c cfg, content string) { + b, err := c.Marshal() + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(content)) + }, + Entry("for a basic configuration", c1, s1), + Entry("for a full configuration", c2, s2), + ) + + DescribeTable("Marshal should fail", + func(c cfg) { + _, err := c.Marshal() + Expect(err).To(HaveOccurred()) + }, + // TODO (coverage): add cases where yaml.Marshal returns an error + ) + + DescribeTable("Unmarshal should succeed", + func(content string, c cfg) { + var unmarshalled cfg + Expect(unmarshalled.Unmarshal([]byte(content))).To(Succeed()) + Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0)) + Expect(unmarshalled.Domain).To(Equal(c.Domain)) + Expect(unmarshalled.Repository).To(Equal(c.Repository)) + Expect(unmarshalled.MultiGroup).To(Equal(c.MultiGroup)) + Expect(unmarshalled.Gvks).To(Equal(c.Gvks)) + }, + Entry("basic", s1, c1), + Entry("full", s2, c2), + ) + + DescribeTable("Unmarshal should fail", + func(content string) { + var c cfg + Expect(c.Unmarshal([]byte(content))).NotTo(Succeed()) + }, + Entry("for unknown fields", `field: 1 +version: "2"`), + ) + }) +}) + +var _ = Describe("New", func() { + It("should return a new config for project configuration 2", func() { + Expect(New().GetVersion().Compare(Version)).To(Equal(0)) + }) +}) diff --git a/pkg/config/v3alpha/config.go b/pkg/config/v3alpha/config.go new file mode 100644 index 00000000000..43f4bcd6ea4 --- /dev/null +++ b/pkg/config/v3alpha/config.go @@ -0,0 +1,349 @@ +/* +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 v3alpha + +import ( + "fmt" + "strings" + + "sigs.k8s.io/yaml" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +// Version is the config.Version for project configuration 3-alpha +var Version = config.Version{Number: 3, Stage: stage.Alpha} + +type cfg struct { + // Version + Version config.Version `json:"version"` + + // String fields + Domain string `json:"domain,omitempty"` + Repository string `json:"repo,omitempty"` + Name string `json:"projectName,omitempty"` + Layout string `json:"layout,omitempty"` + + // Boolean fields + MultiGroup bool `json:"multigroup,omitempty"` + ComponentConfig bool `json:"componentConfig,omitempty"` + + // Resources + Resources []resource.Resource `json:"resources,omitempty"` + + // Plugins + 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 + +// pluginConfig is an arbitrary plugin configuration object. +type pluginConfig interface{} + +// New returns a new config.Config +func New() config.Config { + return &cfg{Version: Version} +} + +func init() { + config.Register(Version, New) +} + +// GetVersion implements config.Config +func (c cfg) GetVersion() config.Version { + return c.Version +} + +// GetDomain implements config.Config +func (c cfg) GetDomain() string { + return c.Domain +} + +// SetDomain implements config.Config +func (c *cfg) SetDomain(domain string) error { + c.Domain = domain + return nil +} + +// GetRepository implements config.Config +func (c cfg) GetRepository() string { + return c.Repository +} + +// SetRepository implements config.Config +func (c *cfg) SetRepository(repository string) error { + c.Repository = repository + return nil +} + +// GetProjectName implements config.Config +func (c cfg) GetProjectName() string { + return c.Name +} + +// SetName implements config.Config +func (c *cfg) SetName(name string) error { + c.Name = name + return nil +} + +// GetLayout implements config.Config +func (c cfg) GetLayout() string { + return c.Layout +} + +// SetLayout implements config.Config +func (c *cfg) SetLayout(layout string) error { + c.Layout = layout + return nil +} + +// IsMultiGroup implements config.Config +func (c cfg) IsMultiGroup() bool { + return c.MultiGroup +} + +// SetMultiGroup implements config.Config +func (c *cfg) SetMultiGroup() error { + c.MultiGroup = true + return nil +} + +// ClearMultiGroup implements config.Config +func (c *cfg) ClearMultiGroup() error { + c.MultiGroup = false + return nil +} + +// IsComponentConfig implements config.Config +func (c cfg) IsComponentConfig() bool { + return c.ComponentConfig +} + +// SetComponentConfig implements config.Config +func (c *cfg) SetComponentConfig() error { + c.ComponentConfig = true + return nil +} + +// ClearComponentConfig implements config.Config +func (c *cfg) ClearComponentConfig() error { + c.ComponentConfig = false + return nil +} + +// ResourcesLength implements config.Config +func (c cfg) ResourcesLength() int { + return len(c.Resources) +} + +// HasResource implements config.Config +func (c cfg) HasResource(gvk resource.GVK) bool { + gvk.Domain = "" // Version 3 alpha does not include domain per resource + + for _, res := range c.Resources { + if gvk.IsEqualTo(res.GVK) { + return true + } + } + + return false +} + +// GetResource implements config.Config +func (c cfg) GetResource(gvk resource.GVK) (resource.Resource, error) { + gvk.Domain = "" // Version 3 alpha does not include domain per resource + + for _, res := range c.Resources { + if gvk.IsEqualTo(res.GVK) { + return res.Copy(), nil + } + } + + return resource.Resource{}, config.UnknownResource{GVK: gvk} +} + +// GetResources implements config.Config +func (c cfg) GetResources() ([]resource.Resource, error) { + resources := make([]resource.Resource, 0, len(c.Resources)) + for _, res := range c.Resources { + resources = append(resources, res.Copy()) + } + + return resources, nil +} + +func discardNonIncludedFields(res *resource.Resource) { + res.Domain = "" // Version 3 alpha does not include domain per resource + res.Plural = "" // Version 3 alpha does not include plural forms + res.Path = "" // Version 3 alpha does not include paths + if res.API != nil { + res.API.Namespaced = false // Version 3 alpha does not include if the api was namespaced + } + res.Controller = false // Version 3 alpha does not include if the controller was scaffolded + if res.Webhooks != nil { + res.Webhooks.Defaulting = false // Version 3 alpha does not include if the defaulting webhook was scaffolded + res.Webhooks.Validation = false // Version 3 alpha does not include if the validation webhook was scaffolded + res.Webhooks.Conversion = false // Version 3 alpha does not include if the conversion webhook was scaffolded + } +} + +// AddResource implements config.Config +func (c *cfg) AddResource(res resource.Resource) error { + // As res is passed by value it is already a shallow copy, but we need to make a deep copy + res = res.Copy() + + discardNonIncludedFields(&res) // Version 3 alpha does not include several fields from the Resource model + + if !c.HasResource(res.GVK) { + c.Resources = append(c.Resources, res) + } + return nil +} + +// UpdateResource implements config.Config +func (c *cfg) UpdateResource(res resource.Resource) error { + // As res is passed by value it is already a shallow copy, but we need to make a deep copy + res = res.Copy() + + discardNonIncludedFields(&res) // Version 3 alpha does not include several fields from the Resource model + + for i, r := range c.Resources { + if res.GVK.IsEqualTo(r.GVK) { + return c.Resources[i].Update(res) + } + } + + c.Resources = append(c.Resources, res) + return nil +} + +// HasGroup implements config.Config +func (c cfg) HasGroup(group string) bool { + // Return true if the target group is found in the tracked resources + for _, r := range c.Resources { + if strings.EqualFold(group, r.Group) { + return true + } + } + + // Return false otherwise + return false +} + +// IsCRDVersionCompatible implements config.Config +func (c cfg) IsCRDVersionCompatible(crdVersion string) bool { + return c.resourceAPIVersionCompatible("crd", crdVersion) +} + +// IsWebhookVersionCompatible implements config.Config +func (c cfg) IsWebhookVersionCompatible(webhookVersion string) bool { + return c.resourceAPIVersionCompatible("webhook", webhookVersion) +} + +func (c cfg) resourceAPIVersionCompatible(verType, version string) bool { + for _, res := range c.Resources { + var currVersion string + switch verType { + case "crd": + if res.API != nil { + currVersion = res.API.CRDVersion + } + case "webhook": + if res.Webhooks != nil { + currVersion = res.Webhooks.WebhookVersion + } + } + if currVersion != "" && version != currVersion { + return false + } + } + + return true +} + +// DecodePluginConfig implements config.Config +func (c cfg) DecodePluginConfig(key string, configObj interface{}) error { + if len(c.Plugins) == 0 { + return nil + } + + // Get the object blob by key and unmarshal into the object. + if pluginConfig, hasKey := c.Plugins[key]; hasKey { + b, err := yaml.Marshal(pluginConfig) + if err != nil { + return fmt.Errorf("failed to convert extra fields object to bytes: %w", err) + } + if err := yaml.Unmarshal(b, configObj); err != nil { + return fmt.Errorf("failed to unmarshal extra fields object: %w", err) + } + } + + return nil +} + +// EncodePluginConfig will return an error if used on any project version < v3. +func (c *cfg) EncodePluginConfig(key string, configObj interface{}) error { + // Get object's bytes and set them under key in extra fields. + b, err := yaml.Marshal(configObj) + if err != nil { + return fmt.Errorf("failed to convert %T object to bytes: %s", configObj, err) + } + var fields map[string]interface{} + if err := yaml.Unmarshal(b, &fields); err != nil { + return fmt.Errorf("failed to unmarshal %T object bytes: %s", configObj, err) + } + if c.Plugins == nil { + c.Plugins = make(map[string]pluginConfig) + } + c.Plugins[key] = fields + return nil +} + +// Marshal implements config.Config +func (c cfg) Marshal() ([]byte, error) { + for i, r := range c.Resources { + // If API is empty, omit it (prevents `api: {}`). + if r.API != nil && r.API.IsEmpty() { + c.Resources[i].API = nil + } + // If Webhooks is empty, omit it (prevents `webhooks: {}`). + if r.Webhooks != nil && r.Webhooks.IsEmpty() { + c.Resources[i].Webhooks = nil + } + } + + content, err := yaml.Marshal(c) + if err != nil { + return nil, config.MarshalError{Err: err} + } + + return content, nil +} + +// Unmarshal implements config.Config +func (c *cfg) Unmarshal(b []byte) error { + if err := yaml.UnmarshalStrict(b, c); err != nil { + return config.UnmarshalError{Err: err} + } + + return nil +} diff --git a/pkg/config/v3alpha/config_test.go b/pkg/config/v3alpha/config_test.go new file mode 100644 index 00000000000..d05bd7bb8c7 --- /dev/null +++ b/pkg/config/v3alpha/config_test.go @@ -0,0 +1,572 @@ +/* +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 v3alpha + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +func TestConfigV3Alpha(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config V3-Alpha Suite") +} + +var _ = Describe("cfg", func() { + const ( + domain = "my.domain" + repo = "myrepo" + name = "ProjectName" + layout = "go.kubebuilder.io/v2" + + otherDomain = "other.domain" + otherRepo = "otherrepo" + otherName = "OtherProjectName" + otherLayout = "go.kubebuilder.io/v3-alpha" + ) + + var c cfg + + BeforeEach(func() { + c = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + Layout: layout, + } + }) + + Context("Version", func() { + It("GetVersion should return version 3-alpha", func() { + Expect(c.GetVersion().Compare(Version)).To(Equal(0)) + }) + }) + + Context("Domain", func() { + It("GetDomain should return the domain", func() { + Expect(c.GetDomain()).To(Equal(domain)) + }) + + It("SetDomain should set the domain", func() { + Expect(c.SetDomain(otherDomain)).To(Succeed()) + Expect(c.Domain).To(Equal(otherDomain)) + }) + }) + + Context("Repository", func() { + It("GetRepository should return the repository", func() { + Expect(c.GetRepository()).To(Equal(repo)) + }) + + It("SetRepository should set the repository", func() { + Expect(c.SetRepository(otherRepo)).To(Succeed()) + Expect(c.Repository).To(Equal(otherRepo)) + }) + }) + + Context("Name", func() { + It("GetProjectName should return the name", func() { + Expect(c.GetProjectName()).To(Equal(name)) + }) + + It("SetName should set the name", func() { + Expect(c.SetName(otherName)).To(Succeed()) + Expect(c.Name).To(Equal(otherName)) + }) + }) + + Context("Layout", func() { + It("GetLayout should return the layout", func() { + Expect(c.GetLayout()).To(Equal(layout)) + }) + + It("SetLayout should set the layout", func() { + Expect(c.SetLayout(otherLayout)).To(Succeed()) + Expect(c.Layout).To(Equal(otherLayout)) + }) + }) + + Context("Multi group", func() { + It("IsMultiGroup should return false if not set", func() { + Expect(c.IsMultiGroup()).To(BeFalse()) + }) + + It("IsMultiGroup should return true if set", func() { + c.MultiGroup = true + Expect(c.IsMultiGroup()).To(BeTrue()) + }) + + It("SetMultiGroup should enable multi-group support", func() { + Expect(c.SetMultiGroup()).To(Succeed()) + Expect(c.MultiGroup).To(BeTrue()) + }) + + It("ClearMultiGroup should disable multi-group support", func() { + c.MultiGroup = true + Expect(c.ClearMultiGroup()).To(Succeed()) + Expect(c.MultiGroup).To(BeFalse()) + }) + }) + + Context("Component config", func() { + It("IsComponentConfig should return false if not set", func() { + Expect(c.IsComponentConfig()).To(BeFalse()) + }) + + It("IsComponentConfig should return true if set", func() { + c.ComponentConfig = true + Expect(c.IsComponentConfig()).To(BeTrue()) + }) + + It("SetComponentConfig should fail to enable component config support", func() { + Expect(c.SetComponentConfig()).To(Succeed()) + Expect(c.ComponentConfig).To(BeTrue()) + }) + + It("ClearComponentConfig should fail to disable component config support", func() { + c.ComponentConfig = false + Expect(c.ClearComponentConfig()).To(Succeed()) + Expect(c.ComponentConfig).To(BeFalse()) + }) + }) + + Context("Resources", func() { + var res = resource.Resource{ + GVK: resource.GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + }, + API: &resource.API{ + CRDVersion: "v1", + Namespaced: true, + }, + Controller: true, + Webhooks: &resource.Webhooks{ + WebhookVersion: "v1", + Defaulting: true, + Validation: true, + Conversion: true, + }, + } + + DescribeTable("ResourcesLength should return the number of resources", + func(n int) { + for i := 0; i < n; i++ { + c.Resources = append(c.Resources, res) + } + Expect(c.ResourcesLength()).To(Equal(n)) + }, + Entry("for no resources", 0), + Entry("for one resource", 1), + Entry("for several resources", 3), + ) + + It("HasResource should return false for a non-existent resource", func() { + Expect(c.HasResource(res.GVK)).To(BeFalse()) + }) + + It("HasResource should return true for an existent resource", func() { + c.Resources = append(c.Resources, res) + Expect(c.HasResource(res.GVK)).To(BeTrue()) + }) + + It("GetResource should fail for a non-existent resource", func() { + _, err := c.GetResource(res.GVK) + Expect(err).To(HaveOccurred()) + }) + + It("GetResource should return an existent resource", func() { + c.Resources = append(c.Resources, res) + r, err := c.GetResource(res.GVK) + Expect(err).NotTo(HaveOccurred()) + Expect(r.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + Expect(r.API).NotTo(BeNil()) + Expect(r.API.CRDVersion).To(Equal(res.API.CRDVersion)) + Expect(r.Webhooks).NotTo(BeNil()) + Expect(r.Webhooks.WebhookVersion).To(Equal(res.Webhooks.WebhookVersion)) + }) + + It("GetResources should return a slice of the tracked resources", func() { + c.Resources = append(c.Resources, res, res, res) + resources, err := c.GetResources() + Expect(err).NotTo(HaveOccurred()) + Expect(resources).To(Equal([]resource.Resource{res, res, res})) + }) + + // Auxiliary function for AddResource and UpdateResource tests + checkResource := func(result, expected resource.Resource) { + Expect(result.GVK.IsEqualTo(expected.GVK)).To(BeTrue()) + Expect(result.API).NotTo(BeNil()) + Expect(result.API.CRDVersion).To(Equal(expected.API.CRDVersion)) + Expect(result.API.Namespaced).To(BeFalse()) + Expect(result.Controller).To(BeFalse()) + Expect(result.Webhooks).NotTo(BeNil()) + Expect(result.Webhooks.WebhookVersion).To(Equal(expected.Webhooks.WebhookVersion)) + Expect(result.Webhooks.Defaulting).To(BeFalse()) + Expect(result.Webhooks.Validation).To(BeFalse()) + Expect(result.Webhooks.Conversion).To(BeFalse()) + } + + It("AddResource should add the provided resource if non-existent", func() { + l := len(c.Resources) + Expect(c.AddResource(res)).To(Succeed()) + Expect(len(c.Resources)).To(Equal(l + 1)) + + checkResource(c.Resources[0], res) + }) + + It("AddResource should do nothing if the resource already exists", func() { + c.Resources = append(c.Resources, res) + l := len(c.Resources) + Expect(c.AddResource(res)).To(Succeed()) + Expect(len(c.Resources)).To(Equal(l)) + }) + + It("UpdateResource should add the provided resource if non-existent", func() { + l := len(c.Resources) + Expect(c.UpdateResource(res)).To(Succeed()) + Expect(len(c.Resources)).To(Equal(l + 1)) + + checkResource(c.Resources[0], res) + }) + + It("UpdateResource should update it if the resource already exists", func() { + c.Resources = append(c.Resources, resource.Resource{ + GVK: resource.GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + }, + }) + l := len(c.Resources) + Expect(c.Resources[0].GVK.IsEqualTo(res.GVK)).To(BeTrue()) + Expect(c.Resources[0].API).To(BeNil()) + Expect(c.Resources[0].Controller).To(BeFalse()) + Expect(c.Resources[0].Webhooks).To(BeNil()) + + Expect(c.UpdateResource(res)).To(Succeed()) + Expect(len(c.Resources)).To(Equal(l)) + + r := c.Resources[0] + Expect(r.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + Expect(r.API).NotTo(BeNil()) + Expect(r.API.CRDVersion).To(Equal(res.API.CRDVersion)) + Expect(r.API.Namespaced).To(BeFalse()) + Expect(r.Controller).To(BeFalse()) + Expect(r.Webhooks).NotTo(BeNil()) + Expect(r.Webhooks.WebhookVersion).To(Equal(res.Webhooks.WebhookVersion)) + Expect(r.Webhooks.Defaulting).To(BeFalse()) + Expect(r.Webhooks.Validation).To(BeFalse()) + Expect(r.Webhooks.Conversion).To(BeFalse()) + }) + + It("HasGroup should return false with no tracked resources", func() { + Expect(c.HasGroup(res.Group)).To(BeFalse()) + }) + + It("HasGroup should return true with tracked resources in the same group", func() { + c.Resources = append(c.Resources, res) + Expect(c.HasGroup(res.Group)).To(BeTrue()) + }) + + It("HasGroup should return false with tracked resources in other group", func() { + c.Resources = append(c.Resources, res) + Expect(c.HasGroup("other-group")).To(BeFalse()) + }) + + It("IsCRDVersionCompatible should return true with no tracked resources", func() { + Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) + Expect(c.IsCRDVersionCompatible("v1")).To(BeTrue()) + }) + + It("IsCRDVersionCompatible should return true only for matching CRD versions of tracked resources", func() { + c.Resources = append(c.Resources, resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: res.Kind, + }, + API: &resource.API{CRDVersion: "v1beta1"}, + }) + Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) + Expect(c.IsCRDVersionCompatible("v1")).To(BeFalse()) + Expect(c.IsCRDVersionCompatible("v2")).To(BeFalse()) + }) + + It("IsWebhookVersionCompatible should return true with no tracked resources", func() { + Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible("v1")).To(BeTrue()) + }) + + It("IsWebhookVersionCompatible should return true only for matching webhook versions of tracked resources", func() { + c.Resources = append(c.Resources, resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: res.Kind, + }, + Webhooks: &resource.Webhooks{WebhookVersion: "v1beta1"}, + }) + Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible("v1")).To(BeFalse()) + Expect(c.IsWebhookVersionCompatible("v2")).To(BeFalse()) + }) + }) + + Context("Plugins", func() { + // Test plugin config. Don't want to export this config, but need it to + // be accessible by test. + type PluginConfig struct { + Data1 string `json:"data-1"` + Data2 string `json:"data-2,omitempty"` + } + + const ( + key = "plugin-x" + ) + + var ( + c0 = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + Layout: layout, + } + c1 = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + Layout: layout, + Plugins: PluginConfigs{ + "plugin-x": map[string]interface{}{ + "data-1": "", + }, + }, + } + c2 = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + Layout: layout, + Plugins: PluginConfigs{ + "plugin-x": map[string]interface{}{ + "data-1": "plugin value 1", + "data-2": "plugin value 2", + }, + }, + } + pluginConfig = PluginConfig{ + Data1: "plugin value 1", + Data2: "plugin value 2", + } + ) + + DescribeTable("DecodePluginConfig should retrieve the plugin data correctly", + func(inputConfig cfg, expectedPluginConfig PluginConfig) { + var pluginConfig PluginConfig + Expect(inputConfig.DecodePluginConfig(key, &pluginConfig)).To(Succeed()) + Expect(pluginConfig).To(Equal(expectedPluginConfig)) + }, + Entry("for no plugin config object", c0, nil), + Entry("for an empty plugin config object", c1, PluginConfig{}), + Entry("for a full plugin config object", c2, pluginConfig), + // TODO (coverage): add cases where yaml.Marshal returns an error + // TODO (coverage): add cases where yaml.Unmarshal returns an error + ) + + DescribeTable("EncodePluginConfig should encode the plugin data correctly", + func(pluginConfig PluginConfig, expectedConfig cfg) { + Expect(c.EncodePluginConfig(key, pluginConfig)).To(Succeed()) + Expect(c).To(Equal(expectedConfig)) + }, + Entry("for an empty plugin config object", PluginConfig{}, c1), + Entry("for a full plugin config object", pluginConfig, c2), + // TODO (coverage): add cases where yaml.Marshal returns an error + // TODO (coverage): add cases where yaml.Unmarshal returns an error + ) + }) + + Context("Persistence", func() { + var ( + // BeforeEach is called after the entries are evaluated, and therefore, c is not available + c1 = cfg{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + Layout: layout, + } + c2 = cfg{ + Version: Version, + Domain: otherDomain, + Repository: otherRepo, + Name: otherName, + Layout: otherLayout, + MultiGroup: true, + ComponentConfig: true, + Resources: []resource.Resource{ + { + GVK: resource.GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + }, + }, + { + GVK: resource.GVK{ + Group: "group", + Version: "v1", + Kind: "Kind2", + }, + API: &resource.API{CRDVersion: "v1"}, + Webhooks: &resource.Webhooks{WebhookVersion: "v1"}, + }, + { + GVK: resource.GVK{ + Group: "group", + Version: "v1-beta", + Kind: "Kind", + }, + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + }, + { + GVK: resource.GVK{ + Group: "group2", + Version: "v1", + Kind: "Kind", + }, + }, + }, + Plugins: PluginConfigs{ + "plugin-x": map[string]interface{}{ + "data-1": "single plugin datum", + }, + "plugin-y/v1": map[string]interface{}{ + "data-1": "plugin value 1", + "data-2": "plugin value 2", + "data-3": []string{"plugin value 3", "plugin value 4"}, + }, + }, + } + // TODO: include cases with Plural, Path, API.namespaced, Controller, Webhooks.Defaulting, + // Webhooks.Validation and Webhooks.Conversion when added + s1 = `domain: my.domain +layout: go.kubebuilder.io/v2 +projectName: ProjectName +repo: myrepo +version: 3-alpha +` + s2 = `componentConfig: true +domain: other.domain +layout: go.kubebuilder.io/v3-alpha +multigroup: true +plugins: + plugin-x: + data-1: single plugin datum + plugin-y/v1: + data-1: plugin value 1 + data-2: plugin value 2 + data-3: + - plugin value 3 + - plugin value 4 +projectName: OtherProjectName +repo: otherrepo +resources: +- group: group + kind: Kind + version: v1 +- api: + crdVersion: v1 + group: group + kind: Kind2 + version: v1 + webhooks: + webhookVersion: v1 +- group: group + kind: Kind + version: v1-beta +- group: group2 + kind: Kind + version: v1 +version: 3-alpha +` + ) + + DescribeTable("Marshal should succeed", + func(c cfg, content string) { + b, err := c.Marshal() + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(content)) + }, + Entry("for a basic configuration", c1, s1), + Entry("for a full configuration", c2, s2), + ) + + DescribeTable("Marshal should fail", + func(c cfg) { + _, err := c.Marshal() + Expect(err).To(HaveOccurred()) + }, + // TODO (coverage): add cases where yaml.Marshal returns an error + ) + + DescribeTable("Unmarshal should succeed", + func(content string, c cfg) { + var unmarshalled cfg + Expect(unmarshalled.Unmarshal([]byte(content))).To(Succeed()) + Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0)) + Expect(unmarshalled.Domain).To(Equal(c.Domain)) + Expect(unmarshalled.Repository).To(Equal(c.Repository)) + Expect(unmarshalled.Name).To(Equal(c.Name)) + Expect(unmarshalled.Layout).To(Equal(c.Layout)) + Expect(unmarshalled.MultiGroup).To(Equal(c.MultiGroup)) + Expect(unmarshalled.ComponentConfig).To(Equal(c.ComponentConfig)) + Expect(unmarshalled.Resources).To(Equal(c.Resources)) + Expect(unmarshalled.Plugins).To(HaveLen(len(c.Plugins))) + // TODO: fully test Plugins field and not on its length + }, + Entry("basic", s1, c1), + Entry("full", s2, c2), + ) + + DescribeTable("Unmarshal should fail", + func(content string) { + var c cfg + Expect(c.Unmarshal([]byte(content))).NotTo(Succeed()) + }, + Entry("for unknown fields", `field: 1 +version: 3-alpha`), + ) + }) +}) + +var _ = Describe("New", func() { + It("should return a new config for project configuration 3-alpha", func() { + Expect(New().GetVersion().Compare(Version)).To(Equal(0)) + }) +}) diff --git a/pkg/config/version.go b/pkg/config/version.go new file mode 100644 index 00000000000..4b3d4ead71c --- /dev/null +++ b/pkg/config/version.go @@ -0,0 +1,122 @@ +/* +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 config + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +var ( + errNonPositive = errors.New("project version number must be positive") + errEmpty = errors.New("project version is empty") +) + +// Version is a project version containing a non-zero positive integer and a stage value that represents stability. +type Version struct { + // Number denotes the current version of a plugin. Two different numbers between versions + // indicate that they are incompatible. + Number int + // Stage indicates stability. + Stage stage.Stage +} + +// Parse parses version inline, assuming it adheres to format: [1-9][0-9]*(-(alpha|beta))? +func (v *Version) Parse(version string) error { + if len(version) == 0 { + return errEmpty + } + + substrings := strings.SplitN(version, "-", 2) + + var err error + if v.Number, err = strconv.Atoi(substrings[0]); err != nil { + // Lets check if the `-` belonged to a negative number + if n, err := strconv.Atoi(version); err == nil && n < 0 { + return errNonPositive + } + return err + } else if v.Number == 0 { + return errNonPositive + } + + if len(substrings) > 1 { + if err = v.Stage.Parse(substrings[1]); err != nil { + return err + } + } + + return nil +} + +// String returns the string representation of v. +func (v Version) String() string { + stageStr := v.Stage.String() + if len(stageStr) == 0 { + return fmt.Sprintf("%d", v.Number) + } + return fmt.Sprintf("%d-%s", v.Number, stageStr) +} + +// Validate ensures that the version number is positive and the stage is one of the valid stages. +func (v Version) Validate() error { + if v.Number < 1 { + return errNonPositive + } + + return v.Stage.Validate() +} + +// Compare returns -1 if v < other, 0 if v == other, and 1 if v > other. +func (v Version) Compare(other Version) int { + if v.Number > other.Number { + return 1 + } else if v.Number < other.Number { + return -1 + } + + return v.Stage.Compare(other.Stage) +} + +// IsStable returns true if v is stable. +func (v Version) IsStable() bool { + return v.Stage.IsStable() +} + +// MarshalJSON implements json.Marshaller +func (v Version) MarshalJSON() ([]byte, error) { + if err := v.Validate(); err != nil { + return []byte{}, err + } + + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements json.Unmarshaller +func (v *Version) UnmarshalJSON(b []byte) error { + var str string + if err := json.Unmarshal(b, &str); err != nil { + return err + } + + return v.Parse(str) +} diff --git a/pkg/config/version_test.go b/pkg/config/version_test.go new file mode 100644 index 00000000000..23a4ec0f417 --- /dev/null +++ b/pkg/config/version_test.go @@ -0,0 +1,168 @@ +/* +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 config + +import ( + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +var _ = Describe("Version", func() { + // Parse, String and Validate are tested by MarshalJSON and UnmarshalJSON + + Context("Compare", func() { + // Test Compare() by sorting a list. + var ( + versions = []Version{ + {Number: 2, Stage: stage.Alpha}, + {Number: 44, Stage: stage.Alpha}, + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 4, Stage: stage.Beta}, + {Number: 1, Stage: stage.Alpha}, + {Number: 4}, + {Number: 44, Stage: stage.Alpha}, + {Number: 30}, + {Number: 4, Stage: stage.Alpha}, + } + + sortedVersions = []Version{ + {Number: 1, Stage: stage.Alpha}, + {Number: 1}, + {Number: 2, Stage: stage.Alpha}, + {Number: 2, Stage: stage.Beta}, + {Number: 4, Stage: stage.Alpha}, + {Number: 4, Stage: stage.Beta}, + {Number: 4}, + {Number: 30}, + {Number: 44, Stage: stage.Alpha}, + {Number: 44, Stage: stage.Alpha}, + } + ) + + 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 + }) + Expect(versions).To(Equal(sortedVersions)) + }) + + }) + + 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}), + Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), + Entry("for version 22", Version{Number: 22}), + Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), + ) + + DescribeTable("should return false for unstable versions", + func(version Version) { Expect(version.IsStable()).To(BeFalse()) }, + Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), + Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), + Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), + Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), + ) + }) + + Context("MarshalJSON", func() { + DescribeTable("should be marshalled appropriately", + func(version Version, str string) { + b, err := version.MarshalJSON() + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(str)) + }, + Entry("for version 1", Version{Number: 1}, `"1"`), + Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}, `"1"`), + Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}, `"1-alpha"`), + Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}, `"1-beta"`), + Entry("for version 22", Version{Number: 22}, `"22"`), + Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}, `"22"`), + Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}, `"22-alpha"`), + Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}, `"22-beta"`), + ) + + DescribeTable("should fail to be marshalled", + func(version Version) { + _, err := version.MarshalJSON() + Expect(err).To(HaveOccurred()) + }, + Entry("for version 0", Version{Number: 0}), + Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), + Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), + Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), + Entry("for version 0 (implicit)", Version{}), + Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), + Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), + Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), + Entry("for version -1", Version{Number: -1}), + Entry("for version -1 (stable)", Version{Number: -1, Stage: stage.Stable}), + Entry("for version -1 (alpha)", Version{Number: -1, Stage: stage.Alpha}), + Entry("for version -1 (beta)", Version{Number: -1, Stage: stage.Beta}), + Entry("for invalid stage", Version{Stage: stage.Stage(34)}), + ) + }) + + Context("UnmarshalJSON", func() { + DescribeTable("should be unmarshalled appropriately", + func(str string, number int, s stage.Stage) { + var v Version + err := v.UnmarshalJSON([]byte(str)) + Expect(err).NotTo(HaveOccurred()) + Expect(v.Number).To(Equal(number)) + Expect(v.Stage).To(Equal(s)) + }, + Entry("for version string `1`", `"1"`, 1, stage.Stable), + Entry("for version string `1-alpha`", `"1-alpha"`, 1, stage.Alpha), + Entry("for version string `1-beta`", `"1-beta"`, 1, stage.Beta), + Entry("for version string `22`", `"22"`, 22, stage.Stable), + Entry("for version string `22-alpha`", `"22-alpha"`, 22, stage.Alpha), + Entry("for version string `22-beta`", `"22-beta"`, 22, stage.Beta), + ) + + DescribeTable("should fail to be unmarshalled", + func(str string) { + var v Version + err := v.UnmarshalJSON([]byte(str)) + Expect(err).To(HaveOccurred()) + }, + Entry("for empty version string", ``), + Entry("for version string ``", `""`), + Entry("for version string `0`", `"0"`), + Entry("for version string `0-alpha`", `"0-alpha"`), + Entry("for version string `0-beta`", `"0-beta"`), + Entry("for version string `-1`", `"-1"`), + Entry("for version string `-1-alpha`", `"-1-alpha"`), + Entry("for version string `-1-beta`", `"-1-beta"`), + Entry("for version string `v1`", `"v1"`), + Entry("for version string `v1-alpha`", `"v1-alpha"`), + Entry("for version string `v1-beta`", `"v1-beta"`), + Entry("for version string `1.0`", `"1.0"`), + Entry("for version string `v1.0`", `"v1.0"`), + Entry("for version string `v1.0-alpha`", `"v1.0-alpha"`), + Entry("for version string `1.0.0`", `"1.0.0"`), + Entry("for version string `1-a`", `"1-a"`), + ) + }) +}) diff --git a/pkg/internal/validation/project.go b/pkg/internal/validation/project.go deleted file mode 100644 index 7aa7eee8325..00000000000 --- a/pkg/internal/validation/project.go +++ /dev/null @@ -1,38 +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 validation - -import ( - "errors" - "regexp" -) - -// projectVersionFmt defines the project version format from a project config. -const projectVersionFmt string = "[1-9][0-9]*(?:-(?:alpha|beta))?" - -var projectVersionRe = regexp.MustCompile("^" + projectVersionFmt + "$") - -// ValidateProjectVersion ensures version adheres to the project version format. -func ValidateProjectVersion(version string) error { - if version == "" { - return errors.New("project version is empty") - } - if !projectVersionRe.MatchString(version) { - return errors.New(regexError("invalid value for project version", projectVersionFmt)) - } - return nil -} diff --git a/pkg/model/config/config.go b/pkg/model/config/config.go deleted file mode 100644 index f7fd95fe199..00000000000 --- a/pkg/model/config/config.go +++ /dev/null @@ -1,345 +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 ( - "fmt" - "strings" - - "sigs.k8s.io/yaml" -) - -// Scaffolding versions -const ( - Version2 = "2" - Version3Alpha = "3-alpha" -) - -// Config is the unmarshalled representation of the configuration file -type Config struct { - // Version is the project version, defaults to "1" (backwards compatibility) - Version string `json:"version,omitempty"` - - // Domain is the domain associated with the project and used for API groups - Domain string `json:"domain,omitempty"` - - // Repo is the go package name of the project root - Repo string `json:"repo,omitempty"` - - // ProjectName is the name of this controller project set on initialization. - ProjectName string `json:"projectName,omitempty"` - - // Resources tracks scaffolded resources in the project - // This info is tracked only in project with version 2 - Resources []ResourceData `json:"resources,omitempty"` - - // Multigroup tracks if the project has more than one group - MultiGroup bool `json:"multigroup,omitempty"` - - // ComponentConfig tracks if the project uses a config file for configuring - // the ctrl.Manager - ComponentConfig bool `json:"componentConfig,omitempty"` - - // Layout contains a key specifying which plugin created a project. - Layout string `json:"layout,omitempty"` - - // Plugins holds plugin-specific configs mapped by plugin key. These configs should be - // encoded/decoded using EncodePluginConfig/DecodePluginConfig, respectively. - Plugins PluginConfigs `json:"plugins,omitempty"` -} - -// 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{} - -// IsV2 returns true if it is a v2 project -func (c Config) IsV2() bool { - return c.Version == Version2 -} - -// IsV3 returns true if it is a v3 project -func (c Config) IsV3() bool { - return c.Version == Version3Alpha -} - -// GetResource returns the GKV if the resource is found -func (c Config) GetResource(target ResourceData) *ResourceData { - // Return true if the target resource is found in the tracked resources - for _, r := range c.Resources { - if r.isGVKEqualTo(target) { - return &r - } - } - return nil -} - -// UpdateResources either adds gvk to the tracked set or, if the resource already exists, -// updates the the equivalent resource in the set. -func (c *Config) UpdateResources(resource ResourceData) { - // If the resource already exists, update it. - for i, r := range c.Resources { - if r.isGVKEqualTo(resource) { - c.Resources[i].merge(resource) - return - } - } - - // The resource does not exist, append the resource to the tracked ones. - c.Resources = append(c.Resources, resource) -} - -// HasGroup returns true if group is already tracked -func (c Config) HasGroup(group string) bool { - // Return true if the target group is found in the tracked resources - for _, r := range c.Resources { - if strings.EqualFold(group, r.Group) { - return true - } - } - - // Return false otherwise - return false -} - -// HasWebhook returns true if webhook is already present -func (c Config) HasWebhook(resource ResourceData) bool { - for _, r := range c.Resources { - if r.isGVKEqualTo(resource) { - return r.Webhooks != nil - } - } - - return false -} - -// IsCRDVersionCompatible returns true if crdVersion can be added to the existing set of CRD versions. -func (c Config) IsCRDVersionCompatible(crdVersion string) bool { - return c.resourceAPIVersionCompatible("crd", crdVersion) -} - -// IsWebhookVersionCompatible returns true if webhookVersion can be added to the existing set of Webhook versions. -func (c Config) IsWebhookVersionCompatible(webhookVersion string) bool { - return c.resourceAPIVersionCompatible("webhook", webhookVersion) -} - -// resourceAPIVersionCompatible returns true if version can be added to the existing set of versions -// for a given verType. -func (c Config) resourceAPIVersionCompatible(verType, version string) bool { - for _, res := range c.Resources { - var currVersion string - switch verType { - case "crd": - if res.API != nil { - currVersion = res.API.CRDVersion - } - case "webhook": - if res.Webhooks != nil { - currVersion = res.Webhooks.WebhookVersion - } - } - if currVersion != "" && version != currVersion { - return false - } - } - return true -} - -// ResourceData contains information about scaffolded resources -type ResourceData struct { - Group string `json:"group,omitempty"` - Version string `json:"version,omitempty"` - Kind string `json:"kind,omitempty"` - - // API holds the API data - API *API `json:"api,omitempty"` - - // Webhooks holds the Webhooks data - Webhooks *Webhooks `json:"webhooks,omitempty"` -} - -// API contains information about scaffolded APIs -type API struct { - // CRDVersion holds the CustomResourceDefinition API version used for the ResourceData. - CRDVersion string `json:"crdVersion,omitempty"` -} - -// Webhooks contains information about scaffolded webhooks -type Webhooks struct { - // WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the Options. - WebhookVersion string `json:"webhookVersion,omitempty"` -} - -// isGVKEqualTo compares it with another resource -func (r ResourceData) isGVKEqualTo(other ResourceData) bool { - return r.Group == other.Group && - r.Version == other.Version && - r.Kind == other.Kind -} - -// merge combines fields of two GVKs that have matching group, version, and kind, -// favoring the receiver's values. -func (r *ResourceData) merge(other ResourceData) { - if other.Webhooks != nil { - if r.Webhooks == nil { - r.Webhooks = other.Webhooks - } else { - r.Webhooks.merge(other.Webhooks) - } - } - - if other.API != nil { - if r.API == nil { - r.API = other.API - } else { - r.API.merge(other.API) - } - } -} - -// merge compares it with another webhook by setting each webhook type individually so existing values are -// not overwritten. -func (w *Webhooks) merge(other *Webhooks) { - if w.WebhookVersion == "" && other.WebhookVersion != "" { - w.WebhookVersion = other.WebhookVersion - } -} - -// merge compares it with another api by setting each api type individually so existing values are -// not overwritten. -func (a *API) merge(other *API) { - if a.CRDVersion == "" && other.CRDVersion != "" { - a.CRDVersion = other.CRDVersion - } -} - -// Marshal returns the bytes of c. -func (c Config) Marshal() ([]byte, error) { - // Ignore extra fields at first. - cfg := c - cfg.Plugins = nil - - // Ignore some fields if v2. - if cfg.IsV2() { - for i := range cfg.Resources { - cfg.Resources[i].API = nil - cfg.Resources[i].Webhooks = nil - } - } - - for i, r := range cfg.Resources { - // If API is empty, omit it (prevents `api: {}`). - if r.API != nil && r.API.CRDVersion == "" { - cfg.Resources[i].API = nil - } - // If Webhooks is empty, omit it (prevents `webhooks: {}`). - if r.Webhooks != nil && r.Webhooks.WebhookVersion == "" { - cfg.Resources[i].Webhooks = nil - } - } - - content, err := yaml.Marshal(cfg) - if err != nil { - return nil, fmt.Errorf("error marshalling project configuration: %v", err) - } - - // Empty config strings are "{}" due to the map field. - if strings.TrimSpace(string(content)) == "{}" { - content = []byte{} - } - - // Append extra fields to put them at the config's bottom. - if len(c.Plugins) != 0 { - // Unless the project version is v2 which does not support a plugins field. - if cfg.IsV2() { - return nil, fmt.Errorf("error marshalling project configuration: plugin field found for v2") - } - - pluginConfigBytes, err := yaml.Marshal(Config{Plugins: c.Plugins}) - if err != nil { - return nil, fmt.Errorf("error marshalling project configuration extra fields: %v", err) - } - content = append(content, pluginConfigBytes...) - } - - return content, nil -} - -// Unmarshal unmarshals the bytes of a Config into c. -func (c *Config) Unmarshal(b []byte) error { - if err := yaml.UnmarshalStrict(b, c); err != nil { - return fmt.Errorf("error unmarshalling project configuration: %v", err) - } - - // Project versions < v3 do not support a plugins field. - if !c.IsV3() { - c.Plugins = nil - } - return nil -} - -// EncodePluginConfig encodes a config object into c by overwriting the existing -// object stored under key. This method is intended to be used for custom -// configuration objects, which were introduced in project version 3-alpha. -// EncodePluginConfig will return an error if used on any project version < v3. -func (c *Config) EncodePluginConfig(key string, configObj interface{}) error { - // Short-circuit project versions < v3. - if !c.IsV3() { - return fmt.Errorf("project versions < v3 do not support extra fields") - } - - // Get object's bytes and set them under key in extra fields. - b, err := yaml.Marshal(configObj) - if err != nil { - return fmt.Errorf("failed to convert %T object to bytes: %s", configObj, err) - } - var fields map[string]interface{} - if err := yaml.Unmarshal(b, &fields); err != nil { - return fmt.Errorf("failed to unmarshal %T object bytes: %s", configObj, err) - } - if c.Plugins == nil { - c.Plugins = make(map[string]pluginConfig) - } - c.Plugins[key] = fields - return nil -} - -// DecodePluginConfig decodes a plugin config stored in c into configObj, which must be a pointer -// This method is intended to be used for custom configuration objects, which were introduced -// in project version 3-alpha. EncodePluginConfig will return an error if used on any project version < v3. -func (c Config) DecodePluginConfig(key string, configObj interface{}) error { - // Short-circuit project versions < v3. - if !c.IsV3() { - return fmt.Errorf("project versions < v3 do not support extra fields") - } - if len(c.Plugins) == 0 { - return nil - } - - // Get the object blob by key and unmarshal into the object. - if pluginConfig, hasKey := c.Plugins[key]; hasKey { - b, err := yaml.Marshal(pluginConfig) - if err != nil { - return fmt.Errorf("failed to convert extra fields object to bytes: %s", err) - } - if err := yaml.Unmarshal(b, configObj); err != nil { - return fmt.Errorf("failed to unmarshal extra fields object: %s", err) - } - } - return nil -} diff --git a/pkg/model/config/config_test.go b/pkg/model/config/config_test.go deleted file mode 100644 index 12009711998..00000000000 --- a/pkg/model/config/config_test.go +++ /dev/null @@ -1,312 +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 ( - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -const v1beta1 = "v1beta1" - -var _ = Describe("PluginConfig", func() { - // Test plugin config. Don't want to export this config, but need it to - // be accessible by test. - type PluginConfig struct { - Data1 string `json:"data-1"` - Data2 string `json:"data-2"` - } - const defaultWebhookVersion = "v1" - - resource := ResourceData{Group: "Foo", Kind: "Baz", Version: "v1"} - resource.Webhooks = &Webhooks{defaultWebhookVersion} - - It("should return true when has the ResourceData is equals", func() { - Expect(resource.isGVKEqualTo(ResourceData{Group: "Foo", Kind: "Baz", Version: "v1"})).To(BeTrue()) - }) - - It("should return false when ResourceData is NOT equals", func() { - Expect(resource.isGVKEqualTo(ResourceData{Group: "Foo", Kind: "Baz", Version: "v2"})).To(BeFalse()) - }) - - It("IsV2 should return true when the config is V2", func() { - cfg := Config{Version: Version2} - Expect(cfg.IsV2()).To(BeTrue()) - }) - - It("IsV3 should return true when the config is V3", func() { - cfg := Config{Version: Version3Alpha} - Expect(cfg.IsV3()).To(BeTrue()) - }) - - It("should encode correctly", func() { - var ( - key = "plugin-x" - config Config - pluginConfig PluginConfig - expectedConfig Config - ) - - By("Using config version empty") - config = Config{} - pluginConfig = PluginConfig{} - Expect(config.EncodePluginConfig(key, pluginConfig)).NotTo(Succeed()) - - By("Using config version 2") - config = Config{Version: Version2} - pluginConfig = PluginConfig{} - Expect(config.EncodePluginConfig(key, pluginConfig)).NotTo(Succeed()) - - By("Using config version 2 with extra fields") - config = Config{Version: Version2} - pluginConfig = PluginConfig{ - Data1: "single plugin datum", - } - Expect(config.EncodePluginConfig(key, pluginConfig)).NotTo(Succeed()) - - By("Using config version 3-alpha") - config = Config{Version: Version3Alpha} - pluginConfig = PluginConfig{} - expectedConfig = Config{ - Version: Version3Alpha, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "", - "data-2": "", - }, - }, - } - Expect(config.EncodePluginConfig(key, pluginConfig)).To(Succeed()) - Expect(config).To(Equal(expectedConfig)) - - By("Using config version 3-alpha with extra fields as struct") - config = Config{Version: Version3Alpha} - pluginConfig = PluginConfig{ - "plugin value 1", - "plugin value 2", - } - expectedConfig = Config{ - Version: Version3Alpha, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "plugin value 1", - "data-2": "plugin value 2", - }, - }, - } - Expect(config.EncodePluginConfig(key, pluginConfig)).To(Succeed()) - Expect(config).To(Equal(expectedConfig)) - }) - - It("should decode correctly", func() { - var ( - key = "plugin-x" - config Config - pluginConfig PluginConfig - expectedPluginConfig PluginConfig - ) - - By("Using config version 2") - config = Config{Version: Version2} - pluginConfig = PluginConfig{} - Expect(config.DecodePluginConfig(key, &pluginConfig)).NotTo(Succeed()) - - By("Using config version 2 with extra fields") - config = Config{Version: Version2} - pluginConfig = PluginConfig{ - Data1: "single plugin datum", - } - Expect(config.DecodePluginConfig(key, &pluginConfig)).NotTo(Succeed()) - - By("Using empty config version 3-alpha") - config = Config{ - Version: Version3Alpha, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{}, - }, - } - pluginConfig = PluginConfig{} - expectedPluginConfig = PluginConfig{} - Expect(config.DecodePluginConfig(key, &pluginConfig)).To(Succeed()) - Expect(pluginConfig).To(Equal(expectedPluginConfig)) - - By("Using config version 3-alpha") - config = Config{Version: Version3Alpha} - pluginConfig = PluginConfig{} - expectedPluginConfig = PluginConfig{} - Expect(config.DecodePluginConfig(key, &pluginConfig)).To(Succeed()) - Expect(pluginConfig).To(Equal(expectedPluginConfig)) - - By("Using config version 3-alpha with extra fields as struct") - config = Config{ - Version: Version3Alpha, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "plugin value 1", - "data-2": "plugin value 2", - }, - }, - } - pluginConfig = PluginConfig{} - expectedPluginConfig = PluginConfig{ - "plugin value 1", - "plugin value 2", - } - Expect(config.DecodePluginConfig(key, &pluginConfig)).To(Succeed()) - Expect(pluginConfig).To(Equal(expectedPluginConfig)) - }) - - It("should Marshal and Unmarshal a plugin", func() { - By("Using config with extra fields as struct") - cfg := Config{ - Version: Version3Alpha, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ - "data-1": "plugin value 1", - }, - }, - } - b, err := cfg.Marshal() - Expect(err).NotTo(HaveOccurred()) - Expect(string(b)).To(Equal("version: 3-alpha\nplugins:\n plugin-x:\n data-1: plugin value 1\n")) - Expect(cfg.Unmarshal(b)).To(Succeed()) - }) -}) - -var _ = Describe("ResourceData Version Compatibility", func() { - - var ( - c *Config - resource1, resource2 ResourceData - - defaultVersion = "v1" - ) - - BeforeEach(func() { - c = &Config{} - resource1 = ResourceData{Group: "example", Version: "v1", Kind: "TestKind"} - resource2 = ResourceData{Group: "example", Version: "v1", Kind: "TestKind2"} - }) - - Context("resourceAPIVersionCompatible", func() { - It("returns true for a list of empty resources", func() { - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeTrue()) - }) - It("returns true for one resource with an empty version", func() { - c.Resources = []ResourceData{resource1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeTrue()) - }) - It("returns true for one resource with matching version", func() { - resource1.API = &API{CRDVersion: defaultVersion} - resource1.Webhooks = &Webhooks{WebhookVersion: defaultVersion} - c.Resources = []ResourceData{resource1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeTrue()) - }) - It("returns true for two resources with matching versions", func() { - resource1.API = &API{CRDVersion: defaultVersion} - resource1.Webhooks = &Webhooks{WebhookVersion: defaultVersion} - resource2.API = &API{CRDVersion: defaultVersion} - resource2.Webhooks = &Webhooks{WebhookVersion: defaultVersion} - c.Resources = []ResourceData{resource1, resource2} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeTrue()) - }) - It("returns false for one resource with a non-matching version", func() { - resource1.API = &API{CRDVersion: v1beta1} - resource1.Webhooks = &Webhooks{WebhookVersion: v1beta1} - c.Resources = []ResourceData{resource1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeFalse()) - }) - It("returns false for two resources containing a non-matching version", func() { - resource1.API = &API{CRDVersion: v1beta1} - resource1.Webhooks = &Webhooks{WebhookVersion: v1beta1} - resource2.API = &API{CRDVersion: defaultVersion} - resource2.Webhooks = &Webhooks{WebhookVersion: defaultVersion} - c.Resources = []ResourceData{resource1, resource2} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse()) - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeFalse()) - }) - }) -}) - -var _ = Describe("Config", func() { - var ( - c *Config - gvk1, gvk2, gvk3 ResourceData - ) - - BeforeEach(func() { - c = &Config{} - gvk1 = ResourceData{Group: "example", Version: "v1", Kind: "TestKind"} - gvk2 = ResourceData{Group: "example", Version: "v1", Kind: "TestKind2"} - gvk3 = ResourceData{Group: "example", Version: "v1", Kind: "TestKind", Webhooks: &Webhooks{WebhookVersion: v1beta1}} - }) - - Context("UpdateResource", func() { - It("Adds a non-existing resource", func() { - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]ResourceData{gvk1})) - // Update again to ensure idempotency. - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]ResourceData{gvk1})) - }) - It("Updates an existing resource", func() { - c.UpdateResources(gvk1) - resource := ResourceData{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind} - c.UpdateResources(resource) - Expect(c.Resources).To(Equal([]ResourceData{resource})) - }) - It("Updates an existing resource with more than one resource present", func() { - c.UpdateResources(gvk1) - c.UpdateResources(gvk2) - gvk := ResourceData{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind} - c.UpdateResources(gvk) - Expect(c.Resources).To(Equal([]ResourceData{gvk, gvk2})) - }) - }) - - Context("HasGroup", func() { - It("should return true when config has a resource with the group", func() { - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]ResourceData{gvk1})) - Expect(c.HasGroup(gvk1.Group)).To(BeTrue()) - }) - It("should return false when config has a resource with not the same group", func() { - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]ResourceData{gvk1})) - Expect(c.HasGroup("hasNot")).To(BeFalse()) - }) - - }) - - Context("HasWebhook", func() { - It("should return true when config has a webhook for the GVK", func() { - c.UpdateResources(gvk3) - Expect(c.Resources).To(Equal([]ResourceData{gvk3})) - Expect(c.HasWebhook(gvk3)).To(BeTrue()) - }) - It("should return false when config does not have a webhook for the GVK", func() { - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]ResourceData{gvk1})) - Expect(c.HasWebhook(gvk1)).To(BeFalse()) - }) - }) -}) diff --git a/pkg/model/resource/api.go b/pkg/model/resource/api.go new file mode 100644 index 00000000000..770171e04ae --- /dev/null +++ b/pkg/model/resource/api.go @@ -0,0 +1,64 @@ +/* +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 resource + +import ( + "fmt" +) + +// API contains information about scaffolded APIs +type API struct { + // CRDVersion holds the CustomResourceDefinition API version used for the resource. + CRDVersion string `json:"crdVersion,omitempty"` + + // Namespaced is true if the API is namespaced. + Namespaced bool `json:"namespaced,omitempty"` +} + +// Copy returns a deep copy of the API that can be safely modified without affecting the original. +func (api API) Copy() API { + // As this function doesn't use a pointer receiver, api is already a shallow copy. + // Any field that is a pointer, slice or map needs to be deep copied. + return api +} + +// Update combines fields of the APIs of two resources. +func (api *API) Update(other *API) error { + // If other is nil, nothing to merge + if other == nil { + return nil + } + + // Update the version. + if other.CRDVersion != "" { + if api.CRDVersion == "" { + api.CRDVersion = other.CRDVersion + } else if api.CRDVersion != other.CRDVersion { + return fmt.Errorf("CRD versions do not match") + } + } + + // Update the namespace. + api.Namespaced = api.Namespaced || other.Namespaced + + return nil +} + +// IsEmpty returns if the API's fields all contain zero-values. +func (api API) IsEmpty() bool { + return api.CRDVersion == "" && !api.Namespaced +} diff --git a/pkg/model/resource/api_test.go b/pkg/model/resource/api_test.go new file mode 100644 index 00000000000..52faad9a748 --- /dev/null +++ b/pkg/model/resource/api_test.go @@ -0,0 +1,127 @@ +/* +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 resource + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +//nolint:dupl +var _ = Describe("API", func() { + Context("Update", func() { + var api, other API + + It("should do nothing if provided a nil pointer", func() { + api = API{} + Expect(api.Update(nil)).To(Succeed()) + Expect(api.CRDVersion).To(Equal("")) + Expect(api.Namespaced).To(BeFalse()) + + api = API{ + CRDVersion: v1, + Namespaced: true, + } + Expect(api.Update(nil)).To(Succeed()) + Expect(api.CRDVersion).To(Equal(v1)) + Expect(api.Namespaced).To(BeTrue()) + }) + + Context("CRD version", func() { + It("should modify the CRD version if provided and not previously set", func() { + api = API{} + other = API{CRDVersion: v1} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.CRDVersion).To(Equal(v1)) + }) + + It("should keep the CRD version if not provided", func() { + api = API{CRDVersion: v1} + other = API{} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.CRDVersion).To(Equal(v1)) + }) + + It("should keep the CRD version if provided the same as previously set", func() { + api = API{CRDVersion: v1} + other = API{CRDVersion: v1} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.CRDVersion).To(Equal(v1)) + }) + + It("should fail if previously set and provided CRD versions do not match", func() { + api = API{CRDVersion: v1} + other = API{CRDVersion: "v1beta1"} + Expect(api.Update(&other)).NotTo(Succeed()) + }) + }) + + Context("Namespaced", func() { + It("should set the namespace scope if provided and not previously set", func() { + api = API{} + other = API{Namespaced: true} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.Namespaced).To(BeTrue()) + }) + + It("should keep the namespace scope if previously set", func() { + api = API{Namespaced: true} + + By("not providing it") + other = API{} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.Namespaced).To(BeTrue()) + + By("providing it") + other = API{Namespaced: true} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.Namespaced).To(BeTrue()) + }) + + It("should not set the namespace scope if not provided and not previously set", func() { + api = API{} + other = API{} + Expect(api.Update(&other)).To(Succeed()) + Expect(api.Namespaced).To(BeFalse()) + }) + }) + }) + + Context("IsEmpty", func() { + var ( + none = API{} + cluster = API{ + CRDVersion: v1, + } + namespaced = API{ + CRDVersion: v1, + Namespaced: true, + } + ) + + It("should return true fo an empty object", func() { + Expect(none.IsEmpty()).To(BeTrue()) + }) + + DescribeTable("should return false for non-empty objects", + func(api API) { Expect(api.IsEmpty()).To(BeFalse()) }, + Entry("cluster-scope", cluster), + Entry("namespace-scope", namespaced), + ) + }) +}) diff --git a/pkg/model/resource/gvk.go b/pkg/model/resource/gvk.go new file mode 100644 index 00000000000..feb8368887d --- /dev/null +++ b/pkg/model/resource/gvk.go @@ -0,0 +1,50 @@ +/* +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 resource + +import ( + "fmt" +) + +// GVK stores the Group - Version - Kind triplet that uniquely identifies a resource. +// In kubebuilder, the k8s fully qualified group is stored as Group and Domain to improve UX. +type GVK struct { + Group string `json:"group,omitempty"` + Domain string `json:"domain,omitempty"` + Version string `json:"version"` + Kind string `json:"kind"` +} + +// QualifiedGroup returns the fully qualified group name with the available information. +func (gvk GVK) QualifiedGroup() string { + switch "" { + case gvk.Domain: + return gvk.Group + case gvk.Group: + return gvk.Domain + default: + return fmt.Sprintf("%s.%s", gvk.Group, gvk.Domain) + } +} + +// IsEqualTo compares two GVK objects. +func (gvk GVK) IsEqualTo(other GVK) bool { + return gvk.Group == other.Group && + gvk.Domain == other.Domain && + gvk.Version == other.Version && + gvk.Kind == other.Kind +} diff --git a/pkg/model/resource/gvk_test.go b/pkg/model/resource/gvk_test.go new file mode 100644 index 00000000000..c3adf999561 --- /dev/null +++ b/pkg/model/resource/gvk_test.go @@ -0,0 +1,58 @@ +/* +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 resource + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("GVK", func() { + const ( + group = "group" + domain = "my.domain" + version = "v1" + kind = "Kind" + ) + + Context("QualifiedGroup", func() { + DescribeTable("should return the correct string", + func(gvk GVK, qualifiedGroup string) { Expect(gvk.QualifiedGroup()).To(Equal(qualifiedGroup)) }, + Entry("fully qualified resource", GVK{Group: group, Domain: domain, Version: version, Kind: kind}, + group+"."+domain), + Entry("empty group name", GVK{Domain: domain, Version: version, Kind: kind}, domain), + Entry("empty domain", GVK{Group: group, Version: version, Kind: kind}, group), + ) + }) + + Context("IsEqualTo", func() { + var gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind} + + It("should return true for the same resource", func() { + Expect(gvk.IsEqualTo(GVK{Group: group, Domain: domain, Version: version, Kind: kind})).To(BeTrue()) + }) + + DescribeTable("should return false for different resources", + func(other GVK) { Expect(gvk.IsEqualTo(other)).To(BeFalse()) }, + Entry("different kind", GVK{Group: group, Domain: domain, Version: version, Kind: "Kind2"}), + Entry("different version", GVK{Group: group, Domain: domain, Version: "v2", Kind: kind}), + Entry("different domain", GVK{Group: group, Domain: "other.domain", Version: version, Kind: kind}), + Entry("different group", GVK{Group: "group2", Domain: domain, Version: version, Kind: kind}), + ) + }) +}) diff --git a/pkg/model/resource/options.go b/pkg/model/resource/options.go deleted file mode 100644 index b763a607ab2..00000000000 --- a/pkg/model/resource/options.go +++ /dev/null @@ -1,295 +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 resource - -import ( - "fmt" - "path" - "regexp" - "strings" - - "github.com/gobuffalo/flect" - - "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" -) - -const ( - versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$" - groupRequired = "group cannot be empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - -var ( - versionRegex = regexp.MustCompile(versionPattern) - - 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 -type Options struct { - // Group is the API Group. Does not contain the domain. - Group string - - // Version is the API version. - Version string - - // Kind is the API Kind. - Kind string - - // Plural is the API Kind plural form. - // Optional - Plural string - - // Namespaced is true if the resource is namespaced. - Namespaced bool - - // API holds the api data - API config.API - - // Webhooks holds the webhooks data - Webhooks config.Webhooks -} - -// ValidateV2 verifies that V2 project has all the fields have valid values -func (opts *Options) ValidateV2() 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(groupRequired) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionRequired) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindRequired) - } - // 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) - } - - // Check if the Group has a valid DNS1123 subdomain value - if err := validation.IsDNS1123Subdomain(opts.Group); err != nil { - return fmt.Errorf("group name is invalid: (%v)", err) - } - - // Check if the version follows the valid pattern - if !versionRegex.MatchString(opts.Version) { - return fmt.Errorf("version must match %s (was %s)", versionPattern, opts.Version) - } - - validationErrors := []string{} - - // require Kind to start with an uppercase character - if string(opts.Kind[0]) == strings.ToLower(string(opts.Kind[0])) { - validationErrors = append(validationErrors, "kind must start with an uppercase character") - } - - validationErrors = append(validationErrors, validation.IsDNS1035Label(strings.ToLower(opts.Kind))...) - - if len(validationErrors) != 0 { - return fmt.Errorf("invalid Kind: %#v", validationErrors) - } - - // TODO: validate plural strings if provided - - return nil -} - -// 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.Version, "-") { - return fmt.Errorf(versionRequired) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindRequired) - } - // Now we can check that all the required flags are not empty - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - // Check if the Group has a valid DNS1123 subdomain value - if len(opts.Group) != 0 { - if err := validation.IsDNS1123Subdomain(opts.Group); err != nil { - return fmt.Errorf("group name is invalid: (%v)", err) - } - } - - // Check if the version follows the valid pattern - if !versionRegex.MatchString(opts.Version) { - return fmt.Errorf("version must match %s (was %s)", versionPattern, opts.Version) - } - - validationErrors := []string{} - - // require Kind to start with an uppercase character - if string(opts.Kind[0]) == strings.ToLower(string(opts.Kind[0])) { - validationErrors = append(validationErrors, "kind must start with an uppercase character") - } - - validationErrors = append(validationErrors, validation.IsDNS1035Label(strings.ToLower(opts.Kind))...) - - if len(validationErrors) != 0 { - return fmt.Errorf("invalid Kind: %#v", validationErrors) - } - - // Ensure apiVersions for k8s types are empty or valid. - for typ, apiVersion := range map[string]string{ - "CRD": opts.API.CRDVersion, - "Webhook": opts.Webhooks.WebhookVersion, - } { - switch apiVersion { - case "", "v1", "v1beta1": - default: - return fmt.Errorf("%s version must be one of: v1, v1beta1", typ) - } - } - - // TODO: validate plural strings if provided - - return nil -} - -// Data returns the ResourceData information to check against tracked resources in the configuration file -func (opts *Options) Data() config.ResourceData { - return config.ResourceData{ - Group: opts.Group, - Version: opts.Version, - Kind: opts.Kind, - API: &opts.API, - Webhooks: &opts.Webhooks, - } -} - -// safeImport returns a cleaned version of the provided string that can be used for imports -func (opts *Options) safeImport(unsafe string) string { - safe := unsafe - - // Remove dashes and dots - safe = strings.Replace(safe, "-", "", -1) - safe = strings.Replace(safe, ".", "", -1) - - return safe -} - -// NewResource creates a new resource from the options -func (opts *Options) NewResource(c *config.Config, doResource bool) *Resource { - res := opts.newResource() - - replacer := res.Replacer() - - pkg := replacer.Replace(path.Join(c.Repo, "api", "%[version]")) - if c.MultiGroup { - if opts.Group != "" { - pkg = replacer.Replace(path.Join(c.Repo, "apis", "%[group]", "%[version]")) - } else { - pkg = replacer.Replace(path.Join(c.Repo, "apis", "%[version]")) - } - } - domain := c.Domain - - // pkg and domain 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 !doResource { - if c.GetResource(opts.Data()) == nil { - if coreDomain, found := coreGroups[opts.Group]; found { - pkg = replacer.Replace(path.Join("k8s.io", "api", "%[group]", "%[version]")) - domain = coreDomain - } - } - } - - res.Package = pkg - res.Domain = opts.Group - if domain != "" && opts.Group != "" { - res.Domain += "." + domain - } else if opts.Group == "" && !c.IsV2() { - // Empty group overrides the default values provided by newResource(). - // GroupPackageName and ImportAlias includes domain instead of group name as user provided group is empty. - res.Domain = domain - res.GroupPackageName = opts.safeImport(domain) - res.ImportAlias = opts.safeImport(domain + opts.Version) - } - - return res -} - -func (opts *Options) newResource() *Resource { - // If not provided, compute a plural for for Kind - plural := opts.Plural - if plural == "" { - plural = flect.Pluralize(strings.ToLower(opts.Kind)) - } - - return &Resource{ - Namespaced: opts.Namespaced, - Group: opts.Group, - GroupPackageName: opts.safeImport(opts.Group), - Version: opts.Version, - Kind: opts.Kind, - Plural: plural, - ImportAlias: opts.safeImport(opts.Group + opts.Version), - API: opts.API, - Webhooks: opts.Webhooks, - } -} diff --git a/pkg/model/resource/options_test.go b/pkg/model/resource/options_test.go deleted file mode 100644 index 65f8511a821..00000000000 --- a/pkg/model/resource/options_test.go +++ /dev/null @@ -1,215 +0,0 @@ -package resource_test - -import ( - "strings" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - - . "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -var _ = Describe("Resource Options", func() { - Describe("scaffolding an API", func() { - It("should succeed if the Options is valid", func() { - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - }) - - It("should succeed if the Group is not specified for V3", func() { - options := &Options{Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - }) - - It("should fail if the Group is not all lowercase", func() { - options := &Options{Group: "Crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring("group name is invalid: " + - "([a DNS-1123 subdomain must consist of lower case alphanumeric characters")) - }) - - It("should fail if the Group contains non-alpha characters", func() { - options := &Options{Group: "crew1*?", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring("group name is invalid: " + - "([a DNS-1123 subdomain must consist of lower case alphanumeric characters")) - }) - - It("should fail if the Version is not specified", func() { - options := &Options{Group: "crew", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring("version cannot be empty")) - }) - - //nolint:dupl - It("should fail if the Version does not match the version format", func() { - options := &Options{Group: "crew", Version: "1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was 1)`)) - - options = &Options{Group: "crew", Version: "1beta1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was 1beta1)`)) - - options = &Options{Group: "crew", Version: "a1beta1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was a1beta1)`)) - - options = &Options{Group: "crew", Version: "v1beta", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was v1beta)`)) - - options = &Options{Group: "crew", Version: "v1beta1alpha1", Kind: "FirstMate"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was v1beta1alpha1)`)) - }) - - It("should fail if the Kind is not specified", func() { - options := &Options{Group: "crew", Version: "v1"} - Expect(options.Validate()).NotTo(Succeed()) - Expect(options.Validate().Error()).To(ContainSubstring("kind cannot be empty")) - }) - - DescribeTable("valid Kind values-according to core Kubernetes", - func(kind string) { - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - Expect(options.Validate()).To(Succeed()) - }, - Entry("should pass validation if Kind is camelcase", "FirstMate"), - Entry("should pass validation if Kind has more than one caps at the start", "FIRSTMate"), - ) - - It("should fail if Kind is too long", func() { - kind := strings.Repeat("a", 64) - - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - err := options.Validate() - Expect(err).To(MatchError(ContainSubstring("must be no more than 63 characters"))) - }) - - DescribeTable("invalid Kind values-according to core Kubernetes", - func(kind string) { - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - Expect(options.Validate()).To(MatchError( - ContainSubstring("a DNS-1035 label must consist of lower case alphanumeric characters"))) - }, - Entry("should fail validation if Kind contains whitespaces", "Something withSpaces"), - Entry("should fail validation if Kind ends in -", "KindEndingIn-"), - Entry("should fail validation if Kind starts with number", "0ValidityKind"), - ) - - It("should fail if Kind starts with a lowercase character", func() { - options := &Options{Group: "crew", Kind: "lOWERCASESTART", Version: "v1"} - err := options.Validate() - Expect(err).To(MatchError(ContainSubstring("kind must start with an uppercase character"))) - }) - }) - - // We are duplicating the test cases for ValidateV2 with the Validate(). This test cases will be removed when - // the V2 will no longer be supported. - Describe("scaffolding an API for V2", func() { - It("should succeed if the Options is valid for V2", func() { - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.ValidateV2()).To(Succeed()) - }) - - It("should not succeed if the Group is not specified for V2", func() { - options := &Options{Version: "v1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring("group cannot be empty")) - }) - - It("should fail if the Group is not all lowercase for V2", func() { - options := &Options{Group: "Crew", Version: "v1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring("group name is invalid: " + - "([a DNS-1123 subdomain must consist of lower case alphanumeric characters")) - }) - - It("should fail if the Group contains non-alpha characters for V2", func() { - options := &Options{Group: "crew1*?", Version: "v1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring("group name is invalid: " + - "([a DNS-1123 subdomain must consist of lower case alphanumeric characters")) - }) - - It("should fail if the Version is not specified for V2", func() { - options := &Options{Group: "crew", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring("version cannot be empty")) - }) - //nolint:dupl - It("should fail if the Version does not match the version format for V2", func() { - options := &Options{Group: "crew", Version: "1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was 1)`)) - - options = &Options{Group: "crew", Version: "1beta1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was 1beta1)`)) - - options = &Options{Group: "crew", Version: "a1beta1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was a1beta1)`)) - - options = &Options{Group: "crew", Version: "v1beta", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was v1beta)`)) - - options = &Options{Group: "crew", Version: "v1beta1alpha1", Kind: "FirstMate"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring( - `version must match ^v\d+(?:alpha\d+|beta\d+)?$ (was v1beta1alpha1)`)) - }) - - It("should fail if the Kind is not specified for V2", func() { - options := &Options{Group: "crew", Version: "v1"} - Expect(options.ValidateV2()).NotTo(Succeed()) - Expect(options.ValidateV2().Error()).To(ContainSubstring("kind cannot be empty")) - }) - - DescribeTable("valid Kind values-according to core Kubernetes for V2", - func(kind string) { - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - Expect(options.ValidateV2()).To(Succeed()) - }, - Entry("should pass validation if Kind is camelcase", "FirstMate"), - Entry("should pass validation if Kind has more than one caps at the start", "FIRSTMate"), - ) - - It("should fail if Kind is too long for V2", func() { - kind := strings.Repeat("a", 64) - - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - err := options.ValidateV2() - Expect(err).To(MatchError(ContainSubstring("must be no more than 63 characters"))) - }) - - DescribeTable("invalid Kind values-according to core Kubernetes for V2", - func(kind string) { - options := &Options{Group: "crew", Kind: kind, Version: "v1"} - Expect(options.ValidateV2()).To(MatchError( - ContainSubstring("a DNS-1035 label must consist of lower case alphanumeric characters"))) - }, - Entry("should fail validation if Kind contains whitespaces", "Something withSpaces"), - Entry("should fail validation if Kind ends in -", "KindEndingIn-"), - Entry("should fail validation if Kind starts with number", "0ValidityKind"), - ) - - It("should fail if Kind starts with a lowercase character for V2", func() { - options := &Options{Group: "crew", Kind: "lOWERCASESTART", Version: "v1"} - err := options.ValidateV2() - Expect(err).To(MatchError(ContainSubstring("kind must start with an uppercase character"))) - }) - }) -}) diff --git a/pkg/model/resource/resource.go b/pkg/model/resource/resource.go index ce3e223411d..a9237fa7a4a 100644 --- a/pkg/model/resource/resource.go +++ b/pkg/model/resource/resource.go @@ -19,55 +19,123 @@ package resource import ( "fmt" "strings" - - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" ) // Resource contains the information required to scaffold files for a resource. type Resource struct { - // Group is the API Group. Does not contain the domain. - Group string `json:"group,omitempty"` + // GVK contains the resource's Group-Version-Kind triplet. + GVK `json:",inline"` + + // Plural is the resource's kind plural form. + Plural string `json:"plural,omitempty"` - // GroupPackageName is the API Group cleaned to be used as the package name. - GroupPackageName string `json:"-"` + // Path is the path to the go package where the types are defined. + Path string `json:"path,omitempty"` - // Version is the API version. - Version string `json:"version,omitempty"` + // API holds the information related to the resource API. + API *API `json:"api,omitempty"` - // Kind is the API Kind. - Kind string `json:"kind,omitempty"` + // Controller specifies if a controller has been scaffolded. + Controller bool `json:"controller,omitempty"` - // Plural is the API Kind plural form. - Plural string `json:"plural,omitempty"` + // Webhooks holds the information related to the associated webhooks. + Webhooks *Webhooks `json:"webhooks,omitempty"` +} + +// PackageName returns a name valid to be used por go packages. +func (r Resource) PackageName() string { + if r.Group == "" { + return safeImport(r.Domain) + } + + return safeImport(r.Group) +} + +// ImportAlias returns a identifier usable as an import alias for this resource. +func (r Resource) ImportAlias() string { + if r.Group == "" { + return safeImport(r.Domain + r.Version) + } - // ImportAlias is a cleaned concatenation of Group and Version. - ImportAlias string `json:"-"` + return safeImport(r.Group + r.Version) +} - // Package is the go package of the Resource. - Package string `json:"package,omitempty"` +// HasAPI returns true if the resource has an associated API. +func (r Resource) HasAPI() bool { + return r.API != nil && r.API.CRDVersion != "" +} - // Domain is the Group + "." + Domain of the Resource. - Domain string `json:"domain,omitempty"` +// HasController returns true if the resource has an associated controller. +func (r Resource) HasController() bool { + return r.Controller +} - // Namespaced is true if the resource is namespaced. - Namespaced bool `json:"namespaced,omitempty"` +// HasDefaultingWebhook returns true if the resource has an associated defaulting webhook. +func (r Resource) HasDefaultingWebhook() bool { + return r.Webhooks != nil && r.Webhooks.Defaulting +} - // API holds the the api data that is scaffolded - API config.API `json:"api,omitempty"` +// HasValidationWebhook returns true if the resource has an associated validation webhook. +func (r Resource) HasValidationWebhook() bool { + return r.Webhooks != nil && r.Webhooks.Validation +} - // Webhooks holds webhooks data that is scaffolded - Webhooks config.Webhooks `json:"webhooks,omitempty"` +// HasConversionWebhook returns true if the resource has an associated conversion webhook. +func (r Resource) HasConversionWebhook() bool { + return r.Webhooks != nil && r.Webhooks.Conversion } -// Data returns the ResourceData information to check against tracked resources in the configuration file -func (r *Resource) Data() config.ResourceData { - return config.ResourceData{ - Group: r.Group, - Version: r.Version, - Kind: r.Kind, - API: &r.API, - Webhooks: &r.Webhooks, +// Copy returns a deep copy of the Resource that can be safely modified without affecting the original. +func (r Resource) Copy() Resource { + // As this function doesn't use a pointer receiver, r is already a shallow copy. + // Any field that is a pointer, slice or map needs to be deep copied. + if r.API != nil { + api := r.API.Copy() + r.API = &api } + if r.Webhooks != nil { + webhooks := r.Webhooks.Copy() + r.Webhooks = &webhooks + } + return r +} + +// Update combines fields of two resources that have matching GVK favoring the receiver's values. +func (r *Resource) Update(other Resource) error { + // If self is nil, return an error + if r == nil { + return fmt.Errorf("unable to update a nil Resource") + } + + // Make sure we are not merging resources for different GVKs. + if !r.GVK.IsEqualTo(other.GVK) { + return fmt.Errorf("unable to update a Resource with another with non-matching GVK") + } + + // TODO: currently Plural & Path will always match. In the future, this may not be true (e.g. providing a + // --plural flag). In that case, we should yield an error in case of updating two resources with different + // values for these fields. + + // Update API. + if r.API == nil && other.API != nil { + r.API = &API{} + } + if err := r.API.Update(other.API); err != nil { + return err + } + + // Update controller. + r.Controller = r.Controller || other.Controller + + // Update Webhooks. + if r.Webhooks == nil && other.Webhooks != nil { + r.Webhooks = &Webhooks{} + } + if err := r.Webhooks.Update(other.Webhooks); err != nil { + return err + } + + return nil } func wrapKey(key string) string { @@ -79,10 +147,10 @@ func (r Resource) Replacer() *strings.Replacer { var replacements []string replacements = append(replacements, wrapKey("group"), r.Group) - replacements = append(replacements, wrapKey("group-package-name"), r.GroupPackageName) replacements = append(replacements, wrapKey("version"), r.Version) replacements = append(replacements, wrapKey("kind"), strings.ToLower(r.Kind)) replacements = append(replacements, wrapKey("plural"), strings.ToLower(r.Plural)) + replacements = append(replacements, wrapKey("package-name"), r.PackageName()) return strings.NewReplacer(replacements...) } diff --git a/pkg/model/resource/resource_test.go b/pkg/model/resource/resource_test.go index b213511e371..b0bd7fd7f8e 100644 --- a/pkg/model/resource/resource_test.go +++ b/pkg/model/resource/resource_test.go @@ -14,248 +14,429 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resource_test +package resource import ( - "path" - . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" - . "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) +//nolint:dupl var _ = Describe("Resource", func() { - Describe("scaffolding an API", func() { - It("should succeed if the Resource is valid", func() { - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource := options.NewResource( - &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - }, - true, - ) - Expect(resource.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("crew")) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.Plural).To(Equal("firstmates")) - Expect(resource.ImportAlias).To(Equal("crewv1")) - Expect(resource.Package).To(Equal(path.Join("test", "api", "v1"))) - Expect(resource.Domain).To(Equal("crew.test.io")) - - resource = options.NewResource( - &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - MultiGroup: true, - }, - true, - ) - Expect(resource.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("crew")) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.Plural).To(Equal("firstmates")) - Expect(resource.ImportAlias).To(Equal("crewv1")) - Expect(resource.Package).To(Equal(path.Join("test", "apis", "crew", "v1"))) - Expect(resource.Domain).To(Equal("crew.test.io")) - }) - - It("should default the Plural by pluralizing the Kind", func() { - singleGroupConfig := &config.Config{ - Version: config.Version2, - } - multiGroupConfig := &config.Config{ - Version: config.Version2, - MultiGroup: true, - } - - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource := options.NewResource(singleGroupConfig, true) - Expect(resource.Plural).To(Equal("firstmates")) - - resource = options.NewResource(multiGroupConfig, true) - Expect(resource.Plural).To(Equal("firstmates")) + const ( + group = "group" + domain = "test.io" + version = "v1" + kind = "Kind" + ) + + var ( + res1 = Resource{ + GVK: GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + } + res2 = Resource{ + GVK: GVK{ + // Empty group + Domain: domain, + Version: version, + Kind: kind, + }, + } + res3 = Resource{ + GVK: GVK{ + Group: group, + // Empty domain + Version: version, + Kind: kind, + }, + } + ) + + Context("compound field", func() { + const ( + safeDomain = "testio" + groupVersion = group + version + domainVersion = safeDomain + version + ) + + DescribeTable("PackageName should return the correct string", + func(res Resource, packageName string) { Expect(res.PackageName()).To(Equal(packageName)) }, + Entry("fully qualified resource", res1, group), + Entry("empty group name", res2, safeDomain), + Entry("empty domain", res3, group), + ) + + DescribeTable("ImportAlias", + func(res Resource, importAlias string) { Expect(res.ImportAlias()).To(Equal(importAlias)) }, + Entry("fully qualified resource", res1, groupVersion), + Entry("empty group name", res2, domainVersion), + Entry("empty domain", res3, groupVersion), + ) + }) - options = &Options{Group: "crew", Version: "v1", Kind: "Fish"} - Expect(options.Validate()).To(Succeed()) + Context("part check", func() { + Context("HasAPI", func() { + It("should return true if the API is scaffolded", func() { + Expect(Resource{API: &API{CRDVersion: "v1"}}.HasAPI()).To(BeTrue()) + }) - resource = options.NewResource(singleGroupConfig, true) - Expect(resource.Plural).To(Equal("fish")) + DescribeTable("should return false if the API is not scaffolded", + func(res Resource) { Expect(res.HasAPI()).To(BeFalse()) }, + Entry("nil API", Resource{API: nil}), + Entry("empty CRD version", Resource{API: &API{}}), + ) + }) - resource = options.NewResource(multiGroupConfig, true) - Expect(resource.Plural).To(Equal("fish")) + Context("HasController", func() { + It("should return true if the controller is scaffolded", func() { + Expect(Resource{Controller: true}.HasController()).To(BeTrue()) + }) - options = &Options{Group: "crew", Version: "v1", Kind: "Helmswoman"} - Expect(options.Validate()).To(Succeed()) + It("should return false if the controller is not scaffolded", func() { + Expect(Resource{Controller: false}.HasController()).To(BeFalse()) + }) + }) - resource = options.NewResource(singleGroupConfig, true) - Expect(resource.Plural).To(Equal("helmswomen")) + Context("HasDefaultingWebhook", func() { + It("should return true if the defaulting webhook is scaffolded", func() { + Expect(Resource{Webhooks: &Webhooks{Defaulting: true}}.HasDefaultingWebhook()).To(BeTrue()) + }) - resource = options.NewResource(multiGroupConfig, true) - Expect(resource.Plural).To(Equal("helmswomen")) + DescribeTable("should return false if the defaulting webhook is not scaffolded", + func(res Resource) { Expect(res.HasDefaultingWebhook()).To(BeFalse()) }, + Entry("nil webhooks", Resource{Webhooks: nil}), + Entry("no defaulting", Resource{Webhooks: &Webhooks{Defaulting: false}}), + ) }) - It("should keep the Plural if specified", func() { - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate", Plural: "mates"} - Expect(options.Validate()).To(Succeed()) + Context("HasValidationWebhook", func() { + It("should return true if the validation webhook is scaffolded", func() { + Expect(Resource{Webhooks: &Webhooks{Validation: true}}.HasValidationWebhook()).To(BeTrue()) + }) - resource := options.NewResource( - &config.Config{ - Version: config.Version2, - }, - true, + DescribeTable("should return false if the validation webhook is not scaffolded", + func(res Resource) { Expect(res.HasValidationWebhook()).To(BeFalse()) }, + Entry("nil webhooks", Resource{Webhooks: nil}), + Entry("no validation", Resource{Webhooks: &Webhooks{Validation: false}}), ) - Expect(resource.Plural).To(Equal("mates")) + }) - resource = options.NewResource( - &config.Config{ - Version: config.Version2, - MultiGroup: true, - }, - true, + Context("HasConversionWebhook", func() { + It("should return true if the conversion webhook is scaffolded", func() { + Expect(Resource{Webhooks: &Webhooks{Conversion: true}}.HasConversionWebhook()).To(BeTrue()) + }) + + DescribeTable("should return false if the conversion webhook is not scaffolded", + func(res Resource) { Expect(res.HasConversionWebhook()).To(BeFalse()) }, + Entry("nil webhooks", Resource{Webhooks: nil}), + Entry("no conversion", Resource{Webhooks: &Webhooks{Conversion: false}}), ) - Expect(resource.Plural).To(Equal("mates")) }) + }) - It("should allow hyphens and dots in group names", func() { - singleGroupConfig := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - } - multiGroupConfig := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - MultiGroup: true, - } - - options := &Options{Group: "my-project", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource := options.NewResource(singleGroupConfig, true) - - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("myproject")) - Expect(resource.ImportAlias).To(Equal("myprojectv1")) - Expect(resource.Package).To(Equal(path.Join("test", "api", "v1"))) - Expect(resource.Domain).To(Equal("my-project.test.io")) - - resource = options.NewResource(multiGroupConfig, true) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("myproject")) - Expect(resource.ImportAlias).To(Equal("myprojectv1")) - Expect(resource.Package).To(Equal(path.Join("test", "apis", "my-project", "v1"))) - Expect(resource.Domain).To(Equal("my-project.test.io")) - - options = &Options{Group: "my.project", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource = options.NewResource(singleGroupConfig, true) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("myproject")) - Expect(resource.ImportAlias).To(Equal("myprojectv1")) - Expect(resource.Package).To(Equal(path.Join("test", "api", "v1"))) - Expect(resource.Domain).To(Equal("my.project.test.io")) - - resource = options.NewResource(multiGroupConfig, true) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.GroupPackageName).To(Equal("myproject")) - Expect(resource.ImportAlias).To(Equal("myprojectv1")) - Expect(resource.Package).To(Equal(path.Join("test", "apis", "my.project", "v1"))) - Expect(resource.Domain).To(Equal("my.project.test.io")) + Context("Copy", func() { + const ( + plural = "kinds" + path = "api/v1" + crdVersion = "v1" + webhookVersion = "v1" + ) + + res := Resource{ + GVK: GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: plural, + Path: path, + API: &API{ + CRDVersion: crdVersion, + Namespaced: true, + }, + Controller: true, + Webhooks: &Webhooks{ + WebhookVersion: webhookVersion, + Defaulting: true, + Validation: true, + Conversion: true, + }, + } + + It("should return an exact copy", func() { + other := res.Copy() + Expect(other.Group).To(Equal(res.Group)) + Expect(other.Domain).To(Equal(res.Domain)) + Expect(other.Version).To(Equal(res.Version)) + Expect(other.Kind).To(Equal(res.Kind)) + Expect(other.Plural).To(Equal(res.Plural)) + Expect(other.Path).To(Equal(res.Path)) + Expect(other.API).NotTo(BeNil()) + Expect(other.API.CRDVersion).To(Equal(res.API.CRDVersion)) + Expect(other.API.Namespaced).To(Equal(res.API.Namespaced)) + Expect(other.Controller).To(Equal(res.Controller)) + Expect(other.Webhooks).NotTo(BeNil()) + Expect(other.Webhooks.WebhookVersion).To(Equal(res.Webhooks.WebhookVersion)) + Expect(other.Webhooks.Defaulting).To(Equal(res.Webhooks.Defaulting)) + Expect(other.Webhooks.Validation).To(Equal(res.Webhooks.Validation)) + Expect(other.Webhooks.Conversion).To(Equal(res.Webhooks.Conversion)) }) - It("should not append '.' if provided an empty domain", func() { - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) + It("modifying the copy should not affect the original", func() { + other := res.Copy() + other.Group = "group2" + other.Domain = "other.domain" + other.Version = "v2" + other.Kind = "kind2" + other.Plural = "kind2s" + other.Path = "api/v2" + other.API.CRDVersion = "v1beta1" + other.API.Namespaced = false + other.API = nil // Change fields before changing pointer + other.Controller = false + other.Webhooks.WebhookVersion = "v1beta1" + other.Webhooks.Defaulting = false + other.Webhooks.Validation = false + other.Webhooks.Conversion = false + other.Webhooks = nil // Change fields before changing pointer + + Expect(res.Group).To(Equal(group)) + Expect(res.Domain).To(Equal(domain)) + Expect(res.Version).To(Equal(version)) + Expect(res.Kind).To(Equal(kind)) + Expect(res.Plural).To(Equal(plural)) + Expect(res.Path).To(Equal(path)) + Expect(res.API).NotTo(BeNil()) + Expect(res.API.CRDVersion).To(Equal(crdVersion)) + Expect(res.API.Namespaced).To(BeTrue()) + Expect(res.Controller).To(BeTrue()) + Expect(res.Webhooks).NotTo(BeNil()) + Expect(res.Webhooks.WebhookVersion).To(Equal(webhookVersion)) + Expect(res.Webhooks.Defaulting).To(BeTrue()) + Expect(res.Webhooks.Validation).To(BeTrue()) + Expect(res.Webhooks.Conversion).To(BeTrue()) + }) + }) - resource := options.NewResource( - &config.Config{ - Version: config.Version2, - }, - true, - ) - Expect(resource.Domain).To(Equal("crew")) + Context("Update", func() { + var r, other Resource - resource = options.NewResource( - &config.Config{ - Version: config.Version2, - MultiGroup: true, - }, - true, - ) - Expect(resource.Domain).To(Equal("crew")) + It("should fail for nil objects", func() { + var nilResource *Resource + Expect(nilResource.Update(other)).NotTo(Succeed()) }) - It("should use core apis", func() { - singleGroupConfig := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", + It("should fail for different GVKs", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, } - multiGroupConfig := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - MultiGroup: true, + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: "OtherKind", + }, } + Expect(r.Update(other)).NotTo(Succeed()) + }) - options := &Options{Group: "apps", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource := options.NewResource(singleGroupConfig, false) - Expect(resource.Package).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.Domain).To(Equal("apps")) - - resource = options.NewResource(multiGroupConfig, false) - Expect(resource.Package).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.Domain).To(Equal("apps")) - - options = &Options{Group: "authentication", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource = options.NewResource(singleGroupConfig, false) - Expect(resource.Package).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.Domain).To(Equal("authentication.k8s.io")) + Context("API", func() { + It("should work with nil APIs", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + API: &API{CRDVersion: v1}, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.API).NotTo(BeNil()) + Expect(r.API.CRDVersion).To(Equal(v1)) + }) + + It("should fail if API.Update fails", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + API: &API{CRDVersion: v1}, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + API: &API{CRDVersion: "v1beta1"}, + } + Expect(r.Update(other)).NotTo(Succeed()) + }) + + // The rest of the cases are tested in API.Update + }) - resource = options.NewResource(multiGroupConfig, false) - Expect(resource.Package).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.Domain).To(Equal("authentication.k8s.io")) + Context("Controller", func() { + It("should set the controller flag if provided and not previously set", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Controller: true, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Controller).To(BeTrue()) + }) + + It("should keep the controller flag if previously set", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Controller: true, + } + + By("not providing it") + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Controller).To(BeTrue()) + + By("providing it") + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Controller: true, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Controller).To(BeTrue()) + }) + + It("should not set the controller flag if not provided and not previously set", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Controller).To(BeFalse()) + }) }) - It("should use domain if the group is empty for version v3", func() { - options := &Options{Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - resource := options.NewResource( - &config.Config{ - Version: config.Version3Alpha, - Domain: "test.io", - Repo: "test", - }, - true, - ) - Expect(resource.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.Group).To(Equal("")) - Expect(resource.GroupPackageName).To(Equal("testio")) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.Plural).To(Equal("firstmates")) - Expect(resource.ImportAlias).To(Equal("testiov1")) - Expect(resource.Package).To(Equal(path.Join("test", "api", "v1"))) - Expect(resource.Domain).To(Equal("test.io")) + + Context("Webhooks", func() { + It("should work with nil Webhooks", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Webhooks: &Webhooks{WebhookVersion: v1}, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Webhooks).NotTo(BeNil()) + Expect(r.Webhooks.WebhookVersion).To(Equal(v1)) + }) + + It("should fail if Webhooks.Update fails", func() { + r = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Webhooks: &Webhooks{WebhookVersion: v1}, + } + other = Resource{ + GVK: GVK{ + Group: group, + Version: version, + Kind: kind, + }, + Webhooks: &Webhooks{WebhookVersion: "v1beta1"}, + } + Expect(r.Update(other)).NotTo(Succeed()) + }) + + // The rest of the cases are tested in Webhooks.Update }) }) + + Context("Replacer", func() { + res := Resource{ + GVK: GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "kinds", + } + replacer := res.Replacer() + + DescribeTable("should replace the following strings", + func(pattern, result string) { Expect(replacer.Replace(pattern)).To(Equal(result)) }, + Entry("no pattern", "version", "version"), + Entry("pattern `%[group]`", "%[group]", res.Group), + Entry("pattern `%[version]`", "%[version]", res.Version), + Entry("pattern `%[kind]`", "%[kind]", "kind"), + Entry("pattern `%[plural]`", "%[plural]", res.Plural), + Entry("pattern `%[package-name]`", "%[package-name]", res.PackageName()), + ) + }) }) diff --git a/pkg/model/resource/resource_suite_test.go b/pkg/model/resource/suite_test.go similarity index 95% rename from pkg/model/resource/resource_suite_test.go rename to pkg/model/resource/suite_test.go index 9f897cb6ce2..eae9ac5c2d8 100644 --- a/pkg/model/resource/resource_suite_test.go +++ b/pkg/model/resource/suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resource_test +package resource import ( "testing" @@ -23,6 +23,8 @@ import ( . "github.com/onsi/gomega" ) +const v1 = "v1" + func TestResource(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Resource Suite") diff --git a/pkg/model/resource/utils.go b/pkg/model/resource/utils.go new file mode 100644 index 00000000000..5d4d4c23516 --- /dev/null +++ b/pkg/model/resource/utils.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 resource + +import ( + "path" + "strings" + + "github.com/gobuffalo/flect" +) + +// safeImport returns a cleaned version of the provided string that can be used for imports +func safeImport(unsafe string) string { + safe := unsafe + + // Remove dashes and dots + safe = strings.Replace(safe, "-", "", -1) + safe = strings.Replace(safe, ".", "", -1) + + return safe +} + +// APIPackagePath returns the default path +func APIPackagePath(repo, group, version string, multiGroup bool) string { + if multiGroup { + if group != "" { + return path.Join(repo, "apis", group, version) + } + return path.Join(repo, "apis", version) + } + return path.Join(repo, "api", version) +} + +// RegularPlural returns a default plural form when none was specified +func RegularPlural(singular string) string { + return flect.Pluralize(strings.ToLower(singular)) +} diff --git a/pkg/model/resource/utils_test.go b/pkg/model/resource/utils_test.go new file mode 100644 index 00000000000..229262ea8ef --- /dev/null +++ b/pkg/model/resource/utils_test.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 resource + +import ( + "path" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = DescribeTable("safeImport should remove unsupported characters", + func(unsafe, safe string) { Expect(safeImport(unsafe)).To(Equal(safe)) }, + Entry("no dots nor dashes", "text", "text"), + Entry("one dot", "my.domain", "mydomain"), + Entry("several dots", "example.my.domain", "examplemydomain"), + Entry("one dash", "example-text", "exampletext"), + Entry("several dashes", "other-example-text", "otherexampletext"), + Entry("both dots and dashes", "my-example.my.domain", "myexamplemydomain"), +) + +var _ = Describe("APIPackagePath", func() { + const ( + repo = "github.com/kubernetes-sigs/kubebuilder" + group = "group" + version = "v1" + ) + + DescribeTable("should work", + func(repo, group, version string, multiGroup bool, p string) { + Expect(APIPackagePath(repo, group, version, multiGroup)).To(Equal(p)) + }, + Entry("single group setup", repo, group, version, false, path.Join(repo, "api", version)), + Entry("multiple group setup", repo, group, version, true, path.Join(repo, "apis", group, version)), + Entry("multiple group setup with empty group", repo, "", version, true, path.Join(repo, "apis", version)), + ) +}) + +var _ = DescribeTable("RegularPlural should return the regular plural form", + func(singular, plural string) { Expect(RegularPlural(singular)).To(Equal(plural)) }, + Entry("basic singular", "firstmate", "firstmates"), + Entry("capitalized singular", "Firstmate", "firstmates"), + Entry("camel-cased singular", "FirstMate", "firstmates"), + Entry("irregular well-known plurals", "fish", "fish"), + Entry("irregular well-known plurals", "helmswoman", "helmswomen"), +) diff --git a/pkg/model/resource/webhooks.go b/pkg/model/resource/webhooks.go new file mode 100644 index 00000000000..1fa745bdbf3 --- /dev/null +++ b/pkg/model/resource/webhooks.go @@ -0,0 +1,76 @@ +/* +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 resource + +import ( + "fmt" +) + +// Webhooks contains information about scaffolded webhooks +type Webhooks struct { + // WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the resource. + WebhookVersion string `json:"webhookVersion,omitempty"` + + // Defaulting specifies if a defaulting webhook is associated to the resource. + Defaulting bool `json:"defaulting,omitempty"` + + // Validation specifies if a validation webhook is associated to the resource. + Validation bool `json:"validation,omitempty"` + + // Conversion specifies if a conversion webhook is associated to the resource. + Conversion bool `json:"conversion,omitempty"` +} + +// Copy returns a deep copy of the API that can be safely modified without affecting the original. +func (webhooks Webhooks) Copy() Webhooks { + // As this function doesn't use a pointer receiver, webhooks is already a shallow copy. + // Any field that is a pointer, slice or map needs to be deep copied. + return webhooks +} + +// Update combines fields of the webhooks of two resources. +func (webhooks *Webhooks) Update(other *Webhooks) error { + // If other is nil, nothing to merge + if other == nil { + return nil + } + + // Update the version. + if other.WebhookVersion != "" { + if webhooks.WebhookVersion == "" { + webhooks.WebhookVersion = other.WebhookVersion + } else if webhooks.WebhookVersion != other.WebhookVersion { + return fmt.Errorf("webhook versions do not match") + } + } + + // Update defaulting. + webhooks.Defaulting = webhooks.Defaulting || other.Defaulting + + // Update validation. + webhooks.Validation = webhooks.Validation || other.Validation + + // Update conversion. + webhooks.Conversion = webhooks.Conversion || other.Conversion + + return nil +} + +// IsEmpty returns if the Webhooks' fields all contain zero-values. +func (webhooks Webhooks) IsEmpty() bool { + return webhooks.WebhookVersion == "" && !webhooks.Defaulting && !webhooks.Validation && !webhooks.Conversion +} diff --git a/pkg/model/resource/webhooks_test.go b/pkg/model/resource/webhooks_test.go new file mode 100644 index 00000000000..f8ea6858f25 --- /dev/null +++ b/pkg/model/resource/webhooks_test.go @@ -0,0 +1,233 @@ +/* +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 resource + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +//nolint:dupl +var _ = Describe("Webhooks", func() { + Context("Update", func() { + var webhook, other Webhooks + + It("should do nothing if provided a nil pointer", func() { + webhook = Webhooks{} + Expect(webhook.Update(nil)).To(Succeed()) + Expect(webhook.WebhookVersion).To(Equal("")) + Expect(webhook.Defaulting).To(BeFalse()) + Expect(webhook.Validation).To(BeFalse()) + Expect(webhook.Conversion).To(BeFalse()) + + webhook = Webhooks{ + WebhookVersion: v1, + Defaulting: true, + Validation: true, + Conversion: true, + } + Expect(webhook.Update(nil)).To(Succeed()) + Expect(webhook.WebhookVersion).To(Equal(v1)) + Expect(webhook.Defaulting).To(BeTrue()) + Expect(webhook.Validation).To(BeTrue()) + Expect(webhook.Conversion).To(BeTrue()) + }) + + Context("webhooks version", func() { + It("should modify the webhooks version if provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{WebhookVersion: v1} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.WebhookVersion).To(Equal(v1)) + }) + + It("should keep the webhooks version if not provided", func() { + webhook = Webhooks{WebhookVersion: v1} + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.WebhookVersion).To(Equal(v1)) + }) + + It("should keep the webhooks version if provided the same as previously set", func() { + webhook = Webhooks{WebhookVersion: v1} + other = Webhooks{WebhookVersion: v1} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.WebhookVersion).To(Equal(v1)) + }) + + It("should fail if previously set and provided webhooks versions do not match", func() { + webhook = Webhooks{WebhookVersion: v1} + other = Webhooks{WebhookVersion: "v1beta1"} + Expect(webhook.Update(&other)).NotTo(Succeed()) + }) + }) + + Context("Defaulting", func() { + It("should set the defaulting webhook if provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{Defaulting: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Defaulting).To(BeTrue()) + }) + + It("should keep the defaulting webhook if previously set", func() { + webhook = Webhooks{Defaulting: true} + + By("not providing it") + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Defaulting).To(BeTrue()) + + By("providing it") + other = Webhooks{Defaulting: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Defaulting).To(BeTrue()) + }) + + It("should not set the defaulting webhook if not provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Defaulting).To(BeFalse()) + }) + }) + + Context("Validation", func() { + It("should set the validation webhook if provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{Validation: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Validation).To(BeTrue()) + }) + + It("should keep the validation webhook if previously set", func() { + webhook = Webhooks{Validation: true} + + By("not providing it") + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Validation).To(BeTrue()) + + By("providing it") + other = Webhooks{Validation: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Validation).To(BeTrue()) + }) + + It("should not set the validation webhook if not provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Validation).To(BeFalse()) + }) + }) + + Context("Conversion", func() { + It("should set the conversion webhook if provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{Conversion: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Conversion).To(BeTrue()) + }) + + It("should keep the conversion webhook if previously set", func() { + webhook = Webhooks{Conversion: true} + + By("not providing it") + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Conversion).To(BeTrue()) + + By("providing it") + other = Webhooks{Conversion: true} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Conversion).To(BeTrue()) + }) + + It("should not set the conversion webhook if not provided and not previously set", func() { + webhook = Webhooks{} + other = Webhooks{} + Expect(webhook.Update(&other)).To(Succeed()) + Expect(webhook.Conversion).To(BeFalse()) + }) + }) + }) + + Context("IsEmpty", func() { + var ( + none = Webhooks{} + defaulting = Webhooks{ + WebhookVersion: "v1", + Defaulting: true, + Validation: false, + Conversion: false, + } + validation = Webhooks{ + WebhookVersion: "v1", + Defaulting: false, + Validation: true, + Conversion: false, + } + conversion = Webhooks{ + WebhookVersion: "v1", + Defaulting: false, + Validation: false, + Conversion: true, + } + defaultingAndValidation = Webhooks{ + WebhookVersion: "v1", + Defaulting: true, + Validation: true, + Conversion: false, + } + defaultingAndConversion = Webhooks{ + WebhookVersion: "v1", + Defaulting: true, + Validation: false, + Conversion: true, + } + validationAndConversion = Webhooks{ + WebhookVersion: "v1", + Defaulting: false, + Validation: true, + Conversion: true, + } + all = Webhooks{ + WebhookVersion: "v1", + Defaulting: true, + Validation: true, + Conversion: true, + } + ) + + It("should return true fo an empty object", func() { + Expect(none.IsEmpty()).To(BeTrue()) + }) + + DescribeTable("should return false for non-empty objects", + func(webhooks Webhooks) { Expect(webhooks.IsEmpty()).To(BeFalse()) }, + Entry("defaulting", defaulting), + Entry("validation", validation), + Entry("conversion", conversion), + Entry("defaulting and validation", defaultingAndValidation), + Entry("defaulting and conversion", defaultingAndConversion), + Entry("validation and conversion", validationAndConversion), + Entry("defaulting and validation and conversion", all), + ) + }) +}) diff --git a/pkg/model/stage/stage.go b/pkg/model/stage/stage.go new file mode 100644 index 00000000000..cf7c001aec5 --- /dev/null +++ b/pkg/model/stage/stage.go @@ -0,0 +1,109 @@ +/* +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 stage + +import ( + "errors" +) + +var errInvalid = errors.New("invalid version stage") + +// Stage represents the stability of a version +type Stage uint8 + +// Order Stage in decreasing degree of stability for comparison purposes. +// Stable must be 0 so that it is the default Stage +const ( // The order in this const declaration will be used to order version stages except for Stable + // Stable should be used for plugins that are rarely changed in backwards-compatible ways, e.g. bug fixes. + Stable Stage = iota + // Beta should be used for plugins that may be changed in minor ways and are not expected to break between uses. + Beta Stage = iota + // Alpha should be used for plugins that are frequently changed and may break between uses. + Alpha Stage = iota +) + +const ( + alpha = "alpha" + beta = "beta" + stable = "" +) + +// ParseStage parses stage into a Stage, assuming it is one of the valid stages +func ParseStage(stage string) (Stage, error) { + var s Stage + return s, s.Parse(stage) +} + +// Parse parses stage inline, assuming it is one of the valid stages +func (s *Stage) Parse(stage string) error { + switch stage { + case alpha: + *s = Alpha + case beta: + *s = Beta + case stable: + *s = Stable + default: + return errInvalid + } + return nil +} + +// String returns the string representation of s +func (s Stage) String() string { + switch s { + case Alpha: + return alpha + case Beta: + return beta + case Stable: + return stable + default: + panic(errInvalid) + } +} + +// Validate ensures that the stage is one of the valid stages +func (s Stage) Validate() error { + switch s { + case Alpha: + case Beta: + case Stable: + default: + return errInvalid + } + + return nil +} + +// Compare returns -1 if s < other, 0 if s == other, and 1 if s > other. +func (s Stage) Compare(other Stage) int { + if s == other { + return 0 + } + + // Stage are sorted in decreasing order + if s > other { + return -1 + } + return 1 +} + +// IsStable returns whether the stage is stable or not +func (s Stage) IsStable() bool { + return s == Stable +} diff --git a/pkg/model/stage/stage_test.go b/pkg/model/stage/stage_test.go new file mode 100644 index 00000000000..561aa688c43 --- /dev/null +++ b/pkg/model/stage/stage_test.go @@ -0,0 +1,132 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stage + +import ( + "sort" + "testing" + + g "github.com/onsi/ginkgo" // An alias is required because Context is defined elsewhere in this package. + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +func TestStage(t *testing.T) { + RegisterFailHandler(g.Fail) + g.RunSpecs(t, "Stage Suite") +} + +var _ = g.Describe("ParseStage", func() { + DescribeTable("should be correctly parsed for valid stage strings", + func(str string, stage Stage) { + s, err := ParseStage(str) + Expect(err).NotTo(HaveOccurred()) + Expect(s).To(Equal(stage)) + }, + Entry("for alpha stage", "alpha", Alpha), + Entry("for beta stage", "beta", Beta), + Entry("for stable stage", "", Stable), + ) + + DescribeTable("should error when parsing invalid stage strings", + func(str string) { + _, err := ParseStage(str) + Expect(err).To(HaveOccurred()) + }, + Entry("passing a number as the stage string", "1"), + Entry("passing `gamma` as the stage string", "gamma"), + Entry("passing a dash-prefixed stage string", "-alpha"), + ) +}) + +var _ = g.Describe("Stage", func() { + g.Context("String", func() { + DescribeTable("should return the correct string value", + func(stage Stage, str string) { Expect(stage.String()).To(Equal(str)) }, + Entry("for alpha stage", Alpha, "alpha"), + Entry("for beta stage", Beta, "beta"), + Entry("for stable stage", Stable, ""), + ) + + DescribeTable("should panic", + func(stage Stage) { Expect(func() { _ = stage.String() }).To(Panic()) }, + Entry("for stage 34", Stage(34)), + Entry("for stage 75", Stage(75)), + Entry("for stage 123", Stage(123)), + Entry("for stage 255", Stage(255)), + ) + }) + + g.Context("Validate", func() { + DescribeTable("should validate existing stages", + func(stage Stage) { Expect(stage.Validate()).To(Succeed()) }, + Entry("for alpha stage", Alpha), + Entry("for beta stage", Beta), + Entry("for stable stage", Stable), + ) + + DescribeTable("should fail for non-existing stages", + func(stage Stage) { Expect(stage.Validate()).NotTo(Succeed()) }, + Entry("for stage 34", Stage(34)), + Entry("for stage 75", Stage(75)), + Entry("for stage 123", Stage(123)), + Entry("for stage 255", Stage(255)), + ) + }) + + g.Context("Compare", func() { + // Test Stage.Compare by sorting a list + var ( + stages = []Stage{ + Stable, + Alpha, + Stable, + Beta, + Beta, + Alpha, + } + + sortedStages = []Stage{ + Alpha, + Alpha, + Beta, + Beta, + Stable, + Stable, + } + ) + + g.It("sorts stages correctly", func() { + sort.Slice(stages, func(i int, j int) bool { + return stages[i].Compare(stages[j]) == -1 + }) + Expect(stages).To(Equal(sortedStages)) + }) + }) + + g.Context("IsStable", func() { + g.It("should return true for stable stage", func() { + Expect(Stable.IsStable()).To(BeTrue()) + }) + + DescribeTable("should return false for any unstable stage", + func(stage Stage) { Expect(stage.IsStable()).To(BeFalse()) }, + Entry("for alpha stage", Alpha), + Entry("for beta stage", Beta), + ) + }) +}) diff --git a/pkg/model/universe.go b/pkg/model/universe.go index 1c148b6b350..15d597c88f7 100644 --- a/pkg/model/universe.go +++ b/pkg/model/universe.go @@ -17,7 +17,7 @@ limitations under the License. package model import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/file" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) @@ -25,7 +25,7 @@ import ( // Universe describes the entire state of file generation type Universe struct { // Config stores the project configuration - Config *config.Config `json:"config,omitempty"` + Config config.Config `json:"config,omitempty"` // Boilerplate is the copyright comment added at the top of scaffolded files Boilerplate string `json:"boilerplate,omitempty"` @@ -53,7 +53,7 @@ func NewUniverse(options ...UniverseOption) *Universe { type UniverseOption func(*Universe) // WithConfig stores the already loaded project configuration -func WithConfig(projectConfig *config.Config) UniverseOption { +func WithConfig(projectConfig config.Config) UniverseOption { return func(universe *Universe) { universe.Config = projectConfig } @@ -83,19 +83,19 @@ 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.Domain) + builderWithDomain.InjectDomain(u.Config.GetDomain()) } if builderWithRepository, hasRepository := builder.(file.HasRepository); hasRepository { - builderWithRepository.InjectRepository(u.Config.Repo) + 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.MultiGroup) + builderWithMultiGroup.InjectMultiGroup(u.Config.IsMultiGroup()) } if builderWithComponentConfig, hasComponentConfig := builder.(file.HasComponentConfig); hasComponentConfig { - builderWithComponentConfig.InjectComponentConfig(u.Config.ComponentConfig) - } - if builderWithProjectName, hasProjectName := builder.(file.HasProjectName); hasProjectName { - builderWithProjectName.InjectProjectName(u.Config.ProjectName) + builderWithComponentConfig.InjectComponentConfig(u.Config.IsComponentConfig()) } } // Inject boilerplate diff --git a/pkg/plugin/helpers.go b/pkg/plugin/helpers.go index 4dafbc78dd7..3a2d43f4e4c 100644 --- a/pkg/plugin/helpers.go +++ b/pkg/plugin/helpers.go @@ -21,6 +21,7 @@ import ( "path" "strings" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" ) @@ -64,7 +65,7 @@ func Validate(p Plugin) error { return fmt.Errorf("plugin %q must support at least one project version", KeyFor(p)) } for _, projectVersion := range p.SupportedProjectVersions() { - if err := validation.ValidateProjectVersion(projectVersion); err != nil { + if err := projectVersion.Validate(); err != nil { return fmt.Errorf("plugin %q supports an invalid project version %q: %v", KeyFor(p), projectVersion, err) } } @@ -79,7 +80,8 @@ func ValidateKey(key string) error { } // CLI-set plugins do not have to contain a version. if version != "" { - if _, err := ParseVersion(version); err != nil { + var v Version + if err := v.Parse(version); err != nil { return fmt.Errorf("invalid plugin version %q: %v", version, err) } } @@ -95,9 +97,9 @@ func validateName(name string) error { } // SupportsVersion checks if a plugins supports a project version. -func SupportsVersion(p Plugin, projectVersion string) bool { +func SupportsVersion(p Plugin, projectVersion config.Version) bool { for _, version := range p.SupportedProjectVersions() { - if version == projectVersion { + if projectVersion.Compare(version) == 0 { return true } } diff --git a/pkg/plugin/interfaces.go b/pkg/plugin/interfaces.go index bd6420f7814..1c1cf2da01a 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/interfaces.go @@ -19,7 +19,7 @@ package plugin import ( "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" ) // Plugin is an interface that defines the common base for all plugins @@ -32,9 +32,9 @@ type Plugin interface { // // NOTE: this version is different from config version. Version() Version - // SupportedProjectVersions lists all project configuration versions this plugin supports, ex. []string{"2", "3"}. + // SupportedProjectVersions lists all project configuration versions this plugin supports. // The returned slice cannot be empty. - SupportedProjectVersions() []string + SupportedProjectVersions() []config.Version } // Deprecated is an interface that defines the messages for plugins that are deprecated. @@ -56,7 +56,7 @@ type Subcommand interface { 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) + InjectConfig(config.Config) } // Context is the runtime context for a subcommand. diff --git a/pkg/plugin/version.go b/pkg/plugin/version.go index 1196afdce1e..8f8a4bed430 100644 --- a/pkg/plugin/version.go +++ b/pkg/plugin/version.go @@ -1,3 +1,19 @@ +/* +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 ( @@ -5,146 +21,66 @@ import ( "fmt" "strconv" "strings" -) -var ( - errInvalidStage = errors.New("invalid version stage") - errInvalidVersion = errors.New("version number must be positive") - errEmptyPlugin = errors.New("plugin version is empty") -) - -// Stage represents the stability of a version -type Stage uint8 - -// Order Stage in decreasing degree of stability for comparison purposes. -// StableStage must be 0 so that it is the default Stage -const ( // The order in this const declaration will be used to order version stages except for StableStage - // StableStage should be used for plugins that are rarely changed in backwards-compatible ways, e.g. bug fixes. - StableStage Stage = iota - // BetaStage should be used for plugins that may be changed in minor ways and are not expected to break between uses. - BetaStage Stage = iota - // AlphaStage should be used for plugins that are frequently changed and may break between uses. - AlphaStage Stage = iota + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" ) -const ( - alphaStage = "alpha" - betaStage = "beta" - stableStage = "" +var ( + errNegative = errors.New("plugin version number must be positive") + errEmpty = errors.New("plugin version is empty") ) -// ParseStage parses stage into a Stage, assuming it is one of the valid stages -func ParseStage(stage string) (Stage, error) { - var s Stage - return s, s.Parse(stage) -} - -// Parse parses stage inline, assuming it is one of the valid stages -func (s *Stage) Parse(stage string) error { - switch stage { - case alphaStage: - *s = AlphaStage - case betaStage: - *s = BetaStage - case stableStage: - *s = StableStage - default: - return errInvalidVersion - } - return nil -} - -// String returns the string representation of s -func (s Stage) String() string { - switch s { - case AlphaStage: - return alphaStage - case BetaStage: - return betaStage - case StableStage: - return stableStage - default: - panic(errInvalidStage) - } -} - -// Validate ensures that the stage is one of the valid stages -func (s Stage) Validate() error { - switch s { - case AlphaStage: - case BetaStage: - case StableStage: - default: - return errInvalidStage - } - - return nil -} - -// Compare returns -1 if s < other, 0 if s == other, and 1 if s > other. -func (s Stage) Compare(other Stage) int { - if s == other { - return 0 - } - - // Stage are sorted in decreasing order - if s > other { - return -1 - } - return 1 -} - -// Version is a plugin version containing a non-zero positive integer and a stage value that represents stability. +// Version is a plugin version containing a positive integer and a stage value that represents stability. type Version struct { // Number denotes the current version of a plugin. Two different numbers between versions // indicate that they are incompatible. - Number int64 + Number int // Stage indicates stability. - Stage Stage + Stage stage.Stage } -// ParseVersion parses version into a Version, assuming it adheres to format: (v)?[1-9][0-9]*(-(alpha|beta))? -func ParseVersion(version string) (Version, error) { - var v Version - return v, v.Parse(version) -} - -// Parse parses version inline, assuming it adheres to format: (v)?[1-9][0-9]*(-(alpha|beta))? -func (v *Version) Parse(version string) (err error) { +// Parse parses version inline, assuming it adheres to format: (v)?[0-9]*(-(alpha|beta))? +func (v *Version) Parse(version string) error { version = strings.TrimPrefix(version, "v") if len(version) == 0 { - return errEmptyPlugin + return errEmpty } substrings := strings.SplitN(version, "-", 2) - if v.Number, err = strconv.ParseInt(substrings[0], 10, 64); err != nil { - return - } else if v.Number < 1 { - return errInvalidVersion + var err error + if v.Number, err = strconv.Atoi(substrings[0]); err != nil { + // Lets check if the `-` belonged to a negative number + if n, err := strconv.Atoi(version); err == nil && n < 0 { + return errNegative + } + return err } - if len(substrings) == 1 { - v.Stage = StableStage - } else { - err = v.Stage.Parse(substrings[1]) + if len(substrings) > 1 { + if err = v.Stage.Parse(substrings[1]); err != nil { + return err + } } - return + + return nil } -// String returns the string representation of v +// String returns the string representation of v. func (v Version) String() string { - if len(v.Stage.String()) == 0 { + stageStr := v.Stage.String() + if len(stageStr) == 0 { return fmt.Sprintf("v%d", v.Number) } - return fmt.Sprintf("v%d-%s", v.Number, v.Stage) + return fmt.Sprintf("v%d-%s", v.Number, stageStr) } -// Validate ensures that the version number is positive and the stage is one of the valid stages +// Validate ensures that the version number is positive and the stage is one of the valid stages. func (v Version) Validate() error { if v.Number < 0 { - return errInvalidVersion + return errNegative } + return v.Stage.Validate() } @@ -161,5 +97,11 @@ func (v Version) Compare(other Version) int { // IsStable returns true if v is stable. func (v Version) IsStable() bool { - return v.Stage.Compare(StableStage) == -1 + // Plugin version 0 is not considered stable + if v.Number == 0 { + return false + } + + // Any other version than 0 depends on its stage field + return v.Stage.IsStable() } diff --git a/pkg/plugin/version_test.go b/pkg/plugin/version_test.go index 7a65f7e5531..f1512dbf7d8 100644 --- a/pkg/plugin/version_test.go +++ b/pkg/plugin/version_test.go @@ -21,282 +21,172 @@ import ( "testing" g "github.com/onsi/ginkgo" // An alias is required because Context is defined elsewhere in this package. + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" ) -func TestCLI(t *testing.T) { +func TestPlugin(t *testing.T) { RegisterFailHandler(g.Fail) g.RunSpecs(t, "Plugin Suite") } -var _ = g.Describe("ParseStage", func() { - var ( - s Stage - err error - ) - - g.It("should be correctly parsed for valid stage strings", func() { - g.By("passing an empty stage string") - s, err = ParseStage("") - Expect(err).NotTo(HaveOccurred()) - Expect(s).To(Equal(StableStage)) - - g.By("passing `alpha` as the stage string") - s, err = ParseStage("alpha") - Expect(err).NotTo(HaveOccurred()) - Expect(s).To(Equal(AlphaStage)) - - g.By("passing `beta` as the stage string") - s, err = ParseStage("beta") - Expect(err).NotTo(HaveOccurred()) - Expect(s).To(Equal(BetaStage)) - }) - - g.It("should error when parsing invalid stage strings", func() { - g.By("passing a number as the stage string") - s, err = ParseStage("1") - Expect(err).To(HaveOccurred()) - - g.By("passing `gamma` as the stage string") - s, err = ParseStage("gamma") - Expect(err).To(HaveOccurred()) - - g.By("passing a dash-prefixed stage string") - s, err = ParseStage("-alpha") - Expect(err).To(HaveOccurred()) +var _ = g.Describe("Version", func() { + g.Context("Parse", func() { + DescribeTable("should be correctly parsed for valid version strings", + func(str string, number int, s stage.Stage) { + var v Version + Expect(v.Parse(str)).To(Succeed()) + Expect(v.Number).To(Equal(number)) + Expect(v.Stage).To(Equal(s)) + }, + Entry("for version string `0`", "0", 0, stage.Stable), + Entry("for version string `0-alpha`", "0-alpha", 0, stage.Alpha), + Entry("for version string `0-beta`", "0-beta", 0, stage.Beta), + Entry("for version string `1`", "1", 1, stage.Stable), + Entry("for version string `1-alpha`", "1-alpha", 1, stage.Alpha), + Entry("for version string `1-beta`", "1-beta", 1, stage.Beta), + Entry("for version string `v1`", "v1", 1, stage.Stable), + Entry("for version string `v1-alpha`", "v1-alpha", 1, stage.Alpha), + Entry("for version string `v1-beta`", "v1-beta", 1, stage.Beta), + Entry("for version string `22`", "22", 22, stage.Stable), + Entry("for version string `22-alpha`", "22-alpha", 22, stage.Alpha), + Entry("for version string `22-beta`", "22-beta", 22, stage.Beta), + ) + + DescribeTable("should error when parsing an invalid version string", + func(str string) { + var v Version + Expect(v.Parse(str)).NotTo(Succeed()) + }, + Entry("for version string ``", ""), + Entry("for version string `-1`", "-1"), + Entry("for version string `-1-alpha`", "-1-alpha"), + Entry("for version string `-1-beta`", "-1-beta"), + Entry("for version string `1.0`", "1.0"), + Entry("for version string `v1.0`", "v1.0"), + Entry("for version string `v1.0-alpha`", "v1.0-alpha"), + Entry("for version string `1.0.0`", "1.0.0"), + Entry("for version string `1-a`", "1-a"), + ) }) -}) - -var _ = g.Describe("Stage.String", func() { - g.It("should return the correct string value", func() { - g.By("for stable stage") - Expect(StableStage.String()).To(Equal(stableStage)) - g.By("for alpha stage") - Expect(AlphaStage.String()).To(Equal(alphaStage)) - - g.By("for beta stage") - Expect(BetaStage.String()).To(Equal(betaStage)) + g.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"), + Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}, "v0"), + Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}, "v0-alpha"), + Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}, "v0-beta"), + Entry("for version 0 (implicit)", Version{}, "v0"), + Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}, "v0"), + Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}, "v0-alpha"), + Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}, "v0-beta"), + Entry("for version 1", Version{Number: 1}, "v1"), + Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}, "v1"), + Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}, "v1-alpha"), + Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}, "v1-beta"), + Entry("for version 22", Version{Number: 22}, "v22"), + Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}, "v22"), + Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}, "v22-alpha"), + Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}, "v22-beta"), + ) }) -}) -var _ = g.Describe("Stage.Validate", func() { - g.It("should validate existing stages", func() { - g.By("for stable stage") - Expect(StableStage.Validate()).To(Succeed()) - - g.By("for alpha stage") - Expect(AlphaStage.Validate()).To(Succeed()) - - g.By("for beta stage") - Expect(BetaStage.Validate()).To(Succeed()) + g.Context("Validate", func() { + DescribeTable("should validate valid versions", + func(version Version) { Expect(version.Validate()).To(Succeed()) }, + Entry("for version 0", Version{Number: 0}), + Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), + Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), + Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), + Entry("for version 0 (implicit)", Version{}), + Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), + Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), + Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), + Entry("for version 1", Version{Number: 1}), + Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), + Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), + Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), + Entry("for version 22", Version{Number: 22}), + Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), + Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), + Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), + ) + + DescribeTable("should fail for invalid versions", + func(version Version) { Expect(version.Validate()).NotTo(Succeed()) }, + Entry("for version -1", Version{Number: -1}), + Entry("for version -1 (stable)", Version{Number: -1, Stage: stage.Stable}), + Entry("for version -1 (alpha)", Version{Number: -1, Stage: stage.Alpha}), + Entry("for version -1 (beta)", Version{Number: -1, Stage: stage.Beta}), + Entry("for invalid stage", Version{Stage: stage.Stage(34)}), + ) }) - g.It("should fail for non-existing stages", func() { - Expect(Stage(34).Validate()).NotTo(Succeed()) - Expect(Stage(75).Validate()).NotTo(Succeed()) - Expect(Stage(123).Validate()).NotTo(Succeed()) - Expect(Stage(255).Validate()).NotTo(Succeed()) - }) -}) - -var _ = g.Describe("Stage.Compare", func() { - // Test Stage.Compare by sorting a list - var ( - stages = []Stage{ - StableStage, - BetaStage, - AlphaStage, - } - - sortedStages = []Stage{ - AlphaStage, - BetaStage, - StableStage, - } - ) - g.It("sorts stages correctly", func() { - sort.Slice(stages, func(i int, j int) bool { - return stages[i].Compare(stages[j]) == -1 + g.Context("Compare", func() { + // Test Compare() by sorting a list. + var ( + versions = []Version{ + {Number: 2, Stage: stage.Alpha}, + {Number: 44, Stage: stage.Alpha}, + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 4, Stage: stage.Beta}, + {Number: 1, Stage: stage.Alpha}, + {Number: 4}, + {Number: 44, Stage: stage.Alpha}, + {Number: 30}, + {Number: 4, Stage: stage.Alpha}, + } + + sortedVersions = []Version{ + {Number: 1, Stage: stage.Alpha}, + {Number: 1}, + {Number: 2, Stage: stage.Alpha}, + {Number: 2, Stage: stage.Beta}, + {Number: 4, Stage: stage.Alpha}, + {Number: 4, Stage: stage.Beta}, + {Number: 4}, + {Number: 30}, + {Number: 44, Stage: stage.Alpha}, + {Number: 44, Stage: stage.Alpha}, + } + ) + + g.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 + }) + Expect(versions).To(Equal(sortedVersions)) }) - Expect(stages).To(Equal(sortedStages)) - }) -}) -var _ = g.Describe("ParseVersion", func() { - var ( - v Version - err error - ) - - g.It("should be correctly parsed when a version is positive without a stage", func() { - g.By("passing version string 1") - v, err = ParseVersion("1") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(1))) - Expect(v.Stage).To(Equal(StableStage)) - - g.By("passing version string 22") - v, err = ParseVersion("22") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(22))) - Expect(v.Stage).To(Equal(StableStage)) - - g.By("passing version string v1") - v, err = ParseVersion("v1") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(1))) - Expect(v.Stage).To(Equal(StableStage)) }) - g.It("should be correctly parsed when a version is positive with a stage", func() { - g.By("passing version string 1-alpha") - v, err = ParseVersion("1-alpha") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(1))) - Expect(v.Stage).To(Equal(AlphaStage)) - - g.By("passing version string 1-beta") - v, err = ParseVersion("1-beta") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(1))) - Expect(v.Stage).To(Equal(BetaStage)) - - g.By("passing version string v1-alpha") - v, err = ParseVersion("v1-alpha") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(1))) - Expect(v.Stage).To(Equal(AlphaStage)) - - g.By("passing version string v22-alpha") - v, err = ParseVersion("v22-alpha") - Expect(err).NotTo(HaveOccurred()) - Expect(v.Number).To(BeNumerically("==", int64(22))) - Expect(v.Stage).To(Equal(AlphaStage)) + g.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}), + Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), + Entry("for version 22", Version{Number: 22}), + Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), + ) + + DescribeTable("should return false for unstable versions", + func(version Version) { Expect(version.IsStable()).To(BeFalse()) }, + Entry("for version 0", Version{Number: 0}), + Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), + Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), + Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), + Entry("for version 0 (implicit)", Version{}), + Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), + Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), + Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), + Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), + Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), + Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), + Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), + ) }) - - g.It("should fail for invalid version strings", func() { - g.By("passing version string an empty string") - _, err = ParseVersion("") - Expect(err).To(HaveOccurred()) - - g.By("passing version string 0") - _, err = ParseVersion("0") - Expect(err).To(HaveOccurred()) - - g.By("passing a negative number version string") - _, err = ParseVersion("-1") - Expect(err).To(HaveOccurred()) - }) - - g.It("should fail validation when a version string is semver", func() { - g.By("passing version string 1.0") - _, err = ParseVersion("1.0") - Expect(err).To(HaveOccurred()) - - g.By("passing version string v1.0") - _, err = ParseVersion("v1.0") - Expect(err).To(HaveOccurred()) - - g.By("passing version string v1.0-alpha") - _, err = ParseVersion("v1.0-alpha") - Expect(err).To(HaveOccurred()) - - g.By("passing version string 1.0.0") - _, err = ParseVersion("1.0.0") - Expect(err).To(HaveOccurred()) - }) -}) - -var _ = g.Describe("Version.String", func() { - g.It("should return the correct string value", func() { - g.By("for stable version 1") - Expect(Version{Number: 1}.String()).To(Equal("v1")) - Expect(Version{Number: 1, Stage: StableStage}.String()).To(Equal("v1")) - - g.By("for stable version 22") - Expect(Version{Number: 22}.String()).To(Equal("v22")) - Expect(Version{Number: 22, Stage: StableStage}.String()).To(Equal("v22")) - - g.By("for alpha version 1") - Expect(Version{Number: 1, Stage: AlphaStage}.String()).To(Equal("v1-alpha")) - - g.By("for beta version 1") - Expect(Version{Number: 1, Stage: BetaStage}.String()).To(Equal("v1-beta")) - - g.By("for alpha version 22") - Expect(Version{Number: 22, Stage: AlphaStage}.String()).To(Equal("v22-alpha")) - }) -}) - -var _ = g.Describe("Version.Validate", func() { - g.It("should success for a positive version without a stage", func() { - g.By("passing version 0") - Expect(Version{}.Validate()).To(Succeed()) - Expect(Version{Number: 0}.Validate()).To(Succeed()) - - g.By("for version 1") - Expect(Version{Number: 1}.Validate()).To(Succeed()) - - g.By("for version 22") - Expect(Version{Number: 22}.Validate()).To(Succeed()) - }) - - g.It("should success for a positive version with a stage", func() { - g.By("for version 1 alpha") - Expect(Version{Number: 1, Stage: AlphaStage}.Validate()).To(Succeed()) - - g.By("for version 1 beta") - Expect(Version{Number: 1, Stage: BetaStage}.Validate()).To(Succeed()) - - g.By("for version 22 alpha") - Expect(Version{Number: 22, Stage: AlphaStage}.Validate()).To(Succeed()) - }) - - g.It("should fail for invalid versions", func() { - g.By("passing a negative version") - Expect(Version{Number: -1}.Validate()).NotTo(Succeed()) - - g.By("passing an invalid stage") - Expect(Version{Number: 1, Stage: Stage(173)}.Validate()).NotTo(Succeed()) - }) -}) - -var _ = g.Describe("Version.Compare", func() { - // Test Compare() by sorting a list. - var ( - versions = []Version{ - {Number: 2, Stage: AlphaStage}, - {Number: 44, Stage: AlphaStage}, - {Number: 1}, - {Number: 2, Stage: BetaStage}, - {Number: 4, Stage: BetaStage}, - {Number: 1, Stage: AlphaStage}, - {Number: 4}, - {Number: 44, Stage: AlphaStage}, - {Number: 30}, - {Number: 4, Stage: AlphaStage}, - } - - sortedVersions = []Version{ - {Number: 1, Stage: AlphaStage}, - {Number: 1}, - {Number: 2, Stage: AlphaStage}, - {Number: 2, Stage: BetaStage}, - {Number: 4, Stage: AlphaStage}, - {Number: 4, Stage: BetaStage}, - {Number: 4}, - {Number: 30}, - {Number: 44, Stage: AlphaStage}, - {Number: 44, Stage: AlphaStage}, - } - ) - - g.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 - }) - Expect(versions).To(Equal(sortedVersions)) - }) - }) diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go new file mode 100644 index 00000000000..5dd87ad4820 --- /dev/null +++ b/pkg/plugins/golang/options.go @@ -0,0 +1,238 @@ +/* +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 golang + +import ( + "fmt" + "path" + "regexp" + "strings" + + newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$" + groupPresent = "group flag present but empty" + versionPresent = "version flag present but empty" + kindPresent = "kind flag present but empty" + versionRequired = "version cannot be empty" + kindRequired = "kind cannot be empty" +) + +var ( + versionRegex = regexp.MustCompile(versionPattern) + + 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.Version) == 0 { + return fmt.Errorf(versionRequired) + } + if len(opts.Kind) == 0 { + return fmt.Errorf(kindRequired) + } + + // Check if the qualified group has a valid DNS1123 subdomain value + if err := validation.IsDNS1123Subdomain(opts.QualifiedGroup()); err != nil { + return fmt.Errorf("either group or domain is invalid: (%v)", err) + } + + // Check if the version follows the valid pattern + if !versionRegex.MatchString(opts.Version) { + return fmt.Errorf("version must match %s (was %s)", versionPattern, opts.Version) + } + + validationErrors := []string{} + + // Require kind to start with an uppercase character + if string(opts.Kind[0]) == strings.ToLower(string(opts.Kind[0])) { + validationErrors = append(validationErrors, "kind must start with an uppercase character") + } + + validationErrors = append(validationErrors, validation.IsDNS1035Label(strings.ToLower(opts.Kind))...) + + if len(validationErrors) != 0 { + return fmt.Errorf("invalid Kind: %#v", validationErrors) + } + + // TODO: validate plural strings if provided + + // Ensure apiVersions for k8s types are empty or valid. + for typ, apiVersion := range map[string]string{ + "CRD": opts.CRDVersion, + "Webhook": opts.WebhookVersion, + } { + switch apiVersion { + case "", "v1", "v1beta1": + default: + return fmt.Errorf("%s version must be one of: v1, v1beta1", typ) + } + } + + return nil +} + +// QualifiedGroup returns the fully qualified group name with the available information. +func (opts Options) QualifiedGroup() string { + switch "" { + case opts.Domain: + return opts.Group + case opts.Group: + return opts.Domain + default: + return fmt.Sprintf("%s.%s", opts.Group, opts.Domain) + } +} + +// 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(), + Path: resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()), + Controller: opts.DoController, + } + + 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.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.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 { + if !c.HasResource(opts.GVK()) { + 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/options_test.go b/pkg/plugins/golang/options_test.go new file mode 100644 index 00000000000..27e4db5cdac --- /dev/null +++ b/pkg/plugins/golang/options_test.go @@ -0,0 +1,300 @@ +/* +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 golang + +import ( + "path" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" +) + +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"}), + Entry("kind with multiple initial uppercase characters", + &Options{Group: "crew", Domain: "test.io", 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 and domain", + &Options{Version: "v1", Kind: "FirstMate"}), + Entry("group with uppercase characters", + &Options{Group: "Crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Entry("group with non-alpha characters", + &Options{Group: "crew1*?", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Entry("missing version", + &Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), + Entry("version without v prefix", + &Options{Group: "crew", Domain: "test.io", Version: "1", Kind: "FirstMate"}), + Entry("unstable version without v prefix", + &Options{Group: "crew", Domain: "test.io", Version: "1beta1", Kind: "FirstMate"}), + Entry("unstable version with wrong prefix", + &Options{Group: "crew", Domain: "test.io", Version: "a1beta1", Kind: "FirstMate"}), + Entry("unstable version without alpha/beta number", + &Options{Group: "crew", Domain: "test.io", Version: "v1beta", Kind: "FirstMate"}), + Entry("multiple unstable version", + &Options{Group: "crew", Domain: "test.io", Version: "v1beta1alpha1", Kind: "FirstMate"}), + Entry("missing kind", + &Options{Group: "crew", Domain: "test.io", Version: "v1"}), + Entry("kind is too long", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: strings.Repeat("a", 64)}), + Entry("kind with whitespaces", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "First Mate"}), + Entry("kind ends with `-`", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate-"}), + Entry("kind starts with a decimal character", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "1FirstMate"}), + Entry("kind starts with a lowercase character", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "firstMate"}), + Entry("Invalid CRD version", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate", CRDVersion: "a"}), + Entry("Invalid webhook version", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate", WebhookVersion: "a"}), + ) + }) + + Context("NewResource", func() { + var cfg config.Config + + BeforeEach(func() { + cfg = cfgv3alpha.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()) + } + + resource := options.NewResource(cfg) + 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)) + 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.API.CRDVersion).To(Equal(options.CRDVersion)) + Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) + Expect(resource.Controller).To(Equal(options.DoController)) + 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.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: "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", + DoDefaulting: true, + DoValidation: true, + DoConversion: true, + WebhookVersion: "v1", + }), + ) + + 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()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + 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", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) + Expect(resource.API.CRDVersion).To(Equal("")) + Expect(resource.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", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + 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/suite_test.go b/pkg/plugins/golang/suite_test.go new file mode 100644 index 00000000000..2e805c0e98d --- /dev/null +++ b/pkg/plugins/golang/suite_test.go @@ -0,0 +1,29 @@ +/* +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 golang + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestGoPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Go Plugin Suite") +} diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index e4ab7b8e7d4..6516e75af7a 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -27,9 +27,8 @@ import ( "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/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "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" @@ -38,18 +37,16 @@ import ( ) type createAPISubcommand struct { - config *config.Config + config config.Config // pattern indicates that we should use a plugin to build according to a pattern pattern string - resource *resource.Options + options *Options // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag controllerFlag *pflag.Flag - doResource bool - doController bool // force indicates that the resource should be created even if it already exists force bool @@ -96,13 +93,6 @@ After the scaffold is written, api will run make on the project. func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run make after generating files") - fs.BoolVar(&p.doResource, "resource", true, - "if set, generate the resource without prompting the user") - p.resourceFlag = fs.Lookup("resource") - fs.BoolVar(&p.doController, "controller", true, - "if set, generate the controller without prompting the user") - p.controllerFlag = fs.Lookup("controller") - if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { fs.StringVar(&p.pattern, "pattern", "", "generates an API following an extension pattern (addon)") @@ -110,14 +100,26 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - p.resource = &resource.Options{} - fs.StringVar(&p.resource.Kind, "kind", "", "resource Kind") - fs.StringVar(&p.resource.Group, "group", "", "resource Group") - fs.StringVar(&p.resource.Version, "version", "", "resource Version") - fs.BoolVar(&p.resource.Namespaced, "namespaced", true, "resource is namespaced") + + 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 + + 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, + "if set, generate the controller without prompting the user") + p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c *config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) { p.config = c } @@ -126,29 +128,29 @@ func (p *createAPISubcommand) Run() error { } func (p *createAPISubcommand) Validate() error { - if err := p.resource.ValidateV2(); err != nil { + if err := p.options.Validate(); err != nil { return err } reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") - p.doResource = util.YesNo(reader) + p.options.DoAPI = util.YesNo(reader) } if !p.controllerFlag.Changed { fmt.Println("Create Controller [y/n]") - p.doController = util.YesNo(reader) + p.options.DoController = util.YesNo(reader) } // In case we want to scaffold a resource API we need to do some checks - if p.doResource { + if p.options.DoAPI { // Check that resource doesn't exist or flag force was set - if !p.force && p.config.GetResource(p.resource.Data()) != nil { + if !p.force && p.config.HasResource(p.options.GVK()) { return errors.New("API resource already exists") } // Check that the provided group can be added to the project - if !p.config.MultiGroup && len(p.config.Resources) != 0 && !p.config.HasGroup(p.resource.Group) { + if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.options.Group) { return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s", "kubebuilder.io/migration/multi-group.html") } @@ -175,9 +177,9 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unknown pattern %q", p.pattern) } - // Create the actual resource from the resource options - res := p.resource.NewResource(p.config, p.doResource) - return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.doResource, p.doController, p.force, plugins), nil + // Create the resource from the options + res := p.options.NewResource(p.config) + return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.force, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go index 43c1b134602..7c232a44bec 100644 --- a/pkg/plugins/golang/v2/edit.go +++ b/pkg/plugins/golang/v2/edit.go @@ -21,14 +21,14 @@ import ( "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "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" ) type editSubcommand struct { - config *config.Config + config config.Config multigroup bool } @@ -53,11 +53,7 @@ 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) { - // v3 project configs get a 'layout' value. - if c.IsV3() { - c.Layout = plugin.KeyFor(Plugin{}) - } +func (p *editSubcommand) InjectConfig(c config.Config) { p.config = c } diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index eb95986236a..ebd8aacb6d4 100644 --- a/pkg/plugins/golang/v2/init.go +++ b/pkg/plugins/golang/v2/init.go @@ -24,8 +24,9 @@ import ( "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" - "sigs.k8s.io/kubebuilder/v3/pkg/model/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" @@ -33,7 +34,8 @@ import ( ) type initSubcommand struct { - config *config.Config + config config.Config + // For help text. commandName string @@ -41,6 +43,11 @@ type initSubcommand struct { license string owner string + // config options + domain string + repo string + name string + // flags fetchDeps bool skipGoVersionCheck bool @@ -85,19 +92,20 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright") // project args - fs.StringVar(&p.config.Repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+ + 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.") - fs.StringVar(&p.config.Domain, "domain", "my.domain", "domain for groups") - if p.config.IsV3() { - fs.StringVar(&p.config.ProjectName, "project-name", "", "name of this project") + if p.config.GetVersion().Compare(cfgv3alpha.Version) >= 0 { + fs.StringVar(&p.name, "project-name", "", "name of this project") } } -func (p *initSubcommand) InjectConfig(c *config.Config) { - // v3 project configs get a 'layout' value. - if c.IsV3() { - c.Layout = plugin.KeyFor(Plugin{}) +func (p *initSubcommand) InjectConfig(c config.Config) { + // v2+ project configs get a 'layout' value. + if c.GetVersion().Compare(cfgv3alpha.Version) >= 0 { + _ = c.SetLayout(plugin.KeyFor(Plugin{})) } + p.config = c } @@ -113,36 +121,46 @@ func (p *initSubcommand) Validate() error { } } - // Check if the project name is a valid k8s namespace (DNS 1123 label). - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error getting current directory: %v", err) - } - projectName := strings.ToLower(filepath.Base(dir)) - if p.config.IsV3() { - if p.config.ProjectName == "" { - p.config.ProjectName = projectName - } else { - projectName = p.config.ProjectName + if p.config.GetVersion().Compare(cfgv3alpha.Version) >= 0 { + // Assign a default project name + if p.name == "" { + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting current directory: %v", err) + } + p.name = strings.ToLower(filepath.Base(dir)) + } + // Check if the project name is a valid k8s namespace (DNS 1123 label). + if err := validation.IsDNS1123Label(p.name); err != nil { + return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } - } - if err := validation.IsDNS1123Label(projectName); err != nil { - return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) } // Try to guess repository if flag is not set. - if p.config.Repo == "" { + if p.repo == "" { repoPath, err := util.FindCurrentRepo() if err != nil { return fmt.Errorf("error finding current repository: %v", err) } - p.config.Repo = repoPath + 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(cfgv3alpha.Version) >= 0 { + if err := p.config.SetName(p.name); err != nil { + return nil, err + } + } + return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil } diff --git a/pkg/plugins/golang/v2/options.go b/pkg/plugins/golang/v2/options.go new file mode 100644 index 00000000000..692a57123c7 --- /dev/null +++ b/pkg/plugins/golang/v2/options.go @@ -0,0 +1,241 @@ +/* +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" + "regexp" + "strings" + + newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + v1beta1 = "v1beta1" + v1 = "v1" + + versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$" + 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 ( + versionRegex = regexp.MustCompile(versionPattern) + + 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) + } + + // Check if the qualified group has a valid DNS1123 subdomain value + if err := validation.IsDNS1123Subdomain(opts.QualifiedGroup()); err != nil { + return fmt.Errorf("either group or domain is invalid: (%v)", err) + } + + // Check if the version follows the valid pattern + if !versionRegex.MatchString(opts.Version) { + return fmt.Errorf("version must match %s (was %s)", versionPattern, opts.Version) + } + + validationErrors := []string{} + + // Require kind to start with an uppercase character + if string(opts.Kind[0]) == strings.ToLower(string(opts.Kind[0])) { + validationErrors = append(validationErrors, "kind must start with an uppercase character") + } + + validationErrors = append(validationErrors, validation.IsDNS1035Label(strings.ToLower(opts.Kind))...) + + if len(validationErrors) != 0 { + return fmt.Errorf("invalid Kind: %#v", validationErrors) + } + + // TODO: validate plural strings if provided + + // Ensure apiVersions for k8s types are empty or valid. + for typ, apiVersion := range map[string]string{ + "CRD": opts.CRDVersion, + "Webhook": opts.WebhookVersion, + } { + switch apiVersion { + case "", v1beta1, v1: + default: + return fmt.Errorf("%s version must be one of: v1, v1beta1", typ) + } + } + + return nil +} + +// QualifiedGroup returns the fully qualified group name with the available information. +func (opts Options) QualifiedGroup() string { + if opts.Domain == "" { + return opts.Group + } + return fmt.Sprintf("%s.%s", opts.Group, opts.Domain) +} + +// 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(), + Path: resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()), + 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.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.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 { + if !c.HasResource(opts.GVK()) { + 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 new file mode 100644 index 00000000000..038121e21ba --- /dev/null +++ b/pkg/plugins/golang/v2/options_test.go @@ -0,0 +1,270 @@ +/* +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" + "strings" + + . "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"}), + Entry("kind with multiple initial uppercase characters", + &Options{Group: "crew", Domain: "test.io", 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("group with uppercase characters", + &Options{Group: "Crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Entry("group with non-alpha characters", + &Options{Group: "crew1*?", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Entry("missing version", + &Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), + Entry("version without v prefix", + &Options{Group: "crew", Domain: "test.io", Version: "1", Kind: "FirstMate"}), + Entry("unstable version without v prefix", + &Options{Group: "crew", Domain: "test.io", Version: "1beta1", Kind: "FirstMate"}), + Entry("unstable version with wrong prefix", + &Options{Group: "crew", Domain: "test.io", Version: "a1beta1", Kind: "FirstMate"}), + Entry("unstable version without alpha/beta number", + &Options{Group: "crew", Domain: "test.io", Version: "v1beta", Kind: "FirstMate"}), + Entry("multiple unstable version", + &Options{Group: "crew", Domain: "test.io", Version: "v1beta1alpha1", Kind: "FirstMate"}), + Entry("missing kind", + &Options{Group: "crew", Domain: "test.io", Version: "v1"}), + Entry("kind is too long", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: strings.Repeat("a", 64)}), + Entry("kind with whitespaces", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "First Mate"}), + Entry("kind ends with `-`", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate-"}), + Entry("kind starts with a decimal character", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "1FirstMate"}), + Entry("kind starts with a lowercase character", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "firstMate"}), + Entry("Invalid CRD version", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate", CRDVersion: "a"}), + Entry("Invalid webhook version", + &Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate", WebhookVersion: "a"}), + ) + }) + + 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()) + } + + resource := options.NewResource(cfg) + 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)) + 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.API.CRDVersion).To(Equal(options.CRDVersion)) + Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) + Expect(resource.Controller).To(Equal(options.DoController)) + 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.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()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + 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", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + if multiGroup { + Expect(cfg.SetMultiGroup()).To(Succeed()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + 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()) + } + + resource := options.NewResource(cfg) + Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) + Expect(resource.API.CRDVersion).To(Equal("")) + 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 d106e3f53d4..314ba74d4b4 100644 --- a/pkg/plugins/golang/v2/plugin.go +++ b/pkg/plugins/golang/v2/plugin.go @@ -17,7 +17,9 @@ limitations under the License. package v2 import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) @@ -25,7 +27,7 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []string{config.Version2, config.Version3Alpha} + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3alpha.Version} pluginVersion = plugin.Version{Number: 2} ) @@ -46,7 +48,7 @@ func (Plugin) Name() string { return pluginName } func (Plugin) Version() plugin.Version { return pluginVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin -func (Plugin) SupportedProjectVersions() []string { return supportedProjectVersions } +func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } // GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand } diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go index 02088a8b5cf..37a3c0772ee 100644 --- a/pkg/plugins/golang/v2/scaffolds/api.go +++ b/pkg/plugins/golang/v2/scaffolds/api.go @@ -19,8 +19,8 @@ package scaffolds import ( "fmt" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api" @@ -42,61 +42,58 @@ var _ cmdutil.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 + config config.Config boilerplate string - resource *resource.Resource + resource resource.Resource + // plugins is the list of plugins we should allow to transform our generated scaffolding plugins []model.Plugin - // doResource indicates whether to scaffold API Resource or not - doResource bool - // doController indicates whether to scaffold controller files or not - doController bool + // force indicates whether to scaffold controller files even if it exists or not force bool } // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations func NewAPIScaffolder( - config *config.Config, + config config.Config, boilerplate string, - res *resource.Resource, - doResource, doController, force bool, + res resource.Resource, + force bool, plugins []model.Plugin, ) cmdutil.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - doResource: doResource, - doController: doController, - force: force, + config: config, + boilerplate: boilerplate, + resource: res, + plugins: plugins, + force: force, } } // Scaffold implements Scaffolder func (s *apiScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - - switch { - case s.config.IsV2(), s.config.IsV3(): - return s.scaffold() - default: - return fmt.Errorf("unknown project version %v", s.config.Version) - } + return s.scaffold() } func (s *apiScaffolder) newUniverse() *model.Universe { return model.NewUniverse( model.WithConfig(s.config), model.WithBoilerplate(s.boilerplate), - model.WithResource(s.resource), + model.WithResource(&s.resource), ) } func (s *apiScaffolder) scaffold() error { - if s.doResource { - s.config.UpdateResources(s.resource.Data()) + // Keep track of these values before the update + doAPI := s.resource.HasAPI() + doController := s.resource.HasController() + + if doAPI { + + if err := s.config.UpdateResource(s.resource); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), @@ -108,7 +105,7 @@ func (s *apiScaffolder) scaffold() error { &patches.EnableWebhookPatch{}, &patches.EnableCAInjectionPatch{}, ); err != nil { - return fmt.Errorf("error scaffolding APIs: %v", err) + return fmt.Errorf("error scaffolding APIs: %w", err) } if err := machinery.NewScaffold().Execute( @@ -121,11 +118,11 @@ func (s *apiScaffolder) scaffold() error { } - if s.doController { + if doController { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &controllers.SuiteTest{WireResource: s.doResource, Force: s.force}, - &controllers.Controller{WireResource: s.doResource, Force: s.force}, + &controllers.SuiteTest{Force: s.force}, + &controllers.Controller{Force: s.force}, ); err != nil { return fmt.Errorf("error scaffolding controller: %v", err) } @@ -133,7 +130,7 @@ func (s *apiScaffolder) scaffold() error { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &templates.MainUpdater{WireResource: s.doResource, WireController: s.doController}, + &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 8dcc606488f..4c5def9d576 100644 --- a/pkg/plugins/golang/v2/scaffolds/edit.go +++ b/pkg/plugins/golang/v2/scaffolds/edit.go @@ -21,19 +21,19 @@ import ( "io/ioutil" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) var _ cmdutil.Scaffolder = &editScaffolder{} type editScaffolder struct { - config *config.Config + config config.Config multigroup bool } // 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) cmdutil.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, @@ -63,11 +63,15 @@ func (s *editScaffolder) Scaffold() error { } // Ignore the error encountered, if the file is already in desired format. - if err != nil && s.multigroup != s.config.MultiGroup { + if err != nil && s.multigroup != s.config.IsMultiGroup() { return err } - s.config.MultiGroup = s.multigroup + if s.multigroup { + _ = s.config.SetMultiGroup() + } else { + _ = s.config.ClearMultiGroup() + } // 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. diff --git a/pkg/plugins/golang/v2/scaffolds/init.go b/pkg/plugins/golang/v2/scaffolds/init.go index c1cdd657192..d0674f28395 100644 --- a/pkg/plugins/golang/v2/scaffolds/init.go +++ b/pkg/plugins/golang/v2/scaffolds/init.go @@ -21,8 +21,8 @@ import ( "io/ioutil" "path/filepath" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "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" @@ -49,14 +49,14 @@ const ( var _ cmdutil.Scaffolder = &initScaffolder{} type initScaffolder struct { - config *config.Config + config config.Config boilerplatePath string license string owner string } // 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) cmdutil.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: filepath.Join("hack", "boilerplate.go.txt"), @@ -75,13 +75,7 @@ func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe { // Scaffold implements Scaffolder func (s *initScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - - switch { - case s.config.IsV2(), s.config.IsV3(): - return s.scaffold() - default: - return fmt.Errorf("unknown project version %v", s.config.Version) - } + return s.scaffold() } func (s *initScaffolder) scaffold() error { 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 d35163ebba9..bccba858d49 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go @@ -53,7 +53,7 @@ const groupTemplate = `{{ .Boilerplate }} // Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group //+kubebuilder:object:generate=true -//+groupName={{ .Resource.Domain }} +//+groupName={{ .Resource.QualifiedGroup }} package {{ .Resource.Version }} import ( @@ -63,7 +63,7 @@ import ( var ( // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "{{ .Resource.Domain }}", Version: "{{ .Resource.Version }}"} + GroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 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 675f278a802..462830c5c6f 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go @@ -87,7 +87,7 @@ type {{ .Resource.Kind }}Status struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status -{{ if not .Resource.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} +{{ if not .Resource.API.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} // {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API type {{ .Resource.Kind }} struct { 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 13a09c390d3..806a37d5524 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go @@ -34,12 +34,7 @@ type Webhook struct { // nolint:maligned file.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' - GroupDomainWithDash string - - // If scaffold the defaulting webhook - Defaulting bool - // If scaffold the validating webhook - Validating bool + QualifiedGroupWithDash string } // SetTemplateDefaults implements file.Template @@ -55,17 +50,17 @@ func (f *Webhook) SetTemplateDefaults() error { fmt.Println(f.Path) webhookTemplate := webhookTemplate - if f.Defaulting { + if f.Resource.HasDefaultingWebhook() { webhookTemplate = webhookTemplate + defaultingWebhookTemplate } - if f.Validating { + if f.Resource.HasValidationWebhook() { webhookTemplate = webhookTemplate + validatingWebhookTemplate } f.TemplateBody = webhookTemplate f.IfExistsAction = file.Error - f.GroupDomainWithDash = strings.Replace(f.Resource.Domain, ".", "-", -1) + f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) return nil } @@ -78,10 +73,10 @@ package {{ .Resource.Version }} import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" - {{- if .Validating }} + {{- if .Resource.HasValidationWebhook }} "k8s.io/apimachinery/pkg/runtime" {{- end }} - {{- if or .Validating .Defaulting }} + {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }} "sigs.k8s.io/controller-runtime/pkg/webhook" {{- end }} ) @@ -100,7 +95,7 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { //nolint:lll defaultingWebhookTemplate = ` -//+kubebuilder:webhook:path=/mutate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io +//+kubebuilder:webhook:path=/mutate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io var _ webhook.Defaulter = &{{ .Resource.Kind }}{} @@ -114,7 +109,7 @@ func (r *{{ .Resource.Kind }}) Default() { //nolint:lll validatingWebhookTemplate = ` // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:verbs=create;update,path=/validate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io +//+kubebuilder:webhook:verbs=create;update,path=/validate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io var _ webhook.Validator = &{{ .Resource.Kind }}{} 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 d737c659d3b..fd06bdf7848 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 @@ -78,7 +78,7 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Generate resource code fragments res := make([]string, 0) - res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.Domain, f.Resource.Plural)) + res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.QualifiedGroup(), f.Resource.Plural)) // Generate resource code fragments webhookPatch := make([]string, 0) 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 09ce35d75e2..00d03f090d1 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 @@ -49,5 +49,5 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: {{ .Resource.Plural }}.{{ .Resource.Domain }} + name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} ` 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 b1495ff09ba..8e3fc5d051a 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 @@ -47,7 +47,7 @@ const enableWebhookPatchTemplate = `# The following patch enables conversion web apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: {{ .Resource.Plural }}.{{ .Resource.Domain }} + name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} spec: conversion: strategy: Webhook 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 c62c2cebcd4..3de358308bd 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 @@ -49,7 +49,7 @@ metadata: name: {{ lower .Resource.Kind }}-editor-role rules: - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: @@ -61,7 +61,7 @@ rules: - update - watch - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: 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 b89716c46ca..5898a8fe334 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 @@ -49,7 +49,7 @@ metadata: name: {{ lower .Resource.Kind }}-viewer-role rules: - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: @@ -57,7 +57,7 @@ rules: - list - watch - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: 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 9d51be29b6c..b9b349ff336 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 @@ -50,7 +50,7 @@ func (f *CRDSample) SetTemplateDefaults() error { return nil } -const crdSampleTemplate = `apiVersion: {{ .Resource.Domain }}/{{ .Resource.Version }} +const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }} kind: {{ .Resource.Kind }} metadata: name: {{ lower .Resource.Kind }}-sample 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 61710581e27..213b9b08561 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go @@ -33,9 +33,6 @@ type Controller struct { file.BoilerplateMixin file.ResourceMixin - // WireResource defines the api resources are generated or not. - WireResource bool - Force bool } @@ -73,8 +70,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - {{ if .WireResource -}} - {{ .Resource.ImportAlias }} "{{ .Resource.Package }}" + {{ if .Resource.HasAPI -}} + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" {{- end }} ) @@ -85,8 +82,8 @@ type {{ .Resource.Kind }}Reconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch func (r *{{ .Resource.Kind }}Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { _ = context.Background() @@ -99,7 +96,7 @@ func (r *{{ .Resource.Kind }}Reconciler) Reconcile(req ctrl.Request) (ctrl.Resul func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - {{ if .WireResource -}} + {{ if .Resource.HasAPI -}} For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}). {{- else -}} // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument 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 149828522bf..34e1b418aa9 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 @@ -37,9 +37,6 @@ type SuiteTest struct { // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string - // WireResource defines the api resources are generated or not. - WireResource bool - Force bool } @@ -101,14 +98,14 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) - if f.WireResource { - imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias, f.Resource.Package)) + if f.Resource.HasAPI() { + imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } // Generate add scheme code fragments addScheme := make([]string, 0) - if f.WireResource { - addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias)) + if f.Resource.HasAPI() { + addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias())) } // Only store code fragments in the map if the slices are non-empty diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go index e3dd331ad12..e9f4b64362d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go @@ -139,7 +139,7 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) if f.WireResource { - imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias, f.Resource.Package)) + imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } if f.WireController { @@ -147,14 +147,14 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo)) } else { imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment, - f.Resource.GroupPackageName, f.Repo, f.Resource.Group)) + f.Resource.PackageName(), f.Repo, f.Resource.Group)) } } // Generate add scheme code fragments addScheme := make([]string, 0) if f.WireResource { - addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias)) + addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias())) } // Generate setup code fragments @@ -165,12 +165,12 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { f.Resource.Kind, f.Resource.Kind, f.Resource.Kind)) } else { setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment, - f.Resource.GroupPackageName, f.Resource.Kind, f.Resource.Kind, f.Resource.Kind)) + f.Resource.PackageName(), f.Resource.Kind, f.Resource.Kind, f.Resource.Kind)) } } if f.WireWebhook { setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment, - f.Resource.ImportAlias, f.Resource.Kind, f.Resource.Kind)) + f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind)) } // Only store code fragments in the map if the slices are non-empty diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go index b38642ad011..c404f571eef 100644 --- a/pkg/plugins/golang/v2/scaffolds/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/webhook.go @@ -19,8 +19,8 @@ package scaffolds import ( "fmt" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api" @@ -31,66 +31,51 @@ import ( var _ cmdutil.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { - config *config.Config + config config.Config boilerplate string - resource *resource.Resource - - // v2 - defaulting, validation, conversion bool + resource resource.Resource } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations func NewWebhookScaffolder( - config *config.Config, + config config.Config, boilerplate string, - resource *resource.Resource, - defaulting bool, - validation bool, - conversion bool, + resource resource.Resource, ) cmdutil.Scaffolder { return &webhookScaffolder{ config: config, boilerplate: boilerplate, resource: resource, - defaulting: defaulting, - validation: validation, - conversion: conversion, } } // Scaffold implements Scaffolder func (s *webhookScaffolder) Scaffold() error { fmt.Println("Writing scaffold for you to edit...") - - switch { - case s.config.IsV2(), s.config.IsV3(): - return s.scaffold() - default: - return fmt.Errorf("unknown project version %v", s.config.Version) - } + return s.scaffold() } func (s *webhookScaffolder) newUniverse() *model.Universe { return model.NewUniverse( model.WithConfig(s.config), model.WithBoilerplate(s.boilerplate), - model.WithResource(s.resource), + model.WithResource(&s.resource), ) } func (s *webhookScaffolder) scaffold() error { - if s.conversion { - fmt.Println(`Webhook server has been set up for you. -You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) - } - if err := machinery.NewScaffold().Execute( s.newUniverse(), - &api.Webhook{Defaulting: s.defaulting, Validating: s.validation}, + &api.Webhook{}, &templates.MainUpdater{WireWebhook: true}, ); err != nil { return err } + if s.resource.HasConversionWebhook() { + fmt.Println(`Webhook server has been set up for you. +You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) + } + return nil } diff --git a/pkg/plugins/golang/v2/suite_test.go b/pkg/plugins/golang/v2/suite_test.go new file mode 100644 index 00000000000..de06fb4cf53 --- /dev/null +++ b/pkg/plugins/golang/v2/suite_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestGoPluginV2(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Go Plugin v2 Suite") +} diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index 5fbe63fd24c..f6df11591db 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -23,22 +23,18 @@ import ( "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + newconfig "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" ) type createWebhookSubcommand struct { - config *config.Config + config newconfig.Config // For help text. commandName string - resource *resource.Options - defaulting bool - validation bool - conversion bool + options *Options } var ( @@ -63,21 +59,23 @@ validating and (or) conversion webhooks. } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { - p.resource = &resource.Options{} - fs.StringVar(&p.resource.Group, "group", "", "resource Group") - fs.StringVar(&p.resource.Version, "version", "", "resource Version") - fs.StringVar(&p.resource.Kind, "kind", "", "resource Kind") - fs.StringVar(&p.resource.Plural, "resource", "", "resource Resource") - - fs.BoolVar(&p.defaulting, "defaulting", false, + p.options = &Options{} + fs.StringVar(&p.options.Group, "group", "", "resource Group") + p.options.Domain = p.config.GetDomain() + fs.StringVar(&p.options.Version, "version", "", "resource Version") + fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form") + + p.options.WebhookVersion = "v1beta1" + fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") - fs.BoolVar(&p.validation, "programmatic-validation", false, + fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, "if set, scaffold the validating webhook") - fs.BoolVar(&p.conversion, "conversion", false, + fs.BoolVar(&p.options.DoConversion, "conversion", false, "if set, scaffold the conversion webhook") } -func (p *createWebhookSubcommand) InjectConfig(c *config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c newconfig.Config) { p.config = c } @@ -86,17 +84,17 @@ func (p *createWebhookSubcommand) Run() error { } func (p *createWebhookSubcommand) Validate() error { - if err := p.resource.ValidateV2(); err != nil { + if err := p.options.Validate(); err != nil { return err } - if !p.defaulting && !p.validation && !p.conversion { + if !p.options.DoDefaulting && !p.options.DoValidation && !p.options.DoConversion { return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+ " --programmatic-validation and --conversion to be true", p.commandName) } // check if resource exist to create webhook - if p.config.GetResource(p.resource.Data()) == nil { + if !p.config.HasResource(p.options.GVK()) { return fmt.Errorf("%s create webhook requires an api with the group,"+ " kind and version provided", p.commandName) } @@ -111,9 +109,9 @@ func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unable to load boilerplate: %v", err) } - // Create the actual resource from the resource options - res := p.resource.NewResource(p.config, false) - return scaffolds.NewWebhookScaffolder(p.config, string(bp), res, p.defaulting, p.validation, p.conversion), nil + // Create the resource from the options + res := p.options.NewResource(p.config) + return scaffolds.NewWebhookScaffolder(p.config, string(bp), res), nil } func (p *createWebhookSubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index a1795d86b66..ab8a629f712 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -27,10 +27,10 @@ import ( "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/config" - "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/v3/scaffolds" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" @@ -51,18 +51,16 @@ const ( const DefaultMainPath = "main.go" type createAPISubcommand struct { - config *config.Config + config config.Config // pattern indicates that we should use a plugin to build according to a pattern pattern string - resource *resource.Options + options *goPlugin.Options // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag controllerFlag *pflag.Flag - doResource bool - doController bool // force indicates that the resource should be created even if it already exists force bool @@ -109,13 +107,6 @@ After the scaffold is written, api will run make on the project. func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run make after generating files") - fs.BoolVar(&p.doResource, "resource", true, - "if set, generate the resource without prompting the user") - p.resourceFlag = fs.Lookup("resource") - fs.BoolVar(&p.doController, "controller", true, - "if set, generate the controller without prompting the user") - p.controllerFlag = fs.Lookup("controller") - // TODO: remove this when a better solution for using addons is implemented. if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { fs.StringVar(&p.pattern, "pattern", "", @@ -124,16 +115,27 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - p.resource = &resource.Options{} - fs.StringVar(&p.resource.Kind, "kind", "", "resource Kind") - fs.StringVar(&p.resource.Group, "group", "", "resource Group") - fs.StringVar(&p.resource.Version, "version", "", "resource Version") - fs.BoolVar(&p.resource.Namespaced, "namespaced", true, "resource is namespaced") - fs.StringVar(&p.resource.API.CRDVersion, "crd-version", defaultCRDVersion, + + 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") + // p.options.Plural can be set to specify an irregular plural form + + fs.BoolVar(&p.options.DoAPI, "resource", true, + "if set, generate the resource without prompting the user") + p.resourceFlag = fs.Lookup("resource") + fs.StringVar(&p.options.CRDVersion, "crd-version", defaultCRDVersion, "version of CustomResourceDefinition to scaffold. Options: [v1, v1beta1]") + fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") + + fs.BoolVar(&p.options.DoController, "controller", true, + "if set, generate the controller without prompting the user") + p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c *config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) { p.config = c } @@ -142,11 +144,11 @@ func (p *createAPISubcommand) Run() error { } func (p *createAPISubcommand) Validate() error { - if err := p.resource.Validate(); err != nil { + if err := p.options.Validate(); err != nil { return err } - if p.resource.Group == "" && p.config.Domain == "" { + if p.options.Group == "" && p.options.Domain == "" { return fmt.Errorf("can not have group and domain both empty") } @@ -160,31 +162,30 @@ func (p *createAPISubcommand) Validate() error { reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") - p.doResource = util.YesNo(reader) + p.options.DoAPI = util.YesNo(reader) } if !p.controllerFlag.Changed { fmt.Println("Create Controller [y/n]") - p.doController = util.YesNo(reader) + p.options.DoController = util.YesNo(reader) } // In case we want to scaffold a resource API we need to do some checks - if p.doResource { + if p.options.DoAPI { // Check that resource doesn't exist or flag force was set - res := p.config.GetResource(p.resource.Data()) - if !p.force && (res != nil && res.API != nil) { + if res, err := p.config.GetResource(p.options.GVK()); err == nil && res.HasAPI() && !p.force { return errors.New("API resource already exists") } // Check that the provided group can be added to the project - if !p.config.MultiGroup && len(p.config.Resources) != 0 && !p.config.HasGroup(p.resource.Group) { + if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.options.Group) { return fmt.Errorf("multiple groups are not allowed by default, " + "to enable multi-group visit kubebuilder.io/migration/multi-group.html") } // Check CRDVersion against all other CRDVersions in p.config for compatibility. - if !p.config.IsCRDVersionCompatible(p.resource.API.CRDVersion) { + if !p.config.IsCRDVersionCompatible(p.options.CRDVersion) { return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q", - p.resource.API.CRDVersion) + p.options.CRDVersion) } } @@ -209,9 +210,9 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unknown pattern %q", p.pattern) } - // Create the actual resource from the resource options - res := p.resource.NewResource(p.config, p.doResource) - return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.doResource, p.doController, p.force, plugins), nil + // Create the resource from the options + res := p.options.NewResource(p.config) + return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.force, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go index b42d300f6c7..503ed2fafe3 100644 --- a/pkg/plugins/golang/v3/edit.go +++ b/pkg/plugins/golang/v3/edit.go @@ -21,14 +21,14 @@ import ( "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "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" ) type editSubcommand struct { - config *config.Config + config config.Config multigroup bool } @@ -53,7 +53,7 @@ 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) { p.config = c } diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index 7daabc47b3d..e051c5913d6 100644 --- a/pkg/plugins/golang/v3/init.go +++ b/pkg/plugins/golang/v3/init.go @@ -24,8 +24,8 @@ import ( "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" - "sigs.k8s.io/kubebuilder/v3/pkg/model/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" @@ -33,7 +33,7 @@ import ( ) type initSubcommand struct { - config *config.Config + config config.Config // For help text. commandName string @@ -41,6 +41,12 @@ type initSubcommand struct { license string owner string + // config options + domain string + repo string + name string + componentConfig bool + // flags fetchDeps bool skipGoVersionCheck bool @@ -83,18 +89,19 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.license, "license", "apache2", "license to use to boilerplate, may be one of 'apache2', 'none'") fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright") - fs.BoolVar(&p.config.ComponentConfig, "component-config", false, - "create a versioned ComponentConfig file, may be 'true' or 'false'") // project args - fs.StringVar(&p.config.Repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+ + 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.") - fs.StringVar(&p.config.Domain, "domain", "my.domain", "domain for groups") - fs.StringVar(&p.config.ProjectName, "project-name", "", "name of this project") + fs.StringVar(&p.name, "project-name", "", "name of this project") + fs.BoolVar(&p.componentConfig, "component-config", false, + "create a versioned ComponentConfig file, may be 'true' or 'false'") } -func (p *initSubcommand) InjectConfig(c *config.Config) { - c.Layout = plugin.KeyFor(Plugin{}) +func (p *initSubcommand) InjectConfig(c config.Config) { + _ = c.SetLayout(plugin.KeyFor(Plugin{})) + p.config = c } @@ -115,31 +122,47 @@ func (p *initSubcommand) Validate() error { return err } - // Check if the project name is a valid k8s namespace (DNS 1123 label). - if p.config.ProjectName == "" { + // Assign a default project name + if p.name == "" { dir, err := os.Getwd() if err != nil { return fmt.Errorf("error getting current directory: %v", err) } - p.config.ProjectName = strings.ToLower(filepath.Base(dir)) + p.name = strings.ToLower(filepath.Base(dir)) } - if err := validation.IsDNS1123Label(p.config.ProjectName); err != nil { - return fmt.Errorf("project name (%s) is invalid: %v", p.config.ProjectName, err) + // Check if the project name is a valid k8s namespace (DNS 1123 label). + 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.config.Repo == "" { + if p.repo == "" { repoPath, err := util.FindCurrentRepo() if err != nil { return fmt.Errorf("error finding current repository: %v", err) } - p.config.Repo = repoPath + 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.SetName(p.name); err != nil { + return nil, err + } + if p.componentConfig { + if err := p.config.SetComponentConfig(); err != nil { + return nil, err + } + } + return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil } diff --git a/pkg/plugins/golang/v3/plugin.go b/pkg/plugins/golang/v3/plugin.go index edbae552e3a..f9b2d1af73f 100644 --- a/pkg/plugins/golang/v3/plugin.go +++ b/pkg/plugins/golang/v3/plugin.go @@ -17,7 +17,8 @@ limitations under the License. package v3 import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3alpha "sigs.k8s.io/kubebuilder/v3/pkg/config/v3alpha" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) @@ -25,7 +26,7 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []string{config.Version3Alpha} + supportedProjectVersions = []config.Version{cfgv3alpha.Version} pluginVersion = plugin.Version{Number: 3} ) @@ -46,7 +47,7 @@ func (Plugin) Name() string { return pluginName } func (Plugin) Version() plugin.Version { return pluginVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin -func (Plugin) SupportedProjectVersions() []string { return supportedProjectVersions } +func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } // GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand } diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go index 9d7f798b8ad..0265d705b60 100644 --- a/pkg/plugins/golang/v3/scaffolds/api.go +++ b/pkg/plugins/golang/v3/scaffolds/api.go @@ -19,8 +19,8 @@ package scaffolds import ( "fmt" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "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" @@ -38,35 +38,31 @@ var _ cmdutil.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 + config config.Config boilerplate string - resource *resource.Resource + resource resource.Resource + // plugins is the list of plugins we should allow to transform our generated scaffolding plugins []model.Plugin - // doResource indicates whether to scaffold API Resource or not - doResource bool - // doController indicates whether to scaffold controller files or not - doController bool + // force indicates whether to scaffold controller files even if it exists or not force bool } // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations func NewAPIScaffolder( - config *config.Config, + config config.Config, boilerplate string, - res *resource.Resource, - doResource, doController, force bool, + res resource.Resource, + force bool, plugins []model.Plugin, ) cmdutil.Scaffolder { return &apiScaffolder{ - config: config, - boilerplate: boilerplate, - resource: res, - plugins: plugins, - doResource: doResource, - doController: doController, - force: force, + config: config, + boilerplate: boilerplate, + resource: res, + plugins: plugins, + force: force, } } @@ -80,15 +76,21 @@ func (s *apiScaffolder) newUniverse() *model.Universe { return model.NewUniverse( model.WithConfig(s.config), model.WithBoilerplate(s.boilerplate), - model.WithResource(s.resource), + model.WithResource(&s.resource), ) } // TODO: re-use universe created by s.newUniverse() if possible. func (s *apiScaffolder) scaffold() error { - if s.doResource { + // Keep track of these values before the update + doAPI := s.resource.HasAPI() + doController := s.resource.HasController() - s.config.UpdateResources(s.resource.Data()) + if doAPI { + + if err := s.config.UpdateResource(s.resource); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), @@ -97,8 +99,8 @@ func (s *apiScaffolder) scaffold() error { &samples.CRDSample{Force: s.force}, &rbac.CRDEditorRole{}, &rbac.CRDViewerRole{}, - &patches.EnableWebhookPatch{CRDVersion: s.resource.API.CRDVersion}, - &patches.EnableCAInjectionPatch{CRDVersion: s.resource.API.CRDVersion}, + &patches.EnableWebhookPatch{}, + &patches.EnableCAInjectionPatch{}, ); err != nil { return fmt.Errorf("error scaffolding APIs: %v", err) } @@ -106,19 +108,18 @@ func (s *apiScaffolder) scaffold() error { if err := machinery.NewScaffold().Execute( s.newUniverse(), &crd.Kustomization{}, - &crd.KustomizeConfig{CRDVersion: s.resource.API.CRDVersion}, + &crd.KustomizeConfig{}, ); err != nil { return fmt.Errorf("error scaffolding kustomization: %v", err) } } - if s.doController { + if doController { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &controllers.SuiteTest{WireResource: s.doResource, Force: s.force}, - &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, WireResource: s.doResource, - Force: s.force}, + &controllers.SuiteTest{Force: s.force}, + &controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force}, ); err != nil { return fmt.Errorf("error scaffolding controller: %v", err) } @@ -126,7 +127,7 @@ func (s *apiScaffolder) scaffold() error { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &templates.MainUpdater{WireResource: s.doResource, WireController: s.doController}, + &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 1f210110c82..f4f92785359 100644 --- a/pkg/plugins/golang/v3/scaffolds/edit.go +++ b/pkg/plugins/golang/v3/scaffolds/edit.go @@ -21,19 +21,19 @@ import ( "io/ioutil" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) var _ cmdutil.Scaffolder = &editScaffolder{} type editScaffolder struct { - config *config.Config + config config.Config multigroup bool } // 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) cmdutil.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, @@ -64,11 +64,15 @@ func (s *editScaffolder) Scaffold() error { } // Ignore the error encountered, if the file is already in desired format. - if err != nil && s.multigroup != s.config.MultiGroup { + if err != nil && s.multigroup != s.config.IsMultiGroup() { return err } - s.config.MultiGroup = s.multigroup + if s.multigroup { + _ = s.config.SetMultiGroup() + } else { + _ = s.config.ClearMultiGroup() + } // 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. diff --git a/pkg/plugins/golang/v3/scaffolds/init.go b/pkg/plugins/golang/v3/scaffolds/init.go index cc5fb251aab..e46478d42ed 100644 --- a/pkg/plugins/golang/v3/scaffolds/init.go +++ b/pkg/plugins/golang/v3/scaffolds/init.go @@ -21,8 +21,8 @@ import ( "io/ioutil" "path/filepath" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "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" @@ -48,14 +48,14 @@ const ( var _ cmdutil.Scaffolder = &initScaffolder{} type initScaffolder struct { - config *config.Config + config config.Config boilerplatePath string license string owner string } // 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) cmdutil.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: filepath.Join("hack", "boilerplate.go.txt"), 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 e3ead93db6e..f8c1faa7e2b 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go @@ -57,7 +57,7 @@ const groupTemplate = `{{ .Boilerplate }} // Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group //+kubebuilder:object:generate=true -//+groupName={{ .Resource.Domain }} +//+groupName={{ .Resource.QualifiedGroup }} package {{ .Resource.Version }} import ( @@ -67,7 +67,7 @@ import ( var ( // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "{{ .Resource.Domain }}", Version: "{{ .Resource.Version }}"} + GroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 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 8147a5ad5f7..0875b04df88 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go @@ -91,7 +91,7 @@ type {{ .Resource.Kind }}Status struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status -{{ if not .Resource.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} +{{ if not .Resource.API.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} // {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API type {{ .Resource.Kind }} struct { 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 5d276c012f2..f3f9c6ccd76 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go @@ -34,14 +34,7 @@ type Webhook struct { // nolint:maligned file.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' - GroupDomainWithDash string - - // Version of webhook marker to scaffold - WebhookVersion string - // If scaffold the defaulting webhook - Defaulting bool - // If scaffold the validating webhook - Validating bool + QualifiedGroupWithDash string Force bool } @@ -63,10 +56,10 @@ func (f *Webhook) SetTemplateDefaults() error { fmt.Println(f.Path) webhookTemplate := webhookTemplate - if f.Defaulting { + if f.Resource.HasDefaultingWebhook() { webhookTemplate = webhookTemplate + defaultingWebhookTemplate } - if f.Validating { + if f.Resource.HasValidationWebhook() { webhookTemplate = webhookTemplate + validatingWebhookTemplate } f.TemplateBody = webhookTemplate @@ -77,11 +70,7 @@ func (f *Webhook) SetTemplateDefaults() error { f.IfExistsAction = file.Error } - f.GroupDomainWithDash = strings.Replace(f.Resource.Domain, ".", "-", -1) - - if f.WebhookVersion == "" { - f.WebhookVersion = "v1" - } + f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) return nil } @@ -94,10 +83,10 @@ package {{ .Resource.Version }} import ( ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" - {{- if .Validating }} + {{- if .Resource.HasValidationWebhook }} "k8s.io/apimachinery/pkg/runtime" {{- end }} - {{- if or .Validating .Defaulting }} + {{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }} "sigs.k8s.io/controller-runtime/pkg/webhook" {{- end }} ) @@ -117,7 +106,7 @@ func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error { // TODO(estroz): update admissionReviewVersions to include v1 when controller-runtime supports that version. //nolint:lll defaultingWebhookTemplate = ` -//+kubebuilder:webhook:{{ if ne .WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1} +//+kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/mutate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1} var _ webhook.Defaulter = &{{ .Resource.Kind }}{} @@ -133,7 +122,7 @@ func (r *{{ .Resource.Kind }}) Default() { //nolint:lll validatingWebhookTemplate = ` // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:{{ if ne .WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1} +//+kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}path=/validate-{{ .QualifiedGroupWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}.kb.io,admissionReviewVersions={v1,v1beta1} var _ webhook.Validator = &{{ .Resource.Kind }}{} 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 5d0483cac9d..9a87af34668 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 @@ -78,7 +78,7 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Generate resource code fragments res := make([]string, 0) - res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.Domain, f.Resource.Plural)) + res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.QualifiedGroup(), f.Resource.Plural)) // Generate resource code fragments webhookPatch := make([]string, 0) 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 700ef4df5f9..7b56a21c9df 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 @@ -22,16 +22,12 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/file" ) -const v1 = "v1" - var _ file.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { file.TemplateMixin - - // Version of CRD patch generated. - CRDVersion string + file.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -42,10 +38,6 @@ func (f *KustomizeConfig) SetTemplateDefaults() error { f.TemplateBody = kustomizeConfigTemplate - if f.CRDVersion == "" { - f.CRDVersion = v1 - } - return nil } @@ -56,9 +48,9 @@ nameReference: version: v1 fieldSpecs: - kind: CustomResourceDefinition - version: {{ .CRDVersion }} + version: {{ .Resource.API.CRDVersion }} group: apiextensions.k8s.io - {{- if ne .CRDVersion "v1" }} + {{- if ne .Resource.API.CRDVersion "v1" }} path: spec/conversion/webhookClientConfig/service/name {{- else }} path: spec/conversion/webhook/clientConfig/service/name @@ -66,9 +58,9 @@ nameReference: namespace: - kind: CustomResourceDefinition - version: {{ .CRDVersion }} + version: {{ .Resource.API.CRDVersion }} group: apiextensions.k8s.io - {{- if ne .CRDVersion "v1" }} + {{- if ne .Resource.API.CRDVersion "v1" }} path: spec/conversion/webhookClientConfig/service/namespace {{- else }} path: spec/conversion/webhook/clientConfig/service/namespace 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 df19081c768..c954670d1db 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 @@ -22,17 +22,12 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/file" ) -const v1 = "v1" - var _ file.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { file.TemplateMixin file.ResourceMixin - - // Version of CRD patch to generate. - CRDVersion string } // SetTemplateDefaults implements file.Template @@ -44,22 +39,18 @@ func (f *EnableCAInjectionPatch) SetTemplateDefaults() error { f.TemplateBody = enableCAInjectionPatchTemplate - if f.CRDVersion == "" { - f.CRDVersion = v1 - } - return nil } //nolint:lll const enableCAInjectionPatchTemplate = `# The following patch adds a directive for certmanager to inject CA into the CRD -{{- if ne .CRDVersion "v1" }} +{{- if ne .Resource.API.CRDVersion "v1" }} # CRD conversion requires k8s 1.13 or later. {{- end }} -apiVersion: apiextensions.k8s.io/{{ .CRDVersion }} +apiVersion: apiextensions.k8s.io/{{ .Resource.API.CRDVersion }} kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: {{ .Resource.Plural }}.{{ .Resource.Domain }} + name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} ` 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 9386f0a9c5d..7cc1da1d65e 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 @@ -28,9 +28,6 @@ var _ file.Template = &EnableWebhookPatch{} type EnableWebhookPatch struct { file.TemplateMixin file.ResourceMixin - - // Version of CRD patch to generate. - CRDVersion string } // SetTemplateDefaults implements file.Template @@ -42,25 +39,21 @@ func (f *EnableWebhookPatch) SetTemplateDefaults() error { f.TemplateBody = enableWebhookPatchTemplate - if f.CRDVersion == "" { - f.CRDVersion = v1 - } - return nil } const enableWebhookPatchTemplate = `# The following patch enables a conversion webhook for the CRD -{{- if ne .CRDVersion "v1" }} +{{- if ne .Resource.API.CRDVersion "v1" }} # CRD conversion requires k8s 1.13 or later. {{- end }} -apiVersion: apiextensions.k8s.io/{{ .CRDVersion }} +apiVersion: apiextensions.k8s.io/{{ .Resource.API.CRDVersion }} kind: CustomResourceDefinition metadata: - name: {{ .Resource.Plural }}.{{ .Resource.Domain }} + name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} spec: conversion: strategy: Webhook - {{- if ne .CRDVersion "v1" }} + {{- if ne .Resource.API.CRDVersion "v1" }} webhookClientConfig: service: namespace: system 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 85494a1bd61..a3ba80da2b9 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 @@ -27,9 +27,7 @@ var _ file.Template = &WebhookCAInjectionPatch{} // WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks type WebhookCAInjectionPatch struct { file.TemplateMixin - - // Version of webhook patch to generate. - WebhookVersion string + file.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -43,23 +41,19 @@ func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error { // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = file.Skip - if f.WebhookVersion == "" { - f.WebhookVersion = "v1" - } - return nil } const injectCAPatchTemplate = `# This patch add annotation to admission webhook config and # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. -apiVersion: admissionregistration.k8s.io/{{ .WebhookVersion }} +apiVersion: admissionregistration.k8s.io/{{ .Resource.Webhooks.WebhookVersion }} kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) --- -apiVersion: admissionregistration.k8s.io/{{ .WebhookVersion }} +apiVersion: admissionregistration.k8s.io/{{ .Resource.Webhooks.WebhookVersion }} kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration 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 b13fba6b7d1..a099b595e9b 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 @@ -49,7 +49,7 @@ metadata: name: {{ lower .Resource.Kind }}-editor-role rules: - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: @@ -61,7 +61,7 @@ rules: - update - watch - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: 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 6ef7b28a7dd..0b3311650b7 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 @@ -49,7 +49,7 @@ metadata: name: {{ lower .Resource.Kind }}-viewer-role rules: - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: @@ -57,7 +57,7 @@ rules: - list - watch - apiGroups: - - {{ .Resource.Domain }} + - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: 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 444501525e7..ab68ba16e4c 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 @@ -50,7 +50,7 @@ func (f *CRDSample) SetTemplateDefaults() error { return nil } -const crdSampleTemplate = `apiVersion: {{ .Resource.Domain }}/{{ .Resource.Version }} +const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }} kind: {{ .Resource.Kind }} metadata: name: {{ lower .Resource.Kind }}-sample 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 08fede11e1d..74546ab10ae 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 @@ -27,9 +27,7 @@ var _ file.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { file.TemplateMixin - - // Version of webhook the project was configured with. - WebhookVersion string + file.ResourceMixin Force bool } @@ -49,15 +47,11 @@ func (f *Kustomization) SetTemplateDefaults() error { f.IfExistsAction = file.Skip } - if f.WebhookVersion == "" { - f.WebhookVersion = "v1" - } - return nil } const kustomizeWebhookTemplate = `resources: -- manifests{{ if ne .WebhookVersion "v1" }}.{{ .WebhookVersion }}{{ end }}.yaml +- manifests{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}.{{ .Resource.Webhooks.WebhookVersion }}{{ end }}.yaml - service.yaml configurations: 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 226f0e99c83..fe2aae589b8 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go @@ -35,9 +35,6 @@ type Controller struct { ControllerRuntimeVersion string - // WireResource defines the api resources are generated or not. - WireResource bool - Force bool } @@ -68,7 +65,7 @@ func (f *Controller) SetTemplateDefaults() error { const controllerTemplate = `{{ .Boilerplate }} {{if and .MultiGroup .Resource.Group }} -package {{ .Resource.GroupPackageName }} +package {{ .Resource.PackageName }} {{else}} package controllers {{end}} @@ -79,8 +76,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - {{ if .WireResource -}} - {{ .Resource.ImportAlias }} "{{ .Resource.Package }}" + {{ if .Resource.HasAPI -}} + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" {{- end }} ) @@ -91,9 +88,9 @@ type {{ .Resource.Kind }}Reconciler struct { Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }}/finalizers,verbs=update +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/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. @@ -115,7 +112,7 @@ func (r *{{ .Resource.Kind }}Reconciler) Reconcile(ctx context.Context, req ctrl // SetupWithManager sets up the controller with the Manager. func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - {{ if .WireResource -}} + {{ if .Resource.HasAPI -}} For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}). {{- else -}} // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument 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 ce9182f8ece..d8159a93c3e 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 @@ -37,9 +37,6 @@ type SuiteTest struct { // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string - // WireResource defines the api resources are generated or not. - WireResource bool - Force bool } @@ -101,14 +98,14 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) - if f.WireResource { - imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias, f.Resource.Package)) + if f.Resource.HasAPI() { + imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } // Generate add scheme code fragments addScheme := make([]string, 0) - if f.WireResource { - addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias)) + if f.Resource.HasAPI() { + addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias())) } // Only store code fragments in the map if the slices are non-empty @@ -125,7 +122,7 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { const controllerSuiteTestTemplate = `{{ .Boilerplate }} {{if and .MultiGroup .Resource.Group }} -package {{ .Resource.GroupPackageName }} +package {{ .Resource.PackageName }} {{else}} package controllers {{end}} @@ -166,7 +163,7 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join({{ .CRDDirectoryRelativePath }}, "config", "crd", "bases")}, - ErrorIfCRDPathMissing: {{ .WireResource }}, + ErrorIfCRDPathMissing: {{ .Resource.HasAPI }}, } cfg, err := testEnv.Start() diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go index 4efafcb219b..65863974cb7 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go @@ -134,7 +134,7 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) if f.WireResource { - imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias, f.Resource.Package)) + imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } if f.WireController { @@ -142,14 +142,14 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo)) } else { imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment, - f.Resource.GroupPackageName, f.Repo, f.Resource.Group)) + f.Resource.PackageName(), f.Repo, f.Resource.Group)) } } // Generate add scheme code fragments addScheme := make([]string, 0) if f.WireResource { - addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias)) + addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias())) } // Generate setup code fragments @@ -160,12 +160,12 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { f.Resource.Kind, f.Resource.Kind, f.Resource.Kind)) } else { setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment, - f.Resource.GroupPackageName, f.Resource.Kind, f.Resource.Group, f.Resource.Kind, f.Resource.Kind)) + f.Resource.PackageName(), f.Resource.Kind, f.Resource.Group, f.Resource.Kind, f.Resource.Kind)) } } if f.WireWebhook { setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment, - f.Resource.ImportAlias, f.Resource.Kind, f.Resource.Kind)) + f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind)) } // Only store code fragments in the map if the slices are non-empty diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go index 1dea4143336..b195ee7eef7 100644 --- a/pkg/plugins/golang/v3/scaffolds/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/webhook.go @@ -19,8 +19,8 @@ package scaffolds import ( "fmt" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "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" @@ -33,31 +33,25 @@ import ( var _ cmdutil.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { - config *config.Config + config config.Config boilerplate string - resource *resource.Resource + resource resource.Resource - // Webhook type options. - defaulting, validation, conversion, force bool + // 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, + config config.Config, boilerplate string, - resource *resource.Resource, - defaulting bool, - validation bool, - conversion bool, + resource resource.Resource, force bool, ) cmdutil.Scaffolder { return &webhookScaffolder{ config: config, boilerplate: boilerplate, resource: resource, - defaulting: defaulting, - validation: validation, - conversion: conversion, force: force, } } @@ -72,38 +66,40 @@ func (s *webhookScaffolder) newUniverse() *model.Universe { return model.NewUniverse( model.WithConfig(s.config), model.WithBoilerplate(s.boilerplate), - model.WithResource(s.resource), + model.WithResource(&s.resource), ) } func (s *webhookScaffolder) scaffold() error { - if s.conversion { - fmt.Println(`Webhook server has been set up for you. -You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) - } + // Keep track of these values before the update + doDefaulting := s.resource.HasDefaultingWebhook() + doValidation := s.resource.HasValidationWebhook() + doConversion := s.resource.HasConversionWebhook() - s.config.UpdateResources(s.resource.Data()) + if err := s.config.UpdateResource(s.resource); err != nil { + return fmt.Errorf("error updating resource: %w", err) + } if err := machinery.NewScaffold().Execute( s.newUniverse(), - &api.Webhook{ - WebhookVersion: s.resource.Webhooks.WebhookVersion, - Defaulting: s.defaulting, - Validating: s.validation, - Force: s.force, - }, + &api.Webhook{Force: s.force}, &templates.MainUpdater{WireWebhook: true}, - &kdefault.WebhookCAInjectionPatch{WebhookVersion: s.resource.Webhooks.WebhookVersion}, + &kdefault.WebhookCAInjectionPatch{}, &kdefault.ManagerWebhookPatch{}, - &webhook.Kustomization{WebhookVersion: s.resource.Webhooks.WebhookVersion, Force: s.force}, + &webhook.Kustomization{Force: s.force}, &webhook.KustomizeConfig{}, &webhook.Service{}, ); err != nil { return err } + if doConversion { + fmt.Println(`Webhook server has been set up for you. +You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) + } + // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest. - if s.defaulting || s.validation { + if doDefaulting || doValidation { if err := machinery.NewScaffold().Execute( s.newUniverse(), &api.WebhookSuite{}, diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 96456324644..229fb8579b7 100644 --- a/pkg/plugins/golang/v3/webhook.go +++ b/pkg/plugins/golang/v3/webhook.go @@ -24,9 +24,9 @@ import ( "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/model/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + 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/pkg/plugins/internal/util" @@ -36,14 +36,11 @@ import ( const defaultWebhookVersion = "v1" type createWebhookSubcommand struct { - config *config.Config + config config.Config // For help text. commandName string - resource *resource.Options - defaulting bool - validation bool - conversion bool + options *goPlugin.Options // force indicates that the resource should be created even if it already exists force bool @@ -74,27 +71,28 @@ validating and (or) conversion webhooks. } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { - p.resource = &resource.Options{} - fs.StringVar(&p.resource.Group, "group", "", "resource Group") - fs.StringVar(&p.resource.Version, "version", "", "resource Version") - fs.StringVar(&p.resource.Kind, "kind", "", "resource Kind") - fs.StringVar(&p.resource.Plural, "resource", "", "resource Resource") - fs.StringVar(&p.resource.Webhooks.WebhookVersion, "webhook-version", defaultWebhookVersion, + 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, "resource", "", "resource irregular plural form") + + fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion, "version of {Mutating,Validating}WebhookConfigurations to scaffold. Options: [v1, v1beta1]") + fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, + "if set, scaffold the defaulting webhook") + fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, + "if set, scaffold the validating webhook") + fs.BoolVar(&p.options.DoConversion, "conversion", false, + "if set, scaffold the conversion webhook") fs.BoolVar(&p.runMake, "make", true, "if true, run make after generating files") fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - - fs.BoolVar(&p.defaulting, "defaulting", false, - "if set, scaffold the defaulting webhook") - fs.BoolVar(&p.validation, "programmatic-validation", false, - "if set, scaffold the validating webhook") - fs.BoolVar(&p.conversion, "conversion", false, - "if set, scaffold the conversion webhook") } -func (p *createWebhookSubcommand) InjectConfig(c *config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) { p.config = c } @@ -103,28 +101,26 @@ func (p *createWebhookSubcommand) Run() error { } func (p *createWebhookSubcommand) Validate() error { - if err := p.resource.Validate(); err != nil { + if err := p.options.Validate(); err != nil { return err } - if !p.defaulting && !p.validation && !p.conversion { + if !p.options.DoDefaulting && !p.options.DoValidation && !p.options.DoConversion { return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+ " --programmatic-validation and --conversion to be true", p.commandName) } // check if resource exist to create webhook - if p.config.GetResource(p.resource.Data()) == nil { + if r, err := p.config.GetResource(p.options.GVK()); err != nil { return fmt.Errorf("%s create webhook requires an api with the group,"+ " kind and version provided", p.commandName) - } - - if p.config.HasWebhook(p.resource.Data()) && !p.force { + } else if r.Webhooks != nil && !r.Webhooks.IsEmpty() && !p.force { return errors.New("webhook resource already exists") } - if !p.config.IsWebhookVersionCompatible(p.resource.Webhooks.WebhookVersion) { + if !p.config.IsWebhookVersionCompatible(p.options.WebhookVersion) { return fmt.Errorf("only one webhook version can be used for all resources, cannot add %q", - p.resource.Webhooks.WebhookVersion) + p.options.WebhookVersion) } return nil @@ -137,10 +133,9 @@ func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { return nil, fmt.Errorf("unable to load boilerplate: %v", err) } - // Create the actual resource from the resource options - res := p.resource.NewResource(p.config, false) - return scaffolds.NewWebhookScaffolder(p.config, string(bp), res, p.defaulting, p.validation, p.conversion, - p.force), nil + // Create the resource from the options + res := p.options.NewResource(p.config) + return scaffolds.NewWebhookScaffolder(p.config, string(bp), res, p.force), nil } func (p *createWebhookSubcommand) PostScaffold() error { diff --git a/pkg/plugins/internal/machinery/scaffold.go b/pkg/plugins/internal/machinery/scaffold.go index 32e5832b810..5391c45bb2e 100644 --- a/pkg/plugins/internal/machinery/scaffold.go +++ b/pkg/plugins/internal/machinery/scaffold.go @@ -69,7 +69,7 @@ func (s *scaffold) Execute(universe *model.Universe, files ...file.Builder) erro // Set the repo as the local prefix so that it knows how to group imports if universe.Config != nil { - imports.LocalPrefix = universe.Config.Repo + imports.LocalPrefix = universe.Config.GetRepository() } for _, f := range files { diff --git a/plugins/addon/controller.go b/plugins/addon/controller.go index 0c70e80242f..032c77aef9c 100644 --- a/plugins/addon/controller.go +++ b/plugins/addon/controller.go @@ -48,7 +48,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - api "{{ .Resource.Package }}" + api "{{ .Resource.Path }}" ) var _ reconcile.Reconciler = &{{ .Resource.Kind }}Reconciler{} @@ -62,8 +62,8 @@ type {{ .Resource.Kind }}Reconciler struct { declarative.Reconciler } -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups={{ .Resource.Domain }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { addon.Init() diff --git a/plugins/addon/type.go b/plugins/addon/type.go index 4046a190d38..82f24680e21 100644 --- a/plugins/addon/type.go +++ b/plugins/addon/type.go @@ -20,7 +20,7 @@ func ReplaceTypes(u *model.Universe) error { } var path string - if u.Config.MultiGroup { + 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") @@ -66,8 +66,8 @@ type {{.Resource.Kind}}Spec struct { // Important: Run "make" to regenerate code after modifying this file } -// {{.Resource.Kind}}Status defines the observed state of {{.Resource.Kind}} -type {{.Resource.Kind}}Status struct { +// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }} +type {{ .Resource.Kind }}Status struct { addonv1alpha1.CommonStatus {{ JSONTag ",inline" }} // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster @@ -76,50 +76,50 @@ type {{.Resource.Kind}}Status struct { //+kubebuilder:object:root=true //+kubebuilder:subresource:status -{{ if not .Resource.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} +{{ if not .Resource.API.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} // {{.Resource.Kind}} is the Schema for the {{ .Resource.Plural }} API -type {{.Resource.Kind}} struct { +type {{ .Resource.Kind }} struct { metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` - Spec {{.Resource.Kind}}Spec ` + "`" + `json:"spec,omitempty"` + "`" + ` - Status {{.Resource.Kind}}Status ` + "`" + `json:"status,omitempty"` + "`" + ` + Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec,omitempty"` + "`" + ` + Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitempty"` + "`" + ` } -var _ addonv1alpha1.CommonObject = &{{.Resource.Kind}}{} +var _ addonv1alpha1.CommonObject = &{{ .Resource.Kind }}{} -func (o *{{.Resource.Kind}}) ComponentName() string { +func (o *{{ .Resource.Kind }}) ComponentName() string { return "{{ .Resource.Kind | lower }}" } -func (o *{{.Resource.Kind}}) CommonSpec() addonv1alpha1.CommonSpec { +func (o *{{ .Resource.Kind }}) CommonSpec() addonv1alpha1.CommonSpec { return o.Spec.CommonSpec } -func (o *{{.Resource.Kind}}) PatchSpec() addonv1alpha1.PatchSpec { +func (o *{{ .Resource.Kind }}) PatchSpec() addonv1alpha1.PatchSpec { return o.Spec.PatchSpec } -func (o *{{.Resource.Kind}}) GetCommonStatus() addonv1alpha1.CommonStatus { +func (o *{{ .Resource.Kind }}) GetCommonStatus() addonv1alpha1.CommonStatus { return o.Status.CommonStatus } -func (o *{{.Resource.Kind}}) SetCommonStatus(s addonv1alpha1.CommonStatus) { +func (o *{{ .Resource.Kind }}) SetCommonStatus(s addonv1alpha1.CommonStatus) { o.Status.CommonStatus = s } //+kubebuilder:object:root=true -{{ if not .Resource.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} +{{ if not .Resource.API.Namespaced }} //+kubebuilder:resource:scope=Cluster {{ end }} -// {{.Resource.Kind}}List contains a list of {{.Resource.Kind}} -type {{.Resource.Kind}}List struct { +// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }} +type {{ .Resource.Kind }}List struct { metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + ` } func init() { - SchemeBuilder.Register(&{{.Resource.Kind}}{}, &{{.Resource.Kind}}List{}) + SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{}) } `