Remove BPA from Makefile
[icn.git] / cmd / bpa-operator / vendor / sigs.k8s.io / controller-tools / pkg / internal / codegen / parse / util.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         "fmt"
21         "log"
22         "path/filepath"
23         "strconv"
24         "strings"
25
26         "github.com/pkg/errors"
27         "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
28         "k8s.io/gengo/types"
29 )
30
31 const (
32         specReplicasPath   = "specpath"
33         statusReplicasPath = "statuspath"
34         labelSelectorPath  = "selectorpath"
35         jsonPathError      = "invalid scale path. specpath, statuspath key-value pairs are required, only selectorpath key-value is optinal. For example: // +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=.spec.Label"
36         printColumnName    = "name"
37         printColumnType    = "type"
38         printColumnDescr   = "description"
39         printColumnPath    = "JSONPath"
40         printColumnFormat  = "format"
41         printColumnPri     = "priority"
42         printColumnError   = "invalid printcolumn path. name,type, and JSONPath are required kye-value pairs and rest of the fields are optinal. For example: // +kubebuilder:printcolumn:name=abc,type=string,JSONPath=status"
43 )
44
45 // Options contains the parser options
46 type Options struct {
47         SkipMapValidation bool
48
49         // SkipRBACValidation flag determines whether to check RBAC annotations
50         // for the controller or not at parse stage.
51         SkipRBACValidation bool
52 }
53
54 // IsAPIResource returns true if either of the two conditions become true:
55 // 1. t has a +resource/+kubebuilder:resource comment tag
56 // 2. t has TypeMeta and ObjectMeta in its member list.
57 func IsAPIResource(t *types.Type) bool {
58         for _, c := range t.CommentLines {
59                 if strings.Contains(c, "+resource") || strings.Contains(c, "+kubebuilder:resource") {
60                         return true
61                 }
62         }
63
64         typeMetaFound, objMetaFound := false, false
65         for _, m := range t.Members {
66                 if m.Name == "TypeMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta" {
67                         typeMetaFound = true
68                 }
69                 if m.Name == "ObjectMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" {
70                         objMetaFound = true
71                 }
72                 if typeMetaFound && objMetaFound {
73                         return true
74                 }
75         }
76         return false
77 }
78
79 // IsNonNamespaced returns true if t has a +nonNamespaced comment tag
80 func IsNonNamespaced(t *types.Type) bool {
81         if !IsAPIResource(t) {
82                 return false
83         }
84
85         for _, c := range t.CommentLines {
86                 if strings.Contains(c, "+genclient:nonNamespaced") {
87                         return true
88                 }
89         }
90
91         for _, c := range t.SecondClosestCommentLines {
92                 if strings.Contains(c, "+genclient:nonNamespaced") {
93                         return true
94                 }
95         }
96
97         return false
98 }
99
100 // IsController returns true if t has a +controller or +kubebuilder:controller tag
101 func IsController(t *types.Type) bool {
102         for _, c := range t.CommentLines {
103                 if strings.Contains(c, "+controller") || strings.Contains(c, "+kubebuilder:controller") {
104                         return true
105                 }
106         }
107         return false
108 }
109
110 // IsRBAC returns true if t has a +rbac or +kubebuilder:rbac tag
111 func IsRBAC(t *types.Type) bool {
112         for _, c := range t.CommentLines {
113                 if strings.Contains(c, "+rbac") || strings.Contains(c, "+kubebuilder:rbac") {
114                         return true
115                 }
116         }
117         return false
118 }
119
120 // hasPrintColumn returns true if t has a +printcolumn or +kubebuilder:printcolumn annotation.
121 func hasPrintColumn(t *types.Type) bool {
122         for _, c := range t.CommentLines {
123                 if strings.Contains(c, "+printcolumn") || strings.Contains(c, "+kubebuilder:printcolumn") {
124                         return true
125                 }
126         }
127         return false
128 }
129
130 // IsInformer returns true if t has a +informers or +kubebuilder:informers tag
131 func IsInformer(t *types.Type) bool {
132         for _, c := range t.CommentLines {
133                 if strings.Contains(c, "+informers") || strings.Contains(c, "+kubebuilder:informers") {
134                         return true
135                 }
136         }
137         return false
138 }
139
140 // IsAPISubresource returns true if t has a +subresource-request comment tag
141 func IsAPISubresource(t *types.Type) bool {
142         for _, c := range t.CommentLines {
143                 if strings.Contains(c, "+subresource-request") {
144                         return true
145                 }
146         }
147         return false
148 }
149
150 // HasSubresource returns true if t is an APIResource with one or more Subresources
151 func HasSubresource(t *types.Type) bool {
152         if !IsAPIResource(t) {
153                 return false
154         }
155         for _, c := range t.CommentLines {
156                 if strings.Contains(c, "subresource") {
157                         return true
158                 }
159         }
160         return false
161 }
162
163 // hasStatusSubresource returns true if t is an APIResource annotated with
164 // +kubebuilder:subresource:status
165 func hasStatusSubresource(t *types.Type) bool {
166         if !IsAPIResource(t) {
167                 return false
168         }
169         for _, c := range t.CommentLines {
170                 if strings.Contains(c, "+kubebuilder:subresource:status") {
171                         return true
172                 }
173         }
174         return false
175 }
176
177 // hasScaleSubresource returns true if t is an APIResource annotated with
178 // +kubebuilder:subresource:scale
179 func hasScaleSubresource(t *types.Type) bool {
180         if !IsAPIResource(t) {
181                 return false
182         }
183         for _, c := range t.CommentLines {
184                 if strings.Contains(c, "+kubebuilder:subresource:scale") {
185                         return true
186                 }
187         }
188         return false
189 }
190
191 // hasCategories returns true if t is an APIResource annotated with
192 // +kubebuilder:categories
193 func hasCategories(t *types.Type) bool {
194         if !IsAPIResource(t) {
195                 return false
196         }
197
198         for _, c := range t.CommentLines {
199                 if strings.Contains(c, "+kubebuilder:categories") {
200                         return true
201                 }
202         }
203         return false
204 }
205
206 // HasDocAnnotation returns true if t is an APIResource with doc annotation
207 // +kubebuilder:doc
208 func HasDocAnnotation(t *types.Type) bool {
209         if !IsAPIResource(t) {
210                 return false
211         }
212         for _, c := range t.CommentLines {
213                 if strings.Contains(c, "+kubebuilder:doc") {
214                         return true
215                 }
216         }
217         return false
218 }
219
220 // hasSingular returns true if t is an APIResource annotated with
221 // +kubebuilder:singular
222 func hasSingular(t *types.Type) bool {
223         if !IsAPIResource(t) {
224                 return false
225         }
226         for _, c := range t.CommentLines{
227                 if strings.Contains(c, "+kubebuilder:singular"){
228                         return true
229                 }
230         }
231         return false
232 }
233
234 // IsUnversioned returns true if t is in given group, and not in versioned path.
235 func IsUnversioned(t *types.Type, group string) bool {
236         return IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) && GetGroup(t) == group
237 }
238
239 // IsVersioned returns true if t is in given group, and in versioned path.
240 func IsVersioned(t *types.Type, group string) bool {
241         dir := filepath.Base(filepath.Dir(filepath.Dir(t.Name.Package)))
242         return IsApisDir(dir) && GetGroup(t) == group
243 }
244
245 // GetVersion returns version of t.
246 func GetVersion(t *types.Type, group string) string {
247         if !IsVersioned(t, group) {
248                 panic(errors.Errorf("Cannot get version for unversioned type %v", t.Name))
249         }
250         return filepath.Base(t.Name.Package)
251 }
252
253 // GetGroup returns group of t.
254 func GetGroup(t *types.Type) string {
255         return filepath.Base(GetGroupPackage(t))
256 }
257
258 // GetGroupPackage returns group package of t.
259 func GetGroupPackage(t *types.Type) string {
260         if IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) {
261                 return t.Name.Package
262         }
263         return filepath.Dir(t.Name.Package)
264 }
265
266 // GetKind returns kind of t.
267 func GetKind(t *types.Type, group string) string {
268         if !IsVersioned(t, group) && !IsUnversioned(t, group) {
269                 panic(errors.Errorf("Cannot get kind for type not in group %v", t.Name))
270         }
271         return t.Name.Name
272 }
273
274 // IsApisDir returns true if a directory path is a Kubernetes api directory
275 func IsApisDir(dir string) bool {
276         return dir == "apis" || dir == "api"
277 }
278
279 // Comments is a structure for using comment tags on go structs and fields
280 type Comments []string
281
282 // GetTags returns the value for the first comment with a prefix matching "+name="
283 // e.g. "+name=foo\n+name=bar" would return "foo"
284 func (c Comments) getTag(name, sep string) string {
285         for _, c := range c {
286                 prefix := fmt.Sprintf("+%s%s", name, sep)
287                 if strings.HasPrefix(c, prefix) {
288                         return strings.Replace(c, prefix, "", 1)
289                 }
290         }
291         return ""
292 }
293
294 // hasTag returns true if the Comments has a tag with the given name
295 func (c Comments) hasTag(name string) bool {
296         for _, c := range c {
297                 prefix := fmt.Sprintf("+%s", name)
298                 if strings.HasPrefix(c, prefix) {
299                         return true
300                 }
301         }
302         return false
303 }
304
305 // GetTags returns the value for all comments with a prefix and separator.  E.g. for "name" and "="
306 // "+name=foo\n+name=bar" would return []string{"foo", "bar"}
307 func (c Comments) getTags(name, sep string) []string {
308         tags := []string{}
309         for _, c := range c {
310                 prefix := fmt.Sprintf("+%s%s", name, sep)
311                 if strings.HasPrefix(c, prefix) {
312                         tags = append(tags, strings.Replace(c, prefix, "", 1))
313                 }
314         }
315         return tags
316 }
317
318 // getCategoriesTag returns the value of the +kubebuilder:categories tags
319 func getCategoriesTag(c *types.Type) string {
320         comments := Comments(c.CommentLines)
321         resource := comments.getTag("kubebuilder:categories", "=")
322         if len(resource) == 0 {
323                 panic(errors.Errorf("Must specify +kubebuilder:categories comment for type %v", c.Name))
324         }
325         return resource
326 }
327
328 // getSingularName returns the value of the +kubebuilder:singular tag
329 func getSingularName(c *types.Type) string {
330         comments := Comments(c.CommentLines)
331         singular := comments.getTag("kubebuilder:singular", "=")
332         if len(singular) == 0 {
333                 panic(errors.Errorf("Must specify a value to use with +kubebuilder:singular comment for type %v", c.Name))
334         }
335         return singular
336 }
337
338 // getDocAnnotation parse annotations of "+kubebuilder:doc:" with tags of "warning" or "doc" for control generating doc config.
339 // E.g. +kubebuilder:doc:warning=foo  +kubebuilder:doc:note=bar
340 func getDocAnnotation(t *types.Type, tags ...string) map[string]string {
341         annotation := make(map[string]string)
342         for _, tag := range tags {
343                 for _, c := range t.CommentLines {
344                         prefix := fmt.Sprintf("+kubebuilder:doc:%s=", tag)
345                         if strings.HasPrefix(c, prefix) {
346                                 annotation[tag] = strings.Replace(c, prefix, "", 1)
347                         }
348                 }
349         }
350         return annotation
351 }
352
353 // parseByteValue returns the literal digital number values from a byte array
354 func parseByteValue(b []byte) string {
355         elem := strings.Join(strings.Fields(fmt.Sprintln(b)), ",")
356         elem = strings.TrimPrefix(elem, "[")
357         elem = strings.TrimSuffix(elem, "]")
358         return elem
359 }
360
361 // parseDescription parse comments above each field in the type definition.
362 func parseDescription(res []string) string {
363         var temp strings.Builder
364         var desc string
365         for _, comment := range res {
366                 if !(strings.Contains(comment, "+kubebuilder") || strings.Contains(comment, "+optional")) {
367                         temp.WriteString(comment)
368                         temp.WriteString(" ")
369                         desc = strings.TrimRight(temp.String(), " ")
370                 }
371         }
372         return desc
373 }
374
375 // parseEnumToString returns a representive validated go format string from JSONSchemaProps schema
376 func parseEnumToString(value []v1beta1.JSON) string {
377         res := "[]v1beta1.JSON{"
378         prefix := "v1beta1.JSON{[]byte{"
379         for _, v := range value {
380                 res = res + prefix + parseByteValue(v.Raw) + "}},"
381         }
382         return strings.TrimSuffix(res, ",") + "}"
383 }
384
385 // check type of enum element value to match type of field
386 func checkType(props *v1beta1.JSONSchemaProps, s string, enums *[]v1beta1.JSON) {
387
388         // TODO support more types check
389         switch props.Type {
390         case "int", "int64", "uint64":
391                 if _, err := strconv.ParseInt(s, 0, 64); err != nil {
392                         log.Fatalf("Invalid integer value [%v] for a field of integer type", s)
393                 }
394                 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
395         case "int32", "unit32":
396                 if _, err := strconv.ParseInt(s, 0, 32); err != nil {
397                         log.Fatalf("Invalid integer value [%v] for a field of integer32 type", s)
398                 }
399                 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
400         case "float", "float32":
401                 if _, err := strconv.ParseFloat(s, 32); err != nil {
402                         log.Fatalf("Invalid float value [%v] for a field of float32 type", s)
403                 }
404                 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
405         case "float64":
406                 if _, err := strconv.ParseFloat(s, 64); err != nil {
407                         log.Fatalf("Invalid float value [%v] for a field of float type", s)
408                 }
409                 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
410         case "string":
411                 *enums = append(*enums, v1beta1.JSON{Raw: []byte(`"` + s + `"`)})
412         }
413 }
414
415 // Scale subresource requires specpath, statuspath, selectorpath key values, represents for JSONPath of
416 // SpecReplicasPath, StatusReplicasPath, LabelSelectorPath separately. e.g.
417 // +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=
418 func parseScaleParams(t *types.Type) (map[string]string, error) {
419         jsonPath := make(map[string]string)
420         for _, c := range t.CommentLines {
421                 if strings.Contains(c, "+kubebuilder:subresource:scale") {
422                         paths := strings.Replace(c, "+kubebuilder:subresource:scale:", "", -1)
423                         path := strings.Split(paths, ",")
424                         if len(path) < 2 {
425                                 return nil, fmt.Errorf(jsonPathError)
426                         }
427                         for _, s := range path {
428                                 kv := strings.Split(s, "=")
429                                 if kv[0] == specReplicasPath || kv[0] == statusReplicasPath || kv[0] == labelSelectorPath {
430                                         jsonPath[kv[0]] = kv[1]
431                                 } else {
432                                         return nil, fmt.Errorf(jsonPathError)
433                                 }
434                         }
435                         var ok bool
436                         _, ok = jsonPath[specReplicasPath]
437                         if !ok {
438                                 return nil, fmt.Errorf(jsonPathError)
439                         }
440                         _, ok = jsonPath[statusReplicasPath]
441                         if !ok {
442                                 return nil, fmt.Errorf(jsonPathError)
443                         }
444                         return jsonPath, nil
445                 }
446         }
447         return nil, fmt.Errorf(jsonPathError)
448 }
449
450 // printColumnKV parses key-value string formatted as "foo=bar" and returns key and value.
451 func printColumnKV(s string) (key, value string, err error) {
452         kv := strings.SplitN(s, "=", 2)
453         if len(kv) != 2 {
454                 err = fmt.Errorf("invalid key value pair")
455                 return key, value, err
456         }
457         key, value = kv[0], kv[1]
458         if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
459                 value = value[1 : len(value)-1]
460         }
461         return key, value, err
462 }
463
464 // helperPrintColumn is a helper function for the parsePrintColumnParams to compute printer columns.
465 func helperPrintColumn(parts string, comment string) (v1beta1.CustomResourceColumnDefinition, error) {
466         config := v1beta1.CustomResourceColumnDefinition{}
467         var count int
468         part := strings.Split(parts, ",")
469         if len(part) < 3 {
470                 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
471         }
472
473         for _, elem := range strings.Split(parts, ",") {
474                 key, value, err := printColumnKV(elem)
475                 if err != nil {
476                         return v1beta1.CustomResourceColumnDefinition{},
477                                 fmt.Errorf("//+kubebuilder:printcolumn: tags must be key value pairs.Expected "+
478                                         "keys [name=<name>,type=<type>,description=<descr>,format=<format>] "+
479                                         "Got string: [%s]", parts)
480                 }
481                 if key == printColumnName || key == printColumnType || key == printColumnPath {
482                         count++
483                 }
484                 switch key {
485                 case printColumnName:
486                         config.Name = value
487                 case printColumnType:
488                         if value == "integer" || value == "number" || value == "string" || value == "boolean" || value == "date" {
489                                 config.Type = value
490                         } else {
491                                 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnType)
492                         }
493                 case printColumnFormat:
494                         if config.Type == "integer" && (value == "int32" || value == "int64") {
495                                 config.Format = value
496                         } else if config.Type == "number" && (value == "float" || value == "double") {
497                                 config.Format = value
498                         } else if config.Type == "string" && (value == "byte" || value == "date" || value == "date-time" || value == "password") {
499                                 config.Format = value
500                         } else {
501                                 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnFormat)
502                         }
503                 case printColumnPath:
504                         config.JSONPath = value
505                 case printColumnPri:
506                         i, err := strconv.Atoi(value)
507                         v := int32(i)
508                         if err != nil {
509                                 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnPri)
510                         }
511                         config.Priority = v
512                 case printColumnDescr:
513                         config.Description = value
514                 default:
515                         return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
516                 }
517         }
518         if count != 3 {
519                 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
520         }
521         return config, nil
522 }
523
524 // printcolumn requires name,type,JSONPath fields and rest of the field are optional
525 // +kubebuilder:printcolumn:name=<name>,type=<type>,description=<desc>,JSONPath:<.spec.Name>,priority=<int32>,format=<format>
526 func parsePrintColumnParams(t *types.Type) ([]v1beta1.CustomResourceColumnDefinition, error) {
527         result := []v1beta1.CustomResourceColumnDefinition{}
528         for _, comment := range t.CommentLines {
529                 if strings.Contains(comment, "+kubebuilder:printcolumn") {
530                         parts := strings.Replace(comment, "+kubebuilder:printcolumn:", "", -1)
531                         res, err := helperPrintColumn(parts, comment)
532                         if err != nil {
533                                 return []v1beta1.CustomResourceColumnDefinition{}, err
534                         }
535                         result = append(result, res)
536                 }
537         }
538         return result, nil
539 }