diff --git a/pkg/model/config/config.go b/pkg/model/config/config.go index 61940266420..82cb67432d1 100644 --- a/pkg/model/config/config.go +++ b/pkg/model/config/config.go @@ -21,6 +21,8 @@ import ( "strings" "sigs.k8s.io/yaml" + + "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" ) // Scaffolding versions @@ -44,8 +46,7 @@ type Config struct { ProjectName string `json:"projectName,omitempty"` // Resources tracks scaffolded resources in the project - // This info is tracked only in project with version 2 - Resources []GVK `json:"resources,omitempty"` + Resources []*resource.Resource `json:"resources,omitempty"` // Multigroup tracks if the project has more than one group MultiGroup bool `json:"multigroup,omitempty"` @@ -78,32 +79,33 @@ func (c Config) IsV3() bool { return c.Version == Version3Alpha } -// HasResource returns true if API resource is already tracked -func (c Config) HasResource(target GVK) bool { - // Return true if the target resource is found in the tracked resources +// GetResource returns the requested resource if it is already tracked +func (c Config) GetResource(target resource.GVK) *resource.Resource { + // Return the target resource if it is found in the tracked resources for _, r := range c.Resources { - if r.isEqualTo(target) { - return true + if r.GVK().IsEqualTo(target) { + return r } } - // Return false otherwise - return false + // Return nil otherwise + 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(gvk GVK) { +// UpdateResources adds the resource to the tracked ones or updates it as needed +func (c *Config) UpdateResources(res *resource.Resource) *resource.Resource { + gvk := res.GVK() // If the resource already exists, update it. for i, r := range c.Resources { - if r.isEqualTo(gvk) { - c.Resources[i].merge(gvk) - return + if r.GVK().IsEqualTo(gvk) { + c.Resources[i].Update(res) + return c.Resources[i] } } // The resource does not exist, append the resource to the tracked ones. - c.Resources = append(c.Resources, gvk) + c.Resources = append(c.Resources, res) + return res } // HasGroup returns true if group is already tracked @@ -136,9 +138,13 @@ func (c Config) resourceAPIVersionCompatible(verType, version string) bool { var currVersion string switch verType { case "crd": - currVersion = res.CRDVersion + if res.API != nil { + currVersion = res.API.Version + } case "webhook": - currVersion = res.WebhookVersion + if res.Webhooks != nil { + currVersion = res.Webhooks.Version + } } if currVersion != "" && version != currVersion { return false @@ -147,41 +153,44 @@ func (c Config) resourceAPIVersionCompatible(verType, version string) bool { return true } -// GVK contains information about scaffolded resources -type GVK struct { - Group string `json:"group,omitempty"` - Version string `json:"version,omitempty"` - Kind string `json:"kind,omitempty"` - - // CRDVersion holds the CustomResourceDefinition API version used for the GVK. - CRDVersion string `json:"crdVersion,omitempty"` - // WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the GVK. - WebhookVersion string `json:"webhookVersion,omitempty"` -} - -// isEqualTo compares it with another resource -func (r GVK) isEqualTo(other GVK) 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 *GVK) merge(other GVK) { - if r.CRDVersion == "" && other.CRDVersion != "" { - r.CRDVersion = other.CRDVersion - } - if r.WebhookVersion == "" && other.WebhookVersion != "" { - r.WebhookVersion = other.WebhookVersion - } -} - // 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].Domain = "" + cfg.Resources[i].Plural = "" + cfg.Resources[i].Path = "" + cfg.Resources[i].API = nil + cfg.Resources[i].Controller = false + cfg.Resources[i].Webhooks = nil + } + } + + // Simplify some fields + for i, r := range cfg.Resources { + // If the plural is regular, omit it + if r.Plural == resource.RegularPlural(r.Kind) { + cfg.Resources[i].Plural = "" + } + // If the path is the default location, omit it + if r.Path == resource.LocalPath(cfg.Repo, r.Group, r.Version, cfg.MultiGroup) { + cfg.Resources[i].Path = "" + } + // If API is empty, omit it (prevents `api: {}`) + if r.API != nil && r.API.IsEmpty() { + cfg.Resources[i].API = nil + } + // If Webhooks is empty, omit it (prevents `webhooks: {}`) + if r.Webhooks != nil && r.Webhooks.IsEmpty() { + cfg.Resources[i].Webhooks = nil + } + } + content, err := yaml.Marshal(cfg) if err != nil { return nil, fmt.Errorf("error marshalling project configuration: %v", err) @@ -209,16 +218,31 @@ func (c Config) Marshal() ([]byte, error) { return content, nil } -// Unmarshal unmarshals the bytes of a Config into c. +// Unmarshal unmarshalls 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() { + // Restore some omitted values + for i, r := range c.Resources { + if r.Plural == "" { + c.Resources[i].Plural = resource.RegularPlural(r.Kind) + } + if r.Path == "" { + c.Resources[i].Path = resource.LocalPath(c.Repo, r.Group, r.Version, c.MultiGroup) + } + } + + // Project version v2 does not support a plugins field. + if c.IsV2() { + // Only the default domain is allowed + for i := range c.Resources { + c.Resources[i].Domain = c.Domain + } c.Plugins = nil } + return nil } diff --git a/pkg/model/config/config_suite_test.go b/pkg/model/config/config_suite_test.go index fffe2f8bb2f..3cb98d73d1c 100644 --- a/pkg/model/config/config_suite_test.go +++ b/pkg/model/config/config_suite_test.go @@ -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/model/config/config_test.go b/pkg/model/config/config_test.go index d9bc70fc15c..b6ed2bed6b6 100644 --- a/pkg/model/config/config_test.go +++ b/pkg/model/config/config_test.go @@ -19,6 +19,8 @@ package config import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" ) const v1beta1 = "v1beta1" @@ -169,53 +171,63 @@ var _ = Describe("Resource Version Compatibility", func() { var ( c *Config - gvk1, gvk2 GVK + res1, res2 *resource.Resource defaultVersion = "v1" ) BeforeEach(func() { c = &Config{} - gvk1 = GVK{Group: "example", Version: "v1", Kind: "TestKind"} - gvk2 = GVK{Group: "example", Version: "v1", Kind: "TestKind2"} + res1 = &resource.Resource{Group: "example", Domain: "test.io", Version: "v1", Kind: "TestKind"} + res2 = &resource.Resource{Group: "example", Domain: "test.io", 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.IsCRDVersionCompatible(defaultVersion)).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeTrue()) }) + It("returns true for one resource with an empty version", func() { - c.Resources = []GVK{gvk1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) + c.Resources = []*resource.Resource{res1} + Expect(c.IsCRDVersionCompatible(defaultVersion)).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeTrue()) }) + It("returns true for one resource with matching version", func() { - gvk1.CRDVersion = defaultVersion - c.Resources = []GVK{gvk1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) + res1.API = &resource.API{Version: defaultVersion} + res1.Webhooks = &resource.Webhooks{Version: defaultVersion} + c.Resources = []*resource.Resource{res1} + Expect(c.IsCRDVersionCompatible(defaultVersion)).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeTrue()) }) + It("returns true for two resources with matching versions", func() { - gvk1.CRDVersion = defaultVersion - gvk2.CRDVersion = defaultVersion - c.Resources = []GVK{gvk1, gvk2} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeTrue()) + res1.API = &resource.API{Version: defaultVersion} + res1.Webhooks = &resource.Webhooks{Version: defaultVersion} + res2.API = &resource.API{Version: defaultVersion} + res2.Webhooks = &resource.Webhooks{Version: defaultVersion} + c.Resources = []*resource.Resource{res1, res2} + Expect(c.IsCRDVersionCompatible(defaultVersion)).To(BeTrue()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeTrue()) }) + It("returns false for one resource with a non-matching version", func() { - gvk1.CRDVersion = v1beta1 - c.Resources = []GVK{gvk1} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse()) - }) - It("returns false for two resources containing a non-matching version", func() { - gvk1.CRDVersion = v1beta1 - gvk2.CRDVersion = defaultVersion - c.Resources = []GVK{gvk1, gvk2} - Expect(c.resourceAPIVersionCompatible("crd", defaultVersion)).To(BeFalse()) + res1.API = &resource.API{Version: v1beta1} + res1.Webhooks = &resource.Webhooks{Version: v1beta1} + c.Resources = []*resource.Resource{res1} + Expect(c.IsCRDVersionCompatible(defaultVersion)).To(BeFalse()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeFalse()) }) - It("returns false for two resources containing a non-matching version (webhooks)", func() { - gvk1.WebhookVersion = v1beta1 - gvk2.WebhookVersion = defaultVersion - c.Resources = []GVK{gvk1, gvk2} - Expect(c.resourceAPIVersionCompatible("webhook", defaultVersion)).To(BeFalse()) + It("returns false for two resources containing a non-matching version", func() { + res1.API = &resource.API{Version: v1beta1} + res1.Webhooks = &resource.Webhooks{Version: v1beta1} + res2.API = &resource.API{Version: defaultVersion} + res2.Webhooks = &resource.Webhooks{Version: defaultVersion} + c.Resources = []*resource.Resource{res1, res2} + Expect(c.IsCRDVersionCompatible(defaultVersion)).To(BeFalse()) + Expect(c.IsWebhookVersionCompatible(defaultVersion)).To(BeFalse()) }) }) }) @@ -223,35 +235,47 @@ var _ = Describe("Resource Version Compatibility", func() { var _ = Describe("Config", func() { var ( c *Config - gvk1, gvk2 GVK + res1, res2 *resource.Resource ) BeforeEach(func() { c = &Config{} - gvk1 = GVK{Group: "example", Version: "v1", Kind: "TestKind"} - gvk2 = GVK{Group: "example", Version: "v1", Kind: "TestKind2"} + res1 = &resource.Resource{Group: "example", Domain: "test.io", Version: "v1", Kind: "TestKind"} + res2 = &resource.Resource{Group: "example", Domain: "test.io", Version: "v1", Kind: "TestKind2"} }) Context("UpdateResource", func() { It("Adds a non-existing resource", func() { - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]GVK{gvk1})) + c.UpdateResources(res1) + Expect(c.Resources).To(Equal([]*resource.Resource{res1})) // Update again to ensure idempotency. - c.UpdateResources(gvk1) - Expect(c.Resources).To(Equal([]GVK{gvk1})) + c.UpdateResources(res1) + Expect(c.Resources).To(Equal([]*resource.Resource{res1})) }) It("Updates an existing resource", func() { - c.UpdateResources(gvk1) - gvk := GVK{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind, CRDVersion: "v1"} - c.UpdateResources(gvk) - Expect(c.Resources).To(Equal([]GVK{gvk})) + c.UpdateResources(res1) + res := &resource.Resource{ + Group: res1.Group, + Domain: res1.Domain, + Version: res1.Version, + Kind: res1.Kind, + API: &resource.API{Version: "v1"}, + } + c.UpdateResources(res) + Expect(c.Resources).To(Equal([]*resource.Resource{res})) }) It("Updates an existing resource with more than one resource present", func() { - c.UpdateResources(gvk1) - c.UpdateResources(gvk2) - gvk := GVK{Group: gvk1.Group, Version: gvk1.Version, Kind: gvk1.Kind, CRDVersion: "v1"} - c.UpdateResources(gvk) - Expect(c.Resources).To(Equal([]GVK{gvk, gvk2})) + c.UpdateResources(res1) + c.UpdateResources(res2) + res := &resource.Resource{ + Group: res1.Group, + Domain: res1.Domain, + Version: res1.Version, + Kind: res1.Kind, + API: &resource.API{Version: "v1"}, + } + c.UpdateResources(res) + Expect(c.Resources).To(Equal([]*resource.Resource{res, res2})) }) }) }) diff --git a/pkg/model/model_suite_test.go b/pkg/model/model_suite_test.go new file mode 100644 index 00000000000..fa68e7716af --- /dev/null +++ b/pkg/model/model_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package model + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestModel(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Model Suite") +} diff --git a/pkg/model/resource/options.go b/pkg/model/options.go similarity index 58% rename from pkg/model/resource/options.go rename to pkg/model/options.go index bc02e3623b5..944105f434b 100644 --- a/pkg/model/resource/options.go +++ b/pkg/model/options.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package resource +package model import ( "fmt" @@ -22,14 +22,16 @@ import ( "regexp" "strings" - "github.com/gobuffalo/flect" - "sigs.k8s.io/kubebuilder/v2/pkg/internal/validation" "sigs.k8s.io/kubebuilder/v2/pkg/model/config" + "sigs.k8s.io/kubebuilder/v2/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" groupRequired = "group cannot be empty" versionRequired = "version cannot be empty" kindRequired = "kind cannot be empty" @@ -65,29 +67,29 @@ var ( } ) -// Options contains the information required to build a new Resource +// Options contains the information required to build a new resource.Resource. type Options struct { - // Group is the API Group. Does not contain the domain. + // Group is the resource's group. Does not contain the domain. Group string - - // Version is the API version. + // Domain is the resource's domain. + Domain string + // Version is the resource's version. Version string - - // Kind is the API Kind. + // Kind is the resource's kind. Kind string - // Plural is the API Kind plural form. + // Plural is the resource's kind plural form. // Optional Plural string - // Namespaced is true if the resource is namespaced. - Namespaced bool - - // CRDVersion holds the CustomResourceDefinition API version used for the Options. + // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. CRDVersion string - // WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the Options. + // 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 DoDefinition bool DoController bool @@ -98,54 +100,15 @@ type Options struct { // 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) + // Common validation + if err := opts.Validate(); err != nil { + return err } - 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 + + // Check that group flag is non-empty as this is not allowed in v2 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 } @@ -156,11 +119,14 @@ func (opts *Options) Validate() error { // 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(versionRequired) + return fmt.Errorf(versionPresent) } if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindRequired) + return fmt.Errorf(kindPresent) } // Now we can check that all the required flags are not empty if len(opts.Version) == 0 { @@ -170,11 +136,9 @@ func (opts *Options) Validate() error { 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 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 @@ -184,19 +148,19 @@ func (opts *Options) Validate() error { validationErrors := []string{} - // require Kind to start with an uppercase character + // 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") } - // TODO: check that at least one of Domain and Group are non-empty - 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, @@ -209,42 +173,51 @@ func (opts *Options) Validate() error { } } - // TODO: validate plural strings if provided - return nil } -// GVK returns the group-version-kind information to check against tracked resources in the configuration file -func (opts *Options) GVK() config.GVK { - return config.GVK{ - Group: opts.Group, - Version: opts.Version, - Kind: opts.Kind, - CRDVersion: opts.CRDVersion, - WebhookVersion: opts.WebhookVersion, +// 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.QualifiedGroup(), + Version: opts.Version, + Kind: opts.Kind, } } // NewResource creates a new resource from the options -func (opts *Options) NewResource(c *config.Config) *Resource { +func (opts Options) NewResource(c *config.Config) *resource.Resource { // If not provided, compute a plural for for Kind plural := opts.Plural if plural == "" { - plural = flect.Pluralize(strings.ToLower(opts.Kind)) + plural = resource.RegularPlural(opts.Kind) } - res := &Resource{ + res := &resource.Resource{ Group: opts.Group, + Domain: opts.Domain, Version: opts.Version, Kind: opts.Kind, Plural: plural, - Domain: c.Domain, - API: &API{ + Path: resource.LocalPath(c.Repo, opts.Group, opts.Version, c.MultiGroup), + API: &resource.API{ Version: opts.CRDVersion, Namespaced: opts.Namespaced, }, Controller: opts.DoController, - Webhooks: &Webhooks{ + Webhooks: &resource.Webhooks{ Version: opts.WebhookVersion, Defaulting: opts.DoDefaulting, Validation: opts.DoValidation, @@ -252,19 +225,6 @@ func (opts *Options) NewResource(c *config.Config) *Resource { }, } - // Group, Version, Kind, Plural and Domain are already set, - // so we can already create a replacer that is needed for path. - replacer := res.Replacer() - if c.MultiGroup { - if opts.Group != "" { - res.Path = replacer.Replace(path.Join(c.Repo, "apis", "%[group]", "%[version]")) - } else { - res.Path = replacer.Replace(path.Join(c.Repo, "apis", "%[version]")) - } - } else { - res.Path = replacer.Replace(path.Join(c.Repo, "api", "%[version]")) - } - // 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 @@ -272,11 +232,11 @@ func (opts *Options) NewResource(c *config.Config) *Resource { // - In any other case, default to => project resource // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath if !opts.DoDefinition { - if !c.HasResource(opts.GVK()) { + if c.GetResource(opts.GVK()) == nil { if coreDomain, found := coreGroups[opts.Group]; found { - res.Path = replacer.Replace(path.Join("k8s.io", "api", "%[group]", "%[version]")) + res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) res.Domain = coreDomain - res.API = &API{External: true} + res.API = &resource.API{External: true} } } } diff --git a/pkg/model/options_test.go b/pkg/model/options_test.go new file mode 100644 index 00000000000..82a96cc42e0 --- /dev/null +++ b/pkg/model/options_test.go @@ -0,0 +1,296 @@ +package model_test + +import ( + "path" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + . "sigs.k8s.io/kubebuilder/v2/pkg/model" + "sigs.k8s.io/kubebuilder/v2/pkg/model/config" +) + +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("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"}), + ) + }) + + // We are duplicating the test cases for Options.ValidateV2. + // TODO: remove this cases when v2 is removed. + Context("ValidateV2", func() { + DescribeTable("should succeed for valid options", + func(options *Options) { Expect(options.ValidateV2()).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.ValidateV2()).NotTo(Succeed()) }, + 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() { + It("should succeed if the Resource is valid", func() { + cfg := &config.Config{Repo: "test"} + + options := &Options{ + Group: "crew", + Domain: "test.io", + Version: "v1", + Kind: "FirstMate", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + resource := options.NewResource(cfg) + + Expect(resource.Group).To(Equal(options.Group)) + Expect(resource.Version).To(Equal(options.Version)) + Expect(resource.Kind).To(Equal(options.Kind)) + Expect(resource.Plural).To(Equal("firstmates")) + if cfg.MultiGroup { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Group, options.Version))) + } else { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "api", options.Version))) + } + Expect(resource.Domain).To(Equal(options.Domain)) + Expect(resource.API.Version).To(Equal(options.CRDVersion)) + Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) + Expect(resource.API.External).To(BeFalse()) + Expect(resource.Controller).To(Equal(options.DoController)) + Expect(resource.Webhooks.Version).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)) + } + }) + + It("should default the Plural by pluralizing the Kind", func() { + cfg := &config.Config{} + + for kind, plural := range map[string]string{ + "FirstMate": "firstmates", + "Fish": "fish", + "Helmswoman": "helmswomen", + } { + options := &Options{Group: "crew", Version: "v1", Kind: kind} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.Plural).To(Equal(plural)) + } + } + }) + + It("should keep the Plural if specified", func() { + cfg := &config.Config{} + + for kind, plural := range map[string]string{ + "FirstMate": "mates", + "Fish": "shoal", + } { + options := &Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.Plural).To(Equal(plural)) + } + } + }) + + It("should allow hyphens and dots in group names", func() { + cfg := &config.Config{Repo: "test"} + + for group, safeGroup := range map[string]string{ + "my-project": "myproject", + "my.project": "myproject", + } { + options := &Options{ + Group: group, + Domain: "test.io", + Version: "v1", + Kind: "FirstMate", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.Group).To(Equal(options.Group)) + if cfg.MultiGroup { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Group, options.Version))) + } else { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "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)) + } + } + }) + + It("should not append '.' if provided an empty domain", func() { + cfg := &config.Config{} + options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.QualifiedGroup()).To(Equal(options.Group)) + } + }) + + It("should use core apis", func() { + cfg := &config.Config{Repo: "test"} + + for group, qualified := range map[string]string{ + "apps": "apps", + "authentication": "authentication.k8s.io", + } { + options := &Options{ + Group: group, + Domain: "test.io", + Version: "v1", + Kind: "FirstMate", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) + Expect(resource.QualifiedGroup()).To(Equal(qualified)) + } + } + }) + + It("should use domain if the group is empty for version v3", func() { + cfg := &config.Config{Repo: "test"} + safeDomain := "testio" + + options := &Options{ + Domain: "test.io", + Version: "v1", + Kind: "FirstMate", + } + Expect(options.Validate()).To(Succeed()) + + for _, multiGroup := range []bool{false, true} { + cfg.MultiGroup = multiGroup + + resource := options.NewResource(cfg) + Expect(resource.Group).To(Equal("")) + if cfg.MultiGroup { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Version))) + } else { + Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "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/model/resource/gvk.go b/pkg/model/resource/gvk.go new file mode 100644 index 00000000000..b6eaf6a061f --- /dev/null +++ b/pkg/model/resource/gvk.go @@ -0,0 +1,36 @@ +/* +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 resource + +// GVK holds the unique identifier of a resource +type GVK struct { + // Group is the qualified resource's group. + Group string + + // Version is the resource's version. + Version string + + // Kind is the resource's kind. + Kind string +} + +// IsEqualTo compares two GVK objects. +func (a GVK) IsEqualTo(b GVK) bool { + return a.Group == b.Group && + a.Version == b.Version && + a.Kind == b.Kind +} diff --git a/pkg/model/resource/gvk_test.go b/pkg/model/resource/gvk_test.go new file mode 100644 index 00000000000..fc459f804ab --- /dev/null +++ b/pkg/model/resource/gvk_test.go @@ -0,0 +1,76 @@ +/* +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 resource_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" +) + +var _ = Describe("GVK", func() { + Context("IsEqualTo", func() { + var ( + a = GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + } + b = GVK{ + Group: "group", + Version: "v1", + Kind: "Kind", + } + c = GVK{ + Group: "group2", + Version: "v1", + Kind: "Kind", + } + d = GVK{ + Group: "group", + Version: "v2", + Kind: "Kind", + } + e = GVK{ + Group: "group", + Version: "v1", + Kind: "Kind2", + } + ) + + It("should return true for itself", func() { + Expect(a.IsEqualTo(a)).To(BeTrue()) + }) + + It("should return true for the same GVK", func() { + Expect(a.IsEqualTo(b)).To(BeTrue()) + }) + + It("should return false if the group is different", func() { + Expect(a.IsEqualTo(c)).To(BeFalse()) + }) + + It("should return false if the version is different", func() { + Expect(a.IsEqualTo(d)).To(BeFalse()) + }) + + It("should return false if the kind is different", func() { + Expect(a.IsEqualTo(e)).To(BeFalse()) + }) + }) +}) diff --git a/pkg/model/resource/options_test.go b/pkg/model/resource/options_test.go deleted file mode 100644 index e27e81ea141..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/v2/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 df9a65ee8b7..41b9cda6071 100644 --- a/pkg/model/resource/resource.go +++ b/pkg/model/resource/resource.go @@ -19,30 +19,25 @@ package resource import ( "fmt" "strings" - - "sigs.k8s.io/kubebuilder/v2/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 is the resource's group. Does not contain the domain. Group string `json:"group,omitempty"` - - // Version is the API version. + // Domain is the resource's domain. + Domain string `json:"domain,omitempty"` + // Version is the resource's version. Version string `json:"version,omitempty"` - - // Kind is the API kind. + // Kind is the resource's kind. Kind string `json:"kind,omitempty"` - // Plural is the API kind plural form. + // Plural is the resource's kind plural form. Plural string `json:"plural,omitempty"` // Path is the path to the go package where the types are defined. Path string `json:"path,omitempty"` - // Domain is the API group domain. - Domain string `json:"domain,omitempty"` - // API holds the information related to the resource API. API *API `json:"api,omitempty"` @@ -83,15 +78,49 @@ func (r Resource) ImportAlias() string { return safeImport(r.Group + r.Version) } -// GVK returns the group-version-kind information to check against tracked resources in the configuration file -func (r *Resource) GVK() config.GVK { - return config.GVK{ - Group: r.Group, - Version: r.Version, - Kind: r.Kind, - CRDVersion: r.API.Version, - WebhookVersion: r.Webhooks.Version, +// GVK returns the GVK identifier of a resource. +func (r Resource) GVK() GVK { + return GVK{ + Group: r.QualifiedGroup(), + Version: r.Version, + Kind: r.Kind, + } +} + +// Update combines fields of two resources that have matching GVK favoring the receiver's values. +func (r *Resource) Update(other *Resource) { + // If other is nil, nothing to merge + if other == nil { + return + } + + // If self is nil, set to other + if r == nil { + r = other + } + + // Make sure we are not merging resources for different GVKs. + if !r.GVK().IsEqualTo(other.GVK()) { + // TODO: return an error + return + } + + // TODO: check that Plural & Path match + + // Update API. + if r.API == nil && other.API != nil { + r.API = &API{} + } + r.API.Update(other.API) + + // Update controller. + r.Controller = r.Controller || other.Controller + + // Update Webhooks. + if r.Webhooks == nil && other.Webhooks != nil { + r.Webhooks = &Webhooks{} } + r.Webhooks.Update(other.Webhooks) } func wrapKey(key string) string { @@ -113,7 +142,7 @@ func (r Resource) Replacer() *strings.Replacer { // API holds the information related to the golang type definition and the CRD. type API struct { - // Version holds the CustomResourceDefinition API version used for the Resource. + // Version holds the CustomResourceDefinition API version used for the resource. Version string `json:"version,omitempty"` // Namespaced is true if the resource is namespaced. @@ -123,17 +152,69 @@ type API struct { External bool `json:"external,omitempty"` } +// Update combines fields of the APIs of two resources. +func (api *API) Update(other *API) { + // If other is nil, nothing to merge + if other == nil { + return + } + + // TODO: check that external matches between api and other as that can't be updated + + // Update the version. + // TODO: verify that api version should be updated + if api.Version == "" && other.Version != "" { + api.Version = other.Version + } + + // Update the namespace. + api.Namespaced = api.Namespaced || other.Namespaced +} + +// IsEmpty returns if the API's fields all contain zero-values. +func (api API) IsEmpty() bool { + return api.Version == "" && !api.Namespaced && !api.External +} + // Webhooks holds the information related to the associated webhooks. type Webhooks struct { - // Version holds the {Validating,Mutating}WebhookConfiguration API version used for the Resource. + // Version holds the {Validating,Mutating}WebhookConfiguration API version used for the resource. Version string `json:"version,omitempty"` - // Defaulting specifies if a defaulting webhook is associated to the Resource. - Defaulting bool `json:"mutating,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 specifies if a validation webhook is associated to the resource. Validation bool `json:"validating,omitempty"` - // Conversion specifies if a conversion webhook is associated to the Resource. + // Conversion specifies if a conversion webhook is associated to the resource. Conversion bool `json:"conversion,omitempty"` } + +// Update combines fields of the webhooks of two resources. +func (webhooks *Webhooks) Update(other *Webhooks) { + // If other is nil, nothing to merge + if other == nil { + return + } + + // Update the version. + // TODO: verify that webhook version should be updated + if webhooks.Version == "" && other.Version != "" { + webhooks.Version = other.Version + } + + // Update defaulting. + webhooks.Defaulting = webhooks.Defaulting || other.Defaulting + + // Update validation. + webhooks.Validation = webhooks.Validation || other.Validation + + // Update conversion. + webhooks.Conversion = webhooks.Conversion || other.Conversion +} + +// IsEmpty returns if the Webhooks' fields all contain zero-values. +func (webhooks Webhooks) IsEmpty() bool { + return webhooks.Version == "" && !webhooks.Defaulting && !webhooks.Validation && !webhooks.Conversion +} diff --git a/pkg/model/resource/resource_test.go b/pkg/model/resource/resource_test.go deleted file mode 100644 index ec64a25376f..00000000000 --- a/pkg/model/resource/resource_test.go +++ /dev/null @@ -1,209 +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 resource_test - -import ( - "path" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v2/pkg/model/config" - . "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" -) - -var _ = Describe("Resource", func() { - Describe("scaffolding an API", func() { - It("should succeed if the Resource is valid", func() { - cfg := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - } - - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - resource := options.NewResource(cfg) - - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.Plural).To(Equal("firstmates")) - if cfg.MultiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "api", options.Version))) - } - Expect(resource.Domain).To(Equal(cfg.Domain)) - Expect(resource.API.Version).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.API.External).To(BeFalse()) - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks.Version).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 + "." + cfg.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) - } - }) - - It("should default the Plural by pluralizing the Kind", func() { - cfg := &config.Config{ - Version: config.Version2, - } - - for kind, plural := range map[string]string{ - "FirstMate": "firstmates", - "Fish": "fish", - "Helmswoman": "helmswomen", - } { - options := &Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.Plural).To(Equal(plural)) - } - } - }) - - It("should keep the Plural if specified", func() { - cfg := &config.Config{ - Version: config.Version2, - } - - for kind, plural := range map[string]string{ - "FirstMate": "mates", - "Fish": "shoal", - } { - options := &Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.Plural).To(Equal(plural)) - } - } - }) - - It("should allow hyphens and dots in group names", func() { - cfg := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - } - - for group, safeGroup := range map[string]string{ - "my-project": "myproject", - "my.project": "myproject", - } { - options := &Options{Group: group, Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.Group).To(Equal(options.Group)) - if cfg.MultiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + cfg.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - } - }) - - It("should not append '.' if provided an empty domain", func() { - cfg := &config.Config{ - Version: config.Version2, - } - options := &Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) - - It("should use core apis", func() { - cfg := &config.Config{ - Version: config.Version2, - Domain: "test.io", - Repo: "test", - } - - for group, qualified := range map[string]string{ - "apps": "apps", - "authentication": "authentication.k8s.io", - } { - options := &Options{Group: group, Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) - } - } - }) - - It("should use domain if the group is empty for version v3", func() { - cfg := &config.Config{ - Version: config.Version3Alpha, - Domain: "test.io", - Repo: "test", - } - safeDomain := "testio" - - options := &Options{Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - cfg.MultiGroup = multiGroup - - resource := options.NewResource(cfg) - Expect(resource.Group).To(Equal("")) - if cfg.MultiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "apis", options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.Repo, "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(cfg.Domain)) - Expect(resource.PackageName()).To(Equal(safeDomain)) - Expect(resource.ImportAlias()).To(Equal(safeDomain + options.Version)) - } - }) - }) -}) diff --git a/pkg/model/resource/utils.go b/pkg/model/resource/utils.go index dbc8d6c30f0..a2febd0259f 100644 --- a/pkg/model/resource/utils.go +++ b/pkg/model/resource/utils.go @@ -17,7 +17,10 @@ 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 @@ -30,3 +33,19 @@ func safeImport(unsafe string) string { return safe } + +// LocalPath returns the default path +func LocalPath(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(kind string) string { + return flect.Pluralize(strings.ToLower(kind)) +} diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index 126983ce07c..b1c20d4c40e 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -29,7 +29,6 @@ import ( "sigs.k8s.io/kubebuilder/v2/pkg/model" "sigs.k8s.io/kubebuilder/v2/pkg/model/config" - "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" "sigs.k8s.io/kubebuilder/v2/pkg/plugin" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/golang/v2/scaffolds" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/internal/cmdutil" @@ -43,7 +42,7 @@ type createAPISubcommand struct { // pattern indicates that we should use a plugin to build according to a pattern pattern string - resource *resource.Options + options *model.Options // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -92,17 +91,17 @@ After the scaffold is written, api will run make on the project. } func (p *createAPISubcommand) 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") + p.options = &model.Options{Domain: p.config.Domain} + fs.StringVar(&p.options.Group, "group", "", "resource Group") + fs.StringVar(&p.options.Version, "version", "", "resource Version") + fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") - fs.BoolVar(&p.resource.DoDefinition, "resource", true, + fs.BoolVar(&p.options.DoDefinition, "resource", true, "if set, generate the resource without prompting the user") p.resourceFlag = fs.Lookup("resource") - fs.BoolVar(&p.resource.Namespaced, "namespaced", true, "resource is namespaced") + fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") - fs.BoolVar(&p.resource.DoController, "controller", true, + fs.BoolVar(&p.options.DoController, "controller", true, "if set, generate the controller without prompting the user") p.controllerFlag = fs.Lookup("controller") @@ -126,29 +125,29 @@ func (p *createAPISubcommand) Run() error { } func (p *createAPISubcommand) Validate() error { - if err := p.resource.ValidateV2(); err != nil { + if err := p.options.ValidateV2(); err != nil { return err } reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") - p.resource.DoDefinition = util.YesNo(reader) + p.options.DoDefinition = util.YesNo(reader) } if !p.controllerFlag.Changed { fmt.Println("Create Controller [y/n]") - p.resource.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.resource.DoDefinition { + if p.options.DoDefinition { // Check that resource doesn't exist or flag force was set - if !p.force && p.config.HasResource(p.resource.GVK()) { + if res := p.config.GetResource(p.options.GVK()); !p.force && res != nil && res.API != nil { 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.MultiGroup && len(p.config.Resources) != 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") } @@ -176,8 +175,8 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { } // Create the actual resource from the resource options - res := p.resource.NewResource(p.config) - return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.resource.DoDefinition, plugins), nil + res := p.options.NewResource(p.config) + return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.options.DoDefinition, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go index d0a8cef2686..7ed386c6b96 100644 --- a/pkg/plugins/golang/v2/scaffolds/api.go +++ b/pkg/plugins/golang/v2/scaffolds/api.go @@ -83,9 +83,12 @@ func (s *apiScaffolder) newUniverse() *model.Universe { } func (s *apiScaffolder) scaffold() error { - if s.doDefinition { + // Check if we need to do the controller before updating with previously existing info + doController := s.resource.Controller - s.config.UpdateResources(s.resource.GVK()) + if s.doDefinition { + // Update the known data about resource + s.resource = s.config.UpdateResources(s.resource) if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), @@ -107,10 +110,9 @@ func (s *apiScaffolder) scaffold() error { ); err != nil { return fmt.Errorf("error scaffolding kustomization: %v", err) } - } - if s.resource.Controller { + if doController { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), &controllers.SuiteTest{WireResource: s.doDefinition}, @@ -122,7 +124,7 @@ func (s *apiScaffolder) scaffold() error { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &templates.MainUpdater{WireResource: s.doDefinition, WireController: s.resource.Controller}, + &templates.MainUpdater{WireResource: s.doDefinition, WireController: doController}, ); err != nil { return fmt.Errorf("error updating main.go: %v", err) } diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go index 89f0d18be45..3ba0f5ca332 100644 --- a/pkg/plugins/golang/v2/scaffolds/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/webhook.go @@ -70,11 +70,6 @@ func (s *webhookScaffolder) newUniverse() *model.Universe { } func (s *webhookScaffolder) scaffold() error { - if s.resource.Webhooks.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{}, @@ -83,5 +78,10 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f return err } + if s.resource.Webhooks.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.`) + } + return nil } diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index 0e076ea262d..7b3ce5399af 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -23,8 +23,8 @@ import ( "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/v2/pkg/model" "sigs.k8s.io/kubebuilder/v2/pkg/model/config" - "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" "sigs.k8s.io/kubebuilder/v2/pkg/plugin" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/golang/v2/scaffolds" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/internal/cmdutil" @@ -35,7 +35,7 @@ type createWebhookSubcommand struct { // For help text. commandName string - resource *resource.Options + options *model.Options } var ( @@ -60,17 +60,17 @@ 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") + p.options = &model.Options{Domain: p.config.Domain} + fs.StringVar(&p.options.Group, "group", "", "resource Group") + fs.StringVar(&p.options.Version, "version", "", "resource Version") + fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "resource", "", "resource Resource") - fs.BoolVar(&p.resource.DoDefaulting, "defaulting", false, + fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") - fs.BoolVar(&p.resource.DoValidation, "programmatic-validation", false, + fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, "if set, scaffold the validating webhook") - fs.BoolVar(&p.resource.DoConversion, "conversion", false, + fs.BoolVar(&p.options.DoConversion, "conversion", false, "if set, scaffold the conversion webhook") } @@ -83,17 +83,17 @@ func (p *createWebhookSubcommand) Run() error { } func (p *createWebhookSubcommand) Validate() error { - if err := p.resource.ValidateV2(); err != nil { + if err := p.options.ValidateV2(); err != nil { return err } - if !p.resource.DoDefaulting && !p.resource.DoValidation && !p.resource.DoConversion { + 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.HasResource(p.resource.GVK()) { + if p.config.GetResource(p.options.GVK()) == nil { return fmt.Errorf("%s create webhook requires an api with the group,"+ " kind and version provided", p.commandName) } @@ -109,7 +109,7 @@ func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { } // Create the actual resource from the resource options - res := p.resource.NewResource(p.config) + res := p.options.NewResource(p.config) return scaffolds.NewWebhookScaffolder(p.config, string(bp), res), nil } diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index 49d53a48099..f0c8cb98258 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -29,7 +29,6 @@ import ( "sigs.k8s.io/kubebuilder/v2/pkg/model" "sigs.k8s.io/kubebuilder/v2/pkg/model/config" - "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" "sigs.k8s.io/kubebuilder/v2/pkg/plugin" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/golang/v3/scaffolds" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/internal/cmdutil" @@ -56,7 +55,7 @@ type createAPISubcommand struct { // pattern indicates that we should use a plugin to build according to a pattern pattern string - resource *resource.Options + options *model.Options // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -105,19 +104,20 @@ After the scaffold is written, api will run make on the project. } func (p *createAPISubcommand) 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") + p.options = &model.Options{Domain: p.config.Domain} + fs.StringVar(&p.options.Group, "group", "", "resource Group") + // TODO: allow the user to specify the domain + fs.StringVar(&p.options.Version, "version", "", "resource Version") + fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") - fs.BoolVar(&p.resource.DoDefinition, "resource", true, + fs.BoolVar(&p.options.DoDefinition, "resource", true, "if set, generate the resource without prompting the user") p.resourceFlag = fs.Lookup("resource") - fs.StringVar(&p.resource.CRDVersion, "crd-version", defaultCRDVersion, + fs.StringVar(&p.options.CRDVersion, "crd-version", defaultCRDVersion, "version of CustomResourceDefinition to scaffold. Options: [v1, v1beta1]") - fs.BoolVar(&p.resource.Namespaced, "namespaced", true, "resource is namespaced") + fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") - fs.BoolVar(&p.resource.DoController, "controller", true, + fs.BoolVar(&p.options.DoController, "controller", true, "if set, generate the controller without prompting the user") p.controllerFlag = fs.Lookup("controller") @@ -141,11 +141,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.config.Domain == "" { return fmt.Errorf("can not have group and domain both empty") } @@ -159,30 +159,30 @@ func (p *createAPISubcommand) Validate() error { reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") - p.resource.DoDefinition = util.YesNo(reader) + p.options.DoDefinition = util.YesNo(reader) } if !p.controllerFlag.Changed { fmt.Println("Create Controller [y/n]") - p.resource.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.resource.DoDefinition { + if p.options.DoDefinition { // Check that resource doesn't exist or flag force was set - if !p.force && p.config.HasResource(p.resource.GVK()) { + if res := p.config.GetResource(p.options.GVK()); !p.force && res != nil && res.API != nil { 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.MultiGroup && len(p.config.Resources) != 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.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.CRDVersion) + p.options.CRDVersion) } } @@ -208,8 +208,8 @@ func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { } // Create the actual resource from the resource options - res := p.resource.NewResource(p.config) - return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.resource.DoDefinition, plugins), nil + res := p.options.NewResource(p.config) + return scaffolds.NewAPIScaffolder(p.config, string(bp), res, p.options.DoDefinition, plugins), nil } func (p *createAPISubcommand) PostScaffold() error { diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go index 7df5f2f288f..35a46999d63 100644 --- a/pkg/plugins/golang/v3/scaffolds/api.go +++ b/pkg/plugins/golang/v3/scaffolds/api.go @@ -80,10 +80,13 @@ func (s *apiScaffolder) newUniverse() *model.Universe { // TODO: re-use universe created by s.newUniverse() if possible. func (s *apiScaffolder) scaffold() error { - if s.doDefinition { + // Check if we need to do the controller before updating with previously existing info + doController := s.resource.Controller - s.config.UpdateResources(s.resource.GVK()) + // Update the known data about resource + s.resource = s.config.UpdateResources(s.resource) + if s.doDefinition { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), &api.Types{}, @@ -104,10 +107,9 @@ func (s *apiScaffolder) scaffold() error { ); err != nil { return fmt.Errorf("error scaffolding kustomization: %v", err) } - } - if s.resource.Controller { + if doController { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), &controllers.SuiteTest{WireResource: s.doDefinition}, @@ -119,7 +121,7 @@ func (s *apiScaffolder) scaffold() error { if err := machinery.NewScaffold(s.plugins...).Execute( s.newUniverse(), - &templates.MainUpdater{WireResource: s.doDefinition, WireController: s.resource.Controller}, + &templates.MainUpdater{WireResource: s.doDefinition, WireController: doController}, ); err != nil { return fmt.Errorf("error updating main.go: %v", err) } 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 e79e6c90dfc..06fa1d4c3b5 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go @@ -55,7 +55,7 @@ func (f *Group) SetTemplateDefaults() error { //nolint:lll const groupTemplate = `{{ .Boilerplate }} -// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group +// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.QualifiedGroup }} {{ .Resource.Version }} API group // +kubebuilder:object:generate=true // +groupName={{ .Resource.QualifiedGroup }} package {{ .Resource.Version }} diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go index 22026dc90a0..86288434189 100644 --- a/pkg/plugins/golang/v3/scaffolds/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/webhook.go @@ -66,12 +66,13 @@ func (s *webhookScaffolder) newUniverse() *model.Universe { } func (s *webhookScaffolder) scaffold() error { - if s.resource.Webhooks.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.`) - } + // Check if we need to do the defaulting or validation before updating with previously existing info + doDefaulting := s.resource.Webhooks.Defaulting + doValidation := s.resource.Webhooks.Validation + doConversion := s.resource.Webhooks.Conversion - s.config.UpdateResources(s.resource.GVK()) + // Update the known data about resource + s.resource = s.config.UpdateResources(s.resource) if err := machinery.NewScaffold().Execute( s.newUniverse(), @@ -87,7 +88,7 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f } // TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest. - if s.resource.Webhooks.Defaulting || s.resource.Webhooks.Validation { + if doDefaulting || doValidation { if err := machinery.NewScaffold().Execute( s.newUniverse(), &api.WebhookSuite{}, @@ -96,5 +97,10 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f } } + 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.`) + } + return nil } diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 25a3c98ef16..310bcb09f47 100644 --- a/pkg/plugins/golang/v3/webhook.go +++ b/pkg/plugins/golang/v3/webhook.go @@ -23,8 +23,8 @@ import ( "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/v2/pkg/model" "sigs.k8s.io/kubebuilder/v2/pkg/model/config" - "sigs.k8s.io/kubebuilder/v2/pkg/model/resource" "sigs.k8s.io/kubebuilder/v2/pkg/plugin" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/golang/v3/scaffolds" "sigs.k8s.io/kubebuilder/v2/pkg/plugins/internal/cmdutil" @@ -39,7 +39,7 @@ type createWebhookSubcommand struct { // For help text. commandName string - resource *resource.Options + options *model.Options // runMake indicates whether to run make or not after scaffolding webhooks runMake bool @@ -67,19 +67,20 @@ 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.WebhookVersion, "webhook-version", defaultWebhookVersion, + p.options = &model.Options{Domain: p.config.Domain} + fs.StringVar(&p.options.Group, "group", "", "resource Group") + // TODO: allow the user to specify the domain + fs.StringVar(&p.options.Version, "version", "", "resource Version") + fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "resource", "", "resource Resource") + + fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion, "version of {Mutating,Validating}WebhookConfigurations to scaffold. Options: [v1, v1beta1]") - fs.BoolVar(&p.resource.DoDefaulting, "defaulting", false, + fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") - fs.BoolVar(&p.resource.DoValidation, "programmatic-validation", false, + fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, "if set, scaffold the validating webhook") - fs.BoolVar(&p.resource.DoConversion, "conversion", false, + 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") @@ -94,24 +95,24 @@ 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.resource.DoDefaulting && !p.resource.DoValidation && !p.resource.DoConversion { + 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.HasResource(p.resource.GVK()) { + if p.config.GetResource(p.options.GVK()) == nil { return fmt.Errorf("%s create webhook requires an api with the group,"+ " kind and version provided", p.commandName) } - if !p.config.IsWebhookVersionCompatible(p.resource.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.WebhookVersion) + p.options.WebhookVersion) } return nil @@ -125,7 +126,7 @@ func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { } // Create the actual resource from the resource options - res := p.resource.NewResource(p.config) + res := p.options.NewResource(p.config) return scaffolds.NewWebhookScaffolder(p.config, string(bp), res), nil } diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT index 825040c5892..447d7984fe5 100644 --- a/testdata/project-v3-addon/PROJECT +++ b/testdata/project-v3-addon/PROJECT @@ -3,15 +3,26 @@ layout: go.kubebuilder.io/v3-alpha projectName: project-v3-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v3-addon resources: -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: Captain version: v1 -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: FirstMate version: v1 -- crdVersion: v1 +- api: + version: v1 + controller: true + domain: testproject.org group: crew kind: Admiral version: v1 diff --git a/testdata/project-v3-addon/api/v1/groupversion_info.go b/testdata/project-v3-addon/api/v1/groupversion_info.go index 16821d46605..f64ad1572dd 100644 --- a/testdata/project-v3-addon/api/v1/groupversion_info.go +++ b/testdata/project-v3-addon/api/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the crew v1 API group +// Package v1 contains API Schema definitions for the crew.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=crew.testproject.org package v1 diff --git a/testdata/project-v3-config/PROJECT b/testdata/project-v3-config/PROJECT index 5e22d5af4b1..bd53f3ef87b 100644 --- a/testdata/project-v3-config/PROJECT +++ b/testdata/project-v3-config/PROJECT @@ -4,19 +4,45 @@ layout: go.kubebuilder.io/v3-alpha projectName: project-v3-config repo: sigs.k8s.io/kubebuilder/testdata/project-v3-config resources: -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: Captain version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + defaulting: true + validating: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: FirstMate version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + conversion: true + version: v1 +- api: + version: v1 + controller: true + domain: testproject.org group: crew kind: Admiral version: v1 - webhookVersion: v1 + webhooks: + defaulting: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org + group: crew + kind: Laker + version: v1 version: 3-alpha diff --git a/testdata/project-v3-config/api/v1/groupversion_info.go b/testdata/project-v3-config/api/v1/groupversion_info.go index 16821d46605..f64ad1572dd 100644 --- a/testdata/project-v3-config/api/v1/groupversion_info.go +++ b/testdata/project-v3-config/api/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the crew v1 API group +// Package v1 contains API Schema definitions for the crew.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=crew.testproject.org package v1 diff --git a/testdata/project-v3-multigroup/PROJECT b/testdata/project-v3-multigroup/PROJECT index a612501d6c5..8514e475a67 100644 --- a/testdata/project-v3-multigroup/PROJECT +++ b/testdata/project-v3-multigroup/PROJECT @@ -4,40 +4,89 @@ multigroup: true projectName: project-v3-multigroup repo: sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup resources: -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: Captain version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + defaulting: true + validating: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: ship kind: Frigate version: v1beta1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + conversion: true + version: v1 +- api: + version: v1 + controller: true + domain: testproject.org group: ship kind: Destroyer version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + defaulting: true + version: v1 +- api: + version: v1 + controller: true + domain: testproject.org group: ship kind: Cruiser version: v2alpha1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + validating: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: sea-creatures kind: Kraken version: v1beta1 -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: sea-creatures kind: Leviathan version: v1beta2 -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: foo.policy kind: HealthCheckPolicy version: v1 -- crdVersion: v1 +- api: + external: true + controller: true + group: apps + kind: Pod + path: k8s.io/api/apps/v1 + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org kind: Lakers version: v1 - webhookVersion: v1 + webhooks: + defaulting: true + validating: true + version: v1 version: 3-alpha diff --git a/testdata/project-v3-multigroup/apis/crew/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/crew/v1/groupversion_info.go index 16821d46605..f64ad1572dd 100644 --- a/testdata/project-v3-multigroup/apis/crew/v1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/crew/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the crew v1 API group +// Package v1 contains API Schema definitions for the crew.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=crew.testproject.org package v1 diff --git a/testdata/project-v3-multigroup/apis/foo.policy/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/foo.policy/v1/groupversion_info.go index 5608d284538..19090ee04c6 100644 --- a/testdata/project-v3-multigroup/apis/foo.policy/v1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/foo.policy/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the foo.policy v1 API group +// Package v1 contains API Schema definitions for the foo.policy.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=foo.policy.testproject.org package v1 diff --git a/testdata/project-v3-multigroup/apis/sea-creatures/v1beta1/groupversion_info.go b/testdata/project-v3-multigroup/apis/sea-creatures/v1beta1/groupversion_info.go index 8126ea238b7..8a8759a7368 100644 --- a/testdata/project-v3-multigroup/apis/sea-creatures/v1beta1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/sea-creatures/v1beta1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1beta1 contains API Schema definitions for the sea-creatures v1beta1 API group +// Package v1beta1 contains API Schema definitions for the sea-creatures.testproject.org v1beta1 API group // +kubebuilder:object:generate=true // +groupName=sea-creatures.testproject.org package v1beta1 diff --git a/testdata/project-v3-multigroup/apis/sea-creatures/v1beta2/groupversion_info.go b/testdata/project-v3-multigroup/apis/sea-creatures/v1beta2/groupversion_info.go index e280a1ae97c..ae56b011a78 100644 --- a/testdata/project-v3-multigroup/apis/sea-creatures/v1beta2/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/sea-creatures/v1beta2/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1beta2 contains API Schema definitions for the sea-creatures v1beta2 API group +// Package v1beta2 contains API Schema definitions for the sea-creatures.testproject.org v1beta2 API group // +kubebuilder:object:generate=true // +groupName=sea-creatures.testproject.org package v1beta2 diff --git a/testdata/project-v3-multigroup/apis/ship/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/ship/v1/groupversion_info.go index 1cb2de6dd6a..c9303129a93 100644 --- a/testdata/project-v3-multigroup/apis/ship/v1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/ship/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the ship v1 API group +// Package v1 contains API Schema definitions for the ship.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=ship.testproject.org package v1 diff --git a/testdata/project-v3-multigroup/apis/ship/v1beta1/groupversion_info.go b/testdata/project-v3-multigroup/apis/ship/v1beta1/groupversion_info.go index 09119c7cd7e..0ce591aa986 100644 --- a/testdata/project-v3-multigroup/apis/ship/v1beta1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/ship/v1beta1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1beta1 contains API Schema definitions for the ship v1beta1 API group +// Package v1beta1 contains API Schema definitions for the ship.testproject.org v1beta1 API group // +kubebuilder:object:generate=true // +groupName=ship.testproject.org package v1beta1 diff --git a/testdata/project-v3-multigroup/apis/ship/v2alpha1/groupversion_info.go b/testdata/project-v3-multigroup/apis/ship/v2alpha1/groupversion_info.go index 93b6d3dc548..f96566b9c72 100644 --- a/testdata/project-v3-multigroup/apis/ship/v2alpha1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/ship/v2alpha1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v2alpha1 contains API Schema definitions for the ship v2alpha1 API group +// Package v2alpha1 contains API Schema definitions for the ship.testproject.org v2alpha1 API group // +kubebuilder:object:generate=true // +groupName=ship.testproject.org package v2alpha1 diff --git a/testdata/project-v3-multigroup/apis/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/v1/groupversion_info.go index 5ae47f26fb7..246e3666fc9 100644 --- a/testdata/project-v3-multigroup/apis/v1/groupversion_info.go +++ b/testdata/project-v3-multigroup/apis/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the v1 API group +// Package v1 contains API Schema definitions for the testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=testproject.org package v1 diff --git a/testdata/project-v3/PROJECT b/testdata/project-v3/PROJECT index e64a848c894..9a42ab6a7eb 100644 --- a/testdata/project-v3/PROJECT +++ b/testdata/project-v3/PROJECT @@ -3,19 +3,45 @@ layout: go.kubebuilder.io/v3-alpha projectName: project-v3 repo: sigs.k8s.io/kubebuilder/testdata/project-v3 resources: -- crdVersion: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: Captain version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + defaulting: true + validating: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org group: crew kind: FirstMate version: v1 - webhookVersion: v1 -- crdVersion: v1 + webhooks: + conversion: true + version: v1 +- api: + version: v1 + controller: true + domain: testproject.org group: crew kind: Admiral version: v1 - webhookVersion: v1 + webhooks: + defaulting: true + version: v1 +- api: + namespaced: true + version: v1 + controller: true + domain: testproject.org + group: crew + kind: Laker + version: v1 version: 3-alpha diff --git a/testdata/project-v3/api/v1/groupversion_info.go b/testdata/project-v3/api/v1/groupversion_info.go index 16821d46605..f64ad1572dd 100644 --- a/testdata/project-v3/api/v1/groupversion_info.go +++ b/testdata/project-v3/api/v1/groupversion_info.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// Package v1 contains API Schema definitions for the crew v1 API group +// Package v1 contains API Schema definitions for the crew.testproject.org v1 API group // +kubebuilder:object:generate=true // +groupName=crew.testproject.org package v1