k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/markers.go (about) 1 /* 2 Copyright 2022 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 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 // commentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations. 65 // These only include the newer prefixed tags. The older tags are still supported, 66 // but are not included in this struct. Comment Tags are transformed into a 67 // *spec.Schema, which is then combined with the older marker comments to produce 68 // the generated OpenAPI spec. 69 // 70 // List of tags not included in this struct: 71 // 72 // - +optional 73 // - +default 74 // - +listType 75 // - +listMapKeys 76 // - +mapType 77 type commentTags struct { 78 spec.SchemaProps 79 80 CEL []CELTag `json:"cel,omitempty"` 81 82 // Future markers can all be parsed into this centralized struct... 83 // Optional bool `json:"optional,omitempty"` 84 // Default any `json:"default,omitempty"` 85 } 86 87 // Returns the schema for the given CommentTags instance. 88 // This is the final authoritative schema for the comment tags 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 // Convert the CELTag to a map[string]interface{} via JSON 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 // validates the parameters in a CommentTags instance. Returns any errors encountered. 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 // Performs type-specific validation for CommentTags porameters. Accepts a Type instance and returns any errors encountered during validation. 171 func (c commentTags) ValidateType(t *types.Type) error { 172 var err error 173 174 resolvedType := resolveAliasAndPtrType(t) 175 typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String()) // will be empty for complicated types 176 177 // Structs and interfaces may dynamically be any type, so we cant validate them 178 // easily. We may be able to if we check that they don't implement all the 179 // override functions, but for now we just skip them. 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 // Parses the given comments into a CommentTags type. Validates the parsed comment tags, and returns the result. 234 // Accepts an optional type to validate against, and a prefix to filter out markers not related to validation. 235 // Accepts a prefix to filter out markers not related to validation. 236 // Returns any errors encountered while parsing or validating the comment tags. 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 // Parse the map into a CommentTags type by marshalling and unmarshalling 249 // as JSON in leiu of an unstructured converter. 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 // Validate the parsed comment tags 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 // extractCommentTags parses comments for lines of the form: 282 // 283 // 'marker' + "key=value" 284 // 285 // or to specify truthy boolean keys: 286 // 287 // 'marker' + "key" 288 // 289 // Values are optional; "" is the default. A tag can be specified more than 290 // one time and all values are returned. Returns a map with an entry for 291 // for each key and a value. 292 // 293 // Similar to version from gengo, but this version support only allows one 294 // value per key (preferring explicit array indices), supports raw strings 295 // with concatenation, and limits the usable characters allowed in a key 296 // (for simpler parsing). 297 // 298 // Assignments and empty values have the same syntax as from gengo. Raw strings 299 // have the syntax: 300 // 301 // 'marker' + "key>value" 302 // 'marker' + "key>value" 303 // 304 // Successive usages of the same raw string key results in concatenating each 305 // line with `\n` in between. It is an error to use `=` to assing to a previously 306 // assigned key 307 // (in contrast to types.ExtractCommentTags which allows array-typed 308 // values to be specified using `=`). 309 func extractCommentTags(marker string, lines []string) (map[string]string, error) { 310 out := map[string]string{} 311 312 // Used to track the the line immediately prior to the one being iterated. 313 // If there was an invalid or ignored line, these values get reset. 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 // Track the current value of the last vars to use in this loop iteration 324 // before they are reset for the next iteration. 325 previousKey := lastKey 326 previousArrayKey := lastArrayKey 327 previousIndex := lastIndex 328 329 // Make sure last vars gets reset if we `continue` 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 // If key exists, throw error. 350 // Some of the old kube open-api gen marker comments like 351 // `+listMapKeys` allowed a list to be specified by writing key=value 352 // multiple times. 353 // 354 // This is not longer supported for the prefixed marker comments. 355 // This is to prevent confusion with the new array syntax which 356 // supports lists of objects. 357 // 358 // The old marker comments like +listMapKeys will remain functional, 359 // but new markers will not support it. 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 // First usage as a raw string. 374 if existing, exists := out[key]; !exists { 375 376 // Encode the raw string as JSON to ensure that it is properly escaped. 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 // Successive usages of the same key of a raw string must be 385 // consecutive 386 return nil, fmt.Errorf("concatenations to key '%s' must be consecutive with its assignment", key) 387 } else { 388 // If it is a consecutive repeat usage, concatenate to the 389 // existing value. 390 // 391 // Decode JSON string, append to it, re-encode JSON string. 392 // Kinda janky but this is a code-generator... 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 // Comment has the correct prefix, but incorrect syntax, so it is an 408 // error 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 // Lint the array subscript for common mistakes. This only lints the last 416 // array index used, (since we do not have a need for nested arrays yet 417 // in markers) 418 if arrayPath, index, hasSubscript, err := extractArraySubscript(key); hasSubscript { 419 // If index is non-zero, check that that previous line was for the same 420 // key and either the same or previous index 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 // Extracts and parses the given marker comments into a map of key -> value. 442 // Accepts a prefix to filter out markers not related to validation. 443 // The prefix is removed from the key in the returned map. 444 // Empty keys and invalid values will return errors, refs are currently unsupported and will be skipped. 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 // Parse the values as JSON 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 // Skip ref markers 460 continue 461 } else if len(value) == 0 { 462 // Empty value means key is implicitly a bool 463 result[key] = true 464 } else if err := json.Unmarshal([]byte(value), &unmarshalled); err != nil { 465 // Not valid JSON, throw error 466 return nil, fmt.Errorf("failed to parse value for key %v as JSON: %w", key, err) 467 } else { 468 // Is is valid JSON, use as a JSON value 469 result[key] = unmarshalled 470 } 471 } 472 return result, nil 473 } 474 475 // Converts a map of: 476 // 477 // "a:b:c": 1 478 // "a:b:d": 2 479 // "a:e": 3 480 // "f": 4 481 // 482 // Into: 483 // 484 // map[string]any{ 485 // "a": map[string]any{ 486 // "b": map[string]any{ 487 // "c": 1, 488 // "d": 2, 489 // }, 490 // "e": 3, 491 // }, 492 // "f": 4, 493 // } 494 // 495 // Returns a list of joined errors for any invalid keys. See putNestedValue for more details. 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 // Recursively puts a value into the given keypath, creating intermediate maps 516 // and slices as needed. If a key is of the form `foo[bar]`, then bar will be 517 // treated as an index into the array foo. If bar is not a valid integer, putNestedValue returns an error. 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 // Array case 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 // Error case. Existing isn't of correct type. Can happen if 536 // someone is subscripting a field that was previously not an array 537 return fmt.Errorf("expected []any at key %v, got %T", key, existing) 538 } else if index >= len(existing) { 539 // Ensure array is big enough 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 // Doesn't exist case, create the destination. 548 // Assumes the destination is a map for now. Theoretically could be 549 // extended to support arrays of arrays, but that's not needed yet. 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 // Already exists case, correct type 557 if putNestedValue(dst, rest, v); err != nil { 558 return err 559 } 560 } else { 561 // Already exists, incorrect type. Error 562 // This shouldn't be possible. 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 // Base case. Single key. Just set into destination 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 // Error case. Existing isn't of correct type. Can happen if prior comment 581 // referred to value as an error 582 return fmt.Errorf("expected map[string]any at key %v, got %T", key, existing) 583 } 584 } 585 586 // extractArraySubscript extracts the left array subscript from a key of 587 // the form `foo[bar][baz]` -> "bar". 588 // Returns the key without the subscript, the index, and a bool indicating if 589 // the key had a subscript. 590 // If the key has a subscript, but the subscript is not a valid integer, returns an error. 591 // 592 // This can be adapted to support multidimensional subscripts probably fairly 593 // easily by retuning a list of ints 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 }