k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/openapi.go (about) 1 /* 2 Copyright 2016 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 "bytes" 21 "encoding/json" 22 "fmt" 23 "io" 24 "path" 25 "reflect" 26 "regexp" 27 "sort" 28 "strings" 29 30 "k8s.io/gengo/v2" 31 "k8s.io/gengo/v2/generator" 32 "k8s.io/gengo/v2/namer" 33 "k8s.io/gengo/v2/types" 34 openapi "k8s.io/kube-openapi/pkg/common" 35 "k8s.io/kube-openapi/pkg/validation/spec" 36 37 "k8s.io/klog/v2" 38 ) 39 40 // This is the comment tag that carries parameters for open API generation. 41 const tagName = "k8s:openapi-gen" 42 const markerPrefix = "+k8s:validation:" 43 const tagOptional = "optional" 44 const tagRequired = "required" 45 const tagDefault = "default" 46 47 // Known values for the tag. 48 const ( 49 tagValueTrue = "true" 50 tagValueFalse = "false" 51 ) 52 53 // Used for temporary validation of patch struct tags. 54 // TODO: Remove patch struct tag validation because they we are now consuming OpenAPI on server. 55 var tempPatchTags = [...]string{ 56 "patchMergeKey", 57 "patchStrategy", 58 } 59 60 func getOpenAPITagValue(comments []string) []string { 61 return gengo.ExtractCommentTags("+", comments)[tagName] 62 } 63 64 func getSingleTagsValue(comments []string, tag string) (string, error) { 65 tags, ok := gengo.ExtractCommentTags("+", comments)[tag] 66 if !ok || len(tags) == 0 { 67 return "", nil 68 } 69 if len(tags) > 1 { 70 return "", fmt.Errorf("multiple values are not allowed for tag %s", tag) 71 } 72 return tags[0], nil 73 } 74 75 func hasOpenAPITagValue(comments []string, value string) bool { 76 tagValues := getOpenAPITagValue(comments) 77 for _, val := range tagValues { 78 if val == value { 79 return true 80 } 81 } 82 return false 83 } 84 85 // isOptional returns error if the member has +optional and +required in 86 // its comments. If +optional is present it returns true. If +required is present 87 // it returns false. Otherwise, it returns true if `omitempty` JSON tag is present 88 func isOptional(m *types.Member) (bool, error) { 89 hasOptionalCommentTag := gengo.ExtractCommentTags( 90 "+", m.CommentLines)[tagOptional] != nil 91 hasRequiredCommentTag := gengo.ExtractCommentTags( 92 "+", m.CommentLines)[tagRequired] != nil 93 if hasOptionalCommentTag && hasRequiredCommentTag { 94 return false, fmt.Errorf("member %s cannot be both optional and required", m.Name) 95 } else if hasRequiredCommentTag { 96 return false, nil 97 } else if hasOptionalCommentTag { 98 return true, nil 99 } 100 101 // If neither +optional nor +required is present in the comments, 102 // infer optional from the json tags. 103 return strings.Contains(reflect.StructTag(m.Tags).Get("json"), "omitempty"), nil 104 } 105 106 func apiTypeFilterFunc(c *generator.Context, t *types.Type) bool { 107 // There is a conflict between this codegen and codecgen, we should avoid types generated for codecgen 108 if strings.HasPrefix(t.Name.Name, "codecSelfer") { 109 return false 110 } 111 pkg := c.Universe.Package(t.Name.Package) 112 if hasOpenAPITagValue(pkg.Comments, tagValueTrue) { 113 return !hasOpenAPITagValue(t.CommentLines, tagValueFalse) 114 } 115 if hasOpenAPITagValue(t.CommentLines, tagValueTrue) { 116 return true 117 } 118 return false 119 } 120 121 const ( 122 specPackagePath = "k8s.io/kube-openapi/pkg/validation/spec" 123 openAPICommonPackagePath = "k8s.io/kube-openapi/pkg/common" 124 ) 125 126 // openApiGen produces a file with auto-generated OpenAPI functions. 127 type openAPIGen struct { 128 generator.GoGenerator 129 // TargetPackage is the package that will get GetOpenAPIDefinitions function returns all open API definitions. 130 targetPackage string 131 imports namer.ImportTracker 132 } 133 134 func newOpenAPIGen(outputFilename string, targetPackage string) generator.Generator { 135 return &openAPIGen{ 136 GoGenerator: generator.GoGenerator{ 137 OutputFilename: outputFilename, 138 }, 139 imports: generator.NewImportTrackerForPackage(targetPackage), 140 targetPackage: targetPackage, 141 } 142 } 143 144 const nameTmpl = "schema_$.type|private$" 145 146 func (g *openAPIGen) Namers(c *generator.Context) namer.NameSystems { 147 // Have the raw namer for this file track what it imports. 148 return namer.NameSystems{ 149 "raw": namer.NewRawNamer(g.targetPackage, g.imports), 150 "private": &namer.NameStrategy{ 151 Join: func(pre string, in []string, post string) string { 152 return strings.Join(in, "_") 153 }, 154 PrependPackageNames: 4, // enough to fully qualify from k8s.io/api/... 155 }, 156 } 157 } 158 159 func (g *openAPIGen) Imports(c *generator.Context) []string { 160 importLines := []string{} 161 for _, singleImport := range g.imports.ImportLines() { 162 importLines = append(importLines, singleImport) 163 } 164 return importLines 165 } 166 167 func argsFromType(t *types.Type) generator.Args { 168 return generator.Args{ 169 "type": t, 170 "ReferenceCallback": types.Ref(openAPICommonPackagePath, "ReferenceCallback"), 171 "OpenAPIDefinition": types.Ref(openAPICommonPackagePath, "OpenAPIDefinition"), 172 "SpecSchemaType": types.Ref(specPackagePath, "Schema"), 173 } 174 } 175 176 func (g *openAPIGen) Init(c *generator.Context, w io.Writer) error { 177 sw := generator.NewSnippetWriter(w, c, "$", "$") 178 sw.Do("func GetOpenAPIDefinitions(ref $.ReferenceCallback|raw$) map[string]$.OpenAPIDefinition|raw$ {\n", argsFromType(nil)) 179 sw.Do("return map[string]$.OpenAPIDefinition|raw${\n", argsFromType(nil)) 180 181 for _, t := range c.Order { 182 err := newOpenAPITypeWriter(sw, c).generateCall(t) 183 if err != nil { 184 return err 185 } 186 } 187 188 sw.Do("}\n", nil) 189 sw.Do("}\n\n", nil) 190 191 return sw.Error() 192 } 193 194 func (g *openAPIGen) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error { 195 klog.V(5).Infof("generating for type %v", t) 196 sw := generator.NewSnippetWriter(w, c, "$", "$") 197 err := newOpenAPITypeWriter(sw, c).generate(t) 198 if err != nil { 199 return err 200 } 201 return sw.Error() 202 } 203 204 func getJsonTags(m *types.Member) []string { 205 jsonTag := reflect.StructTag(m.Tags).Get("json") 206 if jsonTag == "" { 207 return []string{} 208 } 209 return strings.Split(jsonTag, ",") 210 } 211 212 func getReferableName(m *types.Member) string { 213 jsonTags := getJsonTags(m) 214 if len(jsonTags) > 0 { 215 if jsonTags[0] == "-" { 216 return "" 217 } else { 218 return jsonTags[0] 219 } 220 } else { 221 return m.Name 222 } 223 } 224 225 func shouldInlineMembers(m *types.Member) bool { 226 jsonTags := getJsonTags(m) 227 return len(jsonTags) > 1 && jsonTags[1] == "inline" 228 } 229 230 type openAPITypeWriter struct { 231 *generator.SnippetWriter 232 context *generator.Context 233 refTypes map[string]*types.Type 234 enumContext *enumContext 235 GetDefinitionInterface *types.Type 236 } 237 238 func newOpenAPITypeWriter(sw *generator.SnippetWriter, c *generator.Context) openAPITypeWriter { 239 return openAPITypeWriter{ 240 SnippetWriter: sw, 241 context: c, 242 refTypes: map[string]*types.Type{}, 243 enumContext: newEnumContext(c), 244 } 245 } 246 247 func methodReturnsValue(mt *types.Type, pkg, name string) bool { 248 if len(mt.Signature.Parameters) != 0 || len(mt.Signature.Results) != 1 { 249 return false 250 } 251 r := mt.Signature.Results[0] 252 return r.Name.Name == name && r.Name.Package == pkg 253 } 254 255 func hasOpenAPIV3DefinitionMethod(t *types.Type) bool { 256 for mn, mt := range t.Methods { 257 if mn != "OpenAPIV3Definition" { 258 continue 259 } 260 return methodReturnsValue(mt, openAPICommonPackagePath, "OpenAPIDefinition") 261 } 262 return false 263 } 264 265 func hasOpenAPIDefinitionMethod(t *types.Type) bool { 266 for mn, mt := range t.Methods { 267 if mn != "OpenAPIDefinition" { 268 continue 269 } 270 return methodReturnsValue(mt, openAPICommonPackagePath, "OpenAPIDefinition") 271 } 272 return false 273 } 274 275 func hasOpenAPIDefinitionMethods(t *types.Type) bool { 276 var hasSchemaTypeMethod, hasOpenAPISchemaFormat bool 277 for mn, mt := range t.Methods { 278 switch mn { 279 case "OpenAPISchemaType": 280 hasSchemaTypeMethod = methodReturnsValue(mt, "", "[]string") 281 case "OpenAPISchemaFormat": 282 hasOpenAPISchemaFormat = methodReturnsValue(mt, "", "string") 283 } 284 } 285 return hasSchemaTypeMethod && hasOpenAPISchemaFormat 286 } 287 288 func hasOpenAPIV3OneOfMethod(t *types.Type) bool { 289 for mn, mt := range t.Methods { 290 if mn != "OpenAPIV3OneOfTypes" { 291 continue 292 } 293 return methodReturnsValue(mt, "", "[]string") 294 } 295 return false 296 } 297 298 // typeShortName returns short package name (e.g. the name x appears in package x definition) dot type name. 299 func typeShortName(t *types.Type) string { 300 // `path` vs. `filepath` because packages use '/' 301 return path.Base(t.Name.Package) + "." + t.Name.Name 302 } 303 304 func (g openAPITypeWriter) generateMembers(t *types.Type, required []string) ([]string, error) { 305 var err error 306 for t.Kind == types.Pointer { // fast-forward to effective type containing members 307 t = t.Elem 308 } 309 for _, m := range t.Members { 310 if hasOpenAPITagValue(m.CommentLines, tagValueFalse) { 311 continue 312 } 313 if shouldInlineMembers(&m) { 314 required, err = g.generateMembers(m.Type, required) 315 if err != nil { 316 return required, err 317 } 318 continue 319 } 320 name := getReferableName(&m) 321 if name == "" { 322 continue 323 } 324 if isOptional, err := isOptional(&m); err != nil { 325 klog.Errorf("Error when generating: %v, %v\n", name, m) 326 return required, err 327 } else if !isOptional { 328 required = append(required, name) 329 } 330 if err = g.generateProperty(&m, t); err != nil { 331 klog.Errorf("Error when generating: %v, %v\n", name, m) 332 return required, err 333 } 334 } 335 return required, nil 336 } 337 338 func (g openAPITypeWriter) generateCall(t *types.Type) error { 339 // Only generate for struct type and ignore the rest 340 switch t.Kind { 341 case types.Struct: 342 args := argsFromType(t) 343 g.Do("\"$.$\": ", t.Name) 344 345 hasV2Definition := hasOpenAPIDefinitionMethod(t) 346 hasV2DefinitionTypeAndFormat := hasOpenAPIDefinitionMethods(t) 347 hasV3Definition := hasOpenAPIV3DefinitionMethod(t) 348 349 switch { 350 case hasV2DefinitionTypeAndFormat: 351 g.Do(nameTmpl+"(ref),\n", args) 352 case hasV2Definition && hasV3Definition: 353 g.Do("common.EmbedOpenAPIDefinitionIntoV2Extension($.type|raw${}.OpenAPIV3Definition(), $.type|raw${}.OpenAPIDefinition()),\n", args) 354 case hasV2Definition: 355 g.Do("$.type|raw${}.OpenAPIDefinition(),\n", args) 356 case hasV3Definition: 357 g.Do("$.type|raw${}.OpenAPIV3Definition(),\n", args) 358 default: 359 g.Do(nameTmpl+"(ref),\n", args) 360 } 361 } 362 return g.Error() 363 } 364 365 func (g openAPITypeWriter) generateValueValidations(vs *spec.SchemaProps) error { 366 367 if vs == nil { 368 return nil 369 } 370 args := generator.Args{ 371 "ptrTo": &types.Type{ 372 Name: types.Name{ 373 Package: "k8s.io/utils/ptr", 374 Name: "To", 375 }}, 376 "spec": vs, 377 } 378 if vs.Minimum != nil { 379 g.Do("Minimum: $.ptrTo|raw$[float64]($.spec.Minimum$),\n", args) 380 } 381 if vs.Maximum != nil { 382 g.Do("Maximum: $.ptrTo|raw$[float64]($.spec.Maximum$),\n", args) 383 } 384 if vs.ExclusiveMinimum { 385 g.Do("ExclusiveMinimum: true,\n", args) 386 } 387 if vs.ExclusiveMaximum { 388 g.Do("ExclusiveMaximum: true,\n", args) 389 } 390 if vs.MinLength != nil { 391 g.Do("MinLength: $.ptrTo|raw$[int64]($.spec.MinLength$),\n", args) 392 } 393 if vs.MaxLength != nil { 394 g.Do("MaxLength: $.ptrTo|raw$[int64]($.spec.MaxLength$),\n", args) 395 } 396 397 if vs.MinProperties != nil { 398 g.Do("MinProperties: $.ptrTo|raw$[int64]($.spec.MinProperties$),\n", args) 399 } 400 if vs.MaxProperties != nil { 401 g.Do("MaxProperties: $.ptrTo|raw$[int64]($.spec.MaxProperties$),\n", args) 402 } 403 if len(vs.Pattern) > 0 { 404 p, err := json.Marshal(vs.Pattern) 405 if err != nil { 406 return err 407 } 408 g.Do("Pattern: $.$,\n", string(p)) 409 } 410 if vs.MultipleOf != nil { 411 g.Do("MultipleOf: $.ptrTo|raw$[float64]($.spec.MultipleOf$),\n", args) 412 } 413 if vs.MinItems != nil { 414 g.Do("MinItems: $.ptrTo|raw$[int64]($.spec.MinItems$),\n", args) 415 } 416 if vs.MaxItems != nil { 417 g.Do("MaxItems: $.ptrTo|raw$[int64]($.spec.MaxItems$),\n", args) 418 } 419 if vs.UniqueItems { 420 g.Do("UniqueItems: true,\n", nil) 421 } 422 423 return nil 424 } 425 426 func (g openAPITypeWriter) generate(t *types.Type) error { 427 // Only generate for struct type and ignore the rest 428 switch t.Kind { 429 case types.Struct: 430 validationSchema, err := ParseCommentTags(t, t.CommentLines, markerPrefix) 431 if err != nil { 432 return err 433 } 434 435 hasV2Definition := hasOpenAPIDefinitionMethod(t) 436 hasV2DefinitionTypeAndFormat := hasOpenAPIDefinitionMethods(t) 437 hasV3OneOfTypes := hasOpenAPIV3OneOfMethod(t) 438 hasV3Definition := hasOpenAPIV3DefinitionMethod(t) 439 440 if hasV2Definition || (hasV3Definition && !hasV2DefinitionTypeAndFormat) { 441 // already invoked directly 442 return nil 443 } 444 445 args := argsFromType(t) 446 g.Do("func "+nameTmpl+"(ref $.ReferenceCallback|raw$) $.OpenAPIDefinition|raw$ {\n", args) 447 switch { 448 case hasV2DefinitionTypeAndFormat && hasV3Definition: 449 g.Do("return common.EmbedOpenAPIDefinitionIntoV2Extension($.type|raw${}.OpenAPIV3Definition(), $.OpenAPIDefinition|raw${\n"+ 450 "Schema: spec.Schema{\n"+ 451 "SchemaProps: spec.SchemaProps{\n", args) 452 g.generateDescription(t.CommentLines) 453 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 454 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 455 err = g.generateValueValidations(&validationSchema.SchemaProps) 456 if err != nil { 457 return err 458 } 459 g.Do("},\n", nil) 460 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 461 return err 462 } 463 g.Do("},\n", nil) 464 g.Do("})\n}\n\n", args) 465 return nil 466 case hasV2DefinitionTypeAndFormat && hasV3OneOfTypes: 467 // generate v3 def. 468 g.Do("return common.EmbedOpenAPIDefinitionIntoV2Extension($.OpenAPIDefinition|raw${\n"+ 469 "Schema: spec.Schema{\n"+ 470 "SchemaProps: spec.SchemaProps{\n", args) 471 g.generateDescription(t.CommentLines) 472 g.Do("OneOf:common.GenerateOpenAPIV3OneOfSchema($.type|raw${}.OpenAPIV3OneOfTypes()),\n"+ 473 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 474 err = g.generateValueValidations(&validationSchema.SchemaProps) 475 if err != nil { 476 return err 477 } 478 g.Do("},\n", nil) 479 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 480 return err 481 } 482 g.Do("},\n", nil) 483 g.Do("},", args) 484 // generate v2 def. 485 g.Do("$.OpenAPIDefinition|raw${\n"+ 486 "Schema: spec.Schema{\n"+ 487 "SchemaProps: spec.SchemaProps{\n", args) 488 g.generateDescription(t.CommentLines) 489 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 490 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 491 err = g.generateValueValidations(&validationSchema.SchemaProps) 492 if err != nil { 493 return err 494 } 495 g.Do("},\n", nil) 496 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 497 return err 498 } 499 g.Do("},\n", nil) 500 g.Do("})\n}\n\n", args) 501 return nil 502 case hasV2DefinitionTypeAndFormat: 503 g.Do("return $.OpenAPIDefinition|raw${\n"+ 504 "Schema: spec.Schema{\n"+ 505 "SchemaProps: spec.SchemaProps{\n", args) 506 g.generateDescription(t.CommentLines) 507 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 508 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 509 err = g.generateValueValidations(&validationSchema.SchemaProps) 510 if err != nil { 511 return err 512 } 513 g.Do("},\n", nil) 514 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 515 return err 516 } 517 g.Do("},\n", nil) 518 g.Do("}\n}\n\n", args) 519 return nil 520 case hasV3OneOfTypes: 521 // having v3 oneOf types without custom v2 type or format does not make sense. 522 return fmt.Errorf("type %q has v3 one of types but not v2 type or format", t.Name) 523 } 524 525 g.Do("return $.OpenAPIDefinition|raw${\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", args) 526 g.generateDescription(t.CommentLines) 527 g.Do("Type: []string{\"object\"},\n", nil) 528 err = g.generateValueValidations(&validationSchema.SchemaProps) 529 if err != nil { 530 return err 531 } 532 533 // write members into a temporary buffer, in order to postpone writing out the Properties field. We only do 534 // that if it is not empty. 535 propertiesBuf := bytes.Buffer{} 536 bsw := g 537 bsw.SnippetWriter = generator.NewSnippetWriter(&propertiesBuf, g.context, "$", "$") 538 required, err := bsw.generateMembers(t, []string{}) 539 if err != nil { 540 return err 541 } 542 if propertiesBuf.Len() > 0 { 543 g.Do("Properties: map[string]$.SpecSchemaType|raw${\n", args) 544 g.Do(strings.Replace(propertiesBuf.String(), "$", "$\"$\"$", -1), nil) // escape $ (used as delimiter of the templates) 545 g.Do("},\n", nil) 546 } 547 548 if len(required) > 0 { 549 g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\"")) 550 } 551 g.Do("},\n", nil) 552 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 553 return err 554 } 555 g.Do("},\n", nil) 556 557 // Map order is undefined, sort them or we may get a different file generated each time. 558 keys := []string{} 559 for k := range g.refTypes { 560 keys = append(keys, k) 561 } 562 sort.Strings(keys) 563 deps := []string{} 564 for _, k := range keys { 565 v := g.refTypes[k] 566 if t, _ := openapi.OpenAPITypeFormat(v.String()); t != "" { 567 // This is a known type, we do not need a reference to it 568 // Will eliminate special case of time.Time 569 continue 570 } 571 deps = append(deps, k) 572 } 573 if len(deps) > 0 { 574 g.Do("Dependencies: []string{\n", args) 575 for _, k := range deps { 576 g.Do("\"$.$\",", k) 577 } 578 g.Do("},\n", nil) 579 } 580 g.Do("}\n}\n\n", nil) 581 } 582 return nil 583 } 584 585 func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}) error { 586 extensions, errors := parseExtensions(t.CommentLines) 587 // Initially, we will only log struct extension errors. 588 if len(errors) > 0 { 589 for _, e := range errors { 590 klog.Errorf("[%s]: %s\n", t.String(), e) 591 } 592 } 593 unions, errors := parseUnions(t) 594 if len(errors) > 0 { 595 for _, e := range errors { 596 klog.Errorf("[%s]: %s\n", t.String(), e) 597 } 598 } 599 600 // TODO(seans3): Validate struct extensions here. 601 g.emitExtensions(extensions, unions, otherExtensions) 602 return nil 603 } 604 605 func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, otherExtensions map[string]interface{}) error { 606 extensions, parseErrors := parseExtensions(m.CommentLines) 607 validationErrors := validateMemberExtensions(extensions, m) 608 errors := append(parseErrors, validationErrors...) 609 // Initially, we will only log member extension errors. 610 if len(errors) > 0 { 611 errorPrefix := fmt.Sprintf("[%s] %s:", parent.String(), m.String()) 612 for _, e := range errors { 613 klog.V(2).Infof("%s %s\n", errorPrefix, e) 614 } 615 } 616 g.emitExtensions(extensions, nil, otherExtensions) 617 return nil 618 } 619 620 func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}) { 621 // If any extensions exist, then emit code to create them. 622 if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 { 623 return 624 } 625 g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) 626 for _, extension := range extensions { 627 g.Do("\"$.$\": ", extension.xName) 628 if extension.hasMultipleValues() || extension.isAlwaysArrayFormat() { 629 g.Do("[]interface{}{\n", nil) 630 } 631 for _, value := range extension.values { 632 g.Do("\"$.$\",\n", value) 633 } 634 if extension.hasMultipleValues() || extension.isAlwaysArrayFormat() { 635 g.Do("},\n", nil) 636 } 637 } 638 if len(unions) > 0 { 639 g.Do("\"x-kubernetes-unions\": []interface{}{\n", nil) 640 for _, u := range unions { 641 u.emit(g) 642 } 643 g.Do("},\n", nil) 644 } 645 646 if len(otherExtensions) > 0 { 647 for k, v := range otherExtensions { 648 g.Do("$.key$: $.value$,\n", map[string]interface{}{ 649 "key": fmt.Sprintf("%#v", k), 650 "value": fmt.Sprintf("%#v", v), 651 }) 652 } 653 } 654 655 g.Do("},\n},\n", nil) 656 } 657 658 // TODO(#44005): Move this validation outside of this generator (probably to policy verifier) 659 func (g openAPITypeWriter) validatePatchTags(m *types.Member, parent *types.Type) error { 660 // TODO: Remove patch struct tag validation because they we are now consuming OpenAPI on server. 661 for _, tagKey := range tempPatchTags { 662 structTagValue := reflect.StructTag(m.Tags).Get(tagKey) 663 commentTagValue, err := getSingleTagsValue(m.CommentLines, tagKey) 664 if err != nil { 665 return err 666 } 667 if structTagValue != commentTagValue { 668 return fmt.Errorf("Tags in comment and struct should match for member (%s) of (%s)", 669 m.Name, parent.Name.String()) 670 } 671 } 672 return nil 673 } 674 675 func defaultFromComments(comments []string, commentPath string, t *types.Type) (interface{}, *types.Name, error) { 676 var tag string 677 678 for { 679 var err error 680 tag, err = getSingleTagsValue(comments, tagDefault) 681 if err != nil { 682 return nil, nil, err 683 } 684 685 if t == nil || len(tag) > 0 { 686 break 687 } 688 689 comments = t.CommentLines 690 commentPath = t.Name.Package 691 switch t.Kind { 692 case types.Pointer: 693 t = t.Elem 694 case types.Alias: 695 t = t.Underlying 696 default: 697 t = nil 698 } 699 } 700 701 if tag == "" { 702 return nil, nil, nil 703 } 704 705 var i interface{} 706 if id, ok := parseSymbolReference(tag, commentPath); ok { 707 klog.Errorf("%v, %v", id, commentPath) 708 return nil, &id, nil 709 } else if err := json.Unmarshal([]byte(tag), &i); err != nil { 710 return nil, nil, fmt.Errorf("failed to unmarshal default: %v", err) 711 } 712 return i, nil, nil 713 } 714 715 var refRE = regexp.MustCompile(`^ref\((?P<reference>[^"]+)\)$`) 716 var refREIdentIndex = refRE.SubexpIndex("reference") 717 718 // parseSymbolReference looks for strings that match one of the following: 719 // - ref(Ident) 720 // - ref(pkgpath.Ident) 721 // If the input string matches either of these, it will return the (optional) 722 // pkgpath, the Ident, and true. Otherwise it will return empty strings and 723 // false. 724 // 725 // This is borrowed from k8s.io/code-generator. 726 func parseSymbolReference(s, sourcePackage string) (types.Name, bool) { 727 matches := refRE.FindStringSubmatch(s) 728 if len(matches) < refREIdentIndex || matches[refREIdentIndex] == "" { 729 return types.Name{}, false 730 } 731 732 contents := matches[refREIdentIndex] 733 name := types.ParseFullyQualifiedName(contents) 734 if len(name.Package) == 0 { 735 name.Package = sourcePackage 736 } 737 return name, true 738 } 739 740 func implementsCustomUnmarshalling(t *types.Type) bool { 741 switch t.Kind { 742 case types.Pointer: 743 unmarshaller, isUnmarshaller := t.Elem.Methods["UnmarshalJSON"] 744 return isUnmarshaller && unmarshaller.Signature.Receiver.Kind == types.Pointer 745 case types.Struct: 746 _, isUnmarshaller := t.Methods["UnmarshalJSON"] 747 return isUnmarshaller 748 default: 749 return false 750 } 751 } 752 753 func mustEnforceDefault(t *types.Type, omitEmpty bool) (interface{}, error) { 754 // Treat types with custom unmarshalling as a value 755 // (Can be alias, struct, or pointer) 756 if implementsCustomUnmarshalling(t) { 757 // Since Go JSON deserializer always feeds `null` when present 758 // to structs with custom UnmarshalJSON, the zero value for 759 // these structs is also null. 760 // 761 // In general, Kubernetes API types with custom marshalling should 762 // marshal their empty values to `null`. 763 return nil, nil 764 } 765 766 switch t.Kind { 767 case types.Alias: 768 return mustEnforceDefault(t.Underlying, omitEmpty) 769 case types.Pointer, types.Map, types.Slice, types.Array, types.Interface: 770 return nil, nil 771 case types.Struct: 772 if len(t.Members) == 1 && t.Members[0].Embedded { 773 // Treat a struct with a single embedded member the same as an alias 774 return mustEnforceDefault(t.Members[0].Type, omitEmpty) 775 } 776 777 return map[string]interface{}{}, nil 778 case types.Builtin: 779 if !omitEmpty { 780 if zero, ok := openapi.OpenAPIZeroValue(t.String()); ok { 781 return zero, nil 782 } else { 783 return nil, fmt.Errorf("please add type %v to getOpenAPITypeFormat function", t) 784 } 785 } 786 return nil, nil 787 default: 788 return nil, fmt.Errorf("not sure how to enforce default for %v", t.Kind) 789 } 790 } 791 792 func (g openAPITypeWriter) generateDefault(comments []string, t *types.Type, omitEmpty bool, commentOwningType *types.Type) error { 793 def, ref, err := defaultFromComments(comments, commentOwningType.Name.Package, t) 794 if err != nil { 795 return err 796 } 797 if enforced, err := mustEnforceDefault(t, omitEmpty); err != nil { 798 return err 799 } else if enforced != nil { 800 if def == nil { 801 def = enforced 802 } else if !reflect.DeepEqual(def, enforced) { 803 enforcedJson, _ := json.Marshal(enforced) 804 return fmt.Errorf("invalid default value (%#v) for non-pointer/non-omitempty. If specified, must be: %v", def, string(enforcedJson)) 805 } 806 } 807 if def != nil { 808 g.Do("Default: $.$,\n", fmt.Sprintf("%#v", def)) 809 } else if ref != nil { 810 g.Do("Default: $.|raw$,\n", &types.Type{Name: *ref}) 811 } 812 return nil 813 } 814 815 func (g openAPITypeWriter) generateDescription(CommentLines []string) { 816 var buffer bytes.Buffer 817 delPrevChar := func() { 818 if buffer.Len() > 0 { 819 buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n" 820 } 821 } 822 823 for _, line := range CommentLines { 824 // Ignore all lines after --- 825 if line == "---" { 826 break 827 } 828 line = strings.TrimRight(line, " ") 829 leading := strings.TrimLeft(line, " ") 830 switch { 831 case len(line) == 0: // Keep paragraphs 832 delPrevChar() 833 buffer.WriteString("\n\n") 834 case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs 835 case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl 836 default: 837 if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { 838 delPrevChar() 839 line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-something..." 840 } else { 841 line += " " 842 } 843 buffer.WriteString(line) 844 } 845 } 846 847 postDoc := strings.TrimLeft(buffer.String(), "\n") 848 postDoc = strings.TrimRight(postDoc, "\n") 849 postDoc = strings.Replace(postDoc, "\\\"", "\"", -1) // replace user's \" to " 850 postDoc = strings.Replace(postDoc, "\"", "\\\"", -1) // Escape " 851 postDoc = strings.Replace(postDoc, "\n", "\\n", -1) 852 postDoc = strings.Replace(postDoc, "\t", "\\t", -1) 853 postDoc = strings.Trim(postDoc, " ") 854 if postDoc != "" { 855 g.Do("Description: \"$.$\",\n", postDoc) 856 } 857 } 858 859 func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) error { 860 name := getReferableName(m) 861 if name == "" { 862 return nil 863 } 864 validationSchema, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) 865 if err != nil { 866 return err 867 } 868 if err := g.validatePatchTags(m, parent); err != nil { 869 return err 870 } 871 g.Do("\"$.$\": {\n", name) 872 if err := g.generateMemberExtensions(m, parent, validationSchema.Extensions); err != nil { 873 return err 874 } 875 g.Do("SchemaProps: spec.SchemaProps{\n", nil) 876 var extraComments []string 877 if enumType, isEnum := g.enumContext.EnumType(m.Type); isEnum { 878 extraComments = enumType.DescriptionLines() 879 } 880 g.generateDescription(append(m.CommentLines, extraComments...)) 881 jsonTags := getJsonTags(m) 882 if len(jsonTags) > 1 && jsonTags[1] == "string" { 883 g.generateSimpleProperty("string", "") 884 g.Do("},\n},\n", nil) 885 return nil 886 } 887 omitEmpty := strings.Contains(reflect.StructTag(m.Tags).Get("json"), "omitempty") 888 if err := g.generateDefault(m.CommentLines, m.Type, omitEmpty, parent); err != nil { 889 return fmt.Errorf("failed to generate default in %v: %v: %v", parent, m.Name, err) 890 } 891 err = g.generateValueValidations(&validationSchema.SchemaProps) 892 if err != nil { 893 return err 894 } 895 t := resolveAliasAndPtrType(m.Type) 896 // If we can get a openAPI type and format for this type, we consider it to be simple property 897 typeString, format := openapi.OpenAPITypeFormat(t.String()) 898 if typeString != "" { 899 g.generateSimpleProperty(typeString, format) 900 if enumType, isEnum := g.enumContext.EnumType(m.Type); isEnum { 901 // original type is an enum, add "Enum: " and the values 902 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 903 } 904 g.Do("},\n},\n", nil) 905 return nil 906 } 907 switch t.Kind { 908 case types.Builtin: 909 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", t) 910 case types.Map: 911 if err := g.generateMapProperty(t); err != nil { 912 return fmt.Errorf("failed to generate map property in %v: %v: %v", parent, m.Name, err) 913 } 914 case types.Slice, types.Array: 915 if err := g.generateSliceProperty(t); err != nil { 916 return fmt.Errorf("failed to generate slice property in %v: %v: %v", parent, m.Name, err) 917 } 918 case types.Struct, types.Interface: 919 g.generateReferenceProperty(t) 920 default: 921 return fmt.Errorf("cannot generate spec for type %v", t) 922 } 923 g.Do("},\n},\n", nil) 924 return g.Error() 925 } 926 927 func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) { 928 g.Do("Type: []string{\"$.$\"},\n", typeString) 929 g.Do("Format: \"$.$\",\n", format) 930 } 931 932 func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) { 933 g.refTypes[t.Name.String()] = t 934 g.Do("Ref: ref(\"$.$\"),\n", t.Name.String()) 935 } 936 937 func resolveAliasAndPtrType(t *types.Type) *types.Type { 938 var prev *types.Type 939 for prev != t { 940 prev = t 941 if t.Kind == types.Alias { 942 t = t.Underlying 943 } 944 if t.Kind == types.Pointer { 945 t = t.Elem 946 } 947 } 948 return t 949 } 950 951 func (g openAPITypeWriter) generateMapProperty(t *types.Type) error { 952 keyType := resolveAliasAndPtrType(t.Key) 953 elemType := resolveAliasAndPtrType(t.Elem) 954 955 // According to OpenAPI examples, only map from string is supported 956 if keyType.Name.Name != "string" { 957 return fmt.Errorf("map with non-string keys are not supported by OpenAPI in %v", t) 958 } 959 960 g.Do("Type: []string{\"object\"},\n", nil) 961 g.Do("AdditionalProperties: &spec.SchemaOrBool{\nAllows: true,\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil) 962 if err := g.generateDefault(t.Elem.CommentLines, t.Elem, false, t.Elem); err != nil { 963 return err 964 } 965 typeString, format := openapi.OpenAPITypeFormat(elemType.String()) 966 if typeString != "" { 967 g.generateSimpleProperty(typeString, format) 968 if enumType, isEnum := g.enumContext.EnumType(t.Elem); isEnum { 969 // original type is an enum, add "Enum: " and the values 970 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 971 } 972 g.Do("},\n},\n},\n", nil) 973 return nil 974 } 975 switch elemType.Kind { 976 case types.Builtin: 977 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType) 978 case types.Struct: 979 g.generateReferenceProperty(elemType) 980 case types.Slice, types.Array: 981 if err := g.generateSliceProperty(elemType); err != nil { 982 return err 983 } 984 case types.Map: 985 if err := g.generateMapProperty(elemType); err != nil { 986 return err 987 } 988 default: 989 return fmt.Errorf("map Element kind %v is not supported in %v", elemType.Kind, t.Name) 990 } 991 g.Do("},\n},\n},\n", nil) 992 return nil 993 } 994 995 func (g openAPITypeWriter) generateSliceProperty(t *types.Type) error { 996 elemType := resolveAliasAndPtrType(t.Elem) 997 g.Do("Type: []string{\"array\"},\n", nil) 998 g.Do("Items: &spec.SchemaOrArray{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil) 999 if err := g.generateDefault(t.Elem.CommentLines, t.Elem, false, t.Elem); err != nil { 1000 return err 1001 } 1002 typeString, format := openapi.OpenAPITypeFormat(elemType.String()) 1003 if typeString != "" { 1004 g.generateSimpleProperty(typeString, format) 1005 if enumType, isEnum := g.enumContext.EnumType(t.Elem); isEnum { 1006 // original type is an enum, add "Enum: " and the values 1007 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 1008 } 1009 g.Do("},\n},\n},\n", nil) 1010 return nil 1011 } 1012 switch elemType.Kind { 1013 case types.Builtin: 1014 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType) 1015 case types.Struct: 1016 g.generateReferenceProperty(elemType) 1017 case types.Slice, types.Array: 1018 if err := g.generateSliceProperty(elemType); err != nil { 1019 return err 1020 } 1021 case types.Map: 1022 if err := g.generateMapProperty(elemType); err != nil { 1023 return err 1024 } 1025 default: 1026 return fmt.Errorf("slice Element kind %v is not supported in %v", elemType.Kind, t) 1027 } 1028 g.Do("},\n},\n},\n", nil) 1029 return nil 1030 }