Skip to content

Commit

Permalink
Split resource validation between Options and Resource objects so tha…
Browse files Browse the repository at this point in the history
…t Resources built in different ways can still be validated

Signed-off-by: Adrian Orive <adrian.orive.oneca@gmail.com>
  • Loading branch information
Adirio committed Jan 26, 2021
1 parent 7b84f60 commit 8470552
Show file tree
Hide file tree
Showing 17 changed files with 385 additions and 454 deletions.
10 changes: 10 additions & 0 deletions pkg/model/resource/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ type API struct {
Namespaced bool `json:"namespaced,omitempty"`
}

// Validate checks that the API is valid.
func (api API) Validate() error {
// Validate the CRD version
if err := validateAPIVersion(api.CRDVersion); err != nil {
return fmt.Errorf("invalid CRD version: %w", err)
}

return nil
}

// Copy returns a deep copy of the API that can be safely modified without affecting the original.
func (api API) Copy() API {
// As this function doesn't use a pointer receiver, api is already a shallow copy.
Expand Down
13 changes: 13 additions & 0 deletions pkg/model/resource/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ import (

//nolint:dupl
var _ = Describe("API", func() {
Context("Validate", func() {
It("should succeed for a valid API", func() {
Expect(API{CRDVersion: v1}.Validate()).To(Succeed())
})

DescribeTable("should fail for invalid APIs",
func(api API) { Expect(api.Validate()).NotTo(Succeed()) },
// Ensure that the rest of the fields are valid to check each part
Entry("empty CRD version", API{}),
Entry("invalid CRD version", API{CRDVersion: "1"}),
)
})

Context("Update", func() {
var api, other API

Expand Down
40 changes: 40 additions & 0 deletions pkg/model/resource/gvk.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ package resource

import (
"fmt"
"regexp"
"strings"

"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
)

const (
versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$"
)

var (
versionRegex = regexp.MustCompile(versionPattern)
)

// GVK stores the Group - Version - Kind triplet that uniquely identifies a resource.
Expand All @@ -29,6 +41,34 @@ type GVK struct {
Kind string `json:"kind"`
}

// Validate checks that the GVK is valid.
func (gvk GVK) Validate() error {
// Check if the qualified group has a valid DNS1123 subdomain value
if err := validation.IsDNS1123Subdomain(gvk.QualifiedGroup()); err != nil {
// NOTE: IsDNS1123Subdomain returns a slice of strings instead of an error, so no wrapping
return fmt.Errorf("either Group or Domain is invalid: %s", err)
}

// Check if the version follows the valid pattern
if !versionRegex.MatchString(gvk.Version) {
return fmt.Errorf("Version must match %s (was %s)", versionPattern, gvk.Version)
}

// Check if kind has a valid DNS1035 label value
if errors := validation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errors) != 0 {
// NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping
return fmt.Errorf("invalid Kind: %#v", errors)
}

// Require kind to start with an uppercase character
// NOTE: previous validation already fails for empty strings, gvk.Kind[0] will not panic
if string(gvk.Kind[0]) == strings.ToLower(string(gvk.Kind[0])) {
return fmt.Errorf("invalid Kind: must start with an uppercase character")
}

return nil
}

// QualifiedGroup returns the fully qualified group name with the available information.
func (gvk GVK) QualifiedGroup() string {
switch "" {
Expand Down
41 changes: 37 additions & 4 deletions pkg/model/resource/gvk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package resource

import (
"strings"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
Expand All @@ -30,19 +32,50 @@ var _ = Describe("GVK", func() {
kind = "Kind"
)

var gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind}

Context("Validate", func() {
It("should succeed for a valid GVK", func() {
Expect(gvk.Validate()).To(Succeed())
})

DescribeTable("should fail for invalid GVKs",
func(gvk GVK) { Expect(gvk.Validate()).NotTo(Succeed()) },
// Ensure that the rest of the fields are valid to check each part
Entry("Group (uppercase)", GVK{Group: "Group", Domain: domain, Version: version, Kind: kind}),
Entry("Group (non-alpha characters)", GVK{Group: "_*?", Domain: domain, Version: version, Kind: kind}),
Entry("Domain (uppercase)", GVK{Group: group, Domain: "Domain", Version: version, Kind: kind}),
Entry("Domain (non-alpha characters)", GVK{Group: group, Domain: "_*?", Version: version, Kind: kind}),
Entry("Group and Domain (empty)", GVK{Group: "", Domain: "", Version: version, Kind: kind}),
Entry("Version (empty)", GVK{Group: group, Domain: domain, Version: "", Kind: kind}),
Entry("Version (no v prefix)", GVK{Group: group, Domain: domain, Version: "1", Kind: kind}),
Entry("Version (wrong prefix)", GVK{Group: group, Domain: domain, Version: "a1", Kind: kind}),
Entry("Version (unstable no v prefix)", GVK{Group: group, Domain: domain, Version: "1beta1", Kind: kind}),
Entry("Version (unstable no alpha/beta number)",
GVK{Group: group, Domain: domain, Version: "v1beta", Kind: kind}),
Entry("Version (multiple unstable)",
GVK{Group: group, Domain: domain, Version: "v1beta1alpha1", Kind: kind}),
Entry("Kind (empty)", GVK{Group: group, Domain: domain, Version: version, Kind: ""}),
Entry("Kind (whitespaces)", GVK{Group: group, Domain: domain, Version: version, Kind: "Ki nd"}),
Entry("Kind (lowercase)", GVK{Group: group, Domain: domain, Version: version, Kind: "kind"}),
Entry("Kind (starts with number)", GVK{Group: group, Domain: domain, Version: version, Kind: "1Kind"}),
Entry("Kind (ends with `-`)", GVK{Group: group, Domain: domain, Version: version, Kind: "Kind-"}),
Entry("Kind (non-alpha characters)", GVK{Group: group, Domain: domain, Version: version, Kind: "_*?"}),
Entry("Kind (too long)",
GVK{Group: group, Domain: domain, Version: version, Kind: strings.Repeat("a", 64)}),
)
})

Context("QualifiedGroup", func() {
DescribeTable("should return the correct string",
func(gvk GVK, qualifiedGroup string) { Expect(gvk.QualifiedGroup()).To(Equal(qualifiedGroup)) },
Entry("fully qualified resource", GVK{Group: group, Domain: domain, Version: version, Kind: kind},
group+"."+domain),
Entry("fully qualified resource", gvk, group+"."+domain),
Entry("empty group name", GVK{Domain: domain, Version: version, Kind: kind}, domain),
Entry("empty domain", GVK{Group: group, Version: version, Kind: kind}, group),
)
})

Context("IsEqualTo", func() {
var gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind}

It("should return true for the same resource", func() {
Expect(gvk.IsEqualTo(GVK{Group: group, Domain: domain, Version: version, Kind: kind})).To(BeTrue())
})
Expand Down
34 changes: 34 additions & 0 deletions pkg/model/resource/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package resource
import (
"fmt"
"strings"

"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
)

// Resource contains the information required to scaffold files for a resource.
Expand All @@ -42,6 +44,38 @@ type Resource struct {
Webhooks *Webhooks `json:"webhooks,omitempty"`
}

// Validate checks that the Resource is valid.
func (r Resource) Validate() error {
// Validate the GVK
if err := r.GVK.Validate(); err != nil {
return err
}

// Validate the Plural
// NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping
if errors := validation.IsDNS1035Label(r.Plural); len(errors) != 0 {
return fmt.Errorf("invalid Plural: %#v", errors)
}

// TODO: validate the path

// Validate the API
if r.API != nil && !r.API.IsEmpty() {
if err := r.API.Validate(); err != nil {
return fmt.Errorf("invalid API: %w", err)
}
}

// Validate the Webhooks
if r.Webhooks != nil && !r.Webhooks.IsEmpty() {
if err := r.Webhooks.Validate(); err != nil {
return fmt.Errorf("invalid Webhooks: %w", err)
}
}

return nil
}

// PackageName returns a name valid to be used por go packages.
func (r Resource) PackageName() string {
if r.Group == "" {
Expand Down
Loading

0 comments on commit 8470552

Please sign in to comment.