Code refactoring for bpa operator
[icn.git] / cmd / bpa-operator / vendor / sigs.k8s.io / controller-tools / pkg / internal / codegen / parse / crd.go
1 /*
2 Copyright 2018 The Kubernetes Authors.
3
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
7
8     http://www.apache.org/licenses/LICENSE-2.0
9
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.
15 */
16
17 package parse
18
19 import (
20         "bytes"
21         "encoding/json"
22         "fmt"
23         "log"
24         "regexp"
25         "strconv"
26         "strings"
27         "text/template"
28
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"
32         "k8s.io/gengo/types"
33 )
34
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)
44
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, "", "    ")
50                                         if err != nil {
51                                                 log.Fatalf("Could not Marshall validation %v\n", err)
52                                         }
53                                         resource.ValidationComments = string(j)
54
55                                         resource.CRD = v1beta1.CustomResourceDefinition{
56                                                 TypeMeta: metav1.TypeMeta{
57                                                         APIVersion: "apiextensions.k8s.io/v1beta1",
58                                                         Kind:       "CustomResourceDefinition",
59                                                 },
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"},
63                                                 },
64                                                 Spec: v1beta1.CustomResourceDefinitionSpec{
65                                                         Group:   fmt.Sprintf("%s.%s", resource.Group, resource.Domain),
66                                                         Version: resource.Version,
67                                                         Names: v1beta1.CustomResourceDefinitionNames{
68                                                                 Kind:   resource.Kind,
69                                                                 Plural: resource.Resource,
70                                                         },
71                                                         Validation: &v1beta1.CustomResourceValidation{
72                                                                 OpenAPIV3Schema: &resource.JSONSchemaProps,
73                                                         },
74                                                 },
75                                         }
76                                         if resource.NonNamespaced {
77                                                 resource.CRD.Spec.Scope = "Cluster"
78                                         } else {
79                                                 resource.CRD.Spec.Scope = "Namespaced"
80                                         }
81
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
87                                         }
88
89                                         if hasSingular(resource.Type) {
90                                                 singularName := getSingularName(resource.Type)
91                                                 resource.CRD.Spec.Names.Singular = singularName
92                                         }
93
94                                         if hasStatusSubresource(resource.Type) {
95                                                 if resource.CRD.Spec.Subresources == nil {
96                                                         resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
97                                                 }
98                                                 resource.CRD.Spec.Subresources.Status = &v1beta1.CustomResourceSubresourceStatus{}
99                                         }
100
101                                         resource.CRD.Status.Conditions = []v1beta1.CustomResourceDefinitionCondition{}
102                                         resource.CRD.Status.StoredVersions = []string{}
103
104                                         if hasScaleSubresource(resource.Type) {
105                                                 if resource.CRD.Spec.Subresources == nil {
106                                                         resource.CRD.Spec.Subresources = &v1beta1.CustomResourceSubresources{}
107                                                 }
108                                                 jsonPath, err := parseScaleParams(resource.Type)
109                                                 if err != nil {
110                                                         log.Fatalf("failed in parsing CRD, error: %v", err.Error())
111                                                 }
112                                                 resource.CRD.Spec.Subresources.Scale = &v1beta1.CustomResourceSubresourceScale{
113                                                         SpecReplicasPath:   jsonPath[specReplicasPath],
114                                                         StatusReplicasPath: jsonPath[statusReplicasPath],
115                                                 }
116                                                 labelSelctor, ok := jsonPath[labelSelectorPath]
117                                                 if ok && labelSelctor != "" {
118                                                         resource.CRD.Spec.Subresources.Scale.LabelSelectorPath = &labelSelctor
119                                                 }
120                                         }
121                                         if hasPrintColumn(resource.Type) {
122                                                 result, err := parsePrintColumnParams(resource.Type)
123                                                 if err != nil {
124                                                         log.Fatalf("failed to parse printcolumn annotations, error: %v", err.Error())
125                                                 }
126                                                 resource.CRD.Spec.AdditionalPrinterColumns = result
127                                         }
128                                         if len(resource.ShortName) > 0 {
129                                                 resource.CRD.Spec.Names.ShortNames = strings.Split(resource.ShortName, ";")
130                                         }
131                                 }
132                         }
133                 }
134         }
135 }
136
137 func (b *APIs) getTime() string {
138         return `v1beta1.JSONSchemaProps{
139     Type:   "string",
140     Format: "date-time",
141 }`
142 }
143
144 func (b *APIs) getDuration() string {
145         return `v1beta1.JSONSchemaProps{
146     Type:   "string",
147 }`
148 }
149
150 func (b *APIs) getQuantity() string {
151         return `v1beta1.JSONSchemaProps{
152     Type:   "string",
153 }`
154 }
155
156 func (b *APIs) objSchema() string {
157         return `v1beta1.JSONSchemaProps{
158     Type:   "object",
159 }`
160 }
161
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) {
165         // Special cases
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),
176         }
177         for _, l := range comments {
178                 getValidation(l, &specialTypeProps)
179         }
180         switch t.Name {
181         case time:
182                 specialTypeProps.Type = "string"
183                 specialTypeProps.Format = "date-time"
184                 return specialTypeProps, b.getTime()
185         case duration:
186                 specialTypeProps.Type = "string"
187                 return specialTypeProps, b.getDuration()
188         case quantity:
189                 specialTypeProps.Type = "string"
190                 return specialTypeProps, b.getQuantity()
191         case meta, unstructured, rawExtension:
192                 specialTypeProps.Type = "object"
193                 return specialTypeProps, b.objSchema()
194         case intOrString:
195                 specialTypeProps.AnyOf = []v1beta1.JSONSchemaProps{
196                         {
197                                 Type: "string",
198                         },
199                         {
200                                 Type: "integer",
201                         },
202                 }
203                 return specialTypeProps, b.objSchema()
204         }
205
206         var v v1beta1.JSONSchemaProps
207         var s string
208         switch t.Kind {
209         case types.Builtin:
210                 v, s = b.parsePrimitiveValidation(t, found, comments)
211         case types.Struct:
212                 v, s = b.parseObjectValidation(t, found, comments, isRoot)
213         case types.Map:
214                 v, s = b.parseMapValidation(t, found, comments)
215         case types.Slice:
216                 v, s = b.parseArrayValidation(t, found, comments)
217         case types.Array:
218                 v, s = b.parseArrayValidation(t, found, comments)
219         case types.Pointer:
220                 v, s = b.typeToJSONSchemaProps(t.Elem, found, comments, false)
221         case types.Alias:
222                 v, s = b.typeToJSONSchemaProps(t.Underlying, found, comments, false)
223         default:
224                 log.Fatalf("Unknown supported Kind %v\n", t.Kind)
225         }
226
227         return v, s
228 }
229
230 var jsonRegex = regexp.MustCompile("json:\"([a-zA-Z0-9,]+)\"")
231
232 type primitiveTemplateArgs struct {
233         v1beta1.JSONSchemaProps
234         Value       string
235         Format      string
236         EnumValue   string // TODO check type of enum value to match the type of field
237         Description string
238 }
239
240 var primitiveTemplate = template.Must(template.New("map-template").Parse(
241         `v1beta1.JSONSchemaProps{
242     {{ if .Pattern -}}
243     Pattern: "{{ .Pattern }}",
244     {{ end -}}
245     {{ if .Maximum -}}
246     Maximum: getFloat({{ .Maximum }}),
247     {{ end -}}
248     {{ if .ExclusiveMaximum -}}
249     ExclusiveMaximum: {{ .ExclusiveMaximum }},
250     {{ end -}}
251     {{ if .Minimum -}}
252     Minimum: getFloat({{ .Minimum }}),
253     {{ end -}}
254     {{ if .ExclusiveMinimum -}}
255     ExclusiveMinimum: {{ .ExclusiveMinimum }},
256     {{ end -}}
257     Type: "{{ .Value }}",
258     {{ if .Format -}}
259     Format: "{{ .Format }}",
260     {{ end -}}
261     {{ if .EnumValue -}}
262     Enum: {{ .EnumValue }},
263     {{ end -}}
264     {{ if .MaxLength -}}
265     MaxLength: getInt({{ .MaxLength }}),
266     {{ end -}}
267     {{ if .MinLength -}}
268     MinLength: getInt({{ .MinLength }}),
269     {{ end -}}
270 }`))
271
272 // parsePrimitiveValidation returns a JSONSchemaProps object and its
273 // serialization in Go that describe the validations for the given primitive
274 // type.
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)}
277
278         for _, l := range comments {
279                 getValidation(l, &props)
280         }
281
282         buff := &bytes.Buffer{}
283
284         var n, f, s, d string
285         switch t.Name.Name {
286         case "int", "int64", "uint64":
287                 n = "integer"
288                 f = "int64"
289         case "int32", "uint32":
290                 n = "integer"
291                 f = "int32"
292         case "float", "float32":
293                 n = "number"
294                 f = "float"
295         case "float64":
296                 n = "number"
297                 f = "double"
298         case "bool":
299                 n = "boolean"
300         case "string":
301                 n = "string"
302                 f = props.Format
303         default:
304                 n = t.Name.Name
305         }
306         if props.Enum != nil {
307                 s = parseEnumToString(props.Enum)
308         }
309         d = parseDescription(comments)
310         if err := primitiveTemplate.Execute(buff, primitiveTemplateArgs{props, n, f, s, d}); err != nil {
311                 log.Fatalf("%v", err)
312         }
313         props.Type = n
314         props.Format = f
315         props.Description = d
316         return props, buff.String()
317 }
318
319 type mapTempateArgs struct {
320         Result            string
321         SkipMapValidation bool
322 }
323
324 var mapTemplate = template.Must(template.New("map-template").Parse(
325         `v1beta1.JSONSchemaProps{
326     Type:                 "object",
327     {{if not .SkipMapValidation}}AdditionalProperties: &v1beta1.JSONSchemaPropsOrBool{
328         Allows: true,
329         Schema: &{{.Result}},
330     },{{end}}
331 }`))
332
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{
339                 Type:        "object",
340                 Description: parseDescription(comments),
341         }
342         parseOption := b.arguments.CustomArgs.(*Options)
343         if !parseOption.SkipMapValidation {
344                 props.AdditionalProperties = &v1beta1.JSONSchemaPropsOrBool{
345                         Allows: true,
346                         Schema: &additionalProps}
347         }
348
349         for _, l := range comments {
350                 getValidation(l, &props)
351         }
352
353         buff := &bytes.Buffer{}
354         if err := mapTemplate.Execute(buff, mapTempateArgs{Result: result, SkipMapValidation: parseOption.SkipMapValidation}); err != nil {
355                 log.Fatalf("%v", err)
356         }
357         return props, buff.String()
358 }
359
360 var arrayTemplate = template.Must(template.New("array-template").Parse(
361         `v1beta1.JSONSchemaProps{
362     Type:                 "{{.Type}}",
363     {{ if .Format -}}
364     Format: "{{.Format}}",
365     {{ end -}}
366     {{ if .MaxItems -}}
367     MaxItems: getInt({{ .MaxItems }}),
368     {{ end -}}
369     {{ if .MinItems -}}
370     MinItems: getInt({{ .MinItems }}),
371     {{ end -}}
372     {{ if .UniqueItems -}}
373     UniqueItems: {{ .UniqueItems }},
374     {{ end -}}
375     {{ if .Items -}}
376     Items: &v1beta1.JSONSchemaPropsOrArray{
377         Schema: &{{.ItemsSchema}},
378     },
379     {{ end -}}
380 }`))
381
382 type arrayTemplateArgs struct {
383         v1beta1.JSONSchemaProps
384         ItemsSchema string
385 }
386
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{
393                 Type:        "array",
394                 Items:       &v1beta1.JSONSchemaPropsOrArray{Schema: &items},
395                 Description: parseDescription(comments),
396         }
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"
402                 props.Items = nil
403                 props.Description = parseDescription(comments)
404         }
405         for _, l := range comments {
406                 getValidation(l, &props)
407         }
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.
411                 props.Format = ""
412         }
413         buff := &bytes.Buffer{}
414         if err := arrayTemplate.Execute(buff, arrayTemplateArgs{props, result}); err != nil {
415                 log.Fatalf("%v", err)
416         }
417         return props, buff.String()
418 }
419
420 type objectTemplateArgs struct {
421         v1beta1.JSONSchemaProps
422         Fields   map[string]string
423         Required []string
424         IsRoot   bool
425 }
426
427 var objectTemplate = template.Must(template.New("object-template").Parse(
428         `v1beta1.JSONSchemaProps{
429         {{ if not .IsRoot -}}
430     Type:                 "object",
431         {{ end -}}
432     Properties: map[string]v1beta1.JSONSchemaProps{
433         {{ range $k, $v := .Fields -}}
434         "{{ $k }}": {{ $v }},
435         {{ end -}}
436     },
437     {{if .Required}}Required: []string{
438         {{ range $k, $v := .Required -}}
439         "{{ $v }}", 
440         {{ end -}}
441     },{{ end -}}
442 }`))
443
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{
449                 Type:        "object",
450                 Description: parseDescription(comments),
451         }
452
453         for _, l := range comments {
454                 getValidation(l, &props)
455         }
456
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)
460                 }
461         } else {
462                 m, result, required := b.getMembers(t, found)
463                 props.Properties = m
464                 props.Required = required
465
466                 if err := objectTemplate.Execute(buff, objectTemplateArgs{props, result, required, isRoot}); err != nil {
467                         log.Fatalf("%v", err)
468                 }
469         }
470         return props, buff.String()
471 }
472
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:") {
478                 return
479         }
480         c := strings.Replace(comment, "+kubebuilder:validation:", "", -1)
481         parts := strings.Split(c, "=")
482         if len(parts) != 2 {
483                 log.Fatalf("Expected +kubebuilder:validation:<key>=<value> actual: %s", comment)
484                 return
485         }
486         switch parts[0] {
487         case "Maximum":
488                 f, err := strconv.ParseFloat(parts[1], 64)
489                 if err != nil {
490                         log.Fatalf("Could not parse float from %s: %v", comment, err)
491                         return
492                 }
493                 props.Maximum = &f
494         case "ExclusiveMaximum":
495                 b, err := strconv.ParseBool(parts[1])
496                 if err != nil {
497                         log.Fatalf("Could not parse bool from %s: %v", comment, err)
498                         return
499                 }
500                 props.ExclusiveMaximum = b
501         case "Minimum":
502                 f, err := strconv.ParseFloat(parts[1], 64)
503                 if err != nil {
504                         log.Fatalf("Could not parse float from %s: %v", comment, err)
505                         return
506                 }
507                 props.Minimum = &f
508         case "ExclusiveMinimum":
509                 b, err := strconv.ParseBool(parts[1])
510                 if err != nil {
511                         log.Fatalf("Could not parse bool from %s: %v", comment, err)
512                         return
513                 }
514                 props.ExclusiveMinimum = b
515         case "MaxLength":
516                 i, err := strconv.Atoi(parts[1])
517                 v := int64(i)
518                 if err != nil {
519                         log.Fatalf("Could not parse int from %s: %v", comment, err)
520                         return
521                 }
522                 props.MaxLength = &v
523         case "MinLength":
524                 i, err := strconv.Atoi(parts[1])
525                 v := int64(i)
526                 if err != nil {
527                         log.Fatalf("Could not parse int from %s: %v", comment, err)
528                         return
529                 }
530                 props.MinLength = &v
531         case "Pattern":
532                 props.Pattern = parts[1]
533         case "MaxItems":
534                 if props.Type == "array" {
535                         i, err := strconv.Atoi(parts[1])
536                         v := int64(i)
537                         if err != nil {
538                                 log.Fatalf("Could not parse int from %s: %v", comment, err)
539                                 return
540                         }
541                         props.MaxItems = &v
542                 }
543         case "MinItems":
544                 if props.Type == "array" {
545                         i, err := strconv.Atoi(parts[1])
546                         v := int64(i)
547                         if err != nil {
548                                 log.Fatalf("Could not parse int from %s: %v", comment, err)
549                                 return
550                         }
551                         props.MinItems = &v
552                 }
553         case "UniqueItems":
554                 if props.Type == "array" {
555                         b, err := strconv.ParseBool(parts[1])
556                         if err != nil {
557                                 log.Fatalf("Could not parse bool from %s: %v", comment, err)
558                                 return
559                         }
560                         props.UniqueItems = b
561                 }
562         case "MultipleOf":
563                 f, err := strconv.ParseFloat(parts[1], 64)
564                 if err != nil {
565                         log.Fatalf("Could not parse float from %s: %v", comment, err)
566                         return
567                 }
568                 props.MultipleOf = &f
569         case "Enum":
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)
575                         }
576                         props.Enum = enums
577                 }
578         case "Format":
579                 props.Format = parts[1]
580         default:
581                 log.Fatalf("Unsupport validation: %s", comment)
582         }
583 }
584
585 // getMembers builds maps by field name of the JSONSchemaProps and their Go
586 // serializations.
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{}
591
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
597         }
598         found.Insert(t.Name.String())
599
600         for _, member := range t.Members {
601                 tags := jsonRegex.FindStringSubmatch(member.Tags)
602                 if len(tags) == 0 {
603                         // Skip fields without json tags
604                         //fmt.Printf("Skipping member %s %s\n", member.Name, member.Type.Name.String())
605                         continue
606                 }
607                 ts := strings.Split(tags[1], ",")
608                 name := member.Name
609                 strat := ""
610                 if len(ts) > 0 && len(ts[0]) > 0 {
611                         name = ts[0]
612                 }
613                 if len(ts) > 1 {
614                         strat = ts[1]
615                 }
616
617                 // Inline "inline" structs
618                 if strat == "inline" {
619                         m, r, re := b.getMembers(member.Type, found)
620                         for n, v := range m {
621                                 members[n] = v
622                         }
623                         for n, v := range r {
624                                 result[n] = v
625                         }
626                         required = append(required, re...)
627                 } else {
628                         m, r := b.typeToJSONSchemaProps(member.Type, found, member.CommentLines, false)
629                         members[name] = m
630                         result[name] = r
631                         if !strings.HasSuffix(strat, "omitempty") {
632                                 required = append(required, name)
633                         }
634                 }
635         }
636
637         defer found.Delete(t.Name.String())
638         return members, result, required
639 }