Skip to content

Commit cbcbf1d

Browse files
author
Phillip Wittrock
authored
Merge pull request #150 from fanzhangio/issue128
Add more support for CRD validation
2 parents 123e075 + f73c804 commit cbcbf1d

File tree

6 files changed

+276
-45
lines changed

6 files changed

+276
-45
lines changed

cmd/internal/codegen/parse/crd.go

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,9 @@ var jsonRegex = regexp.MustCompile("json:\"([a-zA-Z,]+)\"")
148148

149149
type primitiveTemplateArgs struct {
150150
v1beta1.JSONSchemaProps
151-
Value string
152-
Format string
151+
Value string
152+
Format string
153+
EnumValue string // TODO check type of enum value to match the type of field
153154
}
154155

155156
var primitiveTemplate = template.Must(template.New("map-template").Parse(
@@ -173,6 +174,15 @@ var primitiveTemplate = template.Must(template.New("map-template").Parse(
173174
{{ if .Format -}}
174175
Format: "{{ .Format }}",
175176
{{ end -}}
177+
{{ if .EnumValue -}}
178+
Enum: {{ .EnumValue }},
179+
{{ end -}}
180+
{{ if .MaxLength -}}
181+
MaxLength: getInt({{ .MaxLength }}),
182+
{{ end -}}
183+
{{ if .MinLength -}}
184+
MinLength: getInt({{ .MinLength }}),
185+
{{ end -}}
176186
}`))
177187

178188
// parsePrimitiveValidation returns a JSONSchemaProps object and its
@@ -187,7 +197,7 @@ func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, commen
187197

188198
buff := &bytes.Buffer{}
189199

190-
var n, f string
200+
var n, f, s string
191201
switch t.Name.Name {
192202
case "int", "int64", "uint64":
193203
n = "integer"
@@ -208,15 +218,17 @@ func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, commen
208218
default:
209219
n = t.Name.Name
210220
}
211-
if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f}); err != nil {
221+
if props.Enum != nil {
222+
s = parseEnumToString(props.Enum)
223+
}
224+
if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f, s}); err != nil {
212225
log.Fatalf("%v", err)
213226
}
214-
215227
return props, buff.String()
216228
}
217229

218230
type mapTempateArgs struct {
219-
Result string
231+
Result string
220232
SkipMapValidation bool
221233
}
222234

@@ -236,7 +248,7 @@ func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []s
236248
props := v1beta1.JSONSchemaProps{
237249
Type: "object",
238250
}
239-
parseOption := b.arguments.CustomArgs.(*ParseOptions)
251+
parseOption := b.arguments.CustomArgs.(*ParseOptions)
240252
if !parseOption.SkipMapValidation {
241253
props.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{
242254
Allows: true,
@@ -253,11 +265,25 @@ func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []s
253265
var arrayTemplate = template.Must(template.New("array-template").Parse(
254266
`v1beta1.JSONSchemaProps{
255267
Type: "array",
268+
{{ if .MaxItems -}}
269+
MaxItems: getInt({{ .MaxItems }}),
270+
{{ end -}}
271+
{{ if .MinItems -}}
272+
MinItems: getInt({{ .MinItems }}),
273+
{{ end -}}
274+
{{ if .UniqueItems -}}
275+
UniqueItems: {{ .UniqueItems }},
276+
{{ end -}}
256277
Items: &v1beta1.JSONSchemaPropsOrArray{
257-
Schema: &{{.}},
278+
Schema: &{{.ItemsSchema}},
258279
},
259280
}`))
260281

282+
type arrayTemplateArgs struct {
283+
v1beta1.JSONSchemaProps
284+
ItemsSchema string
285+
}
286+
261287
// parseArrayValidation returns a JSONSchemaProps object and its serialization in
262288
// Go that describe the validations for the given array type.
263289
func (b *APIs) parseArrayValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
@@ -266,9 +292,11 @@ func (b *APIs) parseArrayValidation(t *types.Type, found sets.String, comments [
266292
Type: "array",
267293
Items: &v1beta1.JSONSchemaPropsOrArray{Schema: &items},
268294
}
269-
295+
for _, l := range comments {
296+
getValidation(l, &props)
297+
}
270298
buff := &bytes.Buffer{}
271-
if err := arrayTemplate.Execute(buff, result); err != nil {
299+
if err := arrayTemplate.Execute(buff, arrayTemplateArgs{props, result}); err != nil {
272300
log.Fatalf("%v", err)
273301
}
274302
return props, buff.String()
@@ -380,28 +408,34 @@ func getValidation(comment string, props *v1beta1.JSONSchemaProps) {
380408
case "Pattern":
381409
props.Pattern = parts[1]
382410
case "MaxItems":
383-
i, err := strconv.Atoi(parts[1])
384-
v := int64(i)
385-
if err != nil {
386-
log.Fatalf("Could not parse int from %s: %v", comment, err)
387-
return
411+
if props.Type == "array" {
412+
i, err := strconv.Atoi(parts[1])
413+
v := int64(i)
414+
if err != nil {
415+
log.Fatalf("Could not parse int from %s: %v", comment, err)
416+
return
417+
}
418+
props.MaxItems = &v
388419
}
389-
props.MaxItems = &v
390420
case "MinItems":
391-
i, err := strconv.Atoi(parts[1])
392-
v := int64(i)
393-
if err != nil {
394-
log.Fatalf("Could not parse int from %s: %v", comment, err)
395-
return
421+
if props.Type == "array" {
422+
i, err := strconv.Atoi(parts[1])
423+
v := int64(i)
424+
if err != nil {
425+
log.Fatalf("Could not parse int from %s: %v", comment, err)
426+
return
427+
}
428+
props.MinItems = &v
396429
}
397-
props.MinItems = &v
398430
case "UniqueItems":
399-
b, err := strconv.ParseBool(parts[1])
400-
if err != nil {
401-
log.Fatalf("Could not parse bool from %s: %v", comment, err)
402-
return
431+
if props.Type == "array" {
432+
b, err := strconv.ParseBool(parts[1])
433+
if err != nil {
434+
log.Fatalf("Could not parse bool from %s: %v", comment, err)
435+
return
436+
}
437+
props.UniqueItems = b
403438
}
404-
props.ExclusiveMinimum = b
405439
case "MultipleOf":
406440
f, err := strconv.ParseFloat(parts[1], 64)
407441
if err != nil {
@@ -410,9 +444,13 @@ func getValidation(comment string, props *v1beta1.JSONSchemaProps) {
410444
}
411445
props.MultipleOf = &f
412446
case "Enum":
413-
enums := strings.Split(parts[1], ",")
414-
for i := range enums {
415-
props.Enum = append(props.Enum, v1beta1.JSON{[]byte(enums[i])})
447+
if props.Type != "array" {
448+
value := strings.Split(parts[1], ",")
449+
enums := []v1beta1.JSON{}
450+
for _, s := range value {
451+
checkType(props, s, &enums)
452+
}
453+
props.Enum = enums
416454
}
417455
case "Format":
418456
props.Format = parts[1]

cmd/internal/codegen/parse/util.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ package parse
1818

1919
import (
2020
"fmt"
21+
"log"
2122
"path/filepath"
23+
"strconv"
2224
"strings"
2325

2426
"github.com/pkg/errors"
2527

28+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
2629
"k8s.io/gengo/types"
2730
)
2831

@@ -239,3 +242,51 @@ func getDocAnnotation(t *types.Type, tags ...string) map[string]string {
239242
}
240243
return annotation
241244
}
245+
246+
// parseByteValue returns the literal digital number values from a byte array
247+
func parseByteValue(b []byte) string {
248+
elem := strings.Join(strings.Fields(fmt.Sprintln(b)), ",")
249+
elem = strings.TrimPrefix(elem, "[")
250+
elem = strings.TrimSuffix(elem, "]")
251+
return elem
252+
}
253+
254+
// parseEnumToString returns a representive validated go format string from JSONSchemaProps schema
255+
func parseEnumToString(value []v1beta1.JSON) string {
256+
res := "[]v1beta1.JSON{"
257+
prefix := "v1beta1.JSON{[]byte{"
258+
for _, v := range value {
259+
res = res + prefix + parseByteValue(v.Raw) + "}},"
260+
}
261+
return strings.TrimSuffix(res, ",") + "}"
262+
}
263+
264+
// check type of enum element value to match type of field
265+
func checkType(props *v1beta1.JSONSchemaProps, s string, enums *[]v1beta1.JSON) {
266+
267+
// TODO support more types check
268+
switch props.Type {
269+
case "int", "int64", "uint64":
270+
if _, err := strconv.ParseInt(s, 0, 64); err != nil {
271+
log.Fatalf("Invalid integer value [%v] for a field of integer type", s)
272+
}
273+
*enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))})
274+
case "int32", "unit32":
275+
if _, err := strconv.ParseInt(s, 0, 32); err != nil {
276+
log.Fatalf("Invalid integer value [%v] for a field of integer32 type", s)
277+
}
278+
*enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))})
279+
case "float", "float32":
280+
if _, err := strconv.ParseFloat(s, 32); err != nil {
281+
log.Fatalf("Invalid float value [%v] for a field of float32 type", s)
282+
}
283+
*enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))})
284+
case "float64":
285+
if _, err := strconv.ParseFloat(s, 64); err != nil {
286+
log.Fatalf("Invalid float value [%v] for a field of float type", s)
287+
}
288+
*enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))})
289+
case "string":
290+
*enums = append(*enums, v1beta1.JSON{[]byte(`"` + s + `"`)})
291+
}
292+
}

cmd/kubebuilder-gen/internal/resourcegen/versioned_generator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ func getFloat(f float64) *float64 {
107107
return &f
108108
}
109109
110+
func getInt(i int64) *int64 {
111+
return &i
112+
}
113+
110114
var (
111115
{{ range $api := .Resources -}}
112116
// Define CRDs for resources

pkg/gen/apis/doc.go

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,47 @@ The apis package describes the comment directives that may be applied to apis /
1919
*/
2020
package apis
2121

22-
// Resource annotates a type as a resource
23-
const Resource = "// +kubebuilder:resource:path="
22+
const (
23+
// Resource annotates a type as a resource
24+
Resource = "// +kubebuilder:resource:path="
2425

25-
// Categories annotates a type as belonging to a comma-delimited list of
26-
// categories
27-
const Categories = "// +kubebuilder:categories="
26+
// Categories annotates a type as belonging to a comma-delimited list of
27+
// categories
28+
Categories = "// +kubebuilder:categories="
2829

29-
// Maximum annotates a numeric go struct field for CRD validation
30-
const Maximum = "// +kubebuilder:validation:Maximum="
30+
// Maximum annotates a numeric go struct field for CRD validation
31+
Maximum = "// +kubebuilder:validation:Maximum="
3132

32-
// ExclusiveMaximum annotates a numeric go struct field for CRD validation
33-
const ExclusiveMaximum = "// +kubebuilder:validation:ExclusiveMaximum="
33+
// ExclusiveMaximum annotates a numeric go struct field for CRD validation
34+
ExclusiveMaximum = "// +kubebuilder:validation:ExclusiveMaximum="
3435

35-
// Minimum annotates a numeric go struct field for CRD validation
36-
const Minimum = "// +kubebuilder:validation:Minimum="
36+
// Minimum annotates a numeric go struct field for CRD validation
37+
Minimum = "// +kubebuilder:validation:Minimum="
3738

38-
// ExclusiveMinimum annotates a numeric go struct field for CRD validation
39-
const ExclusiveMinimum = "// +kubebuilder:validation:ExclusiveMinimum="
39+
// ExclusiveMinimum annotates a numeric go struct field for CRD validation
40+
ExclusiveMinimum = "// +kubebuilder:validation:ExclusiveMinimum="
4041

41-
// Pattern annotates a string go struct field for CRD validation with a regular expression it must match
42-
const Pattern = "// +kubebuilder:validation:Pattern="
42+
// Pattern annotates a string go struct field for CRD validation with a regular expression it must match
43+
Pattern = "// +kubebuilder:validation:Pattern="
44+
45+
// Enum specifies the valid values for a field
46+
Enum = "// +kubebuilder:validation:Enum="
47+
48+
// MaxLength specifies the maximum length of a string field
49+
MaxLength = "// +kubebuilder:validation:MaxLength="
50+
51+
// MinLength specifies the minimum length of a string field
52+
MinLength = "// +kubebuilder:validation:MinLength="
53+
54+
// MaxItems specifies the maximum number of items an array or slice field may contain
55+
MaxItems = "// +kubebuilder:validation:MaxItems="
56+
57+
// MinItems specifies the minimum number of items an array or slice field may contain
58+
MinItems = "// +kubebuilder:validation:MinItems="
59+
60+
// UniqueItems specifies that all values in an array or slice must be unique
61+
UniqueItems = "// +kubebuilder:validation:UniqueItems="
62+
63+
// Format annotates a string go struct field for CRD validation with a specific format
64+
Format = "// +kubebuilder:validation:Format="
65+
)

test.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,54 @@ status:
501501
EOF
502502
}
503503

504+
function test_crd_validation {
505+
header_text "testing crd validation"
506+
507+
# Setup env vars
508+
export PATH=/tmp/kubebuilder/bin/:$PATH
509+
export TEST_ASSET_KUBECTL=/tmp/kubebuilder/bin/kubectl
510+
export TEST_ASSET_KUBE_APISERVER=/tmp/kubebuilder/bin/kube-apiserver
511+
export TEST_ASSET_ETCD=/tmp/kubebuilder/bin/etcd
512+
513+
kubebuilder init repo --domain sample.kubernetes.io
514+
kubebuilder create resource --group got --version v1beta1 --kind House
515+
516+
# Update crd
517+
sed -i -e '/type HouseSpec struct/ a \
518+
// +kubebuilder:validation:Maximum=100\
519+
// +kubebuilder:validation:ExclusiveMinimum=true\
520+
Power float32 \`json:"power"\`\
521+
// +kubebuilder:validation:MaxLength=15\
522+
// +kubebuilder:validation:MinLength=1\
523+
Name string \`json:"name"\`\
524+
// +kubebuilder:validation:MaxItems=500\
525+
// +kubebuilder:validation:MinItems=1\
526+
// +kubebuilder:validation:UniqueItems=false\
527+
Knights []string \`json:"knights"\`\
528+
Winner bool \`json:"winner"\`\
529+
// +kubebuilder:validation:Enum=Lion,Wolf,Dragon\
530+
Alias string \`json:"alias"\`\
531+
// +kubebuilder:validation:Enum=1,2,3\
532+
Rank int \`json:"rank"\`\
533+
' pkg/apis/got/v1beta1/house_types.go
534+
535+
kubebuilder generate
536+
header_text "generating and testing CRD..."
537+
kubebuilder create config --crds --output crd-validation.yaml
538+
diff crd-validation.yaml $kb_orig/test/resource/expected/crd-expected.yaml
539+
540+
kubebuilder create config --controller-image myimage:v1 --name myextensionname --output install.yaml
541+
kubebuilder create controller --group got --version v1beta1 --kind House
542+
543+
header_text "update controller"
544+
sed -i -e '/instance.Name = "instance-1"/ a \
545+
instance.Spec=HouseSpec{Power:89.5,Knights:[]string{"Jaime","Bronn","Gregor Clegane"}, Alias:"Lion", Name:"Lannister", Rank:1}
546+
' ./pkg/apis/got/v1beta1/house_types_test.go
547+
sed -i -e '/instance.Name = "instance-1"/ a \
548+
instance.Spec=HouseSpec{Power:89.5,Knights:[]string{"Jaime","Bronn","Gregor Clegane"}, Alias:"Lion", Name:"Lannister", Rank:1}
549+
' pkg/controller/house/controller_test.go
550+
}
551+
504552
function test_generated_controller {
505553
header_text "building generated code"
506554
# Verify the controller-manager builds and the tests pass
@@ -590,6 +638,10 @@ build_kb
590638

591639
setup_envs
592640

641+
prepare_testdir_under_gopath
642+
test_crd_validation
643+
test_generated_controller
644+
593645
prepare_testdir_under_gopath
594646
generate_crd_resources
595647
generate_controller

0 commit comments

Comments
 (0)