Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚠ Resource validation #1974

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Adirio marked this conversation as resolved.
Show resolved Hide resolved
)

// 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