Skip to content

Commit 90344af

Browse files
authored
Merge pull request #1974 from Adirio/split-resource-validation
⚠ Resource validation
2 parents 564a6e9 + db966c6 commit 90344af

File tree

17 files changed

+390
-458
lines changed

17 files changed

+390
-458
lines changed

pkg/model/resource/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ type API struct {
2929
Namespaced bool `json:"namespaced,omitempty"`
3030
}
3131

32+
// Validate checks that the API is valid.
33+
func (api API) Validate() error {
34+
// Validate the CRD version
35+
if err := validateAPIVersion(api.CRDVersion); err != nil {
36+
return fmt.Errorf("invalid CRD version: %w", err)
37+
}
38+
39+
return nil
40+
}
41+
3242
// Copy returns a deep copy of the API that can be safely modified without affecting the original.
3343
func (api API) Copy() API {
3444
// As this function doesn't use a pointer receiver, api is already a shallow copy.

pkg/model/resource/api_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ import (
2424

2525
//nolint:dupl
2626
var _ = Describe("API", func() {
27+
Context("Validate", func() {
28+
It("should succeed for a valid API", func() {
29+
Expect(API{CRDVersion: v1}.Validate()).To(Succeed())
30+
})
31+
32+
DescribeTable("should fail for invalid APIs",
33+
func(api API) { Expect(api.Validate()).NotTo(Succeed()) },
34+
// Ensure that the rest of the fields are valid to check each part
35+
Entry("empty CRD version", API{}),
36+
Entry("invalid CRD version", API{CRDVersion: "1"}),
37+
)
38+
})
39+
2740
Context("Update", func() {
2841
var api, other API
2942

pkg/model/resource/gvk.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ package resource
1818

1919
import (
2020
"fmt"
21+
"regexp"
22+
"strings"
23+
24+
"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
25+
)
26+
27+
const (
28+
versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$"
29+
)
30+
31+
var (
32+
versionRegex = regexp.MustCompile(versionPattern)
2133
)
2234

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

44+
// Validate checks that the GVK is valid.
45+
func (gvk GVK) Validate() error {
46+
// Check if the qualified group has a valid DNS1123 subdomain value
47+
if err := validation.IsDNS1123Subdomain(gvk.QualifiedGroup()); err != nil {
48+
// NOTE: IsDNS1123Subdomain returns a slice of strings instead of an error, so no wrapping
49+
return fmt.Errorf("either Group or Domain is invalid: %s", err)
50+
}
51+
52+
// Check if the version follows the valid pattern
53+
if !versionRegex.MatchString(gvk.Version) {
54+
return fmt.Errorf("Version must match %s (was %s)", versionPattern, gvk.Version)
55+
}
56+
57+
// Check if kind has a valid DNS1035 label value
58+
if errors := validation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errors) != 0 {
59+
// NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping
60+
return fmt.Errorf("invalid Kind: %#v", errors)
61+
}
62+
63+
// Require kind to start with an uppercase character
64+
// NOTE: previous validation already fails for empty strings, gvk.Kind[0] will not panic
65+
if string(gvk.Kind[0]) == strings.ToLower(string(gvk.Kind[0])) {
66+
return fmt.Errorf("invalid Kind: must start with an uppercase character")
67+
}
68+
69+
return nil
70+
}
71+
3272
// QualifiedGroup returns the fully qualified group name with the available information.
3373
func (gvk GVK) QualifiedGroup() string {
3474
switch "" {

pkg/model/resource/gvk_test.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package resource
1818

1919
import (
20+
"strings"
21+
2022
. "github.com/onsi/ginkgo"
2123
. "github.com/onsi/ginkgo/extensions/table"
2224
. "github.com/onsi/gomega"
@@ -30,19 +32,50 @@ var _ = Describe("GVK", func() {
3032
kind = "Kind"
3133
)
3234

35+
var gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind}
36+
37+
Context("Validate", func() {
38+
It("should succeed for a valid GVK", func() {
39+
Expect(gvk.Validate()).To(Succeed())
40+
})
41+
42+
DescribeTable("should fail for invalid GVKs",
43+
func(gvk GVK) { Expect(gvk.Validate()).NotTo(Succeed()) },
44+
// Ensure that the rest of the fields are valid to check each part
45+
Entry("Group (uppercase)", GVK{Group: "Group", Domain: domain, Version: version, Kind: kind}),
46+
Entry("Group (non-alpha characters)", GVK{Group: "_*?", Domain: domain, Version: version, Kind: kind}),
47+
Entry("Domain (uppercase)", GVK{Group: group, Domain: "Domain", Version: version, Kind: kind}),
48+
Entry("Domain (non-alpha characters)", GVK{Group: group, Domain: "_*?", Version: version, Kind: kind}),
49+
Entry("Group and Domain (empty)", GVK{Group: "", Domain: "", Version: version, Kind: kind}),
50+
Entry("Version (empty)", GVK{Group: group, Domain: domain, Version: "", Kind: kind}),
51+
Entry("Version (no v prefix)", GVK{Group: group, Domain: domain, Version: "1", Kind: kind}),
52+
Entry("Version (wrong prefix)", GVK{Group: group, Domain: domain, Version: "a1", Kind: kind}),
53+
Entry("Version (unstable no v prefix)", GVK{Group: group, Domain: domain, Version: "1beta1", Kind: kind}),
54+
Entry("Version (unstable no alpha/beta number)",
55+
GVK{Group: group, Domain: domain, Version: "v1beta", Kind: kind}),
56+
Entry("Version (multiple unstable)",
57+
GVK{Group: group, Domain: domain, Version: "v1beta1alpha1", Kind: kind}),
58+
Entry("Kind (empty)", GVK{Group: group, Domain: domain, Version: version, Kind: ""}),
59+
Entry("Kind (whitespaces)", GVK{Group: group, Domain: domain, Version: version, Kind: "Ki nd"}),
60+
Entry("Kind (lowercase)", GVK{Group: group, Domain: domain, Version: version, Kind: "kind"}),
61+
Entry("Kind (starts with number)", GVK{Group: group, Domain: domain, Version: version, Kind: "1Kind"}),
62+
Entry("Kind (ends with `-`)", GVK{Group: group, Domain: domain, Version: version, Kind: "Kind-"}),
63+
Entry("Kind (non-alpha characters)", GVK{Group: group, Domain: domain, Version: version, Kind: "_*?"}),
64+
Entry("Kind (too long)",
65+
GVK{Group: group, Domain: domain, Version: version, Kind: strings.Repeat("a", 64)}),
66+
)
67+
})
68+
3369
Context("QualifiedGroup", func() {
3470
DescribeTable("should return the correct string",
3571
func(gvk GVK, qualifiedGroup string) { Expect(gvk.QualifiedGroup()).To(Equal(qualifiedGroup)) },
36-
Entry("fully qualified resource", GVK{Group: group, Domain: domain, Version: version, Kind: kind},
37-
group+"."+domain),
72+
Entry("fully qualified resource", gvk, group+"."+domain),
3873
Entry("empty group name", GVK{Domain: domain, Version: version, Kind: kind}, domain),
3974
Entry("empty domain", GVK{Group: group, Version: version, Kind: kind}, group),
4075
)
4176
})
4277

4378
Context("IsEqualTo", func() {
44-
var gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind}
45-
4679
It("should return true for the same resource", func() {
4780
Expect(gvk.IsEqualTo(GVK{Group: group, Domain: domain, Version: version, Kind: kind})).To(BeTrue())
4881
})

pkg/model/resource/resource.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package resource
1919
import (
2020
"fmt"
2121
"strings"
22+
23+
"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
2224
)
2325

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

47+
// Validate checks that the Resource is valid.
48+
func (r Resource) Validate() error {
49+
// Validate the GVK
50+
if err := r.GVK.Validate(); err != nil {
51+
return err
52+
}
53+
54+
// Validate the Plural
55+
// NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping
56+
if errors := validation.IsDNS1035Label(r.Plural); len(errors) != 0 {
57+
return fmt.Errorf("invalid Plural: %#v", errors)
58+
}
59+
60+
// TODO: validate the path
61+
62+
// Validate the API
63+
if r.API != nil && !r.API.IsEmpty() {
64+
if err := r.API.Validate(); err != nil {
65+
return fmt.Errorf("invalid API: %w", err)
66+
}
67+
}
68+
69+
// Validate the Webhooks
70+
if r.Webhooks != nil && !r.Webhooks.IsEmpty() {
71+
if err := r.Webhooks.Validate(); err != nil {
72+
return fmt.Errorf("invalid Webhooks: %w", err)
73+
}
74+
}
75+
76+
return nil
77+
}
78+
4579
// PackageName returns a name valid to be used por go packages.
4680
func (r Resource) PackageName() string {
4781
if r.Group == "" {

0 commit comments

Comments
 (0)