diff --git a/cmd/internal/codegen/parse/crd.go b/cmd/internal/codegen/parse/crd.go index 5d4e81bf4a..24676e6389 100644 --- a/cmd/internal/codegen/parse/crd.go +++ b/cmd/internal/codegen/parse/crd.go @@ -148,8 +148,9 @@ var jsonRegex = regexp.MustCompile("json:\"([a-zA-Z,]+)\"") type primitiveTemplateArgs struct { v1beta1.JSONSchemaProps - Value string - Format string + Value string + Format string + EnumValue string // TODO check type of enum value to match the type of field } var primitiveTemplate = template.Must(template.New("map-template").Parse( @@ -173,6 +174,15 @@ var primitiveTemplate = template.Must(template.New("map-template").Parse( {{ if .Format -}} Format: "{{ .Format }}", {{ end -}} + {{ if .EnumValue -}} + Enum: {{ .EnumValue }}, + {{ end -}} + {{ if .MaxLength -}} + MaxLength: getInt({{ .MaxLength }}), + {{ end -}} + {{ if .MinLength -}} + MinLength: getInt({{ .MinLength }}), + {{ end -}} }`)) // parsePrimitiveValidation returns a JSONSchemaProps object and its @@ -187,7 +197,7 @@ func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, commen buff := &bytes.Buffer{} - var n, f string + var n, f, s string switch t.Name.Name { case "int", "int64", "uint64": n = "integer" @@ -208,15 +218,17 @@ func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, commen default: n = t.Name.Name } - if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f}); err != nil { + if props.Enum != nil { + s = parseEnumToString(props.Enum) + } + if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f, s}); err != nil { log.Fatalf("%v", err) } - return props, buff.String() } type mapTempateArgs struct { - Result string + Result string SkipMapValidation bool } @@ -236,7 +248,7 @@ func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []s props := v1beta1.JSONSchemaProps{ Type: "object", } - parseOption := b.arguments.CustomArgs.(*ParseOptions) + parseOption := b.arguments.CustomArgs.(*ParseOptions) if !parseOption.SkipMapValidation { props.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{ Allows: true, @@ -253,11 +265,25 @@ func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []s var arrayTemplate = template.Must(template.New("array-template").Parse( `v1beta1.JSONSchemaProps{ Type: "array", + {{ if .MaxItems -}} + MaxItems: getInt({{ .MaxItems }}), + {{ end -}} + {{ if .MinItems -}} + MinItems: getInt({{ .MinItems }}), + {{ end -}} + {{ if .UniqueItems -}} + UniqueItems: {{ .UniqueItems }}, + {{ end -}} Items: &v1beta1.JSONSchemaPropsOrArray{ - Schema: &{{.}}, + Schema: &{{.ItemsSchema}}, }, }`)) +type arrayTemplateArgs struct { + v1beta1.JSONSchemaProps + ItemsSchema string +} + // parseArrayValidation returns a JSONSchemaProps object and its serialization in // Go that describe the validations for the given array type. 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 [ Type: "array", Items: &v1beta1.JSONSchemaPropsOrArray{Schema: &items}, } - + for _, l := range comments { + getValidation(l, &props) + } buff := &bytes.Buffer{} - if err := arrayTemplate.Execute(buff, result); err != nil { + if err := arrayTemplate.Execute(buff, arrayTemplateArgs{props, result}); err != nil { log.Fatalf("%v", err) } return props, buff.String() @@ -380,28 +408,34 @@ func getValidation(comment string, props *v1beta1.JSONSchemaProps) { case "Pattern": props.Pattern = parts[1] case "MaxItems": - i, err := strconv.Atoi(parts[1]) - v := int64(i) - if err != nil { - log.Fatalf("Could not parse int from %s: %v", comment, err) - return + if props.Type == "array" { + i, err := strconv.Atoi(parts[1]) + v := int64(i) + if err != nil { + log.Fatalf("Could not parse int from %s: %v", comment, err) + return + } + props.MaxItems = &v } - props.MaxItems = &v case "MinItems": - i, err := strconv.Atoi(parts[1]) - v := int64(i) - if err != nil { - log.Fatalf("Could not parse int from %s: %v", comment, err) - return + if props.Type == "array" { + i, err := strconv.Atoi(parts[1]) + v := int64(i) + if err != nil { + log.Fatalf("Could not parse int from %s: %v", comment, err) + return + } + props.MinItems = &v } - props.MinItems = &v case "UniqueItems": - b, err := strconv.ParseBool(parts[1]) - if err != nil { - log.Fatalf("Could not parse bool from %s: %v", comment, err) - return + if props.Type == "array" { + b, err := strconv.ParseBool(parts[1]) + if err != nil { + log.Fatalf("Could not parse bool from %s: %v", comment, err) + return + } + props.UniqueItems = b } - props.ExclusiveMinimum = b case "MultipleOf": f, err := strconv.ParseFloat(parts[1], 64) if err != nil { @@ -410,9 +444,13 @@ func getValidation(comment string, props *v1beta1.JSONSchemaProps) { } props.MultipleOf = &f case "Enum": - enums := strings.Split(parts[1], ",") - for i := range enums { - props.Enum = append(props.Enum, v1beta1.JSON{[]byte(enums[i])}) + if props.Type != "array" { + value := strings.Split(parts[1], ",") + enums := []v1beta1.JSON{} + for _, s := range value { + checkType(props, s, &enums) + } + props.Enum = enums } case "Format": props.Format = parts[1] diff --git a/cmd/internal/codegen/parse/util.go b/cmd/internal/codegen/parse/util.go index 2f5cf00b4d..4b5575cfa8 100644 --- a/cmd/internal/codegen/parse/util.go +++ b/cmd/internal/codegen/parse/util.go @@ -18,11 +18,14 @@ package parse import ( "fmt" + "log" "path/filepath" + "strconv" "strings" "github.com/pkg/errors" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/gengo/types" ) @@ -239,3 +242,51 @@ func getDocAnnotation(t *types.Type, tags ...string) map[string]string { } return annotation } + +// parseByteValue returns the literal digital number values from a byte array +func parseByteValue(b []byte) string { + elem := strings.Join(strings.Fields(fmt.Sprintln(b)), ",") + elem = strings.TrimPrefix(elem, "[") + elem = strings.TrimSuffix(elem, "]") + return elem +} + +// parseEnumToString returns a representive validated go format string from JSONSchemaProps schema +func parseEnumToString(value []v1beta1.JSON) string { + res := "[]v1beta1.JSON{" + prefix := "v1beta1.JSON{[]byte{" + for _, v := range value { + res = res + prefix + parseByteValue(v.Raw) + "}}," + } + return strings.TrimSuffix(res, ",") + "}" +} + +// check type of enum element value to match type of field +func checkType(props *v1beta1.JSONSchemaProps, s string, enums *[]v1beta1.JSON) { + + // TODO support more types check + switch props.Type { + case "int", "int64", "uint64": + if _, err := strconv.ParseInt(s, 0, 64); err != nil { + log.Fatalf("Invalid integer value [%v] for a field of integer type", s) + } + *enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))}) + case "int32", "unit32": + if _, err := strconv.ParseInt(s, 0, 32); err != nil { + log.Fatalf("Invalid integer value [%v] for a field of integer32 type", s) + } + *enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))}) + case "float", "float32": + if _, err := strconv.ParseFloat(s, 32); err != nil { + log.Fatalf("Invalid float value [%v] for a field of float32 type", s) + } + *enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))}) + case "float64": + if _, err := strconv.ParseFloat(s, 64); err != nil { + log.Fatalf("Invalid float value [%v] for a field of float type", s) + } + *enums = append(*enums, v1beta1.JSON{[]byte(fmt.Sprintf("%v", s))}) + case "string": + *enums = append(*enums, v1beta1.JSON{[]byte(`"` + s + `"`)}) + } +} diff --git a/cmd/kubebuilder-gen/internal/resourcegen/versioned_generator.go b/cmd/kubebuilder-gen/internal/resourcegen/versioned_generator.go index fb741e3ef0..a5012a09db 100644 --- a/cmd/kubebuilder-gen/internal/resourcegen/versioned_generator.go +++ b/cmd/kubebuilder-gen/internal/resourcegen/versioned_generator.go @@ -107,6 +107,10 @@ func getFloat(f float64) *float64 { return &f } +func getInt(i int64) *int64 { + return &i +} + var ( {{ range $api := .Resources -}} // Define CRDs for resources diff --git a/pkg/gen/apis/doc.go b/pkg/gen/apis/doc.go index 24f58d16f0..06869872ea 100644 --- a/pkg/gen/apis/doc.go +++ b/pkg/gen/apis/doc.go @@ -19,24 +19,47 @@ The apis package describes the comment directives that may be applied to apis / */ package apis -// Resource annotates a type as a resource -const Resource = "// +kubebuilder:resource:path=" +const ( + // Resource annotates a type as a resource + Resource = "// +kubebuilder:resource:path=" -// Categories annotates a type as belonging to a comma-delimited list of -// categories -const Categories = "// +kubebuilder:categories=" + // Categories annotates a type as belonging to a comma-delimited list of + // categories + Categories = "// +kubebuilder:categories=" -// Maximum annotates a numeric go struct field for CRD validation -const Maximum = "// +kubebuilder:validation:Maximum=" + // Maximum annotates a numeric go struct field for CRD validation + Maximum = "// +kubebuilder:validation:Maximum=" -// ExclusiveMaximum annotates a numeric go struct field for CRD validation -const ExclusiveMaximum = "// +kubebuilder:validation:ExclusiveMaximum=" + // ExclusiveMaximum annotates a numeric go struct field for CRD validation + ExclusiveMaximum = "// +kubebuilder:validation:ExclusiveMaximum=" -// Minimum annotates a numeric go struct field for CRD validation -const Minimum = "// +kubebuilder:validation:Minimum=" + // Minimum annotates a numeric go struct field for CRD validation + Minimum = "// +kubebuilder:validation:Minimum=" -// ExclusiveMinimum annotates a numeric go struct field for CRD validation -const ExclusiveMinimum = "// +kubebuilder:validation:ExclusiveMinimum=" + // ExclusiveMinimum annotates a numeric go struct field for CRD validation + ExclusiveMinimum = "// +kubebuilder:validation:ExclusiveMinimum=" -// Pattern annotates a string go struct field for CRD validation with a regular expression it must match -const Pattern = "// +kubebuilder:validation:Pattern=" + // Pattern annotates a string go struct field for CRD validation with a regular expression it must match + Pattern = "// +kubebuilder:validation:Pattern=" + + // Enum specifies the valid values for a field + Enum = "// +kubebuilder:validation:Enum=" + + // MaxLength specifies the maximum length of a string field + MaxLength = "// +kubebuilder:validation:MaxLength=" + + // MinLength specifies the minimum length of a string field + MinLength = "// +kubebuilder:validation:MinLength=" + + // MaxItems specifies the maximum number of items an array or slice field may contain + MaxItems = "// +kubebuilder:validation:MaxItems=" + + // MinItems specifies the minimum number of items an array or slice field may contain + MinItems = "// +kubebuilder:validation:MinItems=" + + // UniqueItems specifies that all values in an array or slice must be unique + UniqueItems = "// +kubebuilder:validation:UniqueItems=" + + // Format annotates a string go struct field for CRD validation with a specific format + Format = "// +kubebuilder:validation:Format=" +) diff --git a/test.sh b/test.sh index 8230f7b017..032174de03 100755 --- a/test.sh +++ b/test.sh @@ -501,6 +501,54 @@ status: EOF } +function test_crd_validation { + header_text "testing crd validation" + + # Setup env vars + export PATH=/tmp/kubebuilder/bin/:$PATH + export TEST_ASSET_KUBECTL=/tmp/kubebuilder/bin/kubectl + export TEST_ASSET_KUBE_APISERVER=/tmp/kubebuilder/bin/kube-apiserver + export TEST_ASSET_ETCD=/tmp/kubebuilder/bin/etcd + + kubebuilder init repo --domain sample.kubernetes.io + kubebuilder create resource --group got --version v1beta1 --kind House + + # Update crd + sed -i -e '/type HouseSpec struct/ a \ + // +kubebuilder:validation:Maximum=100\ + // +kubebuilder:validation:ExclusiveMinimum=true\ + Power float32 \`json:"power"\`\ + // +kubebuilder:validation:MaxLength=15\ + // +kubebuilder:validation:MinLength=1\ + Name string \`json:"name"\`\ + // +kubebuilder:validation:MaxItems=500\ + // +kubebuilder:validation:MinItems=1\ + // +kubebuilder:validation:UniqueItems=false\ + Knights []string \`json:"knights"\`\ + Winner bool \`json:"winner"\`\ + // +kubebuilder:validation:Enum=Lion,Wolf,Dragon\ + Alias string \`json:"alias"\`\ + // +kubebuilder:validation:Enum=1,2,3\ + Rank int \`json:"rank"\`\ + ' pkg/apis/got/v1beta1/house_types.go + + kubebuilder generate + header_text "generating and testing CRD..." + kubebuilder create config --crds --output crd-validation.yaml + diff crd-validation.yaml $kb_orig/test/resource/expected/crd-expected.yaml + + kubebuilder create config --controller-image myimage:v1 --name myextensionname --output install.yaml + kubebuilder create controller --group got --version v1beta1 --kind House + + header_text "update controller" + sed -i -e '/instance.Name = "instance-1"/ a \ + instance.Spec=HouseSpec{Power:89.5,Knights:[]string{"Jaime","Bronn","Gregor Clegane"}, Alias:"Lion", Name:"Lannister", Rank:1} + ' ./pkg/apis/got/v1beta1/house_types_test.go + sed -i -e '/instance.Name = "instance-1"/ a \ + instance.Spec=HouseSpec{Power:89.5,Knights:[]string{"Jaime","Bronn","Gregor Clegane"}, Alias:"Lion", Name:"Lannister", Rank:1} + ' pkg/controller/house/controller_test.go +} + function test_generated_controller { header_text "building generated code" # Verify the controller-manager builds and the tests pass @@ -590,6 +638,10 @@ build_kb setup_envs +prepare_testdir_under_gopath +test_crd_validation +test_generated_controller + prepare_testdir_under_gopath generate_crd_resources generate_controller diff --git a/test/resource/expected/crd-expected.yaml b/test/resource/expected/crd-expected.yaml new file mode 100644 index 0000000000..4c581d525d --- /dev/null +++ b/test/resource/expected/crd-expected.yaml @@ -0,0 +1,63 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + labels: + api: "" + kubebuilder.k8s.io: unknown + name: houses.got.sample.kubernetes.io +spec: + group: got.sample.kubernetes.io + names: + kind: House + plural: houses + scope: Namespaced + validation: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + alias: + enum: + - Lion + - Wolf + - Dragon + type: string + knights: + items: + type: string + maxItems: 500 + minItems: 1 + type: array + name: + maxLength: 15 + minLength: 1 + type: string + power: + exclusiveMinimum: true + maximum: 100 + type: float32 + rank: + enum: + - 1 + - 2 + - 3 + type: int + winner: + type: bool + type: object + status: + type: object + type: object + version: v1beta1 +status: + acceptedNames: + kind: "" + plural: "" + conditions: null