Skip to content

Commit

Permalink
Add support for CRD validaiton
Browse files Browse the repository at this point in the history
- Add suport CRD spec validation.
- Fixed issues in crd validation and generate.
- Add type check in parsing enum elements
- Add crd valuation and controller e2e test in test.sh
  • Loading branch information
fanzhangio committed May 23, 2018
1 parent 123e075 commit f73c804
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 45 deletions.
98 changes: 68 additions & 30 deletions cmd/internal/codegen/parse/crd.go
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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"
Expand All @@ -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
}

Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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]
Expand Down
51 changes: 51 additions & 0 deletions cmd/internal/codegen/parse/util.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 + `"`)})
}
}
Expand Up @@ -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
Expand Down
53 changes: 38 additions & 15 deletions pkg/gen/apis/doc.go
Expand Up @@ -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="
)
52 changes: 52 additions & 0 deletions test.sh
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit f73c804

Please sign in to comment.