2 Copyright 2018 The Kubernetes Authors.
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
29 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/util/sets"
35 // parseCRDs populates the CRD field of each Group.Version.Resource,
36 // creating validations using the annotations on type fields.
37 func (b *APIs) parseCRDs() {
38 for _, group := range b.APIs.Groups {
39 for _, version := range group.Versions {
40 for _, resource := range version.Resources {
41 if IsAPIResource(resource.Type) {
42 resource.JSONSchemaProps, resource.Validation =
43 b.typeToJSONSchemaProps(resource.Type, sets.NewString(), []string{}, true)
45 // Note: Drop the Type field at the root level of validation
46 // schema. Refer to following issue for details.
47 // https://github.com/kubernetes/kubernetes/issues/65293
48 resource.JSONSchemaProps.Type = ""
49 j, err := json.MarshalIndent(resource.JSONSchemaProps, "", " ")
51 log.Fatalf("Could not Marshall validation %v\n", err)
53 resource.ValidationComments = string(j)
55 resource.CRD = v1beta1.CustomResourceDefinition{
56 TypeMeta: metav1.TypeMeta{
57 APIVersion: "apiextensions.k8s.io/v1beta1",
58 Kind: "CustomResourceDefinition",
60 ObjectMeta: metav1.ObjectMeta{
61 Name: fmt.Sprintf("%s.%s.%s", resource.Resource, resource.Group, resource.Domain),
62 Labels: map[string]string{"controller-tools.k8s.io": "1.0"},
64 Spec: v1beta1.CustomResourceDefinitionSpec{
65 Group: fmt.Sprintf("%s.%s", resource.Group, resource.Domain),
66 Version: resource.Version,
67 Names: v1beta1.CustomResourceDefinitionNames{
69 Plural: resource.Resource,
71 Validation: &v1beta1.CustomResourceValidation{
72 OpenAPIV3Schema: &resource.JSONSchemaProps,
76 if resource.NonNamespaced {
77 resource.CRD.Spec.Scope = "Cluster"
79 resource.CRD.Spec.Scope = "Namespaced"
82 if hasCategories(resource.Type) {
83 categoriesTag := getCategoriesTag(resource.Type)
84 categories := strings.Split(categoriesTag, ",")
85 resource.CRD.Spec.Names.Categories = categories
86 resource.Categories = categories
89 if hasSingular(resource.Type) {
90 singularName := getSingularName(resource.Type)
91 resource.CRD.Spec.Names.Singular = singularName
94 if hasStatusSubresource(resource.Type) {
95 if resource.CRD.Spec.Subresources == nil {
96 resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
98 resource.CRD.Spec.Subresources.Status = &v1beta1.CustomResourceSubresourceStatus{}
101 resource.CRD.Status.Conditions = []v1beta1.CustomResourceDefinitionCondition{}
102 resource.CRD.Status.StoredVersions = []string{}
104 if hasScaleSubresource(resource.Type) {
105 if resource.CRD.Spec.Subresources == nil {
106 resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
108 jsonPath, err := parseScaleParams(resource.Type)
110 log.Fatalf("failed in parsing CRD, error: %v", err.Error())
112 resource.CRD.Spec.Subresources.Scale = &v1beta1.CustomResourceSubresourceScale{
113 SpecReplicasPath: jsonPath[specReplicasPath],
114 StatusReplicasPath: jsonPath[statusReplicasPath],
116 labelSelctor, ok := jsonPath[labelSelectorPath]
117 if ok && labelSelctor != "" {
118 resource.CRD.Spec.Subresources.Scale.LabelSelectorPath = &labelSelctor
121 if hasPrintColumn(resource.Type) {
122 result, err := parsePrintColumnParams(resource.Type)
124 log.Fatalf("failed to parse printcolumn annotations, error: %v", err.Error())
126 resource.CRD.Spec.AdditionalPrinterColumns = result
128 if len(resource.ShortName) > 0 {
129 resource.CRD.Spec.Names.ShortNames = strings.Split(resource.ShortName, ";")
137 func (b *APIs) getTime() string {
138 return `v1beta1.JSONSchemaProps{
144 func (b *APIs) getDuration() string {
145 return `v1beta1.JSONSchemaProps{
150 func (b *APIs) getQuantity() string {
151 return `v1beta1.JSONSchemaProps{
156 func (b *APIs) objSchema() string {
157 return `v1beta1.JSONSchemaProps{
162 // typeToJSONSchemaProps returns a JSONSchemaProps object and its serialization
163 // in Go that describe the JSONSchema validations for the given type.
164 func (b *APIs) typeToJSONSchemaProps(t *types.Type, found sets.String, comments []string, isRoot bool) (v1beta1.JSONSchemaProps, string) {
166 time := types.Name{Name: "Time", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
167 duration := types.Name{Name: "Duration", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
168 quantity := types.Name{Name: "Quantity", Package: "k8s.io/apimachinery/pkg/api/resource"}
169 meta := types.Name{Name: "ObjectMeta", Package: "k8s.io/apimachinery/pkg/apis/meta/v1"}
170 unstructured := types.Name{Name: "Unstructured", Package: "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"}
171 rawExtension := types.Name{Name: "RawExtension", Package: "k8s.io/apimachinery/pkg/runtime"}
172 intOrString := types.Name{Name: "IntOrString", Package: "k8s.io/apimachinery/pkg/util/intstr"}
173 // special types first
174 specialTypeProps := v1beta1.JSONSchemaProps{
175 Description: parseDescription(comments),
177 for _, l := range comments {
178 getValidation(l, &specialTypeProps)
182 specialTypeProps.Type = "string"
183 specialTypeProps.Format = "date-time"
184 return specialTypeProps, b.getTime()
186 specialTypeProps.Type = "string"
187 return specialTypeProps, b.getDuration()
189 specialTypeProps.Type = "string"
190 return specialTypeProps, b.getQuantity()
191 case meta, unstructured, rawExtension:
192 specialTypeProps.Type = "object"
193 return specialTypeProps, b.objSchema()
195 specialTypeProps.AnyOf = []v1beta1.JSONSchemaProps{
203 return specialTypeProps, b.objSchema()
206 var v v1beta1.JSONSchemaProps
210 v, s = b.parsePrimitiveValidation(t, found, comments)
212 v, s = b.parseObjectValidation(t, found, comments, isRoot)
214 v, s = b.parseMapValidation(t, found, comments)
216 v, s = b.parseArrayValidation(t, found, comments)
218 v, s = b.parseArrayValidation(t, found, comments)
220 v, s = b.typeToJSONSchemaProps(t.Elem, found, comments, false)
222 v, s = b.typeToJSONSchemaProps(t.Underlying, found, comments, false)
224 log.Fatalf("Unknown supported Kind %v\n", t.Kind)
230 var jsonRegex = regexp.MustCompile("json:\"([a-zA-Z0-9,]+)\"")
232 type primitiveTemplateArgs struct {
233 v1beta1.JSONSchemaProps
236 EnumValue string // TODO check type of enum value to match the type of field
240 var primitiveTemplate = template.Must(template.New("map-template").Parse(
241 `v1beta1.JSONSchemaProps{
243 Pattern: "{{ .Pattern }}",
246 Maximum: getFloat({{ .Maximum }}),
248 {{ if .ExclusiveMaximum -}}
249 ExclusiveMaximum: {{ .ExclusiveMaximum }},
252 Minimum: getFloat({{ .Minimum }}),
254 {{ if .ExclusiveMinimum -}}
255 ExclusiveMinimum: {{ .ExclusiveMinimum }},
257 Type: "{{ .Value }}",
259 Format: "{{ .Format }}",
262 Enum: {{ .EnumValue }},
265 MaxLength: getInt({{ .MaxLength }}),
268 MinLength: getInt({{ .MinLength }}),
272 // parsePrimitiveValidation returns a JSONSchemaProps object and its
273 // serialization in Go that describe the validations for the given primitive
275 func (b *APIs) parsePrimitiveValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
276 props := v1beta1.JSONSchemaProps{Type: string(t.Name.Name)}
278 for _, l := range comments {
279 getValidation(l, &props)
282 buff := &bytes.Buffer{}
284 var n, f, s, d string
286 case "int", "int64", "uint64":
289 case "int32", "uint32":
292 case "float", "float32":
306 if props.Enum != nil {
307 s = parseEnumToString(props.Enum)
309 d = parseDescription(comments)
310 if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f, s, d}); err != nil {
311 log.Fatalf("%v", err)
315 props.Description = d
316 return props, buff.String()
319 type mapTempateArgs struct {
321 SkipMapValidation bool
324 var mapTemplate = template.Must(template.New("map-template").Parse(
325 `v1beta1.JSONSchemaProps{
327 {{if not .SkipMapValidation}}AdditionalProperties: &v1beta1.JSONSchemaPropsOrBool{
329 Schema: &{{.Result}},
333 // parseMapValidation returns a JSONSchemaProps object and its serialization in
334 // Go that describe the validations for the given map type.
335 func (b *APIs) parseMapValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
336 additionalProps, result := b.typeToJSONSchemaProps(t.Elem, found, comments, false)
337 additionalProps.Description = ""
338 props := v1beta1.JSONSchemaProps{
340 Description: parseDescription(comments),
342 parseOption := b.arguments.CustomArgs.(*Options)
343 if !parseOption.SkipMapValidation {
344 props.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{
346 Schema: &additionalProps}
349 for _, l := range comments {
350 getValidation(l, &props)
353 buff := &bytes.Buffer{}
354 if err := mapTemplate.Execute(buff, mapTempateArgs{Result: result, SkipMapValidation: parseOption.SkipMapValidation}); err != nil {
355 log.Fatalf("%v", err)
357 return props, buff.String()
360 var arrayTemplate = template.Must(template.New("array-template").Parse(
361 `v1beta1.JSONSchemaProps{
364 Format: "{{.Format}}",
367 MaxItems: getInt({{ .MaxItems }}),
370 MinItems: getInt({{ .MinItems }}),
372 {{ if .UniqueItems -}}
373 UniqueItems: {{ .UniqueItems }},
376 Items: &v1beta1.JSONSchemaPropsOrArray{
377 Schema: &{{.ItemsSchema}},
382 type arrayTemplateArgs struct {
383 v1beta1.JSONSchemaProps
387 // parseArrayValidation returns a JSONSchemaProps object and its serialization in
388 // Go that describe the validations for the given array type.
389 func (b *APIs) parseArrayValidation(t *types.Type, found sets.String, comments []string) (v1beta1.JSONSchemaProps, string) {
390 items, result := b.typeToJSONSchemaProps(t.Elem, found, comments, false)
391 items.Description = ""
392 props := v1beta1.JSONSchemaProps{
394 Items: &v1beta1.JSONSchemaPropsOrArray{Schema: &items},
395 Description: parseDescription(comments),
397 // To represent byte arrays in the generated code, the property of the OpenAPI definition
398 // should have string as its type and byte as its format.
399 if t.Name.Name == "[]byte" {
400 props.Type = "string"
401 props.Format = "byte"
403 props.Description = parseDescription(comments)
405 for _, l := range comments {
406 getValidation(l, &props)
408 if t.Name.Name != "[]byte" {
409 // Except for the byte array special case above, the "format" property
410 // should be applied to the array items and not the array itself.
413 buff := &bytes.Buffer{}
414 if err := arrayTemplate.Execute(buff, arrayTemplateArgs{props, result}); err != nil {
415 log.Fatalf("%v", err)
417 return props, buff.String()
420 type objectTemplateArgs struct {
421 v1beta1.JSONSchemaProps
422 Fields map[string]string
427 var objectTemplate = template.Must(template.New("object-template").Parse(
428 `v1beta1.JSONSchemaProps{
429 {{ if not .IsRoot -}}
432 Properties: map[string]v1beta1.JSONSchemaProps{
433 {{ range $k, $v := .Fields -}}
434 "{{ $k }}": {{ $v }},
437 {{if .Required}}Required: []string{
438 {{ range $k, $v := .Required -}}
444 // parseObjectValidation returns a JSONSchemaProps object and its serialization in
445 // Go that describe the validations for the given object type.
446 func (b *APIs) parseObjectValidation(t *types.Type, found sets.String, comments []string, isRoot bool) (v1beta1.JSONSchemaProps, string) {
447 buff := &bytes.Buffer{}
448 props := v1beta1.JSONSchemaProps{
450 Description: parseDescription(comments),
453 for _, l := range comments {
454 getValidation(l, &props)
457 if strings.HasPrefix(t.Name.String(), "k8s.io/api") {
458 if err := objectTemplate.Execute(buff, objectTemplateArgs{props, nil, nil, false}); err != nil {
459 log.Fatalf("%v", err)
462 m, result, required := b.getMembers(t, found)
464 props.Required = required
466 if err := objectTemplate.Execute(buff, objectTemplateArgs{props, result, required, isRoot}); err != nil {
467 log.Fatalf("%v", err)
470 return props, buff.String()
473 // getValidation parses the validation tags from the comment and sets the
474 // validation rules on the given JSONSchemaProps.
475 func getValidation(comment string, props *v1beta1.JSONSchemaProps) {
476 comment = strings.TrimLeft(comment, " ")
477 if !strings.HasPrefix(comment, "+kubebuilder:validation:") {
480 c := strings.Replace(comment, "+kubebuilder:validation:", "", -1)
481 parts := strings.Split(c, "=")
483 log.Fatalf("Expected +kubebuilder:validation:<key>=<value> actual: %s", comment)
488 f, err := strconv.ParseFloat(parts[1], 64)
490 log.Fatalf("Could not parse float from %s: %v", comment, err)
494 case "ExclusiveMaximum":
495 b, err := strconv.ParseBool(parts[1])
497 log.Fatalf("Could not parse bool from %s: %v", comment, err)
500 props.ExclusiveMaximum = b
502 f, err := strconv.ParseFloat(parts[1], 64)
504 log.Fatalf("Could not parse float from %s: %v", comment, err)
508 case "ExclusiveMinimum":
509 b, err := strconv.ParseBool(parts[1])
511 log.Fatalf("Could not parse bool from %s: %v", comment, err)
514 props.ExclusiveMinimum = b
516 i, err := strconv.Atoi(parts[1])
519 log.Fatalf("Could not parse int from %s: %v", comment, err)
524 i, err := strconv.Atoi(parts[1])
527 log.Fatalf("Could not parse int from %s: %v", comment, err)
532 props.Pattern = parts[1]
534 if props.Type == "array" {
535 i, err := strconv.Atoi(parts[1])
538 log.Fatalf("Could not parse int from %s: %v", comment, err)
544 if props.Type == "array" {
545 i, err := strconv.Atoi(parts[1])
548 log.Fatalf("Could not parse int from %s: %v", comment, err)
554 if props.Type == "array" {
555 b, err := strconv.ParseBool(parts[1])
557 log.Fatalf("Could not parse bool from %s: %v", comment, err)
560 props.UniqueItems = b
563 f, err := strconv.ParseFloat(parts[1], 64)
565 log.Fatalf("Could not parse float from %s: %v", comment, err)
568 props.MultipleOf = &f
570 if props.Type != "array" {
571 value := strings.Split(parts[1], ",")
572 enums := []v1beta1.JSON{}
573 for _, s := range value {
574 checkType(props, s, &enums)
579 props.Format = parts[1]
581 log.Fatalf("Unsupport validation: %s", comment)
585 // getMembers builds maps by field name of the JSONSchemaProps and their Go
587 func (b *APIs) getMembers(t *types.Type, found sets.String) (map[string]v1beta1.JSONSchemaProps, map[string]string, []string) {
588 members := map[string]v1beta1.JSONSchemaProps{}
589 result := map[string]string{}
590 required := []string{}
592 // Don't allow recursion until we support it through refs
593 // TODO: Support recursion
594 if found.Has(t.Name.String()) {
595 fmt.Printf("Breaking recursion for type %s", t.Name.String())
596 return members, result, required
598 found.Insert(t.Name.String())
600 for _, member := range t.Members {
601 tags := jsonRegex.FindStringSubmatch(member.Tags)
603 // Skip fields without json tags
604 //fmt.Printf("Skipping member %s %s\n", member.Name, member.Type.Name.String())
607 ts := strings.Split(tags[1], ",")
610 if len(ts) > 0 && len(ts[0]) > 0 {
617 // Inline "inline" structs
618 if strat == "inline" {
619 m, r, re := b.getMembers(member.Type, found)
620 for n, v := range m {
623 for n, v := range r {
626 required = append(required, re...)
628 m, r := b.typeToJSONSchemaProps(member.Type, found, member.CommentLines, false)
631 if !strings.HasSuffix(strat, "omitempty") {
632 required = append(required, name)
637 defer found.Delete(t.Name.String())
638 return members, result, required