github.com/cdmixer/woolloomooloo@v0.1.0/pkg/codegen/docs/gen.go (about) 1 //go:generate go run bundler.go 2 3 // Copyright 2016-2020, Pulumi Corporation. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the 18 // goconst linter's warning. 19 // 20 // nolint: lll, goconst 21 package docs 22 23 import ( 24 "bytes" 25 "fmt" 26 "html" 27 "html/template" 28 "path" 29 "regexp" 30 "sort" 31 "strings" 32 33 "github.com/golang/glog" 34 "github.com/pkg/errors" 35 36 "github.com/pulumi/pulumi/pkg/v2/codegen" 37 "github.com/pulumi/pulumi/pkg/v2/codegen/dotnet" 38 go_gen "github.com/pulumi/pulumi/pkg/v2/codegen/go" 39 "github.com/pulumi/pulumi/pkg/v2/codegen/nodejs" 40 "github.com/pulumi/pulumi/pkg/v2/codegen/python" 41 "github.com/pulumi/pulumi/pkg/v2/codegen/schema" 42 "github.com/pulumi/pulumi/sdk/v2/go/common/util/contract" 43 ) 44 45 var ( 46 supportedLanguages = []string{"csharp", "go", "nodejs", "python"} 47 snippetLanguages = []string{"csharp", "go", "python", "typescript"} 48 templates *template.Template 49 packagedTemplates map[string][]byte 50 docHelpers map[string]codegen.DocLanguageHelper 51 52 // The following property case maps are for rendering property 53 // names of nested properties in Python language with the correct 54 // casing. 55 snakeCaseToCamelCase map[string]string 56 camelCaseToSnakeCase map[string]string 57 seenCasingTypes codegen.Set 58 59 // The language-specific info objects for a certain package (provider). 60 goPkgInfo go_gen.GoPackageInfo 61 csharpPkgInfo dotnet.CSharpPackageInfo 62 nodePkgInfo nodejs.NodePackageInfo 63 pythonPkgInfo python.PackageInfo 64 65 // langModuleNameLookup is a map of module name to its language-specific 66 // name. 67 langModuleNameLookup map[string]string 68 // titleLookup is a map to map module package name to the desired display name 69 // for display in the TOC menu under API Reference. 70 titleLookup = map[string]string{ 71 "aiven": "Aiven", 72 "akamai": "Akamai", 73 "alicloud": "AliCloud", 74 "auth0": "Auth0", 75 "aws": "AWS", 76 "azure": "Azure", 77 "azure-nextgen": "Azure NextGen", 78 "azuread": "Azure AD", 79 "azuredevops": "Azure DevOps", 80 "azuresel": "Azure", 81 "civo": "Civo", 82 "cloudamqp": "CloudAMQP", 83 "cloudflare": "Cloudflare", 84 "consul": "Consul", 85 "datadog": "Datadog", 86 "digitalocean": "DigitalOcean", 87 "dnsimple": "DNSimple", 88 "docker": "Docker", 89 "f5bigip": "f5 BIG-IP", 90 "fastly": "Fastly", 91 "gcp": "GCP", 92 "github": "GitHub", 93 "gitlab": "GitLab", 94 "hcloud": "Hetzner Cloud", 95 "kafka": "Kafka", 96 "keycloak": "Keycloak", 97 "kong": "Kong", 98 "kubernetes": "Kubernetes", 99 "linode": "Linode", 100 "mailgun": "Mailgun", 101 "mongodbatlas": "MongoDB Atlas", 102 "mysql": "MySQL", 103 "newrelic": "New Relic", 104 "ns1": "NS1", 105 "okta": "Okta", 106 "openstack": "Open Stack", 107 "packet": "Packet", 108 "pagerduty": "PagerDuty", 109 "postgresql": "PostgreSQL", 110 "rabbitmq": "RabbitMQ", 111 "rancher2": "Rancher 2", 112 "random": "Random", 113 "signalfx": "SignalFx", 114 "spotinst": "Spotinst", 115 "tls": "TLS", 116 "vault": "Vault", 117 "venafi": "Venafi", 118 "vsphere": "vSphere", 119 "wavefront": "Wavefront", 120 } 121 // metaDescriptionRegexp attempts to extract the description from Resource.Comment. 122 // Extracts the first line, essentially the "human-friendly" part of the description. 123 metaDescriptionRegexp = regexp.MustCompile(`(?m)^.*$`) 124 // Property anchor tag separator, used in a property anchor tag id to separate the 125 // property and language (e.g. property~lang). 126 propertyLangSeparator = "_" 127 ) 128 129 func init() { 130 docHelpers = make(map[string]codegen.DocLanguageHelper) 131 for _, lang := range supportedLanguages { 132 switch lang { 133 case "csharp": 134 docHelpers[lang] = &dotnet.DocLanguageHelper{} 135 case "go": 136 docHelpers[lang] = &go_gen.DocLanguageHelper{} 137 case "nodejs": 138 docHelpers[lang] = &nodejs.DocLanguageHelper{} 139 case "python": 140 docHelpers[lang] = &python.DocLanguageHelper{} 141 } 142 } 143 144 snakeCaseToCamelCase = map[string]string{} 145 camelCaseToSnakeCase = map[string]string{} 146 seenCasingTypes = codegen.Set{} 147 langModuleNameLookup = map[string]string{} 148 } 149 150 // header represents the header of each resource markdown file. 151 type header struct { 152 Title string 153 TitleTag string 154 MetaDesc string 155 } 156 157 // property represents an input or an output property. 158 type property struct { 159 // ID is the `id` attribute that will be attached to the DOM element containing the property. 160 ID string 161 // DisplayName is the property name with word-breaks. 162 DisplayName string 163 Name string 164 Comment string 165 Types []propertyType 166 DeprecationMessage string 167 Link string 168 169 IsRequired bool 170 IsInput bool 171 } 172 173 // apiTypeDocLinks represents the links for a type's input and output API doc. 174 type apiTypeDocLinks struct { 175 InputType string 176 OutputType string 177 } 178 179 // docNestedType represents a complex type. 180 type docNestedType struct { 181 Name string 182 AnchorID string 183 APIDocLinks map[string]apiTypeDocLinks 184 Properties map[string][]property 185 } 186 187 // propertyType represents the type of a property. 188 type propertyType struct { 189 DisplayName string 190 Name string 191 // Link can be a link to an anchor tag on the same 192 // page, or to another page/site. 193 Link string 194 } 195 196 // formalParam represents the formal parameters of a constructor 197 // or a lookup function. 198 type formalParam struct { 199 Name string 200 Type propertyType 201 202 // This is the language specific optional type indicator. 203 // For example, in nodejs this is the character "?" and in Go 204 // it's "*". 205 OptionalFlag string 206 207 DefaultValue string 208 209 // Comment is an optional description of the parameter. 210 Comment string 211 } 212 213 type packageDetails struct { 214 Repository string 215 License string 216 Notes string 217 Version string 218 } 219 220 type resourceDocArgs struct { 221 Header header 222 223 Tool string 224 // LangChooserLanguages is a comma-separated list of languages to pass to the 225 // language chooser shortcode. Use this to customize the languages shown for a 226 // resource. By default, the language chooser will show all languages supported 227 // by Pulumi for all resources. 228 LangChooserLanguages string 229 230 // Comment represents the introductory resource comment. 231 Comment string 232 ExamplesSection []exampleSection 233 DeprecationMessage string 234 235 // Import 236 ImportDocs string 237 238 // ConstructorParams is a map from language to the rendered HTML for the constructor's 239 // arguments. 240 ConstructorParams map[string]string 241 // ConstructorParamsTyped is the typed set of parameters for the constructor, in order. 242 ConstructorParamsTyped map[string][]formalParam 243 // ConstructorResource is the resource that is being constructed or 244 // is the result of a constructor-like function. 245 ConstructorResource map[string]propertyType 246 // ArgsRequired is a flag indicating if the args param is required 247 // when creating a new resource. 248 ArgsRequired bool 249 250 // InputProperties is a map per language and a corresponding slice of 251 // input properties accepted as args while creating a new resource. 252 InputProperties map[string][]property 253 // OutputProperties is a map per language and a corresponding slice of 254 // output properties returned when a new instance of the resource is 255 // created. 256 OutputProperties map[string][]property 257 258 // LookupParams is a map of the param string to be rendered per language 259 // for looking-up a resource. 260 LookupParams map[string]string 261 // StateInputs is a map per language and the corresponding slice of 262 // state input properties required while looking-up an existing resource. 263 StateInputs map[string][]property 264 // StateParam is the type name of the state param, if any. 265 StateParam string 266 267 // NestedTypes is a slice of the nested types used in the input and 268 // output properties. 269 NestedTypes []docNestedType 270 271 PackageDetails packageDetails 272 } 273 274 // typeUsage represents a nested type's usage. 275 type typeUsage struct { 276 Input bool 277 Output bool 278 } 279 280 // nestedTypeUsageInfo is a type-alias for a map of Pulumi type-tokens 281 // and whether or not the type is used as an input and/or output 282 // properties. 283 type nestedTypeUsageInfo map[string]typeUsage 284 285 func (ss nestedTypeUsageInfo) add(s string, input bool) { 286 if v, ok := ss[s]; ok { 287 if input { 288 v.Input = true 289 } else { 290 v.Output = true 291 } 292 ss[s] = v 293 return 294 } 295 296 ss[s] = typeUsage{ 297 Input: input, 298 Output: !input, 299 } 300 } 301 302 // contains returns true if the token already exists and matches the 303 // input or output flag of the token. 304 func (ss nestedTypeUsageInfo) contains(token string, input bool) bool { 305 a, ok := ss[token] 306 if !ok { 307 return false 308 } 309 310 if input && a.Input { 311 return true 312 } else if !input && a.Output { 313 return true 314 } 315 return false 316 } 317 318 type modContext struct { 319 pkg *schema.Package 320 mod string 321 resources []*schema.Resource 322 functions []*schema.Function 323 children []*modContext 324 tool string 325 emitAPILinks bool 326 } 327 328 func resourceName(r *schema.Resource) string { 329 if r.IsProvider { 330 return "Provider" 331 } 332 return strings.Title(tokenToName(r.Token)) 333 } 334 335 func getLanguageDocHelper(lang string) codegen.DocLanguageHelper { 336 if h, ok := docHelpers[lang]; ok { 337 return h 338 } 339 panic(errors.Errorf("could not find a doc lang helper for %s", lang)) 340 } 341 342 type propertyCharacteristics struct { 343 // input is a flag indicating if the property is an input type. 344 input bool 345 // optional is a flag indicating if the property is optional. 346 optional bool 347 } 348 349 // getLanguageModuleName transforms the current module's name to a 350 // language-specific name using the language info, if any, for the 351 // current package. 352 func (mod *modContext) getLanguageModuleName(lang string) string { 353 modName := mod.mod 354 lookupKey := lang + "_" + modName 355 if v, ok := langModuleNameLookup[lookupKey]; ok { 356 return v 357 } 358 359 switch lang { 360 case "go": 361 // Go module names use lowercase. 362 modName = strings.ToLower(modName) 363 if override, ok := goPkgInfo.ModuleToPackage[modName]; ok { 364 modName = override 365 } 366 case "csharp": 367 if override, ok := csharpPkgInfo.Namespaces[modName]; ok { 368 modName = override 369 } 370 case "nodejs": 371 if override, ok := nodePkgInfo.ModuleToPackage[modName]; ok { 372 modName = override 373 } 374 case "python": 375 if override, ok := pythonPkgInfo.ModuleNameOverrides[modName]; ok { 376 modName = override 377 } 378 } 379 380 langModuleNameLookup[lookupKey] = modName 381 return modName 382 } 383 384 // cleanTypeString removes any namespaces from the generated type string for all languages. 385 // The result of this function should be used display purposes only. 386 func (mod *modContext) cleanTypeString(t schema.Type, langTypeString, lang, modName string, isInput bool) string { 387 switch lang { 388 case "go", "python": 389 langTypeString = cleanOptionalIdentifier(langTypeString, lang) 390 parts := strings.Split(langTypeString, ".") 391 return parts[len(parts)-1] 392 } 393 394 cleanCSharpName := func(pkgName, objModName string) string { 395 // C# types can be wrapped in enumerable types such as List<> or Dictionary<>, so we have to 396 // only replace the namespace between the < and the > characters. 397 qualifier := "Inputs" 398 if !isInput { 399 qualifier = "Outputs" 400 } 401 402 var csharpNS string 403 // This type could be at the package-level, so it won't have a module name. 404 if objModName != "" { 405 csharpNS = fmt.Sprintf("Pulumi.%s.%s.%s.", title(pkgName, lang), title(objModName, lang), qualifier) 406 } else { 407 csharpNS = fmt.Sprintf("Pulumi.%s.%s.", title(pkgName, lang), qualifier) 408 } 409 return strings.ReplaceAll(langTypeString, csharpNS, "") 410 } 411 412 cleanNodeJSName := func(objModName string) string { 413 // The nodejs codegen currently doesn't use the ModuleToPackage override available 414 // in the k8s package's schema. So we'll manually strip some known module names for k8s. 415 // TODO[pulumi/pulumi#4325]: Remove this block once the nodejs code gen is able to use the 416 // package name overrides for modules. 417 if isKubernetesPackage(mod.pkg) { 418 langTypeString = strings.ReplaceAll(langTypeString, "k8s.io.", "") 419 langTypeString = strings.ReplaceAll(langTypeString, "apiserver.", "") 420 langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1.", "") 421 langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1alpha1.", "") 422 langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1beta1.", "") 423 } 424 objModName = strings.ReplaceAll(objModName, "/", ".") + "." 425 return strings.ReplaceAll(langTypeString, objModName, "") 426 } 427 428 switch t := t.(type) { 429 case *schema.ArrayType: 430 if schema.IsPrimitiveType(t.ElementType) { 431 break 432 } 433 return mod.cleanTypeString(t.ElementType, langTypeString, lang, modName, isInput) 434 case *schema.UnionType: 435 for _, e := range t.ElementTypes { 436 if schema.IsPrimitiveType(e) { 437 continue 438 } 439 return mod.cleanTypeString(e, langTypeString, lang, modName, isInput) 440 } 441 case *schema.ObjectType: 442 objTypeModName := mod.pkg.TokenToModule(t.Token) 443 if objTypeModName != mod.mod { 444 modName = mod.getLanguageModuleName(lang) 445 } 446 } 447 448 if lang == "nodejs" { 449 return cleanNodeJSName(modName) 450 } else if lang == "csharp" { 451 return cleanCSharpName(mod.pkg.Name, modName) 452 } 453 return strings.ReplaceAll(langTypeString, modName, "") 454 } 455 456 // typeString returns a property type suitable for docs with its display name and the anchor link to 457 // a type if the type of the property is an array or an object. 458 func (mod *modContext) typeString(t schema.Type, lang string, characteristics propertyCharacteristics, insertWordBreaks bool) propertyType { 459 docLanguageHelper := getLanguageDocHelper(lang) 460 modName := mod.getLanguageModuleName(lang) 461 langTypeString := docLanguageHelper.GetLanguageTypeString(mod.pkg, modName, t, characteristics.input, characteristics.optional) 462 463 // If the type is an object type, let's also wrap it with a link to the supporting type 464 // on the same page using an anchor tag. 465 var href string 466 switch t := t.(type) { 467 case *schema.ArrayType: 468 elementLangType := mod.typeString(t.ElementType, lang, characteristics, false) 469 href = elementLangType.Link 470 case *schema.ObjectType: 471 tokenName := tokenToName(t.Token) 472 // Links to anchor tags on the same page must be lower-cased. 473 href = "#" + strings.ToLower(tokenName) 474 default: 475 // Check if type is primitive/built-in type if no match for cases listed above. 476 if schema.IsPrimitiveType(t) { 477 href = docLanguageHelper.GetDocLinkForBuiltInType(t.String()) 478 } 479 } 480 481 // Strip the namespace/module prefix for the type's display name. 482 displayName := langTypeString 483 if !schema.IsPrimitiveType(t) { 484 displayName = mod.cleanTypeString(t, langTypeString, lang, modName, characteristics.input) 485 } 486 487 // If word-breaks need to be inserted, then the type string 488 // should be html-encoded first if the language is C# in order 489 // to avoid confusing the Hugo rendering where the word-break 490 // tags are inserted. 491 if insertWordBreaks { 492 if lang == "csharp" { 493 displayName = html.EscapeString(displayName) 494 } 495 displayName = wbr(displayName) 496 } 497 498 displayName = cleanOptionalIdentifier(displayName, lang) 499 langTypeString = cleanOptionalIdentifier(langTypeString, lang) 500 501 return propertyType{ 502 Name: langTypeString, 503 DisplayName: displayName, 504 Link: href, 505 } 506 } 507 508 // cleanOptionalIdentifier removes the type identifier (i.e. "?" in "string?"). 509 func cleanOptionalIdentifier(s, lang string) string { 510 switch lang { 511 case "nodejs": 512 return strings.TrimSuffix(s, "?") 513 case "go": 514 return strings.TrimPrefix(s, "*") 515 case "csharp": 516 return strings.TrimSuffix(s, "?") 517 case "python": 518 if strings.HasPrefix(s, "Optional[") && strings.HasSuffix(s, "]") { 519 s = strings.TrimPrefix(s, "Optional[") 520 s = strings.TrimSuffix(s, "]") 521 return s 522 } 523 } 524 return s 525 } 526 527 // Resources typically take the same set of parameters to their constructors, and these 528 // are the default comments/descriptions for them. 529 const ( 530 ctorNameArgComment = "The unique name of the resource." 531 ctorArgsArgComment = "The arguments to resource properties." 532 ctorOptsArgComment = "Bag of options to control resource's behavior." 533 ) 534 535 func (mod *modContext) genConstructorTS(r *schema.Resource, argsOptional bool) []formalParam { 536 name := resourceName(r) 537 docLangHelper := getLanguageDocHelper("nodejs") 538 // Use the NodeJS module to package lookup to transform the module name to its normalized package name. 539 modName := mod.getLanguageModuleName("nodejs") 540 541 var argsType string 542 var argsDocLink string 543 optsType := "CustomResourceOptions" 544 // The args type for k8s package differs from the rest depending on whether we are dealing with 545 // overlay resources or regular k8s resources. 546 if isKubernetesPackage(mod.pkg) { 547 if mod.isKubernetesOverlayModule() { 548 if name == "CustomResource" { 549 argsType = name + "Args" 550 } else { 551 argsType = name + "Opts" 552 } 553 argsDocLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, argsType) 554 } else { 555 // The non-schema-based k8s codegen does not apply a suffix to the input types. 556 argsType = name 557 // The args types themselves are all under the input types module path, so use the input type link for the args type. 558 argsDocLink = docLangHelper.GetDocLinkForResourceInputOrOutputType(mod.pkg, modName, argsType, true) 559 } 560 561 if mod.isComponentResource() { 562 optsType = "ComponentResourceOptions" 563 } 564 } else { 565 argsType = name + "Args" 566 // All args types are in the same module path as the resource class itself even though it is an "input" type. 567 if mod.emitAPILinks { 568 argsDocLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, argsType) 569 } 570 } 571 572 argsFlag := "" 573 if argsOptional { 574 argsFlag = "?" 575 } 576 577 return []formalParam{ 578 { 579 Name: "name", 580 Type: propertyType{ 581 Name: "string", 582 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 583 }, 584 Comment: ctorNameArgComment, 585 }, 586 { 587 Name: "args", 588 OptionalFlag: argsFlag, 589 Type: propertyType{ 590 Name: argsType, 591 Link: argsDocLink, 592 }, 593 Comment: ctorArgsArgComment, 594 }, 595 { 596 Name: "opts", 597 OptionalFlag: "?", 598 Type: propertyType{ 599 Name: optsType, 600 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, optsType), 601 }, 602 Comment: ctorOptsArgComment, 603 }, 604 } 605 } 606 607 func (mod *modContext) genConstructorGo(r *schema.Resource, argsOptional bool) []formalParam { 608 name := resourceName(r) 609 argsType := name + "Args" 610 argsFlag := "" 611 if argsOptional { 612 argsFlag = "*" 613 } 614 615 docLangHelper := getLanguageDocHelper("go") 616 // Use the Go module to package lookup to transform the module name to its normalized package name. 617 modName := mod.getLanguageModuleName("go") 618 619 var argsTypeLink string 620 if mod.emitAPILinks { 621 argsTypeLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, argsType) 622 } 623 624 return []formalParam{ 625 { 626 Name: "ctx", 627 OptionalFlag: "*", 628 Type: propertyType{ 629 Name: "Context", 630 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "Context"), 631 }, 632 Comment: "Context object for the current deployment.", 633 }, 634 { 635 Name: "name", 636 Type: propertyType{ 637 Name: "string", 638 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 639 }, 640 Comment: ctorNameArgComment, 641 }, 642 { 643 Name: "args", 644 OptionalFlag: argsFlag, 645 Type: propertyType{ 646 Name: argsType, 647 Link: argsTypeLink, 648 }, 649 Comment: ctorArgsArgComment, 650 }, 651 { 652 Name: "opts", 653 OptionalFlag: "...", 654 Type: propertyType{ 655 Name: "ResourceOption", 656 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "ResourceOption"), 657 }, 658 Comment: ctorOptsArgComment, 659 }, 660 } 661 } 662 663 func (mod *modContext) genConstructorCS(r *schema.Resource, argsOptional bool) []formalParam { 664 name := resourceName(r) 665 argsSchemaType := &schema.ObjectType{ 666 Token: r.Token, 667 Package: mod.pkg, 668 } 669 // Get the C#-specific name for the args type, which will be the fully-qualified name. 670 characteristics := propertyCharacteristics{ 671 input: true, 672 optional: argsOptional, 673 } 674 675 var argLangTypeName string 676 optsType := "CustomResourceOptions" 677 678 // Constructor argument types in the k8s package for C# use a different namespace path. 679 // K8s overlay resources are in the same namespace path as the resource itself. 680 if isKubernetesPackage(mod.pkg) { 681 if mod.mod != "" { 682 correctModName := mod.getLanguageModuleName("csharp") 683 if !mod.isKubernetesOverlayModule() { 684 // For k8s, the args type for a resource is part of the `Types.Inputs` namespace. 685 argLangTypeName = "Pulumi.Kubernetes.Types.Inputs." + correctModName + "." + name + "Args" 686 } else { 687 // Helm's resource args type does not use the version number. 688 if strings.HasPrefix(mod.mod, "helm") { 689 correctModName = "Helm" 690 } 691 argLangTypeName = "Pulumi.Kubernetes." + correctModName + "." + name + "Args" 692 } 693 } else { 694 argLangTypeName = "Pulumi.Kubernetes." + name + "Args" 695 } 696 697 if mod.isComponentResource() { 698 optsType = "ComponentResourceOptions" 699 } 700 } else { 701 argLangType := mod.typeString(argsSchemaType, "csharp", characteristics, false) 702 argLangTypeName = strings.ReplaceAll(argLangType.Name, "Inputs.", "") 703 } 704 705 var argsFlag string 706 var argsDefault string 707 if argsOptional { 708 // If the number of required input properties was zero, we can make the args object optional. 709 argsDefault = " = null" 710 argsFlag = "?" 711 } 712 713 docLangHelper := getLanguageDocHelper("csharp") 714 715 var argsTypeLink string 716 if mod.emitAPILinks { 717 argsTypeLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, "", argLangTypeName) 718 } 719 720 return []formalParam{ 721 { 722 Name: "name", 723 Type: propertyType{ 724 Name: "string", 725 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 726 }, 727 Comment: ctorNameArgComment, 728 }, 729 { 730 Name: "args", 731 OptionalFlag: argsFlag, 732 DefaultValue: argsDefault, 733 Type: propertyType{ 734 Name: name + "Args", 735 Link: argsTypeLink, 736 }, 737 Comment: ctorArgsArgComment, 738 }, 739 { 740 Name: "opts", 741 OptionalFlag: "?", 742 DefaultValue: " = null", 743 Type: propertyType{ 744 Name: optsType, 745 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, fmt.Sprintf("Pulumi.%s", optsType)), 746 }, 747 Comment: ctorOptsArgComment, 748 }, 749 } 750 } 751 752 func (mod *modContext) genConstructorPython(r *schema.Resource, argsOptional bool) []formalParam { 753 docLanguageHelper := getLanguageDocHelper("python") 754 isK8sOverlayMod := mod.isKubernetesOverlayModule() 755 isDockerImageResource := mod.pkg.Name == "docker" && resourceName(r) == "Image" 756 757 // Kubernetes overlay resources use a different ordering of formal params in Python. 758 if isK8sOverlayMod { 759 return getKubernetesOverlayPythonFormalParams(mod.mod) 760 } else if isDockerImageResource { 761 return getDockerImagePythonFormalParams() 762 } 763 764 params := make([]formalParam, 0, len(r.InputProperties)+1) 765 // All other resources accept the resource options as a second parameter. 766 params = append(params, formalParam{ 767 Name: "opts", 768 DefaultValue: " = None", 769 Type: propertyType{ 770 Name: "Optional[ResourceOptions]", 771 Link: "/docs/reference/pkg/python/pulumi/#pulumi.ResourceOptions", 772 }, 773 }) 774 for _, p := range r.InputProperties { 775 // If the property defines a const value, then skip it. 776 // For example, in k8s, `apiVersion` and `kind` are often hard-coded 777 // in the SDK and are not really user-provided input properties. 778 if p.ConstValue != nil { 779 continue 780 } 781 typ := docLanguageHelper.GetLanguageTypeString(mod.pkg, mod.mod, p.Type, true /*input*/, false /*optional*/) 782 params = append(params, formalParam{ 783 Name: python.InitParamName(p.Name), 784 DefaultValue: " = None", 785 Type: propertyType{ 786 Name: fmt.Sprintf("Optional[%s]", typ), 787 }, 788 }) 789 } 790 return params 791 } 792 793 func (mod *modContext) genNestedTypes(member interface{}, resourceType bool) []docNestedType { 794 tokens := nestedTypeUsageInfo{} 795 // Collect all of the types for this "member" as a map of resource names 796 // and if it appears in an input object and/or output object. 797 mod.getTypes(member, tokens) 798 799 isK8s := isKubernetesPackage(mod.pkg) 800 801 var objs []docNestedType 802 for token, tyUsage := range tokens { 803 for _, t := range mod.pkg.Types { 804 obj, ok := t.(*schema.ObjectType) 805 if !ok || obj.Token != token { 806 continue 807 } 808 if len(obj.Properties) == 0 { 809 continue 810 } 811 812 // Create maps to hold the per-language properties of this object and links to 813 // the API doc for each language. 814 props := make(map[string][]property) 815 apiDocLinks := make(map[string]apiTypeDocLinks) 816 for _, lang := range supportedLanguages { 817 // The nested type may be under a different package in a language. 818 // For example, in k8s, common types are in the core/v1 module and can appear in 819 // nested types elsewhere. So we use the appropriate name of that type, 820 // as well as its language-specific name. For example, module name for use as a C# namespace 821 // or as a Go package name. 822 modName := mod.getLanguageModuleName(lang) 823 nestedTypeModName := mod.pkg.TokenToModule(token) 824 if nestedTypeModName != mod.mod { 825 modName = mod.getLanguageModuleName(lang) 826 } 827 828 docLangHelper := getLanguageDocHelper(lang) 829 inputCharacteristics := propertyCharacteristics{ 830 input: true, 831 optional: true, 832 } 833 outputCharacteristics := propertyCharacteristics{ 834 input: false, 835 optional: true, 836 } 837 inputObjLangType := mod.typeString(t, lang, inputCharacteristics, false /*insertWordBreaks*/) 838 outputObjLangType := mod.typeString(t, lang, outputCharacteristics, false /*insertWordBreaks*/) 839 840 // Get the doc link for this nested type based on whether the type is for a Function or a Resource. 841 var inputTypeDocLink string 842 var outputTypeDocLink string 843 if resourceType { 844 if tyUsage.Input { 845 inputTypeDocLink = docLangHelper.GetDocLinkForResourceInputOrOutputType(mod.pkg, modName, inputObjLangType.Name, true) 846 } 847 if tyUsage.Output { 848 outputTypeDocLink = docLangHelper.GetDocLinkForResourceInputOrOutputType(mod.pkg, modName, outputObjLangType.Name, false) 849 } 850 } else { 851 if tyUsage.Input { 852 inputTypeDocLink = docLangHelper.GetDocLinkForFunctionInputOrOutputType(mod.pkg, modName, inputObjLangType.Name, true) 853 } 854 if tyUsage.Output { 855 outputTypeDocLink = docLangHelper.GetDocLinkForFunctionInputOrOutputType(mod.pkg, modName, outputObjLangType.Name, false) 856 } 857 } 858 859 props[lang] = mod.getProperties(obj.Properties, lang, true, true) 860 // Don't add C# type links for Kubernetes because there are differences in the namespaces between the schema code gen and 861 // the current code gen that the package uses. So the links will be incorrect. 862 if isK8s && lang == "csharp" { 863 continue 864 } 865 apiDocLinks[lang] = apiTypeDocLinks{ 866 InputType: inputTypeDocLink, 867 OutputType: outputTypeDocLink, 868 } 869 } 870 871 name := strings.Title(tokenToName(obj.Token)) 872 objs = append(objs, docNestedType{ 873 Name: wbr(name), 874 AnchorID: strings.ToLower(name), 875 APIDocLinks: apiDocLinks, 876 Properties: props, 877 }) 878 } 879 } 880 881 sort.Slice(objs, func(i, j int) bool { 882 return objs[i].Name < objs[j].Name 883 }) 884 885 return objs 886 } 887 888 // getProperties returns a slice of properties that can be rendered for docs for 889 // the provided slice of properties in the schema. 890 func (mod *modContext) getProperties(properties []*schema.Property, lang string, input, nested bool) []property { 891 if len(properties) == 0 { 892 return nil 893 } 894 docProperties := make([]property, 0, len(properties)) 895 for _, prop := range properties { 896 if prop == nil { 897 continue 898 } 899 900 // If the property has a const value, then don't show it as an input property. 901 // Even though it is a valid property, it is used by the language code gen to 902 // generate the appropriate defaults for it. These cannot be overridden by users. 903 if prop.ConstValue != nil { 904 continue 905 } 906 907 characteristics := propertyCharacteristics{ 908 input: input, 909 optional: !prop.IsRequired, 910 } 911 912 langDocHelper := getLanguageDocHelper(lang) 913 name, err := langDocHelper.GetPropertyName(prop) 914 if err != nil { 915 panic(err) 916 } 917 propLangName := name 918 919 propID := strings.ToLower(propLangName + propertyLangSeparator + lang) 920 921 propTypes := make([]propertyType, 0) 922 if typ, isUnion := prop.Type.(*schema.UnionType); isUnion { 923 for _, elementType := range typ.ElementTypes { 924 propTypes = append(propTypes, mod.typeString(elementType, lang, characteristics, true)) 925 } 926 } else { 927 propTypes = append(propTypes, mod.typeString(prop.Type, lang, characteristics, true)) 928 } 929 930 docProperties = append(docProperties, property{ 931 ID: propID, 932 DisplayName: wbr(propLangName), 933 Name: propLangName, 934 Comment: prop.Comment, 935 DeprecationMessage: prop.DeprecationMessage, 936 IsRequired: prop.IsRequired, 937 IsInput: input, 938 Link: "#" + propID, 939 Types: propTypes, 940 }) 941 } 942 943 // Sort required props to move them to the top of the properties list, then by name. 944 sort.SliceStable(docProperties, func(i, j int) bool { 945 pi, pj := docProperties[i], docProperties[j] 946 switch { 947 case pi.IsRequired != pj.IsRequired: 948 return pi.IsRequired && !pj.IsRequired 949 default: 950 return pi.Name < pj.Name 951 } 952 }) 953 954 return docProperties 955 } 956 957 func getDockerImagePythonFormalParams() []formalParam { 958 return []formalParam{ 959 { 960 Name: "image_name", 961 }, 962 { 963 Name: "build", 964 }, 965 { 966 Name: "local_image_name", 967 DefaultValue: "=None", 968 }, 969 { 970 Name: "registry", 971 DefaultValue: "=None", 972 }, 973 { 974 Name: "skip_push", 975 DefaultValue: "=None", 976 }, 977 { 978 Name: "opts", 979 DefaultValue: "=None", 980 }, 981 } 982 } 983 984 // Returns the rendered HTML for the resource's constructor, as well as the specific arguments. 985 func (mod *modContext) genConstructors(r *schema.Resource, allOptionalInputs bool) (map[string]string, map[string][]formalParam) { 986 renderedParams := make(map[string]string) 987 formalParams := make(map[string][]formalParam) 988 989 for _, lang := range supportedLanguages { 990 var ( 991 paramTemplate string 992 params []formalParam 993 ) 994 b := &bytes.Buffer{} 995 996 switch lang { 997 case "nodejs": 998 params = mod.genConstructorTS(r, allOptionalInputs) 999 paramTemplate = "ts_formal_param" 1000 case "go": 1001 params = mod.genConstructorGo(r, allOptionalInputs) 1002 paramTemplate = "go_formal_param" 1003 case "csharp": 1004 params = mod.genConstructorCS(r, allOptionalInputs) 1005 paramTemplate = "csharp_formal_param" 1006 case "python": 1007 params = mod.genConstructorPython(r, allOptionalInputs) 1008 paramTemplate = "py_formal_param" 1009 } 1010 1011 for i, p := range params { 1012 if i != 0 { 1013 if err := templates.ExecuteTemplate(b, "param_separator", nil); err != nil { 1014 panic(err) 1015 } 1016 } 1017 if err := templates.ExecuteTemplate(b, paramTemplate, p); err != nil { 1018 panic(err) 1019 } 1020 } 1021 renderedParams[lang] = b.String() 1022 formalParams[lang] = params 1023 } 1024 1025 return renderedParams, formalParams 1026 } 1027 1028 // getConstructorResourceInfo returns a map of per-language information about 1029 // the resource being constructed. 1030 func (mod *modContext) getConstructorResourceInfo(resourceTypeName string) map[string]propertyType { 1031 resourceMap := make(map[string]propertyType) 1032 resourceDisplayName := resourceTypeName 1033 1034 for _, lang := range supportedLanguages { 1035 // Use the module to package lookup to transform the module name to its normalized package name. 1036 modName := mod.getLanguageModuleName(lang) 1037 // Reset the type name back to the display name. 1038 resourceTypeName = resourceDisplayName 1039 1040 docLangHelper := getLanguageDocHelper(lang) 1041 switch lang { 1042 case "nodejs", "go", "python": 1043 // Intentionally left blank. 1044 case "csharp": 1045 namespace := title(mod.pkg.Name, lang) 1046 if ns, ok := csharpPkgInfo.Namespaces[mod.pkg.Name]; ok { 1047 namespace = ns 1048 } 1049 if mod.mod == "" { 1050 resourceTypeName = fmt.Sprintf("Pulumi.%s.%s", namespace, resourceTypeName) 1051 break 1052 } 1053 1054 resourceTypeName = fmt.Sprintf("Pulumi.%s.%s.%s", namespace, modName, resourceTypeName) 1055 default: 1056 panic(errors.Errorf("cannot generate constructor info for unhandled language %q", lang)) 1057 } 1058 1059 parts := strings.Split(resourceTypeName, ".") 1060 displayName := parts[len(parts)-1] 1061 1062 var link string 1063 if mod.emitAPILinks { 1064 link = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, resourceTypeName) 1065 } 1066 1067 resourceMap[lang] = propertyType{ 1068 Name: resourceDisplayName, 1069 DisplayName: displayName, 1070 Link: link, 1071 } 1072 } 1073 1074 return resourceMap 1075 } 1076 1077 func (mod *modContext) getTSLookupParams(r *schema.Resource, stateParam string) []formalParam { 1078 docLangHelper := getLanguageDocHelper("nodejs") 1079 // Use the NodeJS module to package lookup to transform the module name to its normalized package name. 1080 modName := mod.getLanguageModuleName("nodejs") 1081 1082 var stateLink string 1083 if mod.emitAPILinks { 1084 stateLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, stateParam) 1085 } 1086 1087 return []formalParam{ 1088 { 1089 Name: "name", 1090 1091 Type: propertyType{ 1092 Name: "string", 1093 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 1094 }, 1095 }, 1096 { 1097 Name: "id", 1098 Type: propertyType{ 1099 Name: "Input<ID>", 1100 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "ID"), 1101 }, 1102 }, 1103 { 1104 Name: "state", 1105 OptionalFlag: "?", 1106 Type: propertyType{ 1107 Name: stateParam, 1108 Link: stateLink, 1109 }, 1110 }, 1111 { 1112 Name: "opts", 1113 OptionalFlag: "?", 1114 Type: propertyType{ 1115 Name: "CustomResourceOptions", 1116 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "CustomResourceOptions"), 1117 }, 1118 }, 1119 } 1120 } 1121 1122 func (mod *modContext) getGoLookupParams(r *schema.Resource, stateParam string) []formalParam { 1123 docLangHelper := getLanguageDocHelper("go") 1124 // Use the Go module to package lookup to transform the module name to its normalized package name. 1125 modName := mod.getLanguageModuleName("go") 1126 1127 var stateLink string 1128 if mod.emitAPILinks { 1129 stateLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, modName, stateParam) 1130 } 1131 1132 return []formalParam{ 1133 { 1134 Name: "ctx", 1135 OptionalFlag: "*", 1136 Type: propertyType{ 1137 Name: "Context", 1138 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "Context"), 1139 }, 1140 }, 1141 { 1142 Name: "name", 1143 Type: propertyType{ 1144 Name: "string", 1145 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 1146 }, 1147 }, 1148 { 1149 Name: "id", 1150 Type: propertyType{ 1151 Name: "IDInput", 1152 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "IDInput"), 1153 }, 1154 }, 1155 { 1156 Name: "state", 1157 OptionalFlag: "*", 1158 Type: propertyType{ 1159 Name: stateParam, 1160 Link: stateLink, 1161 }, 1162 }, 1163 { 1164 Name: "opts", 1165 OptionalFlag: "...", 1166 Type: propertyType{ 1167 Name: "ResourceOption", 1168 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "ResourceOption"), 1169 }, 1170 }, 1171 } 1172 } 1173 1174 func (mod *modContext) getCSLookupParams(r *schema.Resource, stateParam string) []formalParam { 1175 modName := mod.getLanguageModuleName("csharp") 1176 namespace := title(mod.pkg.Name, "csharp") 1177 if ns, ok := csharpPkgInfo.Namespaces[mod.pkg.Name]; ok { 1178 namespace = ns 1179 } 1180 stateParamFQDN := fmt.Sprintf("Pulumi.%s.%s.%s", namespace, modName, stateParam) 1181 1182 docLangHelper := getLanguageDocHelper("csharp") 1183 1184 var stateLink string 1185 if mod.emitAPILinks { 1186 stateLink = docLangHelper.GetDocLinkForResourceType(mod.pkg, "", stateParamFQDN) 1187 } 1188 1189 return []formalParam{ 1190 { 1191 Name: "name", 1192 Type: propertyType{ 1193 Name: "string", 1194 Link: docLangHelper.GetDocLinkForBuiltInType("string"), 1195 }, 1196 }, 1197 { 1198 Name: "id", 1199 Type: propertyType{ 1200 Name: "Input<string>", 1201 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "Pulumi.Input"), 1202 }, 1203 }, 1204 { 1205 Name: "state", 1206 OptionalFlag: "?", 1207 Type: propertyType{ 1208 Name: stateParam, 1209 Link: stateLink, 1210 }, 1211 }, 1212 { 1213 Name: "opts", 1214 OptionalFlag: "?", 1215 DefaultValue: " = null", 1216 Type: propertyType{ 1217 Name: "CustomResourceOptions", 1218 Link: docLangHelper.GetDocLinkForPulumiType(mod.pkg, "Pulumi.CustomResourceOptions"), 1219 }, 1220 }, 1221 } 1222 } 1223 1224 func (mod *modContext) getPythonLookupParams(r *schema.Resource, stateParam string) []formalParam { 1225 // The input properties for a resource needs to be exploded as 1226 // individual constructor params. 1227 docLanguageHelper := getLanguageDocHelper("python") 1228 params := make([]formalParam, 0, len(r.StateInputs.Properties)) 1229 for _, p := range r.StateInputs.Properties { 1230 typ := docLanguageHelper.GetLanguageTypeString(mod.pkg, mod.mod, p.Type, true /*input*/, false /*optional*/) 1231 params = append(params, formalParam{ 1232 Name: python.PyName(p.Name), 1233 DefaultValue: " = None", 1234 Type: propertyType{ 1235 Name: fmt.Sprintf("Optional[%s]", typ), 1236 }, 1237 }) 1238 } 1239 return params 1240 } 1241 1242 // genLookupParams generates a map of per-language way of rendering the formal parameters of the lookup function 1243 // used to lookup an existing resource. 1244 func (mod *modContext) genLookupParams(r *schema.Resource, stateParam string) map[string]string { 1245 lookupParams := make(map[string]string) 1246 if r.StateInputs == nil { 1247 return lookupParams 1248 } 1249 1250 for _, lang := range supportedLanguages { 1251 var ( 1252 paramTemplate string 1253 params []formalParam 1254 ) 1255 b := &bytes.Buffer{} 1256 1257 switch lang { 1258 case "nodejs": 1259 params = mod.getTSLookupParams(r, stateParam) 1260 paramTemplate = "ts_formal_param" 1261 case "go": 1262 params = mod.getGoLookupParams(r, stateParam) 1263 paramTemplate = "go_formal_param" 1264 case "csharp": 1265 params = mod.getCSLookupParams(r, stateParam) 1266 paramTemplate = "csharp_formal_param" 1267 case "python": 1268 params = mod.getPythonLookupParams(r, stateParam) 1269 paramTemplate = "py_formal_param" 1270 } 1271 1272 n := len(params) 1273 for i, p := range params { 1274 if err := templates.ExecuteTemplate(b, paramTemplate, p); err != nil { 1275 panic(err) 1276 } 1277 if i != n-1 { 1278 if err := templates.ExecuteTemplate(b, "param_separator", nil); err != nil { 1279 panic(err) 1280 } 1281 } 1282 } 1283 lookupParams[lang] = b.String() 1284 } 1285 return lookupParams 1286 } 1287 1288 // filterOutputProperties removes the input properties from the output properties list 1289 // (since input props are implicitly output props), returning only "output" props. 1290 func filterOutputProperties(inputProps []*schema.Property, props []*schema.Property) []*schema.Property { 1291 var outputProps []*schema.Property 1292 inputMap := make(map[string]bool, len(inputProps)) 1293 for _, p := range inputProps { 1294 inputMap[p.Name] = true 1295 } 1296 for _, p := range props { 1297 if _, found := inputMap[p.Name]; !found { 1298 outputProps = append(outputProps, p) 1299 } 1300 } 1301 return outputProps 1302 } 1303 1304 func (mod *modContext) genResourceHeader(r *schema.Resource) header { 1305 packageName := formatTitleText(mod.pkg.Name) 1306 resourceName := resourceName(r) 1307 var baseDescription string 1308 var titleTag string 1309 if mod.mod == "" { 1310 baseDescription = fmt.Sprintf("Explore the %s resource of the %s package, "+ 1311 "including examples, input properties, output properties, "+ 1312 "lookup functions, and supporting types.", resourceName, packageName) 1313 titleTag = fmt.Sprintf("Resource %s | Package %s", resourceName, packageName) 1314 } else { 1315 baseDescription = fmt.Sprintf("Explore the %s resource of the %s module, "+ 1316 "including examples, input properties, output properties, "+ 1317 "lookup functions, and supporting types.", resourceName, mod.mod) 1318 titleTag = fmt.Sprintf("%s.%s.%s", mod.pkg.Name, mod.mod, resourceName) 1319 } 1320 1321 return header{ 1322 Title: resourceName, 1323 TitleTag: titleTag, 1324 MetaDesc: baseDescription + " " + metaDescriptionRegexp.FindString(r.Comment), 1325 } 1326 } 1327 1328 // genResource is the entrypoint for generating a doc for a resource 1329 // from its Pulumi schema. 1330 func (mod *modContext) genResource(r *schema.Resource) resourceDocArgs { 1331 // Create a resource module file into which all of this resource's types will go. 1332 name := resourceName(r) 1333 1334 inputProps := make(map[string][]property) 1335 outputProps := make(map[string][]property) 1336 stateInputs := make(map[string][]property) 1337 1338 var filteredOutputProps []*schema.Property 1339 // Provider resources do not have output properties, so there won't be anything to filter. 1340 if !r.IsProvider { 1341 filteredOutputProps = filterOutputProperties(r.InputProperties, r.Properties) 1342 } 1343 1344 // All resources have an implicit `id` output property, that we must inject into the docs. 1345 filteredOutputProps = append(filteredOutputProps, &schema.Property{ 1346 Name: "id", 1347 Comment: "The provider-assigned unique ID for this managed resource.", 1348 Type: schema.StringType, 1349 IsRequired: true, 1350 }) 1351 1352 for _, lang := range supportedLanguages { 1353 inputProps[lang] = mod.getProperties(r.InputProperties, lang, true, false) 1354 outputProps[lang] = mod.getProperties(filteredOutputProps, lang, false, false) 1355 if r.IsProvider { 1356 continue 1357 } 1358 if r.StateInputs != nil { 1359 stateProps := mod.getProperties(r.StateInputs.Properties, lang, true, false) 1360 for i := 0; i < len(stateProps); i++ { 1361 id := "state_" + stateProps[i].ID 1362 stateProps[i].ID = id 1363 stateProps[i].Link = "#" + id 1364 } 1365 stateInputs[lang] = stateProps 1366 } 1367 } 1368 1369 allOptionalInputs := true 1370 for _, prop := range r.InputProperties { 1371 // If at least one prop is required, then break. 1372 if prop.IsRequired { 1373 allOptionalInputs = false 1374 break 1375 } 1376 } 1377 1378 packageDetails := packageDetails{ 1379 Repository: mod.pkg.Repository, 1380 License: mod.pkg.License, 1381 Notes: mod.pkg.Attribution, 1382 } 1383 1384 renderedCtorParams, typedCtorParams := mod.genConstructors(r, allOptionalInputs) 1385 1386 stateParam := name + "State" 1387 1388 docInfo := decomposeDocstring(r.Comment) 1389 data := resourceDocArgs{ 1390 Header: mod.genResourceHeader(r), 1391 1392 Tool: mod.tool, 1393 1394 Comment: docInfo.description, 1395 DeprecationMessage: r.DeprecationMessage, 1396 ExamplesSection: docInfo.examples, 1397 ImportDocs: docInfo.importDetails, 1398 1399 ConstructorParams: renderedCtorParams, 1400 ConstructorParamsTyped: typedCtorParams, 1401 1402 ConstructorResource: mod.getConstructorResourceInfo(name), 1403 ArgsRequired: !allOptionalInputs, 1404 1405 InputProperties: inputProps, 1406 OutputProperties: outputProps, 1407 LookupParams: mod.genLookupParams(r, stateParam), 1408 StateInputs: stateInputs, 1409 StateParam: stateParam, 1410 NestedTypes: mod.genNestedTypes(r, true /*resourceType*/), 1411 1412 PackageDetails: packageDetails, 1413 } 1414 1415 return data 1416 } 1417 1418 func (mod *modContext) getNestedTypes(t schema.Type, types nestedTypeUsageInfo, input bool) { 1419 switch t := t.(type) { 1420 case *schema.ArrayType: 1421 glog.V(4).Infof("visiting array %s\n", t.ElementType.String()) 1422 skip := false 1423 if o, ok := t.ElementType.(*schema.ObjectType); ok && types.contains(o.Token, input) { 1424 glog.V(4).Infof("already added %s. skipping...\n", o.Token) 1425 skip = true 1426 } 1427 1428 if !skip { 1429 mod.getNestedTypes(t.ElementType, types, input) 1430 } 1431 case *schema.MapType: 1432 glog.V(4).Infof("visiting map %s\n", t.ElementType.String()) 1433 skip := false 1434 if o, ok := t.ElementType.(*schema.ObjectType); ok && types.contains(o.Token, input) { 1435 glog.V(4).Infof("already added %s. skipping...\n", o.Token) 1436 skip = true 1437 } 1438 1439 if !skip { 1440 mod.getNestedTypes(t.ElementType, types, input) 1441 } 1442 case *schema.ObjectType: 1443 glog.V(4).Infof("visiting object %s\n", t.Token) 1444 types.add(t.Token, input) 1445 for _, p := range t.Properties { 1446 if o, ok := p.Type.(*schema.ObjectType); ok && types.contains(o.Token, input) { 1447 glog.V(4).Infof("already added %s. skipping...\n", o.Token) 1448 continue 1449 } 1450 glog.V(4).Infof("visiting object property %s\n", p.Type.String()) 1451 mod.getNestedTypes(p.Type, types, input) 1452 } 1453 case *schema.UnionType: 1454 glog.V(4).Infof("visiting union type %s\n", t.String()) 1455 for _, e := range t.ElementTypes { 1456 if o, ok := e.(*schema.ObjectType); ok && types.contains(o.Token, input) { 1457 glog.V(4).Infof("already added %s. skipping...\n", o.Token) 1458 continue 1459 } 1460 glog.V(4).Infof("visiting union element type %s\n", e.String()) 1461 mod.getNestedTypes(e, types, input) 1462 } 1463 } 1464 } 1465 1466 func (mod *modContext) getTypes(member interface{}, types nestedTypeUsageInfo) { 1467 glog.V(3).Infoln("getting nested types for module", mod.mod) 1468 1469 switch t := member.(type) { 1470 case *schema.ObjectType: 1471 for _, p := range t.Properties { 1472 mod.getNestedTypes(p.Type, types, false) 1473 } 1474 case *schema.Resource: 1475 for _, p := range t.Properties { 1476 mod.getNestedTypes(p.Type, types, false) 1477 } 1478 for _, p := range t.InputProperties { 1479 mod.getNestedTypes(p.Type, types, true) 1480 } 1481 case *schema.Function: 1482 if t.Inputs != nil { 1483 mod.getNestedTypes(t.Inputs, types, true) 1484 } 1485 if t.Outputs != nil { 1486 mod.getNestedTypes(t.Outputs, types, false) 1487 } 1488 } 1489 } 1490 1491 type fs map[string][]byte 1492 1493 func (fs fs) add(path string, contents []byte) { 1494 _, has := fs[path] 1495 contract.Assertf(!has, "duplicate file: %s", path) 1496 fs[path] = contents 1497 } 1498 1499 // getModuleFileName returns the file name to use for a module. 1500 func (mod *modContext) getModuleFileName() string { 1501 if !isKubernetesPackage(mod.pkg) { 1502 return mod.mod 1503 } 1504 1505 // For k8s packages, use the Go-language info to get the file name 1506 // for the module. 1507 if override, ok := goPkgInfo.ModuleToPackage[mod.mod]; ok { 1508 return override 1509 } 1510 return mod.mod 1511 } 1512 1513 func (mod *modContext) gen(fs fs) error { 1514 modName := mod.getModuleFileName() 1515 var files []string 1516 for p := range fs { 1517 d := path.Dir(p) 1518 if d == "." { 1519 d = "" 1520 } 1521 if d == modName { 1522 files = append(files, p) 1523 } 1524 } 1525 1526 addFile := func(name, contents string) { 1527 p := path.Join(modName, name) 1528 files = append(files, p) 1529 fs.add(p, []byte(contents)) 1530 } 1531 1532 // Resources 1533 for _, r := range mod.resources { 1534 data := mod.genResource(r) 1535 1536 title := resourceName(r) 1537 buffer := &bytes.Buffer{} 1538 1539 err := templates.ExecuteTemplate(buffer, "resource.tmpl", data) 1540 if err != nil { 1541 return err 1542 } 1543 1544 addFile(strings.ToLower(title)+".md", buffer.String()) 1545 } 1546 1547 // Functions 1548 for _, f := range mod.functions { 1549 data := mod.genFunction(f) 1550 1551 buffer := &bytes.Buffer{} 1552 err := templates.ExecuteTemplate(buffer, "function.tmpl", data) 1553 if err != nil { 1554 return err 1555 } 1556 1557 addFile(strings.ToLower(tokenToName(f.Token))+".md", buffer.String()) 1558 } 1559 1560 // Generate the index files. 1561 idxData := mod.genIndex() 1562 buffer := &bytes.Buffer{} 1563 err := templates.ExecuteTemplate(buffer, "index.tmpl", idxData) 1564 if err != nil { 1565 return err 1566 } 1567 1568 fs.add(path.Join(modName, "_index.md"), []byte(buffer.String())) 1569 return nil 1570 } 1571 1572 // indexEntry represents an individual entry on an index page. 1573 type indexEntry struct { 1574 Link string 1575 DisplayName string 1576 } 1577 1578 // indexData represents the index file data to be rendered as _index.md. 1579 type indexData struct { 1580 Tool string 1581 1582 Title string 1583 TitleTag string 1584 PackageDescription string 1585 // Menu indicates if an index page should be part of the TOC menu. 1586 Menu bool 1587 1588 LanguageLinks map[string]string 1589 Functions []indexEntry 1590 Resources []indexEntry 1591 Modules []indexEntry 1592 PackageDetails packageDetails 1593 } 1594 1595 // indexEntrySorter implements the sort.Interface for sorting 1596 // a slice of indexEntry struct types. 1597 type indexEntrySorter struct { 1598 entries []indexEntry 1599 } 1600 1601 // Len is part of sort.Interface. Returns the length of the 1602 // entries slice. 1603 func (s *indexEntrySorter) Len() int { 1604 return len(s.entries) 1605 } 1606 1607 // Swap is part of sort.Interface. 1608 func (s *indexEntrySorter) Swap(i, j int) { 1609 s.entries[i], s.entries[j] = s.entries[j], s.entries[i] 1610 } 1611 1612 // Less is part of sort.Interface. It sorts the entries by their 1613 // display name in an ascending order. 1614 func (s *indexEntrySorter) Less(i, j int) bool { 1615 return s.entries[i].DisplayName < s.entries[j].DisplayName 1616 } 1617 1618 func sortIndexEntries(entries []indexEntry) { 1619 if len(entries) == 0 { 1620 return 1621 } 1622 1623 sorter := &indexEntrySorter{ 1624 entries: entries, 1625 } 1626 1627 sort.Sort(sorter) 1628 } 1629 1630 // getLanguageLinks returns a map of links for the current module's language-specific 1631 // docs by language. 1632 func (mod *modContext) getLanguageLinks() map[string]string { 1633 languageLinks := map[string]string{} 1634 1635 if !mod.emitAPILinks { 1636 return languageLinks 1637 } 1638 1639 isK8s := isKubernetesPackage(mod.pkg) 1640 1641 for _, lang := range supportedLanguages { 1642 var link string 1643 var title string 1644 var langTitle string 1645 modName := mod.getLanguageModuleName(lang) 1646 1647 docLangHelper := getLanguageDocHelper(lang) 1648 switch lang { 1649 case "csharp": 1650 langTitle = ".NET" 1651 if override, ok := csharpPkgInfo.Namespaces[modName]; ok { 1652 modName = override 1653 } else if !ok && isK8s { 1654 // For k8s if we don't find a C# namespace override, then don't 1655 // include a link to the module since it would lead to a 404. 1656 continue 1657 } 1658 case "go": 1659 langTitle = "Go" 1660 case "nodejs": 1661 langTitle = "Node.js" 1662 case "python": 1663 langTitle = "Python" 1664 default: 1665 panic(errors.Errorf("Unknown language %s", lang)) 1666 } 1667 1668 title, link = docLangHelper.GetModuleDocLink(mod.pkg, modName) 1669 languageLinks[langTitle] = fmt.Sprintf(`<a href="%s" title="%[2]s">%[2]s</a>`, link, title) 1670 } 1671 return languageLinks 1672 } 1673 1674 // genIndex emits an _index.md file for the module. 1675 func (mod *modContext) genIndex() indexData { 1676 glog.V(4).Infoln("genIndex for", mod.mod) 1677 modules := make([]indexEntry, 0, len(mod.children)) 1678 resources := make([]indexEntry, 0, len(mod.resources)) 1679 functions := make([]indexEntry, 0, len(mod.functions)) 1680 1681 modName := mod.getModuleFileName() 1682 title := modName 1683 menu := false 1684 if title == "" { 1685 title = formatTitleText(mod.pkg.Name) 1686 // Flag top-level entries for inclusion in the table-of-contents menu. 1687 menu = true 1688 } 1689 1690 // If there are submodules, list them. 1691 for _, mod := range mod.children { 1692 modName := mod.getModuleFileName() 1693 parts := strings.Split(modName, "/") 1694 modName = parts[len(parts)-1] 1695 modules = append(modules, indexEntry{ 1696 Link: strings.ToLower(modName) + "/", 1697 DisplayName: modName, 1698 }) 1699 } 1700 sortIndexEntries(modules) 1701 1702 // If there are resources in the root, list them. 1703 for _, r := range mod.resources { 1704 name := resourceName(r) 1705 resources = append(resources, indexEntry{ 1706 Link: strings.ToLower(name), 1707 DisplayName: name, 1708 }) 1709 } 1710 sortIndexEntries(resources) 1711 1712 // If there are functions in the root, list them. 1713 for _, f := range mod.functions { 1714 name := tokenToName(f.Token) 1715 functions = append(functions, indexEntry{ 1716 Link: strings.ToLower(name), 1717 DisplayName: strings.Title(name), 1718 }) 1719 } 1720 sortIndexEntries(functions) 1721 1722 packageDetails := packageDetails{ 1723 Repository: mod.pkg.Repository, 1724 License: mod.pkg.License, 1725 Notes: mod.pkg.Attribution, 1726 Version: mod.pkg.Version.String(), 1727 } 1728 1729 var titleTag string 1730 var packageDescription string 1731 // The same index.tmpl template is used for both top level package and module pages, if modules not present, 1732 // assume top level package index page when formatting title tags otherwise, if contains modules, assume modules 1733 // top level page when generating title tags. 1734 if len(modules) > 0 { 1735 titleTag = fmt.Sprintf("Package %s", formatTitleText(title)) 1736 } else { 1737 pkgName := formatTitleText(mod.pkg.Name) 1738 titleTag = fmt.Sprintf("Module %s | Package %s", title, pkgName) 1739 packageDescription = fmt.Sprintf("Explore the resources and functions of the %s module in the %s package.", title, pkgName) 1740 } 1741 1742 data := indexData{ 1743 Tool: mod.tool, 1744 PackageDescription: packageDescription, 1745 Title: title, 1746 TitleTag: titleTag, 1747 Menu: menu, 1748 Resources: resources, 1749 Functions: functions, 1750 Modules: modules, 1751 PackageDetails: packageDetails, 1752 LanguageLinks: mod.getLanguageLinks(), 1753 } 1754 1755 // If this is the root module, write out the package description. 1756 if mod.mod == "" { 1757 data.PackageDescription = mod.pkg.Description 1758 } 1759 1760 return data 1761 } 1762 1763 func formatTitleText(title string) string { 1764 // If title not found in titleLookup map, default back to title given. 1765 if val, ok := titleLookup[title]; ok { 1766 return val 1767 } 1768 return title 1769 } 1770 1771 func getMod(pkg *schema.Package, token string, modules map[string]*modContext, tool string, 1772 emitAPILinks bool) *modContext { 1773 modName := pkg.TokenToModule(token) 1774 mod, ok := modules[modName] 1775 if !ok { 1776 mod = &modContext{ 1777 pkg: pkg, 1778 mod: modName, 1779 tool: tool, 1780 emitAPILinks: emitAPILinks, 1781 } 1782 1783 if modName != "" { 1784 parentName := path.Dir(modName) 1785 // If the parent name is blank, it means this is the package-level. 1786 if parentName == "." || parentName == "" { 1787 parentName = ":index:" 1788 } else { 1789 parentName = ":" + parentName + ":" 1790 } 1791 parent := getMod(pkg, parentName, modules, tool, emitAPILinks) 1792 parent.children = append(parent.children, mod) 1793 } 1794 1795 modules[modName] = mod 1796 } 1797 return mod 1798 } 1799 1800 func generatePythonPropertyCaseMaps(mod *modContext, r *schema.Resource, seenTypes codegen.Set) { 1801 pyLangHelper := getLanguageDocHelper("python").(*python.DocLanguageHelper) 1802 for _, p := range r.Properties { 1803 pyLangHelper.GenPropertyCaseMap(mod.pkg, mod.mod, mod.tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase, seenTypes) 1804 } 1805 1806 for _, p := range r.InputProperties { 1807 pyLangHelper.GenPropertyCaseMap(mod.pkg, mod.mod, mod.tool, p, snakeCaseToCamelCase, camelCaseToSnakeCase, seenTypes) 1808 } 1809 } 1810 1811 func generateModulesFromSchemaPackage(tool string, pkg *schema.Package, emitAPILinks bool) map[string]*modContext { 1812 // Group resources, types, and functions into modules. 1813 modules := map[string]*modContext{} 1814 1815 // Decode language-specific info. 1816 if err := pkg.ImportLanguages(map[string]schema.Language{ 1817 "go": go_gen.Importer, 1818 "python": python.Importer, 1819 "csharp": dotnet.Importer, 1820 "nodejs": nodejs.Importer, 1821 }); err != nil { 1822 panic(err) 1823 } 1824 goPkgInfo, _ = pkg.Language["go"].(go_gen.GoPackageInfo) 1825 csharpPkgInfo, _ = pkg.Language["csharp"].(dotnet.CSharpPackageInfo) 1826 nodePkgInfo, _ = pkg.Language["nodejs"].(nodejs.NodePackageInfo) 1827 pythonPkgInfo, _ = pkg.Language["python"].(python.PackageInfo) 1828 1829 goLangHelper := getLanguageDocHelper("go").(*go_gen.DocLanguageHelper) 1830 // Generate the Go package map info now, so we can use that to get the type string 1831 // names later. 1832 goLangHelper.GeneratePackagesMap(pkg, tool, goPkgInfo) 1833 1834 csharpLangHelper := getLanguageDocHelper("csharp").(*dotnet.DocLanguageHelper) 1835 csharpLangHelper.Namespaces = csharpPkgInfo.Namespaces 1836 1837 scanResource := func(r *schema.Resource) { 1838 mod := getMod(pkg, r.Token, modules, tool, emitAPILinks) 1839 mod.resources = append(mod.resources, r) 1840 1841 generatePythonPropertyCaseMaps(mod, r, seenCasingTypes) 1842 } 1843 1844 scanK8SResource := func(r *schema.Resource) { 1845 mod := getKubernetesMod(pkg, r.Token, modules, tool) 1846 mod.resources = append(mod.resources, r) 1847 } 1848 1849 glog.V(3).Infoln("scanning resources") 1850 if isKubernetesPackage(pkg) { 1851 scanK8SResource(pkg.Provider) 1852 for _, r := range pkg.Resources { 1853 scanK8SResource(r) 1854 } 1855 } else { 1856 scanResource(pkg.Provider) 1857 for _, r := range pkg.Resources { 1858 scanResource(r) 1859 } 1860 } 1861 glog.V(3).Infoln("done scanning resources") 1862 1863 for _, f := range pkg.Functions { 1864 mod := getMod(pkg, f.Token, modules, tool, emitAPILinks) 1865 mod.functions = append(mod.functions, f) 1866 } 1867 return modules 1868 } 1869 1870 // GeneratePackage generates the docs package with docs for each resource given the Pulumi 1871 // schema. 1872 func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error) { 1873 emitAPILinks := pkg.Name != "azure-nextgen" && pkg.Name != "eks" 1874 1875 templates = template.New("").Funcs(template.FuncMap{ 1876 "htmlSafe": func(html string) template.HTML { 1877 // Markdown fragments in the templates need to be rendered as-is, 1878 // so that html/template package doesn't try to inject data into it, 1879 // which will most certainly fail. 1880 // nolint gosec 1881 return template.HTML(html) 1882 }, 1883 "hasDocLinksForLang": func(m map[string]apiTypeDocLinks, lang string) bool { 1884 if !emitAPILinks { 1885 return false 1886 } 1887 1888 _, ok := m[lang] 1889 return ok 1890 }, 1891 }) 1892 1893 for name, b := range packagedTemplates { 1894 template.Must(templates.New(name).Parse(string(b))) 1895 } 1896 1897 defer glog.Flush() 1898 1899 // Generate the modules from the schema, and for every module 1900 // run the generator functions to generate markdown files. 1901 modules := generateModulesFromSchemaPackage(tool, pkg, emitAPILinks) 1902 glog.V(3).Infoln("generating package now...") 1903 files := fs{} 1904 for _, mod := range modules { 1905 if err := mod.gen(files); err != nil { 1906 return nil, err 1907 } 1908 } 1909 1910 return files, nil 1911 }