1
16
17 package generators
18
19 import (
20 "encoding/json"
21 "errors"
22 "fmt"
23 "regexp"
24 "strconv"
25 "strings"
26
27 "k8s.io/gengo/v2/types"
28 openapi "k8s.io/kube-openapi/pkg/common"
29 "k8s.io/kube-openapi/pkg/validation/spec"
30 )
31
32 type CELTag struct {
33 Rule string `json:"rule,omitempty"`
34 Message string `json:"message,omitempty"`
35 MessageExpression string `json:"messageExpression,omitempty"`
36 OptionalOldSelf *bool `json:"optionalOldSelf,omitempty"`
37 Reason string `json:"reason,omitempty"`
38 FieldPath string `json:"fieldPath,omitempty"`
39 }
40
41 func (c *CELTag) Validate() error {
42 if c == nil || *c == (CELTag{}) {
43 return fmt.Errorf("empty CEL tag is not allowed")
44 }
45
46 var errs []error
47 if c.Rule == "" {
48 errs = append(errs, fmt.Errorf("rule cannot be empty"))
49 }
50 if c.Message == "" && c.MessageExpression == "" {
51 errs = append(errs, fmt.Errorf("message or messageExpression must be set"))
52 }
53 if c.Message != "" && c.MessageExpression != "" {
54 errs = append(errs, fmt.Errorf("message and messageExpression cannot be set at the same time"))
55 }
56
57 if len(errs) > 0 {
58 return errors.Join(errs...)
59 }
60
61 return nil
62 }
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 type commentTags struct {
78 spec.SchemaProps
79
80 CEL []CELTag `json:"cel,omitempty"`
81
82
83
84
85 }
86
87
88
89 func (c commentTags) ValidationSchema() (*spec.Schema, error) {
90 res := spec.Schema{
91 SchemaProps: c.SchemaProps,
92 }
93
94 if len(c.CEL) > 0 {
95
96 celTagJSON, err := json.Marshal(c.CEL)
97 if err != nil {
98 return nil, fmt.Errorf("failed to marshal CEL tag: %w", err)
99 }
100 var celTagMap []interface{}
101 if err := json.Unmarshal(celTagJSON, &celTagMap); err != nil {
102 return nil, fmt.Errorf("failed to unmarshal CEL tag: %w", err)
103 }
104
105 res.VendorExtensible.AddExtension("x-kubernetes-validations", celTagMap)
106 }
107
108 return &res, nil
109 }
110
111
112 func (c commentTags) Validate() error {
113
114 var err error
115
116 if c.MinLength != nil && *c.MinLength < 0 {
117 err = errors.Join(err, fmt.Errorf("minLength cannot be negative"))
118 }
119 if c.MaxLength != nil && *c.MaxLength < 0 {
120 err = errors.Join(err, fmt.Errorf("maxLength cannot be negative"))
121 }
122 if c.MinItems != nil && *c.MinItems < 0 {
123 err = errors.Join(err, fmt.Errorf("minItems cannot be negative"))
124 }
125 if c.MaxItems != nil && *c.MaxItems < 0 {
126 err = errors.Join(err, fmt.Errorf("maxItems cannot be negative"))
127 }
128 if c.MinProperties != nil && *c.MinProperties < 0 {
129 err = errors.Join(err, fmt.Errorf("minProperties cannot be negative"))
130 }
131 if c.MaxProperties != nil && *c.MaxProperties < 0 {
132 err = errors.Join(err, fmt.Errorf("maxProperties cannot be negative"))
133 }
134 if c.Minimum != nil && c.Maximum != nil && *c.Minimum > *c.Maximum {
135 err = errors.Join(err, fmt.Errorf("minimum %f is greater than maximum %f", *c.Minimum, *c.Maximum))
136 }
137 if (c.ExclusiveMinimum || c.ExclusiveMaximum) && c.Minimum != nil && c.Maximum != nil && *c.Minimum == *c.Maximum {
138 err = errors.Join(err, fmt.Errorf("exclusiveMinimum/Maximum cannot be set when minimum == maximum"))
139 }
140 if c.MinLength != nil && c.MaxLength != nil && *c.MinLength > *c.MaxLength {
141 err = errors.Join(err, fmt.Errorf("minLength %d is greater than maxLength %d", *c.MinLength, *c.MaxLength))
142 }
143 if c.MinItems != nil && c.MaxItems != nil && *c.MinItems > *c.MaxItems {
144 err = errors.Join(err, fmt.Errorf("minItems %d is greater than maxItems %d", *c.MinItems, *c.MaxItems))
145 }
146 if c.MinProperties != nil && c.MaxProperties != nil && *c.MinProperties > *c.MaxProperties {
147 err = errors.Join(err, fmt.Errorf("minProperties %d is greater than maxProperties %d", *c.MinProperties, *c.MaxProperties))
148 }
149 if c.Pattern != "" {
150 _, e := regexp.Compile(c.Pattern)
151 if e != nil {
152 err = errors.Join(err, fmt.Errorf("invalid pattern %q: %v", c.Pattern, e))
153 }
154 }
155 if c.MultipleOf != nil && *c.MultipleOf == 0 {
156 err = errors.Join(err, fmt.Errorf("multipleOf cannot be 0"))
157 }
158
159 for i, celTag := range c.CEL {
160 celError := celTag.Validate()
161 if celError == nil {
162 continue
163 }
164 err = errors.Join(err, fmt.Errorf("invalid CEL tag at index %d: %w", i, celError))
165 }
166
167 return err
168 }
169
170
171 func (c commentTags) ValidateType(t *types.Type) error {
172 var err error
173
174 resolvedType := resolveAliasAndPtrType(t)
175 typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String())
176
177
178
179
180 if resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct {
181 return nil
182 }
183
184 isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array
185 isMap := resolvedType.Kind == types.Map
186 isString := typeString == "string"
187 isInt := typeString == "integer"
188 isFloat := typeString == "number"
189
190 if c.MaxItems != nil && !isArray {
191 err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types"))
192 }
193 if c.MinItems != nil && !isArray {
194 err = errors.Join(err, fmt.Errorf("minItems can only be used on array types"))
195 }
196 if c.UniqueItems && !isArray {
197 err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types"))
198 }
199 if c.MaxProperties != nil && !isMap {
200 err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types"))
201 }
202 if c.MinProperties != nil && !isMap {
203 err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types"))
204 }
205 if c.MinLength != nil && !isString {
206 err = errors.Join(err, fmt.Errorf("minLength can only be used on string types"))
207 }
208 if c.MaxLength != nil && !isString {
209 err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types"))
210 }
211 if c.Pattern != "" && !isString {
212 err = errors.Join(err, fmt.Errorf("pattern can only be used on string types"))
213 }
214 if c.Minimum != nil && !isInt && !isFloat {
215 err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types"))
216 }
217 if c.Maximum != nil && !isInt && !isFloat {
218 err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types"))
219 }
220 if c.MultipleOf != nil && !isInt && !isFloat {
221 err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types"))
222 }
223 if c.ExclusiveMinimum && !isInt && !isFloat {
224 err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types"))
225 }
226 if c.ExclusiveMaximum && !isInt && !isFloat {
227 err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types"))
228 }
229
230 return err
231 }
232
233
234
235
236
237 func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) {
238
239 markers, err := parseMarkers(comments, prefix)
240 if err != nil {
241 return nil, fmt.Errorf("failed to parse marker comments: %w", err)
242 }
243 nested, err := nestMarkers(markers)
244 if err != nil {
245 return nil, fmt.Errorf("invalid marker comments: %w", err)
246 }
247
248
249
250 out, err := json.Marshal(nested)
251 if err != nil {
252 return nil, fmt.Errorf("failed to marshal marker comments: %w", err)
253 }
254
255 var commentTags commentTags
256 if err = json.Unmarshal(out, &commentTags); err != nil {
257 return nil, fmt.Errorf("failed to unmarshal marker comments: %w", err)
258 }
259
260
261 validationErrors := commentTags.Validate()
262
263 if t != nil {
264 validationErrors = errors.Join(validationErrors, commentTags.ValidateType(t))
265 }
266
267 if validationErrors != nil {
268 return nil, fmt.Errorf("invalid marker comments: %w", validationErrors)
269 }
270
271 return commentTags.ValidationSchema()
272 }
273
274 var (
275 allowedKeyCharacterSet = `[:_a-zA-Z0-9\[\]\-]`
276 valueEmpty = regexp.MustCompile(fmt.Sprintf(`^(%s*)$`, allowedKeyCharacterSet))
277 valueAssign = regexp.MustCompile(fmt.Sprintf(`^(%s*)=(.*)$`, allowedKeyCharacterSet))
278 valueRawString = regexp.MustCompile(fmt.Sprintf(`^(%s*)>(.*)$`, allowedKeyCharacterSet))
279 )
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309 func extractCommentTags(marker string, lines []string) (map[string]string, error) {
310 out := map[string]string{}
311
312
313
314 lastKey := ""
315 lastIndex := -1
316 lastArrayKey := ""
317
318 var lintErrors []error
319
320 for _, line := range lines {
321 line = strings.Trim(line, " ")
322
323
324
325 previousKey := lastKey
326 previousArrayKey := lastArrayKey
327 previousIndex := lastIndex
328
329
330 lastIndex = -1
331 lastArrayKey = ""
332 lastKey = ""
333
334 if len(line) == 0 {
335 continue
336 } else if !strings.HasPrefix(line, marker) {
337 continue
338 }
339
340 line = strings.TrimPrefix(line, marker)
341
342 key := ""
343 value := ""
344
345 if matches := valueAssign.FindStringSubmatch(line); matches != nil {
346 key = matches[1]
347 value = matches[2]
348
349
350
351
352
353
354
355
356
357
358
359
360 if _, ok := out[key]; ok {
361 return nil, fmt.Errorf("cannot have multiple values for key '%v'", key)
362 }
363
364 } else if matches := valueEmpty.FindStringSubmatch(line); matches != nil {
365 key = matches[1]
366 value = ""
367
368 } else if matches := valueRawString.FindStringSubmatch(line); matches != nil {
369 toAdd := strings.Trim(string(matches[2]), " ")
370
371 key = matches[1]
372
373
374 if existing, exists := out[key]; !exists {
375
376
377 valueBytes, err := json.Marshal(toAdd)
378 if err != nil {
379 return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
380 }
381
382 value = string(valueBytes)
383 } else if key != previousKey {
384
385
386 return nil, fmt.Errorf("concatenations to key '%s' must be consecutive with its assignment", key)
387 } else {
388
389
390
391
392
393 var unmarshalled string
394 if err := json.Unmarshal([]byte(existing), &unmarshalled); err != nil {
395 return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
396 } else {
397 unmarshalled += "\n" + toAdd
398 valueBytes, err := json.Marshal(unmarshalled)
399 if err != nil {
400 return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
401 }
402
403 value = string(valueBytes)
404 }
405 }
406 } else {
407
408
409 return nil, fmt.Errorf("invalid marker comment does not match expected `+key=<json formatted value>` pattern: %v", line)
410 }
411
412 out[key] = value
413 lastKey = key
414
415
416
417
418 if arrayPath, index, hasSubscript, err := extractArraySubscript(key); hasSubscript {
419
420
421 if err != nil {
422 lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: expected integer index in key '%v'", line, key))
423 } else if previousArrayKey != arrayPath && index != 0 {
424 lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
425 } else if index != previousIndex+1 && index != previousIndex {
426 lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
427 }
428
429 lastIndex = index
430 lastArrayKey = arrayPath
431 }
432 }
433
434 if len(lintErrors) > 0 {
435 return nil, errors.Join(lintErrors...)
436 }
437
438 return out, nil
439 }
440
441
442
443
444
445 func parseMarkers(markerComments []string, prefix string) (map[string]any, error) {
446 markers, err := extractCommentTags(prefix, markerComments)
447 if err != nil {
448 return nil, err
449 }
450
451
452 result := map[string]any{}
453 for key, value := range markers {
454 var unmarshalled interface{}
455
456 if len(key) == 0 {
457 return nil, fmt.Errorf("cannot have empty key for marker comment")
458 } else if _, ok := parseSymbolReference(value, ""); ok {
459
460 continue
461 } else if len(value) == 0 {
462
463 result[key] = true
464 } else if err := json.Unmarshal([]byte(value), &unmarshalled); err != nil {
465
466 return nil, fmt.Errorf("failed to parse value for key %v as JSON: %w", key, err)
467 } else {
468
469 result[key] = unmarshalled
470 }
471 }
472 return result, nil
473 }
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496 func nestMarkers(markers map[string]any) (map[string]any, error) {
497 nested := make(map[string]any)
498 var errs []error
499 for key, value := range markers {
500 var err error
501 keys := strings.Split(key, ":")
502
503 if err = putNestedValue(nested, keys, value); err != nil {
504 errs = append(errs, err)
505 }
506 }
507
508 if len(errs) > 0 {
509 return nil, errors.Join(errs...)
510 }
511
512 return nested, nil
513 }
514
515
516
517
518 func putNestedValue(m map[string]any, k []string, v any) error {
519 if len(k) == 0 {
520 return nil
521 }
522
523 key := k[0]
524 rest := k[1:]
525
526
527 if arrayKeyWithoutSubscript, index, hasSubscript, err := extractArraySubscript(key); err != nil {
528 return fmt.Errorf("error parsing subscript for key %v: %w", key, err)
529 } else if hasSubscript {
530 key = arrayKeyWithoutSubscript
531 var arrayDestination []any
532 if existing, ok := m[key]; !ok {
533 arrayDestination = make([]any, index+1)
534 } else if existing, ok := existing.([]any); !ok {
535
536
537 return fmt.Errorf("expected []any at key %v, got %T", key, existing)
538 } else if index >= len(existing) {
539
540 arrayDestination = append(existing, make([]any, index-len(existing)+1)...)
541 } else {
542 arrayDestination = existing
543 }
544
545 m[key] = arrayDestination
546 if arrayDestination[index] == nil {
547
548
549
550 destination := make(map[string]any)
551 arrayDestination[index] = destination
552 if err = putNestedValue(destination, rest, v); err != nil {
553 return err
554 }
555 } else if dst, ok := arrayDestination[index].(map[string]any); ok {
556
557 if putNestedValue(dst, rest, v); err != nil {
558 return err
559 }
560 } else {
561
562
563 return fmt.Errorf("expected map at %v[%v], got %T", key, index, arrayDestination[index])
564 }
565
566 return nil
567 } else if len(rest) == 0 {
568
569 m[key] = v
570 return nil
571 }
572
573 if existing, ok := m[key]; !ok {
574 destination := make(map[string]any)
575 m[key] = destination
576 return putNestedValue(destination, rest, v)
577 } else if destination, ok := existing.(map[string]any); ok {
578 return putNestedValue(destination, rest, v)
579 } else {
580
581
582 return fmt.Errorf("expected map[string]any at key %v, got %T", key, existing)
583 }
584 }
585
586
587
588
589
590
591
592
593
594 func extractArraySubscript(str string) (string, int, bool, error) {
595 subscriptIdx := strings.Index(str, "[")
596 if subscriptIdx == -1 {
597 return "", -1, false, nil
598 }
599
600 subscript := strings.Split(str[subscriptIdx+1:], "]")[0]
601 if len(subscript) == 0 {
602 return "", -1, false, fmt.Errorf("empty subscript not allowed")
603 }
604
605 index, err := strconv.Atoi(subscript)
606 if err != nil {
607 return "", -1, false, fmt.Errorf("expected integer index in key %v", str)
608 } else if index < 0 {
609 return "", -1, false, fmt.Errorf("subscript '%v' is invalid. index must be positive", subscript)
610 }
611
612 return str[:subscriptIdx], index, true, nil
613 }
614
View as plain text