k8s.io/kube-openapi@v0.0.0-20240826222958-65a50c78dec5/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.Type.Name.Name == name && r.Type.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 // Generates Go code to represent an OpenAPI schema. May be refactored in 366 // the future to take more responsibility as we transition from an on-line 367 // approach to parsing the comments to spec.Schema 368 func (g openAPITypeWriter) generateSchema(s *spec.Schema) error { 369 if !reflect.DeepEqual(s.SchemaProps, spec.SchemaProps{}) { 370 g.Do("SchemaProps: spec.SchemaProps{\n", nil) 371 err := g.generateValueValidations(&s.SchemaProps) 372 if err != nil { 373 return err 374 } 375 376 if len(s.Properties) > 0 { 377 g.Do("Properties: map[string]spec.Schema{\n", nil) 378 379 // Sort property names to generate deterministic output 380 keys := []string{} 381 for k := range s.Properties { 382 keys = append(keys, k) 383 } 384 sort.Strings(keys) 385 386 for _, k := range keys { 387 v := s.Properties[k] 388 g.Do("$.$: {\n", fmt.Sprintf("%#v", k)) 389 err := g.generateSchema(&v) 390 if err != nil { 391 return err 392 } 393 g.Do("},\n", nil) 394 } 395 g.Do("},\n", nil) 396 } 397 398 if s.AdditionalProperties != nil && s.AdditionalProperties.Schema != nil { 399 g.Do("AdditionalProperties: &spec.SchemaOrBool{\n", nil) 400 g.Do("Allows: true,\n", nil) 401 g.Do("Schema: &spec.Schema{\n", nil) 402 err := g.generateSchema(s.AdditionalProperties.Schema) 403 if err != nil { 404 return err 405 } 406 g.Do("},\n", nil) 407 g.Do("},\n", nil) 408 } 409 410 if s.Items != nil && s.Items.Schema != nil { 411 g.Do("Items: &spec.SchemaOrArray{\n", nil) 412 g.Do("Schema: &spec.Schema{\n", nil) 413 err := g.generateSchema(s.Items.Schema) 414 if err != nil { 415 return err 416 } 417 g.Do("},\n", nil) 418 g.Do("},\n", nil) 419 } 420 421 g.Do("},\n", nil) 422 } 423 424 if len(s.Extensions) > 0 { 425 g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) 426 427 // Sort extension keys to generate deterministic output 428 keys := []string{} 429 for k := range s.Extensions { 430 keys = append(keys, k) 431 } 432 sort.Strings(keys) 433 434 for _, k := range keys { 435 v := s.Extensions[k] 436 g.Do("$.key$: $.value$,\n", map[string]interface{}{ 437 "key": fmt.Sprintf("%#v", k), 438 "value": fmt.Sprintf("%#v", v), 439 }) 440 } 441 g.Do("},\n},\n", nil) 442 } 443 444 return nil 445 } 446 447 func (g openAPITypeWriter) generateValueValidations(vs *spec.SchemaProps) error { 448 449 if vs == nil { 450 return nil 451 } 452 args := generator.Args{ 453 "ptrTo": &types.Type{ 454 Name: types.Name{ 455 Package: "k8s.io/utils/ptr", 456 Name: "To", 457 }}, 458 "spec": vs, 459 } 460 if vs.Minimum != nil { 461 g.Do("Minimum: $.ptrTo|raw$[float64]($.spec.Minimum$),\n", args) 462 } 463 if vs.Maximum != nil { 464 g.Do("Maximum: $.ptrTo|raw$[float64]($.spec.Maximum$),\n", args) 465 } 466 if vs.ExclusiveMinimum { 467 g.Do("ExclusiveMinimum: true,\n", args) 468 } 469 if vs.ExclusiveMaximum { 470 g.Do("ExclusiveMaximum: true,\n", args) 471 } 472 if vs.MinLength != nil { 473 g.Do("MinLength: $.ptrTo|raw$[int64]($.spec.MinLength$),\n", args) 474 } 475 if vs.MaxLength != nil { 476 g.Do("MaxLength: $.ptrTo|raw$[int64]($.spec.MaxLength$),\n", args) 477 } 478 479 if vs.MinProperties != nil { 480 g.Do("MinProperties: $.ptrTo|raw$[int64]($.spec.MinProperties$),\n", args) 481 } 482 if vs.MaxProperties != nil { 483 g.Do("MaxProperties: $.ptrTo|raw$[int64]($.spec.MaxProperties$),\n", args) 484 } 485 if len(vs.Pattern) > 0 { 486 p, err := json.Marshal(vs.Pattern) 487 if err != nil { 488 return err 489 } 490 g.Do("Pattern: $.$,\n", string(p)) 491 } 492 if vs.MultipleOf != nil { 493 g.Do("MultipleOf: $.ptrTo|raw$[float64]($.spec.MultipleOf$),\n", args) 494 } 495 if vs.MinItems != nil { 496 g.Do("MinItems: $.ptrTo|raw$[int64]($.spec.MinItems$),\n", args) 497 } 498 if vs.MaxItems != nil { 499 g.Do("MaxItems: $.ptrTo|raw$[int64]($.spec.MaxItems$),\n", args) 500 } 501 if vs.UniqueItems { 502 g.Do("UniqueItems: true,\n", nil) 503 } 504 505 if len(vs.AllOf) > 0 { 506 g.Do("AllOf: []spec.Schema{\n", nil) 507 for _, s := range vs.AllOf { 508 g.Do("{\n", nil) 509 if err := g.generateSchema(&s); err != nil { 510 return err 511 } 512 g.Do("},\n", nil) 513 } 514 g.Do("},\n", nil) 515 } 516 517 return nil 518 } 519 520 func (g openAPITypeWriter) generate(t *types.Type) error { 521 // Only generate for struct type and ignore the rest 522 switch t.Kind { 523 case types.Struct: 524 validationSchema, err := ParseCommentTags(t, t.CommentLines, markerPrefix) 525 if err != nil { 526 return fmt.Errorf("failed parsing comment tags for %v: %w", t.String(), err) 527 } 528 529 hasV2Definition := hasOpenAPIDefinitionMethod(t) 530 hasV2DefinitionTypeAndFormat := hasOpenAPIDefinitionMethods(t) 531 hasV3OneOfTypes := hasOpenAPIV3OneOfMethod(t) 532 hasV3Definition := hasOpenAPIV3DefinitionMethod(t) 533 534 if hasV2Definition || (hasV3Definition && !hasV2DefinitionTypeAndFormat) { 535 // already invoked directly 536 return nil 537 } 538 539 args := argsFromType(t) 540 g.Do("func "+nameTmpl+"(ref $.ReferenceCallback|raw$) $.OpenAPIDefinition|raw$ {\n", args) 541 switch { 542 case hasV2DefinitionTypeAndFormat && hasV3Definition: 543 g.Do("return common.EmbedOpenAPIDefinitionIntoV2Extension($.type|raw${}.OpenAPIV3Definition(), $.OpenAPIDefinition|raw${\n"+ 544 "Schema: spec.Schema{\n"+ 545 "SchemaProps: spec.SchemaProps{\n", args) 546 g.generateDescription(t.CommentLines) 547 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 548 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 549 err = g.generateValueValidations(&validationSchema.SchemaProps) 550 if err != nil { 551 return err 552 } 553 g.Do("},\n", nil) 554 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 555 return err 556 } 557 g.Do("},\n", nil) 558 g.Do("})\n}\n\n", args) 559 return nil 560 case hasV2DefinitionTypeAndFormat && hasV3OneOfTypes: 561 // generate v3 def. 562 g.Do("return common.EmbedOpenAPIDefinitionIntoV2Extension($.OpenAPIDefinition|raw${\n"+ 563 "Schema: spec.Schema{\n"+ 564 "SchemaProps: spec.SchemaProps{\n", args) 565 g.generateDescription(t.CommentLines) 566 g.Do("OneOf:common.GenerateOpenAPIV3OneOfSchema($.type|raw${}.OpenAPIV3OneOfTypes()),\n"+ 567 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 568 err = g.generateValueValidations(&validationSchema.SchemaProps) 569 if err != nil { 570 return err 571 } 572 g.Do("},\n", nil) 573 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 574 return err 575 } 576 g.Do("},\n", nil) 577 g.Do("},", args) 578 // generate v2 def. 579 g.Do("$.OpenAPIDefinition|raw${\n"+ 580 "Schema: spec.Schema{\n"+ 581 "SchemaProps: spec.SchemaProps{\n", args) 582 g.generateDescription(t.CommentLines) 583 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 584 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 585 err = g.generateValueValidations(&validationSchema.SchemaProps) 586 if err != nil { 587 return err 588 } 589 g.Do("},\n", nil) 590 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 591 return err 592 } 593 g.Do("},\n", nil) 594 g.Do("})\n}\n\n", args) 595 return nil 596 case hasV2DefinitionTypeAndFormat: 597 g.Do("return $.OpenAPIDefinition|raw${\n"+ 598 "Schema: spec.Schema{\n"+ 599 "SchemaProps: spec.SchemaProps{\n", args) 600 g.generateDescription(t.CommentLines) 601 g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ 602 "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) 603 err = g.generateValueValidations(&validationSchema.SchemaProps) 604 if err != nil { 605 return err 606 } 607 g.Do("},\n", nil) 608 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 609 return err 610 } 611 g.Do("},\n", nil) 612 g.Do("}\n}\n\n", args) 613 return nil 614 case hasV3OneOfTypes: 615 // having v3 oneOf types without custom v2 type or format does not make sense. 616 return fmt.Errorf("type %q has v3 one of types but not v2 type or format", t.Name) 617 } 618 619 g.Do("return $.OpenAPIDefinition|raw${\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", args) 620 g.generateDescription(t.CommentLines) 621 g.Do("Type: []string{\"object\"},\n", nil) 622 err = g.generateValueValidations(&validationSchema.SchemaProps) 623 if err != nil { 624 return err 625 } 626 627 // write members into a temporary buffer, in order to postpone writing out the Properties field. We only do 628 // that if it is not empty. 629 propertiesBuf := bytes.Buffer{} 630 bsw := g 631 bsw.SnippetWriter = generator.NewSnippetWriter(&propertiesBuf, g.context, "$", "$") 632 required, err := bsw.generateMembers(t, []string{}) 633 if err != nil { 634 return err 635 } 636 if propertiesBuf.Len() > 0 { 637 g.Do("Properties: map[string]$.SpecSchemaType|raw${\n", args) 638 g.Do(strings.Replace(propertiesBuf.String(), "$", "$\"$\"$", -1), nil) // escape $ (used as delimiter of the templates) 639 g.Do("},\n", nil) 640 } 641 642 if len(required) > 0 { 643 g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\"")) 644 } 645 g.Do("},\n", nil) 646 if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { 647 return err 648 } 649 g.Do("},\n", nil) 650 651 // Map order is undefined, sort them or we may get a different file generated each time. 652 keys := []string{} 653 for k := range g.refTypes { 654 keys = append(keys, k) 655 } 656 sort.Strings(keys) 657 deps := []string{} 658 for _, k := range keys { 659 v := g.refTypes[k] 660 if t, _ := openapi.OpenAPITypeFormat(v.String()); t != "" { 661 // This is a known type, we do not need a reference to it 662 // Will eliminate special case of time.Time 663 continue 664 } 665 deps = append(deps, k) 666 } 667 if len(deps) > 0 { 668 g.Do("Dependencies: []string{\n", args) 669 for _, k := range deps { 670 g.Do("\"$.$\",", k) 671 } 672 g.Do("},\n", nil) 673 } 674 g.Do("}\n}\n\n", nil) 675 } 676 return nil 677 } 678 679 func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}) error { 680 extensions, errors := parseExtensions(t.CommentLines) 681 // Initially, we will only log struct extension errors. 682 if len(errors) > 0 { 683 for _, e := range errors { 684 klog.Errorf("[%s]: %s\n", t.String(), e) 685 } 686 } 687 unions, errors := parseUnions(t) 688 if len(errors) > 0 { 689 for _, e := range errors { 690 klog.Errorf("[%s]: %s\n", t.String(), e) 691 } 692 } 693 694 // TODO(seans3): Validate struct extensions here. 695 g.emitExtensions(extensions, unions, otherExtensions) 696 return nil 697 } 698 699 func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, otherExtensions map[string]interface{}) error { 700 extensions, parseErrors := parseExtensions(m.CommentLines) 701 validationErrors := validateMemberExtensions(extensions, m) 702 errors := append(parseErrors, validationErrors...) 703 // Initially, we will only log member extension errors. 704 if len(errors) > 0 { 705 errorPrefix := fmt.Sprintf("[%s] %s:", parent.String(), m.String()) 706 for _, e := range errors { 707 klog.V(2).Infof("%s %s\n", errorPrefix, e) 708 } 709 } 710 g.emitExtensions(extensions, nil, otherExtensions) 711 return nil 712 } 713 714 func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}) { 715 // If any extensions exist, then emit code to create them. 716 if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 { 717 return 718 } 719 g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) 720 for _, extension := range extensions { 721 g.Do("\"$.$\": ", extension.xName) 722 if extension.hasMultipleValues() || extension.isAlwaysArrayFormat() { 723 g.Do("[]interface{}{\n", nil) 724 } 725 for _, value := range extension.values { 726 g.Do("\"$.$\",\n", value) 727 } 728 if extension.hasMultipleValues() || extension.isAlwaysArrayFormat() { 729 g.Do("},\n", nil) 730 } 731 } 732 if len(unions) > 0 { 733 g.Do("\"x-kubernetes-unions\": []interface{}{\n", nil) 734 for _, u := range unions { 735 u.emit(g) 736 } 737 g.Do("},\n", nil) 738 } 739 740 if len(otherExtensions) > 0 { 741 // Sort extension keys to generate deterministic output 742 keys := []string{} 743 for k := range otherExtensions { 744 keys = append(keys, k) 745 } 746 sort.Strings(keys) 747 748 for _, k := range keys { 749 v := otherExtensions[k] 750 g.Do("$.key$: $.value$,\n", map[string]interface{}{ 751 "key": fmt.Sprintf("%#v", k), 752 "value": fmt.Sprintf("%#v", v), 753 }) 754 } 755 } 756 757 g.Do("},\n},\n", nil) 758 } 759 760 // TODO(#44005): Move this validation outside of this generator (probably to policy verifier) 761 func (g openAPITypeWriter) validatePatchTags(m *types.Member, parent *types.Type) error { 762 // TODO: Remove patch struct tag validation because they we are now consuming OpenAPI on server. 763 for _, tagKey := range tempPatchTags { 764 structTagValue := reflect.StructTag(m.Tags).Get(tagKey) 765 commentTagValue, err := getSingleTagsValue(m.CommentLines, tagKey) 766 if err != nil { 767 return err 768 } 769 if structTagValue != commentTagValue { 770 return fmt.Errorf("Tags in comment and struct should match for member (%s) of (%s)", 771 m.Name, parent.Name.String()) 772 } 773 } 774 return nil 775 } 776 777 func defaultFromComments(comments []string, commentPath string, t *types.Type) (interface{}, *types.Name, error) { 778 var tag string 779 780 for { 781 var err error 782 tag, err = getSingleTagsValue(comments, tagDefault) 783 if err != nil { 784 return nil, nil, err 785 } 786 787 if t == nil || len(tag) > 0 { 788 break 789 } 790 791 comments = t.CommentLines 792 commentPath = t.Name.Package 793 switch t.Kind { 794 case types.Pointer: 795 t = t.Elem 796 case types.Alias: 797 t = t.Underlying 798 default: 799 t = nil 800 } 801 } 802 803 if tag == "" { 804 return nil, nil, nil 805 } 806 807 var i interface{} 808 if id, ok := parseSymbolReference(tag, commentPath); ok { 809 klog.V(5).Infof("%v, %v", id, commentPath) 810 return nil, &id, nil 811 } else if err := json.Unmarshal([]byte(tag), &i); err != nil { 812 return nil, nil, fmt.Errorf("failed to unmarshal default: %v", err) 813 } 814 return i, nil, nil 815 } 816 817 var refRE = regexp.MustCompile(`^ref\((?P<reference>[^"]+)\)$`) 818 var refREIdentIndex = refRE.SubexpIndex("reference") 819 820 // parseSymbolReference looks for strings that match one of the following: 821 // - ref(Ident) 822 // - ref(pkgpath.Ident) 823 // If the input string matches either of these, it will return the (optional) 824 // pkgpath, the Ident, and true. Otherwise it will return empty strings and 825 // false. 826 // 827 // This is borrowed from k8s.io/code-generator. 828 func parseSymbolReference(s, sourcePackage string) (types.Name, bool) { 829 matches := refRE.FindStringSubmatch(s) 830 if len(matches) < refREIdentIndex || matches[refREIdentIndex] == "" { 831 return types.Name{}, false 832 } 833 834 contents := matches[refREIdentIndex] 835 name := types.ParseFullyQualifiedName(contents) 836 if len(name.Package) == 0 { 837 name.Package = sourcePackage 838 } 839 return name, true 840 } 841 842 func implementsCustomUnmarshalling(t *types.Type) bool { 843 switch t.Kind { 844 case types.Pointer: 845 unmarshaller, isUnmarshaller := t.Elem.Methods["UnmarshalJSON"] 846 return isUnmarshaller && unmarshaller.Signature.Receiver.Kind == types.Pointer 847 case types.Struct: 848 _, isUnmarshaller := t.Methods["UnmarshalJSON"] 849 return isUnmarshaller 850 default: 851 return false 852 } 853 } 854 855 func mustEnforceDefault(t *types.Type, omitEmpty bool) (interface{}, error) { 856 // Treat types with custom unmarshalling as a value 857 // (Can be alias, struct, or pointer) 858 if implementsCustomUnmarshalling(t) { 859 // Since Go JSON deserializer always feeds `null` when present 860 // to structs with custom UnmarshalJSON, the zero value for 861 // these structs is also null. 862 // 863 // In general, Kubernetes API types with custom marshalling should 864 // marshal their empty values to `null`. 865 return nil, nil 866 } 867 868 switch t.Kind { 869 case types.Alias: 870 return mustEnforceDefault(t.Underlying, omitEmpty) 871 case types.Pointer, types.Map, types.Slice, types.Array, types.Interface: 872 return nil, nil 873 case types.Struct: 874 if len(t.Members) == 1 && t.Members[0].Embedded { 875 // Treat a struct with a single embedded member the same as an alias 876 return mustEnforceDefault(t.Members[0].Type, omitEmpty) 877 } 878 879 return map[string]interface{}{}, nil 880 case types.Builtin: 881 if !omitEmpty { 882 if zero, ok := openapi.OpenAPIZeroValue(t.String()); ok { 883 return zero, nil 884 } else { 885 return nil, fmt.Errorf("please add type %v to getOpenAPITypeFormat function", t) 886 } 887 } 888 return nil, nil 889 default: 890 return nil, fmt.Errorf("not sure how to enforce default for %v", t.Kind) 891 } 892 } 893 894 func (g openAPITypeWriter) generateDefault(comments []string, t *types.Type, omitEmpty bool, commentOwningType *types.Type) error { 895 def, ref, err := defaultFromComments(comments, commentOwningType.Name.Package, t) 896 if err != nil { 897 return err 898 } 899 if enforced, err := mustEnforceDefault(t, omitEmpty); err != nil { 900 return err 901 } else if enforced != nil { 902 if def == nil { 903 def = enforced 904 } else if !reflect.DeepEqual(def, enforced) { 905 enforcedJson, _ := json.Marshal(enforced) 906 return fmt.Errorf("invalid default value (%#v) for non-pointer/non-omitempty. If specified, must be: %v", def, string(enforcedJson)) 907 } 908 } 909 if def != nil { 910 g.Do("Default: $.$,\n", fmt.Sprintf("%#v", def)) 911 } else if ref != nil { 912 g.Do("Default: $.|raw$,\n", &types.Type{Name: *ref}) 913 } 914 return nil 915 } 916 917 func (g openAPITypeWriter) generateDescription(CommentLines []string) { 918 var buffer bytes.Buffer 919 delPrevChar := func() { 920 if buffer.Len() > 0 { 921 buffer.Truncate(buffer.Len() - 1) // Delete the last " " or "\n" 922 } 923 } 924 925 for _, line := range CommentLines { 926 // Ignore all lines after --- 927 if line == "---" { 928 break 929 } 930 line = strings.TrimRight(line, " ") 931 leading := strings.TrimLeft(line, " ") 932 switch { 933 case len(line) == 0: // Keep paragraphs 934 delPrevChar() 935 buffer.WriteString("\n\n") 936 case strings.HasPrefix(leading, "TODO"): // Ignore one line TODOs 937 case strings.HasPrefix(leading, "+"): // Ignore instructions to go2idl 938 default: 939 if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { 940 delPrevChar() 941 line = "\n" + line + "\n" // Replace it with newline. This is useful when we have a line with: "Example:\n\tJSON-something..." 942 } else { 943 line += " " 944 } 945 buffer.WriteString(line) 946 } 947 } 948 949 postDoc := strings.TrimSpace(buffer.String()) 950 if len(postDoc) > 0 { 951 g.Do("Description: $.$,\n", fmt.Sprintf("%#v", postDoc)) 952 } 953 } 954 955 func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) error { 956 name := getReferableName(m) 957 if name == "" { 958 return nil 959 } 960 validationSchema, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) 961 if err != nil { 962 return err 963 } 964 if err := g.validatePatchTags(m, parent); err != nil { 965 return err 966 } 967 g.Do("\"$.$\": {\n", name) 968 if err := g.generateMemberExtensions(m, parent, validationSchema.Extensions); err != nil { 969 return err 970 } 971 g.Do("SchemaProps: spec.SchemaProps{\n", nil) 972 var extraComments []string 973 if enumType, isEnum := g.enumContext.EnumType(m.Type); isEnum { 974 extraComments = enumType.DescriptionLines() 975 } 976 g.generateDescription(append(m.CommentLines, extraComments...)) 977 jsonTags := getJsonTags(m) 978 if len(jsonTags) > 1 && jsonTags[1] == "string" { 979 g.generateSimpleProperty("string", "") 980 g.Do("},\n},\n", nil) 981 return nil 982 } 983 omitEmpty := strings.Contains(reflect.StructTag(m.Tags).Get("json"), "omitempty") 984 if err := g.generateDefault(m.CommentLines, m.Type, omitEmpty, parent); err != nil { 985 return fmt.Errorf("failed to generate default in %v: %v: %v", parent, m.Name, err) 986 } 987 err = g.generateValueValidations(&validationSchema.SchemaProps) 988 if err != nil { 989 return err 990 } 991 t := resolveAliasAndPtrType(m.Type) 992 // If we can get a openAPI type and format for this type, we consider it to be simple property 993 typeString, format := openapi.OpenAPITypeFormat(t.String()) 994 if typeString != "" { 995 g.generateSimpleProperty(typeString, format) 996 if enumType, isEnum := g.enumContext.EnumType(m.Type); isEnum { 997 // original type is an enum, add "Enum: " and the values 998 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 999 } 1000 g.Do("},\n},\n", nil) 1001 return nil 1002 } 1003 switch t.Kind { 1004 case types.Builtin: 1005 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", t) 1006 case types.Map: 1007 if err := g.generateMapProperty(t); err != nil { 1008 return fmt.Errorf("failed to generate map property in %v: %v: %v", parent, m.Name, err) 1009 } 1010 case types.Slice, types.Array: 1011 if err := g.generateSliceProperty(t); err != nil { 1012 return fmt.Errorf("failed to generate slice property in %v: %v: %v", parent, m.Name, err) 1013 } 1014 case types.Struct, types.Interface: 1015 g.generateReferenceProperty(t) 1016 default: 1017 return fmt.Errorf("cannot generate spec for type %v", t) 1018 } 1019 g.Do("},\n},\n", nil) 1020 return g.Error() 1021 } 1022 1023 func (g openAPITypeWriter) generateSimpleProperty(typeString, format string) { 1024 g.Do("Type: []string{\"$.$\"},\n", typeString) 1025 g.Do("Format: \"$.$\",\n", format) 1026 } 1027 1028 func (g openAPITypeWriter) generateReferenceProperty(t *types.Type) { 1029 g.refTypes[t.Name.String()] = t 1030 g.Do("Ref: ref(\"$.$\"),\n", t.Name.String()) 1031 } 1032 1033 func resolvePtrType(t *types.Type) *types.Type { 1034 var prev *types.Type 1035 for prev != t { 1036 prev = t 1037 if t.Kind == types.Pointer { 1038 t = t.Elem 1039 } 1040 } 1041 return t 1042 } 1043 1044 func resolveAliasAndPtrType(t *types.Type) *types.Type { 1045 var prev *types.Type 1046 for prev != t { 1047 prev = t 1048 if t.Kind == types.Alias { 1049 t = t.Underlying 1050 } 1051 if t.Kind == types.Pointer { 1052 t = t.Elem 1053 } 1054 } 1055 return t 1056 } 1057 1058 func (g openAPITypeWriter) generateMapProperty(t *types.Type) error { 1059 keyType := resolveAliasAndPtrType(t.Key) 1060 elemType := resolveAliasAndPtrType(t.Elem) 1061 1062 // According to OpenAPI examples, only map from string is supported 1063 if keyType.Name.Name != "string" { 1064 return fmt.Errorf("map with non-string keys are not supported by OpenAPI in %v", t) 1065 } 1066 1067 g.Do("Type: []string{\"object\"},\n", nil) 1068 g.Do("AdditionalProperties: &spec.SchemaOrBool{\nAllows: true,\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil) 1069 if err := g.generateDefault(t.Elem.CommentLines, t.Elem, false, t.Elem); err != nil { 1070 return err 1071 } 1072 typeString, format := openapi.OpenAPITypeFormat(elemType.String()) 1073 if typeString != "" { 1074 g.generateSimpleProperty(typeString, format) 1075 if enumType, isEnum := g.enumContext.EnumType(t.Elem); isEnum { 1076 // original type is an enum, add "Enum: " and the values 1077 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 1078 } 1079 g.Do("},\n},\n},\n", nil) 1080 return nil 1081 } 1082 switch elemType.Kind { 1083 case types.Builtin: 1084 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType) 1085 case types.Struct: 1086 g.generateReferenceProperty(elemType) 1087 case types.Slice, types.Array: 1088 if err := g.generateSliceProperty(elemType); err != nil { 1089 return err 1090 } 1091 case types.Map: 1092 if err := g.generateMapProperty(elemType); err != nil { 1093 return err 1094 } 1095 default: 1096 return fmt.Errorf("map Element kind %v is not supported in %v", elemType.Kind, t.Name) 1097 } 1098 g.Do("},\n},\n},\n", nil) 1099 return nil 1100 } 1101 1102 func (g openAPITypeWriter) generateSliceProperty(t *types.Type) error { 1103 elemType := resolveAliasAndPtrType(t.Elem) 1104 g.Do("Type: []string{\"array\"},\n", nil) 1105 g.Do("Items: &spec.SchemaOrArray{\nSchema: &spec.Schema{\nSchemaProps: spec.SchemaProps{\n", nil) 1106 if err := g.generateDefault(t.Elem.CommentLines, t.Elem, false, t.Elem); err != nil { 1107 return err 1108 } 1109 typeString, format := openapi.OpenAPITypeFormat(elemType.String()) 1110 if typeString != "" { 1111 g.generateSimpleProperty(typeString, format) 1112 if enumType, isEnum := g.enumContext.EnumType(t.Elem); isEnum { 1113 // original type is an enum, add "Enum: " and the values 1114 g.Do("Enum: []interface{}{$.$},\n", strings.Join(enumType.ValueStrings(), ", ")) 1115 } 1116 g.Do("},\n},\n},\n", nil) 1117 return nil 1118 } 1119 switch elemType.Kind { 1120 case types.Builtin: 1121 return fmt.Errorf("please add type %v to getOpenAPITypeFormat function", elemType) 1122 case types.Struct: 1123 g.generateReferenceProperty(elemType) 1124 case types.Slice, types.Array: 1125 if err := g.generateSliceProperty(elemType); err != nil { 1126 return err 1127 } 1128 case types.Map: 1129 if err := g.generateMapProperty(elemType); err != nil { 1130 return err 1131 } 1132 default: 1133 return fmt.Errorf("slice Element kind %v is not supported in %v", elemType.Kind, t) 1134 } 1135 g.Do("},\n},\n},\n", nil) 1136 return nil 1137 }