github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/pkg/crd/markers/validation.go (about) 1 /* 2 Copyright 2019 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 markers 18 19 import ( 20 "bytes" 21 "encoding/json" 22 "fmt" 23 "go/ast" 24 "go/parser" 25 "go/token" 26 "math" 27 "strings" 28 29 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 30 "sigs.k8s.io/yaml" 31 32 "sigs.k8s.io/controller-tools/pkg/markers" 33 ) 34 35 const ( 36 SchemalessName = "kubebuilder:validation:Schemaless" 37 ) 38 39 // ValidationMarkers lists all available markers that affect CRD schema generation, 40 // except for the few that don't make sense as type-level markers (see FieldOnlyMarkers). 41 // All markers start with `+kubebuilder:validation:`, and continue with their type name. 42 // A copy is produced of all markers that describes types as well, for making types 43 // reusable and writing complex validations on slice items. 44 var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField, 45 46 // numeric markers 47 48 Maximum(0), 49 Minimum(0), 50 ExclusiveMaximum(false), 51 ExclusiveMinimum(false), 52 MultipleOf(0), 53 MinProperties(0), 54 MaxProperties(0), 55 56 // string markers 57 58 MaxLength(0), 59 MinLength(0), 60 Pattern(""), 61 62 // slice markers 63 64 MaxItems(0), 65 MinItems(0), 66 UniqueItems(false), 67 68 // general markers 69 70 Enum(nil), 71 Format(""), 72 Type(""), 73 OneOf(""), 74 XPreserveUnknownFields{}, 75 XEmbeddedResource{}, 76 XIntOrString{}, 77 XValidation{}, 78 ) 79 80 // FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make 81 // sense on a type, and thus aren't in ValidationMarkers). 82 var FieldOnlyMarkers = []*definitionWithHelp{ 83 must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})). 84 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")), 85 must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})). 86 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), 87 must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})). 88 WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")), 89 90 must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})). 91 WithHelp(Nullable{}.Help()), 92 93 must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})). 94 WithHelp(Default{}.Help()), 95 96 must(markers.MakeAnyTypeDefinition("kubebuilder:example", markers.DescribesField, Example{})). 97 WithHelp(Example{}.Help()), 98 99 must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})). 100 WithHelp(XEmbeddedResource{}.Help()), 101 102 must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})). 103 WithHelp(Schemaless{}.Help()), 104 105 must(markers.MakeAnyTypeDefinition("deckhouse:xdoc:default", markers.DescribesField, DeckhouseDocDefault{})). 106 WithHelp(DeckhouseDocDefault{}.Help()), 107 108 must(markers.MakeAnyTypeDefinition("deckhouse:xdoc:example", markers.DescribesField, DeckhouseDocExample{})). 109 WithHelp(DeckhouseDocExample{}.Help()), 110 } 111 112 // ValidationIshMarkers are field-and-type markers that don't fall under the 113 // :validation: prefix, and/or don't have a name that directly matches their 114 // type. 115 var ValidationIshMarkers = []*definitionWithHelp{ 116 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})). 117 WithHelp(XPreserveUnknownFields{}.Help()), 118 must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})). 119 WithHelp(XPreserveUnknownFields{}.Help()), 120 } 121 122 func init() { 123 AllDefinitions = append(AllDefinitions, ValidationMarkers...) 124 125 for _, def := range ValidationMarkers { 126 newDef := *def.Definition 127 // copy both parts so we don't change the definition 128 typDef := definitionWithHelp{ 129 Definition: &newDef, 130 Help: def.Help, 131 } 132 typDef.Target = markers.DescribesType 133 AllDefinitions = append(AllDefinitions, &typDef) 134 } 135 136 AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...) 137 AllDefinitions = append(AllDefinitions, ValidationIshMarkers...) 138 } 139 140 // +controllertools:marker:generateHelp:category="CRD validation" 141 // Maximum specifies the maximum numeric value that this field can have. 142 type Maximum float64 143 144 func (m Maximum) Value() float64 { 145 return float64(m) 146 } 147 148 // +controllertools:marker:generateHelp:category="CRD validation" 149 // Minimum specifies the minimum numeric value that this field can have. Negative numbers are supported. 150 type Minimum float64 151 152 func (m Minimum) Value() float64 { 153 return float64(m) 154 } 155 156 // +controllertools:marker:generateHelp:category="CRD validation" 157 // ExclusiveMinimum indicates that the minimum is "up to" but not including that value. 158 type ExclusiveMinimum bool 159 160 // +controllertools:marker:generateHelp:category="CRD validation" 161 // ExclusiveMaximum indicates that the maximum is "up to" but not including that value. 162 type ExclusiveMaximum bool 163 164 // +controllertools:marker:generateHelp:category="CRD validation" 165 // MultipleOf specifies that this field must have a numeric value that's a multiple of this one. 166 type MultipleOf float64 167 168 func (m MultipleOf) Value() float64 { 169 return float64(m) 170 } 171 172 // +controllertools:marker:generateHelp:category="CRD validation" 173 // MaxLength specifies the maximum length for this string. 174 type MaxLength int 175 176 // +controllertools:marker:generateHelp:category="CRD validation" 177 // MinLength specifies the minimum length for this string. 178 type MinLength int 179 180 // +controllertools:marker:generateHelp:category="CRD validation" 181 // Pattern specifies that this string must match the given regular expression. 182 type Pattern string 183 184 // +controllertools:marker:generateHelp:category="CRD validation" 185 // MaxItems specifies the maximum length for this list. 186 type MaxItems int 187 188 // +controllertools:marker:generateHelp:category="CRD validation" 189 // MinItems specifies the minimum length for this list. 190 type MinItems int 191 192 // +controllertools:marker:generateHelp:category="CRD validation" 193 // UniqueItems specifies that all items in this list must be unique. 194 type UniqueItems bool 195 196 // +controllertools:marker:generateHelp:category="CRD validation" 197 // MaxProperties restricts the number of keys in an object 198 type MaxProperties int 199 200 // +controllertools:marker:generateHelp:category="CRD validation" 201 // MinProperties restricts the number of keys in an object 202 type MinProperties int 203 204 // +controllertools:marker:generateHelp:category="CRD validation" 205 // Enum specifies that this (scalar) field is restricted to the *exact* values specified here. 206 type Enum []interface{} 207 208 // +controllertools:marker:generateHelp:category="CRD validation" 209 // Format specifies additional "complex" formatting for this field. 210 // 211 // For example, a date-time field would be marked as "type: string" and 212 // "format: date-time". 213 type Format string 214 215 // +controllertools:marker:generateHelp:category="CRD validation" 216 // Type overrides the type for this field (which defaults to the equivalent of the Go type). 217 // 218 // This generally must be paired with custom serialization. For example, the 219 // metav1.Time field would be marked as "type: string" and "format: date-time". 220 type Type string 221 222 // +controllertools:marker:generateHelp:category="CRD validation" 223 // Nullable marks this field as allowing the "null" value. 224 // 225 // This is often not necessary, but may be helpful with custom serialization. 226 type Nullable struct{} 227 228 // +controllertools:marker:generateHelp:category="CRD validation" 229 // Default sets the default value for this field. 230 // 231 // A default value will be accepted as any value valid for the 232 // field. Formatting for common types include: boolean: `true`, string: 233 // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: 234 // "delete"}`). Defaults should be defined in pruned form, and only best-effort 235 // validation will be performed. Full validation of a default requires 236 // submission of the containing CRD to an apiserver. 237 type Default struct { 238 Value interface{} 239 } 240 241 // +controllertools:marker:generateHelp:category="CRD validation" 242 // Example sets the example value for this field. 243 // 244 // An example value will be accepted as any value valid for the 245 // field. Formatting for common types include: boolean: `true`, string: 246 // `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: 247 // "delete"}`). Examples should be defined in pruned form, and only best-effort 248 // validation will be performed. Full validation of an example requires 249 // submission of the containing CRD to an apiserver. 250 type Example struct { 251 Value interface{} 252 } 253 254 // +controllertools:marker:generateHelp:category="CRD processing" 255 // PreserveUnknownFields stops the apiserver from pruning fields which are not specified. 256 // 257 // By default the apiserver drops unknown fields from the request payload 258 // during the decoding step. This marker stops the API server from doing so. 259 // It affects fields recursively, but switches back to normal pruning behaviour 260 // if nested properties or additionalProperties are specified in the schema. 261 // This can either be true or undefined. False 262 // is forbidden. 263 // 264 // NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated 265 // in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function 266 // identically. 267 type XPreserveUnknownFields struct{} 268 269 // +controllertools:marker:generateHelp:category="CRD validation" 270 // EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. 271 // 272 // An embedded resource is a value that has apiVersion, kind and metadata fields. 273 // They are validated implicitly according to the semantics of the currently 274 // running apiserver. It is not necessary to add any additional schema for these 275 // field, yet it is possible. This can be combined with PreserveUnknownFields. 276 type XEmbeddedResource struct{} 277 278 // +controllertools:marker:generateHelp:category="CRD validation" 279 // IntOrString marks a fields as an IntOrString. 280 // 281 // This is required when applying patterns or other validations to an IntOrString 282 // field. Knwon information about the type is applied during the collapse phase 283 // and as such is not normally available during marker application. 284 type XIntOrString struct{} 285 286 // +controllertools:marker:generateHelp:category="CRD validation" 287 // Schemaless marks a field as being a schemaless object. 288 // 289 // Schemaless objects are not introspected, so you must provide 290 // any type and validation information yourself. One use for this 291 // tag is for embedding fields that hold JSONSchema typed objects. 292 // Because this field disables all type checking, it is recommended 293 // to be used only as a last resort. 294 type Schemaless struct{} 295 296 func hasNumericType(schema *apiext.JSONSchemaProps) bool { 297 return schema.Type == "integer" || schema.Type == "number" 298 } 299 300 func isIntegral(value float64) bool { 301 return value == math.Trunc(value) && !math.IsNaN(value) && !math.IsInf(value, 0) 302 } 303 304 // +controllertools:marker:generateHelp:category="CRD validation" 305 // XValidation marks a field as requiring a value for which a given 306 // expression evaluates to true. 307 // 308 // This marker may be repeated to specify multiple expressions, all of 309 // which must evaluate to true. 310 type XValidation struct { 311 Rule string 312 Message string `marker:",optional"` 313 } 314 315 func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 316 if !hasNumericType(schema) { 317 return fmt.Errorf("must apply maximum to a numeric value, found %s", schema.Type) 318 } 319 320 if schema.Type == "integer" && !isIntegral(m.Value()) { 321 return fmt.Errorf("cannot apply non-integral maximum validation (%v) to integer value", m.Value()) 322 } 323 324 val := m.Value() 325 schema.Maximum = &val 326 return nil 327 } 328 329 func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 330 if !hasNumericType(schema) { 331 return fmt.Errorf("must apply minimum to a numeric value, found %s", schema.Type) 332 } 333 334 if schema.Type == "integer" && !isIntegral(m.Value()) { 335 return fmt.Errorf("cannot apply non-integral minimum validation (%v) to integer value", m.Value()) 336 } 337 338 val := m.Value() 339 schema.Minimum = &val 340 return nil 341 } 342 343 func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 344 if !hasNumericType(schema) { 345 return fmt.Errorf("must apply exclusivemaximum to a numeric value, found %s", schema.Type) 346 } 347 schema.ExclusiveMaximum = bool(m) 348 return nil 349 } 350 351 func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 352 if !hasNumericType(schema) { 353 return fmt.Errorf("must apply exclusiveminimum to a numeric value, found %s", schema.Type) 354 } 355 356 schema.ExclusiveMinimum = bool(m) 357 return nil 358 } 359 360 func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 361 if !hasNumericType(schema) { 362 return fmt.Errorf("must apply multipleof to a numeric value, found %s", schema.Type) 363 } 364 365 if schema.Type == "integer" && !isIntegral(m.Value()) { 366 return fmt.Errorf("cannot apply non-integral multipleof validation (%v) to integer value", m.Value()) 367 } 368 369 val := m.Value() 370 schema.MultipleOf = &val 371 return nil 372 } 373 374 func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 375 if schema.Type != "string" { 376 return fmt.Errorf("must apply maxlength to a string") 377 } 378 val := int64(m) 379 schema.MaxLength = &val 380 return nil 381 } 382 383 func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 384 if schema.Type != "string" { 385 return fmt.Errorf("must apply minlength to a string") 386 } 387 val := int64(m) 388 schema.MinLength = &val 389 return nil 390 } 391 392 func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 393 // Allow string types or IntOrStrings. An IntOrString will still 394 // apply the pattern validation when a string is detected, the pattern 395 // will not apply to ints though. 396 if schema.Type != "string" && !schema.XIntOrString { 397 return fmt.Errorf("must apply pattern to a `string` or `IntOrString`") 398 } 399 schema.Pattern = string(m) 400 return nil 401 } 402 403 func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 404 if schema.Type != "array" { 405 return fmt.Errorf("must apply maxitem to an array") 406 } 407 val := int64(m) 408 schema.MaxItems = &val 409 return nil 410 } 411 412 func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 413 if schema.Type != "array" { 414 return fmt.Errorf("must apply minitems to an array") 415 } 416 val := int64(m) 417 schema.MinItems = &val 418 return nil 419 } 420 421 func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 422 if schema.Type != "array" { 423 return fmt.Errorf("must apply uniqueitems to an array") 424 } 425 schema.UniqueItems = bool(m) 426 return nil 427 } 428 429 func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 430 if schema.Type != "object" { 431 return fmt.Errorf("must apply minproperties to an object") 432 } 433 val := int64(m) 434 schema.MinProperties = &val 435 return nil 436 } 437 438 func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 439 if schema.Type != "object" { 440 return fmt.Errorf("must apply maxproperties to an object") 441 } 442 val := int64(m) 443 schema.MaxProperties = &val 444 return nil 445 } 446 447 func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 448 // TODO(directxman12): this is a bit hacky -- we should 449 // probably support AnyType better + using the schema structure 450 vals := make([]apiext.JSON, len(m)) 451 for i, val := range m { 452 // TODO(directxman12): check actual type with schema type? 453 // if we're expecting a string, marshal the string properly... 454 // NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly 455 valMarshalled, err := json.Marshal(val) 456 if err != nil { 457 return err 458 } 459 vals[i] = apiext.JSON{Raw: valMarshalled} 460 } 461 schema.Enum = vals 462 return nil 463 } 464 465 func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 466 schema.Format = string(m) 467 return nil 468 } 469 470 // NB(directxman12): we "typecheck" on target schema properties here, 471 // which means the "Type" marker *must* be applied first. 472 // TODO(directxman12): find a less hacky way to do this 473 // (we could preserve ordering of markers, but that feels bad in its own right). 474 475 func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 476 schema.Type = string(m) 477 return nil 478 } 479 480 func (m Type) ApplyFirst() {} 481 482 func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 483 schema.Nullable = true 484 return nil 485 } 486 487 // Defaults are only valid CRDs created with the v1 API 488 func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 489 marshalledDefault, err := json.Marshal(m.Value) 490 if err != nil { 491 return err 492 } 493 schema.Default = &apiext.JSON{Raw: marshalledDefault} 494 return nil 495 } 496 497 func (m Example) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 498 marshalledExample, err := json.Marshal(m.Value) 499 if err != nil { 500 return err 501 } 502 schema.Example = &apiext.JSON{Raw: marshalledExample} 503 return nil 504 } 505 506 func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 507 defTrue := true 508 schema.XPreserveUnknownFields = &defTrue 509 return nil 510 } 511 512 func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 513 schema.XEmbeddedResource = true 514 return nil 515 } 516 517 // NB(JoelSpeed): we use this property in other markers here, 518 // which means the "XIntOrString" marker *must* be applied first. 519 520 func (m XIntOrString) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 521 schema.XIntOrString = true 522 return nil 523 } 524 525 func (m XIntOrString) ApplyFirst() {} 526 527 func (m XValidation) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 528 schema.XValidations = append(schema.XValidations, apiext.ValidationRule{ 529 Rule: m.Rule, 530 Message: m.Message, 531 }) 532 return nil 533 } 534 535 // +controllertools:marker:generateHelp:category=CRD 536 537 // DeckhouseDocDefault configures the additional x-doc-default field 538 // for property with default value for deckhouse documentation. 539 type DeckhouseDocDefault struct { 540 Value interface{} 541 } 542 543 func (m DeckhouseDocDefault) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 544 marshalledDocDefault, err := json.Marshal(m.Value) 545 if err != nil { 546 return err 547 } 548 schema.XDocDefault = &apiext.JSON{Raw: marshalledDocDefault} 549 return nil 550 } 551 552 // +controllertools:marker:generateHelp:category=CRD 553 554 // DeckhouseDocDefault configures the additional x-doc-example field 555 // for property with example usage for deckhouse documentation. 556 type DeckhouseDocExample struct { 557 Value interface{} 558 } 559 560 func (m DeckhouseDocExample) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 561 marshalledDocExample, err := json.Marshal(m.Value) 562 if err != nil { 563 return err 564 } 565 if schema.XDocExample != nil && len(schema.XDocExample.Raw) > 0 { 566 fullString := bytes.Join([][]byte{ 567 escapeString(schema.XDocExample.Raw), 568 escapeString(marshalledDocExample), 569 }, []byte("\n")) 570 marshalFullString, err := json.Marshal(string(fullString)) 571 if err != nil { 572 return err 573 } 574 marshalledDocExample = marshalFullString 575 } 576 schema.XDocExample = &apiext.JSON{Raw: marshalledDocExample} 577 578 return nil 579 } 580 581 func escapeString(b []byte) []byte { 582 return bytes.ReplaceAll(bytes.ReplaceAll(b, []byte("\""), nil), []byte("\\n"), []byte("\n")) 583 } 584 585 // +controllertools:marker:generateHelp:category=CRD 586 587 // OneOf configures file name and variable name at package level 588 // that will be used to generate oneOf CRD field. 589 // for Example, for comment deckhouse:one:of=./cronjob_types.go=OneOfCRD 590 // generator will search in package root for file ./cronjob_types.go and declared variable "OneOfCRD" in it 591 // 592 // file: ./cronjob_types.go: 593 // 594 // package api 595 // 596 // import ( 597 // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 598 // ) 599 // 600 // const OneOfCRD = ` 601 // - required: [layout] 602 // properties: 603 // layout: 604 // enum: [Standard] 605 // - required: [layout] 606 // properties: 607 // layout: 608 // enum: [WithoutNAT] 609 // masterNodeGroup: 610 // properties: 611 // instanceClass: 612 // type: object 613 // properties: 614 // disableExternalIP: 615 // enum: [false] 616 // 617 // ` 618 // 619 // const "OneOfCRD" would be parsed to oneOf field of CRD as it is 620 type OneOf string 621 622 func (m OneOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error { 623 jsonProps, err := parseJSONSchemaPropsFromMarkerValue(string(m)) 624 if err != nil { 625 return err 626 } 627 schema.OneOf = append(schema.OneOf, jsonProps...) 628 return nil 629 } 630 631 func parseJSONSchemaPropsFromMarkerValue(m string) ([]apiext.JSONSchemaProps, error) { 632 fileName, varName, err := fileNameVarNameFromMarkerValue(m) 633 if err != nil { 634 return nil, err 635 } 636 637 constant, err := constFromFileNameVarName(fileName, varName) 638 if err != nil { 639 return nil, err 640 } 641 642 jsonProps, err := parseJSONSchemaPropsFromAstVariable(constant) 643 if err != nil { 644 return nil, err 645 } 646 return jsonProps, nil 647 } 648 649 func fileNameVarNameFromMarkerValue(m string) (string, string, error) { 650 fc := strings.SplitN(string(m), "=", 2) 651 if len(fc) < 2 { 652 return "", "", fmt.Errorf("deckhouse:one:of not in format '<file name>=<variable name>'") 653 } 654 return fc[0], fc[1], nil 655 } 656 657 func constFromFileNameVarName(fileName, varName string) (*ast.ValueSpec, error) { 658 file, err := parser.ParseFile(token.NewFileSet(), fileName, nil, 0) 659 if err != nil { 660 return nil, err 661 } 662 astVar, ok := file.Scope.Objects[varName] 663 if !ok { 664 return nil, fmt.Errorf("no variable found with name '%s' in file '%s'", varName, fileName) 665 } 666 667 if astVar.Kind.String() != "const" { 668 return nil, fmt.Errorf("variable '%s' from file '%s' should be 'const'", varName, fileName) 669 } 670 671 variable, ok := astVar.Decl.(*ast.ValueSpec) 672 if !ok { 673 return nil, fmt.Errorf("bad const format, should be 'const <var name> = ...' at package level") 674 } 675 return variable, nil 676 } 677 678 func parseJSONSchemaPropsFromAstVariable(variable *ast.ValueSpec) ([]apiext.JSONSchemaProps, error) { 679 value, ok := variable.Values[0].(*ast.BasicLit) 680 if !ok { 681 return nil, fmt.Errorf("bad variable format, should be 'var/const <var name> = <string value>' at package level") 682 } 683 684 if value.Kind.String() != "STRING" { 685 return nil, fmt.Errorf("variable should be of type `string`") 686 } 687 688 stringValue := strings.Trim(value.Value, "`\"'") 689 var props []apiext.JSONSchemaProps 690 if err := yaml.Unmarshal([]byte(stringValue), &props); err != nil { 691 return nil, err 692 } 693 return props, nil 694 }