//go:generate go run bundler.go // Copyright 2016-2020, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the // goconst linter's warning. // //nolint:lll, goconst package docs import ( "bytes" "embed" "errors" "fmt" "html" "html/template" "path" "sort" "strings" "github.com/golang/glog" "github.com/pgavlin/goldmark" "github.com/pulumi/pulumi-java/pkg/codegen/java" yaml "github.com/pulumi/pulumi-yaml/pkg/pulumiyaml/codegen" "github.com/pulumi/pulumi/pkg/v3/codegen" "github.com/pulumi/pulumi/pkg/v3/codegen/dotnet" go_gen "github.com/pulumi/pulumi/pkg/v3/codegen/go" "github.com/pulumi/pulumi/pkg/v3/codegen/nodejs" "github.com/pulumi/pulumi/pkg/v3/codegen/python" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/sdk/v3/go/common/slice" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) //go:embed templates/*.tmpl var packagedTemplates embed.FS // NOTE: This lookup map can be removed when all Pulumi-managed packages // have a DisplayName in their schema. See pulumi/pulumi#7813. // This lookup table no longer needs to be updated for new providers // and is considered stale. // // titleLookup is a map of package name to the desired display name. func titleLookup(shortName string) (string, bool) { v, ok := map[string]string{ "aiven": "Aiven", "akamai": "Akamai", "alicloud": "Alibaba Cloud", "auth0": "Auth0", "aws": "AWS Classic", "awsx": "AWSx (Pulumi Crosswalk for AWS)", "aws-apigateway": "AWS API Gateway", "aws-miniflux": "Miniflux", "aws-native": "AWS Native", "aws-quickstart-aurora-mysql": "AWS QuickStart Aurora MySQL", "aws-quickstart-aurora-postgres": "AWS QuickStart Aurora PostgreSQL", "aws-quickstart-redshift": "AWS QuickStart Redshift", "aws-serverless": "AWS Serverless", "aws-quickstart-vpc": "AWS QuickStart VPC", "aws-s3-replicated-bucket": "AWS S3 Replicated Bucket", "azure": "Azure Classic", "azure-justrun": "Azure Justrun", "azure-native": "Azure Native", "azure-quickstart-acr-geo-replication": "Azure QuickStart ACR Geo Replication", "azure-quickstart-aks": "Azure QuickStart AKS", "azure-quickstart-compute": "Azure QuickStart Compute", "azure-quickstart-sql": "Azure QuickStart SQL", "azuread": "Azure Active Directory (Azure AD)", "azuredevops": "Azure DevOps", "azuresel": "Azure", "civo": "Civo", "cloudamqp": "CloudAMQP", "cloudflare": "Cloudflare", "cloudinit": "cloud-init", "confluentcloud": "Confluent Cloud", "confluent": "Confluent Cloud (Deprecated)", "consul": "HashiCorp Consul", "coredns-helm": "CoreDNS (Helm)", "datadog": "Datadog", "digitalocean": "DigitalOcean", "dnsimple": "DNSimple", "docker": "Docker", "docker-buildkit": "Docker BuildKit", "eks": "Amazon EKS", "equinix-metal": "Equinix Metal", "f5bigip": "f5 BIG-IP", "fastly": "Fastly", "gcp": "Google Cloud (GCP) Classic", "gcp-global-cloudrun": "Google Global Cloud Run", "gcp-project-scaffold": "Google Project Scaffolding", "google-native": "Google Cloud Native", "github": "GitHub", "github-serverless-webhook": "GitHub Serverless Webhook", "gitlab": "GitLab", "hcloud": "Hetzner Cloud", "istio-helm": "Istio (Helm)", "jaeger-helm": "Jaeger (Helm)", "kafka": "Kafka", "keycloak": "Keycloak", "kong": "Kong", "kubernetes": "Kubernetes", "libvirt": "libvirt", "linode": "Linode", "mailgun": "Mailgun", "minio": "MinIO", "mongodbatlas": "MongoDB Atlas", "mysql": "MySQL", "newrelic": "New Relic", "kubernetes-ingress-nginx": "NGINX Ingress Controller (Helm)", "kubernetes-coredns": "CoreDNS (Helm)", "kubernetes-cert-manager": "Jetstack Cert Manager (Helm)", "nomad": "HashiCorp Nomad", "ns1": "NS1", "okta": "Okta", "openstack": "OpenStack", "opsgenie": "Opsgenie", "packet": "Packet", "pagerduty": "PagerDuty", "pulumi-std": "Pulumi Standard Library", "postgresql": "PostgreSQL", "prometheus-helm": "Prometheus (Helm)", "rabbitmq": "RabbitMQ", "rancher2": "Rancher2", "random": "random", "rke": "Rancher Kubernetes Engine (RKE)", "run-my-darn-container": "Run My Darn Container", "shipa": "Shipa", "signalfx": "SignalFx", "snowflake": "Snowflake", "splunk": "Splunk", "spotinst": "Spotinst", "sumologic": "Sumo Logic", "tls": "TLS", "vault": "Vault", "venafi": "Venafi", "vsphere": "vSphere", "wavefront": "Wavefront", "yandex": "Yandex", }[shortName] return v, ok } // Property anchor tag separator, used in a property anchor tag id to separate the // property and language (e.g. property~lang). const propertyLangSeparator = "_" type docGenContext struct { internalModMap map[string]*modContext supportedLanguages []string snippetLanguages []string templates *template.Template docHelpers map[string]codegen.DocLanguageHelper // The language-specific info objects for a certain package (provider). goPkgInfo go_gen.GoPackageInfo csharpPkgInfo dotnet.CSharpPackageInfo nodePkgInfo nodejs.NodePackageInfo pythonPkgInfo python.PackageInfo // langModuleNameLookup is a map of module name to its language-specific // name. langModuleNameLookup map[string]string // Maps a *modContext, *schema.Resource, or *schema.Function to the link that was assigned to it. moduleConflictLinkMap map[interface{}]string } // modules is a map of a module name and information // about it. This is crux of all API docs generation // as the modContext carries information about the resources, // functions, as well other modules within each module. func (dctx *docGenContext) modules() map[string]*modContext { return dctx.internalModMap } func (dctx *docGenContext) setModules(modules map[string]*modContext) { m := map[string]*modContext{} for k, v := range modules { m[k] = v.withDocGenContext(dctx) } dctx.internalModMap = m } func newDocGenContext() *docGenContext { supportedLanguages := []string{"csharp", "go", "nodejs", "python", "yaml", "java"} docHelpers := make(map[string]codegen.DocLanguageHelper) for _, lang := range supportedLanguages { switch lang { case "csharp": docHelpers[lang] = &dotnet.DocLanguageHelper{} case "go": docHelpers[lang] = &go_gen.DocLanguageHelper{} case "nodejs": docHelpers[lang] = &nodejs.DocLanguageHelper{} case "python": docHelpers[lang] = &python.DocLanguageHelper{} case "yaml": docHelpers[lang] = &yaml.DocLanguageHelper{} case "java": docHelpers[lang] = &java.DocLanguageHelper{} } } return &docGenContext{ supportedLanguages: supportedLanguages, snippetLanguages: []string{"csharp", "go", "python", "typescript", "yaml", "java"}, langModuleNameLookup: map[string]string{}, docHelpers: docHelpers, moduleConflictLinkMap: map[interface{}]string{}, } } type typeDetails struct { inputType bool } // header represents the header of each resource markdown file. type header struct { Title string TitleTag string MetaDesc string } // property represents an input or an output property. type property struct { // ID is the `id` attribute that will be attached to the DOM element containing the property. ID string // DisplayName is the property name with word-breaks. DisplayName string Name string Comment string Types []propertyType DeprecationMessage string Link string IsRequired bool IsInput bool IsReplaceOnChanges bool } // enum represents an enum. type enum struct { ID string // ID is the `id` attribute attached to the DOM element containing the enum. DisplayName string // DisplayName is the enum name with word-breaks. Name string // Name is the language-specific enum name. Value string Comment string DeprecationMessage string } // docNestedType represents a complex type. type docNestedType struct { Name string Input bool AnchorID string Properties map[string][]property EnumValues map[string][]enum } // propertyType represents the type of a property. type propertyType struct { DisplayName string DescriptionName string // Name used in description list. Name string // Link can be a link to an anchor tag on the same // page, or to another page/site. Link string } // paramSeparator is for data passed to the separator template. type paramSeparator struct { Indent string } // formalParam represents the formal parameters of a constructor // or a lookup function. type formalParam struct { Name string Type propertyType // This is the language specific optional type indicator. // For example, in nodejs this is the character "?" and in Go // it's "*". OptionalFlag string DefaultValue string // Comment is an optional description of the parameter. Comment string } type packageDetails struct { DisplayName string Repository string RepositoryName string License string Notes string Version string } type resourceDocArgs struct { Header header Tool string // LangChooserLanguages is a comma-separated list of languages to pass to the // language chooser shortcode. Use this to customize the languages shown for a // resource. By default, the language chooser will show all languages supported // by Pulumi for all resources. LangChooserLanguages string // Comment represents the introductory resource comment. Comment string ExamplesSection []exampleSection DeprecationMessage string // Import ImportDocs string // ConstructorParams is a map from language to the rendered HTML for the constructor's // arguments. ConstructorParams map[string]string // ConstructorParamsTyped is the typed set of parameters for the constructor, in order. ConstructorParamsTyped map[string][]formalParam // ConstructorResource is the resource that is being constructed or // is the result of a constructor-like function. ConstructorResource map[string]propertyType // ArgsRequired is a flag indicating if the args param is required // when creating a new resource. ArgsRequired bool // InputProperties is a map per language and a corresponding slice of // input properties accepted as args while creating a new resource. InputProperties map[string][]property // OutputProperties is a map per language and a corresponding slice of // output properties returned when a new instance of the resource is // created. OutputProperties map[string][]property // LookupParams is a map of the param string to be rendered per language // for looking-up a resource. LookupParams map[string]string // StateInputs is a map per language and the corresponding slice of // state input properties required while looking-up an existing resource. StateInputs map[string][]property // StateParam is the type name of the state param, if any. StateParam string // NestedTypes is a slice of the nested types used in the input and // output properties. NestedTypes []docNestedType // A list of methods associated with the resource. Methods []methodDocArgs PackageDetails packageDetails } // typeUsage represents a nested type's usage. type typeUsage struct { Input bool Output bool } // nestedTypeUsageInfo is a type-alias for a map of Pulumi type-tokens // and whether or not the type is used as an input and/or output // properties. type nestedTypeUsageInfo map[string]typeUsage func (ss nestedTypeUsageInfo) add(s string, input bool) { if v, ok := ss[s]; ok { if input { v.Input = true } else { v.Output = true } ss[s] = v return } ss[s] = typeUsage{ Input: input, Output: !input, } } // contains returns true if the token already exists and matches the // input or output flag of the token. func (ss nestedTypeUsageInfo) contains(token string, input bool) bool { a, ok := ss[token] if !ok { return false } if input && a.Input { return true } else if !input && a.Output { return true } return false } type modContext struct { pkg schema.PackageReference mod string inputTypes []*schema.ObjectType resources []*schema.Resource functions []*schema.Function typeDetails map[*schema.ObjectType]*typeDetails children []*modContext tool string docGenContext *docGenContext } func (mod *modContext) withDocGenContext(dctx *docGenContext) *modContext { if mod == nil { return nil } newctx := *mod newctx.docGenContext = dctx children := slice.Prealloc[*modContext](len(newctx.children)) for _, c := range newctx.children { children = append(children, c.withDocGenContext(dctx)) } newctx.children = children return &newctx } func resourceName(r *schema.Resource) string { if r.IsProvider { return "Provider" } return strings.Title(tokenToName(r.Token)) } func (dctx *docGenContext) getLanguageDocHelper(lang string) codegen.DocLanguageHelper { if h, ok := dctx.docHelpers[lang]; ok { return h } panic(fmt.Errorf("could not find a doc lang helper for %s", lang)) } type propertyCharacteristics struct { // input is a flag indicating if the property is an input type. input bool } func (mod *modContext) details(t *schema.ObjectType) *typeDetails { details, ok := mod.typeDetails[t] if !ok { details = &typeDetails{} if mod.typeDetails == nil { mod.typeDetails = map[*schema.ObjectType]*typeDetails{} } mod.typeDetails[t] = details } return details } // getLanguageModuleName transforms the current module's name to a // language-specific name using the language info, if any, for the // current package. func (mod *modContext) getLanguageModuleName(lang string) string { dctx := mod.docGenContext modName := mod.mod lookupKey := lang + "_" + modName if v, ok := mod.docGenContext.langModuleNameLookup[lookupKey]; ok { return v } switch lang { case "go": // Go module names use lowercase. modName = strings.ToLower(modName) if override, ok := dctx.goPkgInfo.ModuleToPackage[modName]; ok { modName = override } case "csharp": if override, ok := dctx.csharpPkgInfo.Namespaces[modName]; ok { modName = override } case "nodejs": if override, ok := dctx.nodePkgInfo.ModuleToPackage[modName]; ok { modName = override } case "python": if override, ok := dctx.pythonPkgInfo.ModuleNameOverrides[modName]; ok { modName = override } } mod.docGenContext.langModuleNameLookup[lookupKey] = modName return modName } // cleanTypeString removes any namespaces from the generated type string for all languages. // The result of this function should be used display purposes only. func (mod *modContext) cleanTypeString(t schema.Type, langTypeString, lang, modName string, isInput bool) string { switch lang { case "go", "python": langTypeString = cleanOptionalIdentifier(langTypeString, lang) parts := strings.Split(langTypeString, ".") return parts[len(parts)-1] } cleanCSharpName := func(pkgName, objModName string) string { // C# types can be wrapped in enumerable types such as List<> or Dictionary<>, so we have to // only replace the namespace between the < and the > characters. qualifier := "Inputs" if !isInput { qualifier = "Outputs" } var csharpNS string // This type could be at the package-level, so it won't have a module name. if objModName != "" { csharpNS = fmt.Sprintf("Pulumi.%s.%s.%s.", title(pkgName, lang), title(objModName, lang), qualifier) } else { csharpNS = fmt.Sprintf("Pulumi.%s.%s.", title(pkgName, lang), qualifier) } return strings.ReplaceAll(langTypeString, csharpNS, "") } cleanNodeJSName := func(objModName string) string { // The nodejs codegen currently doesn't use the ModuleToPackage override available // in the k8s package's schema. So we'll manually strip some known module names for k8s. // TODO[pulumi/pulumi#4325]: Remove this block once the nodejs code gen is able to use the // package name overrides for modules. if isKubernetesPackage(mod.pkg) { langTypeString = strings.ReplaceAll(langTypeString, "k8s.io.", "") langTypeString = strings.ReplaceAll(langTypeString, "apiserver.", "") langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1.", "") langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1alpha1.", "") langTypeString = strings.ReplaceAll(langTypeString, "rbac.authorization.v1beta1.", "") } objModName = strings.ReplaceAll(objModName, "/", ".") + "." return strings.ReplaceAll(langTypeString, objModName, "") } switch t := t.(type) { case *schema.ObjectType: // Strip "Args" suffixes from display names for everything but Python inputs. if lang != "python" || (lang == "python" && !isInput) { name := tokenToName(t.Token) nameWithArgs := name + "Args" // If the langTypeString looks like it's a concatenation of its name and "Args", strip out the "Args". if strings.Contains(langTypeString, nameWithArgs) { langTypeString = strings.ReplaceAll(langTypeString, nameWithArgs, name) } } } switch t := t.(type) { case *schema.ArrayType: if schema.IsPrimitiveType(t.ElementType) { break } return mod.cleanTypeString(t.ElementType, langTypeString, lang, modName, isInput) case *schema.UnionType: for _, e := range t.ElementTypes { if schema.IsPrimitiveType(e) { continue } return mod.cleanTypeString(e, langTypeString, lang, modName, isInput) } case *schema.ObjectType: objTypeModName := mod.pkg.TokenToModule(t.Token) if objTypeModName != mod.mod { modName = mod.getLanguageModuleName(lang) } } if lang == "nodejs" { return cleanNodeJSName(modName) } else if lang == "csharp" { return cleanCSharpName(mod.pkg.Name(), modName) } return strings.ReplaceAll(langTypeString, modName, "") } // typeString returns a property type suitable for docs with its display name and the anchor link to // a type if the type of the property is an array or an object. func (mod *modContext) typeString(t schema.Type, lang string, characteristics propertyCharacteristics, insertWordBreaks bool) propertyType { t = codegen.PlainType(t) docLanguageHelper := mod.docGenContext.getLanguageDocHelper(lang) modName := mod.getLanguageModuleName(lang) def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get package definition for %q", mod.pkg.Name()) langTypeString := docLanguageHelper.GetLanguageTypeString(def, modName, t, characteristics.input) if optional, ok := t.(*schema.OptionalType); ok { t = optional.ElementType } // If the type is an object type, let's also wrap it with a link to the supporting type // on the same page using an anchor tag. var href string switch t := t.(type) { case *schema.ArrayType: elementLangType := mod.typeString(t.ElementType, lang, characteristics, false) href = elementLangType.Link case *schema.ObjectType: tokenName := tokenToName(t.Token) // Links to anchor tags on the same page must be lower-cased. href = "#" + strings.ToLower(tokenName) case *schema.EnumType: tokenName := tokenToName(t.Token) // Links to anchor tags on the same page must be lower-cased. href = "#" + strings.ToLower(tokenName) case *schema.UnionType: var elements []string for _, e := range t.ElementTypes { elementLangType := mod.typeString(e, lang, characteristics, false) elements = append(elements, elementLangType.DisplayName) } langTypeString = strings.Join(elements, " | ") } // Strip the namespace/module prefix for the type's display name. displayName := langTypeString if !schema.IsPrimitiveType(t) { displayName = mod.cleanTypeString(t, langTypeString, lang, modName, characteristics.input) } displayName = cleanOptionalIdentifier(displayName, lang) langTypeString = cleanOptionalIdentifier(langTypeString, lang) // Name and DisplayName should be html-escaped to avoid throwing off rendering for template types in languages like // csharp, Java etc. If word-breaks need to be inserted, then the type string should be html-escaped first. displayName = html.EscapeString(displayName) if insertWordBreaks { displayName = wbr(displayName) } return propertyType{ Name: html.EscapeString(langTypeString), DisplayName: displayName, Link: href, } } // cleanOptionalIdentifier removes the type identifier (i.e. "?" in "string?"). func cleanOptionalIdentifier(s, lang string) string { switch lang { case "nodejs": return strings.TrimSuffix(s, "?") case "go": return strings.TrimPrefix(s, "*") case "csharp": return strings.TrimSuffix(s, "?") case "python": if strings.HasPrefix(s, "Optional[") && strings.HasSuffix(s, "]") { s = strings.TrimPrefix(s, "Optional[") s = strings.TrimSuffix(s, "]") return s } } return s } // Resources typically take the same set of parameters to their constructors, and these // are the default comments/descriptions for them. const ( ctorNameArgComment = "The unique name of the resource." ctorArgsArgComment = "The arguments to resource properties." ctorOptsArgComment = "Bag of options to control resource's behavior." ) func (mod *modContext) genConstructorTS(r *schema.Resource, argsOptional bool) []formalParam { name := resourceName(r) docLangHelper := mod.docGenContext.getLanguageDocHelper("nodejs") var argsType string optsType := "CustomResourceOptions" // The args type for k8s package differs from the rest depending on whether we are dealing with // overlay resources or regular k8s resources. if isKubernetesPackage(mod.pkg) { if mod.isKubernetesOverlayModule() { if name == "CustomResource" { argsType = name + "Args" } else { argsType = name + "Opts" } } else { // The non-schema-based k8s codegen does not apply a suffix to the input types. argsType = name } if mod.isComponentResource() { optsType = "ComponentResourceOptions" } } else { argsType = name + "Args" } argsFlag := "" if argsOptional { argsFlag = "?" } def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "name", Type: propertyType{ Name: "string", }, Comment: ctorNameArgComment, }, { Name: "args", OptionalFlag: argsFlag, Type: propertyType{ Name: argsType, Link: "#inputs", }, Comment: ctorArgsArgComment, }, { Name: "opts", OptionalFlag: "?", Type: propertyType{ Name: optsType, Link: docLangHelper.GetDocLinkForPulumiType(def, optsType), }, Comment: ctorOptsArgComment, }, } } func (mod *modContext) genConstructorGo(r *schema.Resource, argsOptional bool) []formalParam { name := resourceName(r) argsType := name + "Args" argsFlag := "" if argsOptional { argsFlag = "*" } docLangHelper := mod.docGenContext.getLanguageDocHelper("go") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "ctx", OptionalFlag: "*", Type: propertyType{ Name: "Context", Link: docLangHelper.GetDocLinkForPulumiType(def, "Context"), }, Comment: "Context object for the current deployment.", }, { Name: "name", Type: propertyType{ Name: "string", }, Comment: ctorNameArgComment, }, { Name: "args", OptionalFlag: argsFlag, Type: propertyType{ Name: argsType, Link: "#inputs", }, Comment: ctorArgsArgComment, }, { Name: "opts", OptionalFlag: "...", Type: propertyType{ Name: "ResourceOption", Link: docLangHelper.GetDocLinkForPulumiType(def, "ResourceOption"), }, Comment: ctorOptsArgComment, }, } } func (mod *modContext) genConstructorCS(r *schema.Resource, argsOptional bool) []formalParam { name := resourceName(r) optsType := "CustomResourceOptions" if isKubernetesPackage(mod.pkg) && mod.isComponentResource() { optsType = "ComponentResourceOptions" } var argsFlag string var argsDefault string if argsOptional { // If the number of required input properties was zero, we can make the args object optional. argsDefault = " = null" argsFlag = "?" } docLangHelper := mod.docGenContext.getLanguageDocHelper("csharp") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "name", Type: propertyType{ Name: "string", }, Comment: ctorNameArgComment, }, { Name: "args", OptionalFlag: argsFlag, DefaultValue: argsDefault, Type: propertyType{ Name: name + "Args", Link: "#inputs", }, Comment: ctorArgsArgComment, }, { Name: "opts", OptionalFlag: "?", DefaultValue: " = null", Type: propertyType{ Name: optsType, Link: docLangHelper.GetDocLinkForPulumiType(def, fmt.Sprintf("Pulumi.%s", optsType)), }, Comment: ctorOptsArgComment, }, } } func (mod *modContext) genConstructorYaml() []formalParam { return []formalParam{ { Name: "properties", Comment: ctorArgsArgComment, }, { Name: "options", Comment: ctorOptsArgComment, }, } } func (mod *modContext) genConstructorJava(r *schema.Resource, argsOverload bool) []formalParam { name := resourceName(r) optsType := "CustomResourceOptions" if mod.isComponentResource() { optsType = "ComponentResourceOptions" } docLangHelper := mod.docGenContext.getLanguageDocHelper("java") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) result := []formalParam{ { Name: "name", Type: propertyType{ Name: "String", }, Comment: ctorNameArgComment, }, { Name: "args", Type: propertyType{ Name: name + "Args", Link: "#inputs", }, Comment: ctorArgsArgComment, }, } if !argsOverload { result = append(result, formalParam{ Name: "options", OptionalFlag: "@Nullable", Type: propertyType{ Name: optsType, Link: docLangHelper.GetDocLinkForPulumiType(def, optsType), }, Comment: ctorOptsArgComment, }) } return result } func (mod *modContext) genConstructorPython(r *schema.Resource, argsOptional, argsOverload bool) []formalParam { docLanguageHelper := mod.docGenContext.getLanguageDocHelper("python") isK8sOverlayMod := mod.isKubernetesOverlayModule() isDockerImageResource := mod.pkg.Name() == "docker" && resourceName(r) == "Image" // Kubernetes overlay resources use a different ordering of formal params in Python. if isK8sOverlayMod && r.IsOverlay { return getKubernetesOverlayPythonFormalParams(mod.mod) } else if isDockerImageResource { return getDockerImagePythonFormalParams() } // We perform at least three appends before iterating over input types. params := slice.Prealloc[formalParam](3 + len(r.InputProperties)) params = append(params, formalParam{ Name: "resource_name", Type: propertyType{ Name: "str", }, Comment: ctorNameArgComment, }) if argsOverload { // Determine whether we need to use the alternate args class name (e.g. `<Resource>InitArgs` instead of // `<Resource>Args`) due to an input type with the same name as the resource in the same module. resName := resourceName(r) resArgsName := fmt.Sprintf("%sArgs", resName) for _, inputType := range mod.inputTypes { inputTypeName := strings.Title(tokenToName(inputType.Token)) if resName == inputTypeName { resArgsName = fmt.Sprintf("%sInitArgs", resName) } } optionalFlag, defaultVal, descriptionName := "", "", resArgsName typeName := descriptionName if argsOptional { optionalFlag, defaultVal, typeName = "optional", " = None", fmt.Sprintf("Optional[%s]", typeName) } params = append(params, formalParam{ Name: "args", OptionalFlag: optionalFlag, DefaultValue: defaultVal, Type: propertyType{ Name: typeName, DescriptionName: descriptionName, Link: "#inputs", }, Comment: ctorArgsArgComment, }) } params = append(params, formalParam{ Name: "opts", OptionalFlag: "optional", DefaultValue: " = None", Type: propertyType{ Name: "Optional[ResourceOptions]", DescriptionName: "ResourceOptions", Link: "/docs/reference/pkg/python/pulumi/#pulumi.ResourceOptions", }, Comment: ctorOptsArgComment, }) if argsOverload { return params } for _, p := range r.InputProperties { // If the property defines a const value, then skip it. // For example, in k8s, `apiVersion` and `kind` are often hard-coded // in the SDK and are not really user-provided input properties. if p.ConstValue != nil { continue } def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) typ := docLanguageHelper.GetLanguageTypeString(def, mod.mod, codegen.PlainType(codegen.OptionalType(p)), true /*input*/) params = append(params, formalParam{ Name: python.InitParamName(p.Name), DefaultValue: " = None", Type: propertyType{ Name: typ, }, }) } return params } func (mod *modContext) genNestedTypes(member interface{}, resourceType bool) []docNestedType { dctx := mod.docGenContext tokens := nestedTypeUsageInfo{} // Collect all of the types for this "member" as a map of resource names // and if it appears in an input object and/or output object. mod.getTypes(member, tokens) sortedTokens := slice.Prealloc[string](len(tokens)) for token := range tokens { sortedTokens = append(sortedTokens, token) } sort.Strings(sortedTokens) var typs []docNestedType for _, token := range sortedTokens { for iter := mod.pkg.Types().Range(); iter.Next(); { t, err := iter.Type() contract.AssertNoErrorf(err, "error iterating types") switch typ := t.(type) { case *schema.ObjectType: if typ.Token != token || len(typ.Properties) == 0 || typ.IsInputShape() { continue } // Create a map to hold the per-language properties of this object. props := make(map[string][]property) for _, lang := range dctx.supportedLanguages { props[lang] = mod.getProperties(typ.Properties, lang, true, true, false) } name := strings.Title(tokenToName(typ.Token)) typs = append(typs, docNestedType{ Name: wbr(name), AnchorID: strings.ToLower(name), Properties: props, }) case *schema.EnumType: if typ.Token != token || len(typ.Elements) == 0 { continue } name := strings.Title(tokenToName(typ.Token)) enums := make(map[string][]enum) for _, lang := range dctx.supportedLanguages { docLangHelper := dctx.getLanguageDocHelper(lang) var langEnumValues []enum for _, e := range typ.Elements { enumName, err := docLangHelper.GetEnumName(e, name) if err != nil { panic(err) } enumID := strings.ToLower(name + propertyLangSeparator + lang) langEnumValues = append(langEnumValues, enum{ ID: enumID, DisplayName: wbr(enumName), Name: enumName, Value: fmt.Sprintf("%v", e.Value), Comment: e.Comment, DeprecationMessage: e.DeprecationMessage, }) } enums[lang] = langEnumValues } typs = append(typs, docNestedType{ Name: wbr(name), AnchorID: strings.ToLower(name), EnumValues: enums, }) } } } sort.Slice(typs, func(i, j int) bool { return typs[i].Name < typs[j].Name }) return typs } // getProperties returns a slice of properties that can be rendered for docs for // the provided slice of properties in the schema. func (mod *modContext) getProperties(properties []*schema.Property, lang string, input, nested, isProvider bool, ) []property { return mod.getPropertiesWithIDPrefixAndExclude(properties, lang, input, nested, isProvider, "", nil) } func (mod *modContext) getPropertiesWithIDPrefixAndExclude(properties []*schema.Property, lang string, input, nested, isProvider bool, idPrefix string, exclude func(name string) bool, ) []property { dctx := mod.docGenContext if len(properties) == 0 { return nil } docProperties := slice.Prealloc[property](len(properties)) for _, prop := range properties { if prop == nil { continue } if exclude != nil && exclude(prop.Name) { continue } // If the property has a const value, then don't show it as an input property. // Even though it is a valid property, it is used by the language code gen to // generate the appropriate defaults for it. These cannot be overridden by users. if prop.ConstValue != nil { continue } characteristics := propertyCharacteristics{input: input} langDocHelper := dctx.getLanguageDocHelper(lang) name, err := langDocHelper.GetPropertyName(prop) if err != nil { panic(err) } propLangName := name propID := idPrefix + strings.ToLower(propLangName+propertyLangSeparator+lang) propTypes := make([]propertyType, 0) if typ, isUnion := codegen.UnwrapType(prop.Type).(*schema.UnionType); isUnion { for _, elementType := range typ.ElementTypes { propTypes = append(propTypes, mod.typeString(elementType, lang, characteristics, true)) } } else { propTypes = append(propTypes, mod.typeString(prop.Type, lang, characteristics, true)) } comment := prop.Comment // Default values for Provider inputs correspond to environment variables, so add that info to the docs. if isProvider && input && prop.DefaultValue != nil && len(prop.DefaultValue.Environment) > 0 { var suffix string if len(prop.DefaultValue.Environment) > 1 { suffix = "s" } comment += fmt.Sprintf(" It can also be sourced from the following environment variable%s: ", suffix) for i, v := range prop.DefaultValue.Environment { comment += fmt.Sprintf("`%s`", v) if i != len(prop.DefaultValue.Environment)-1 { comment += ", " } } } docProperties = append(docProperties, property{ ID: propID, DisplayName: wbr(propLangName), Name: propLangName, Comment: comment, DeprecationMessage: prop.DeprecationMessage, IsRequired: prop.IsRequired(), IsInput: input, // We indicate that a property will replace if either // a) we will force the replace at the engine level // b) we are told that the provider will require a replace IsReplaceOnChanges: prop.ReplaceOnChanges || prop.WillReplaceOnChanges, Link: "#" + propID, Types: propTypes, }) } // Sort required props to move them to the top of the properties list, then by name. sort.SliceStable(docProperties, func(i, j int) bool { pi, pj := docProperties[i], docProperties[j] switch { case pi.IsRequired != pj.IsRequired: return pi.IsRequired && !pj.IsRequired default: return pi.Name < pj.Name } }) return docProperties } func getDockerImagePythonFormalParams() []formalParam { return []formalParam{ { Name: "image_name", }, { Name: "build", }, { Name: "local_image_name", DefaultValue: "=None", }, { Name: "registry", DefaultValue: "=None", }, { Name: "skip_push", DefaultValue: "=None", }, { Name: "opts", DefaultValue: "=None", }, } } // Returns the rendered HTML for the resource's constructor, as well as the specific arguments. func (mod *modContext) genConstructors(r *schema.Resource, allOptionalInputs bool) (map[string]string, map[string][]formalParam) { dctx := mod.docGenContext renderedParams := make(map[string]string) formalParams := make(map[string][]formalParam) // Add an extra language for Python's ResourceArg __init__ overload. langs := append(dctx.supportedLanguages, "pythonargs") // Add an extra language for Java's ResourceArg overload. langs = append(langs, "javaargs") for _, lang := range langs { var ( paramTemplate string params []formalParam ) b := &bytes.Buffer{} paramSeparatorTemplate := "param_separator" ps := paramSeparator{} switch lang { case "nodejs": params = mod.genConstructorTS(r, allOptionalInputs) paramTemplate = "ts_formal_param" case "go": params = mod.genConstructorGo(r, allOptionalInputs) paramTemplate = "go_formal_param" case "csharp": params = mod.genConstructorCS(r, allOptionalInputs) paramTemplate = "csharp_formal_param" case "java": fallthrough case "javaargs": argsOverload := lang == "javaargs" params = mod.genConstructorJava(r, argsOverload) paramTemplate = "java_formal_param" case "python": fallthrough case "pythonargs": argsOverload := lang == "pythonargs" params = mod.genConstructorPython(r, allOptionalInputs, argsOverload) paramTemplate = "py_formal_param" paramSeparatorTemplate = "py_param_separator" ps = paramSeparator{Indent: strings.Repeat(" ", len("def (")+len(resourceName(r)))} case "yaml": params = mod.genConstructorYaml() } if paramTemplate != "" { for i, p := range params { if i != 0 { if err := dctx.templates.ExecuteTemplate(b, paramSeparatorTemplate, ps); err != nil { panic(err) } } if err := dctx.templates.ExecuteTemplate(b, paramTemplate, p); err != nil { panic(err) } } } renderedParams[lang] = b.String() formalParams[lang] = params } return renderedParams, formalParams } // getConstructorResourceInfo returns a map of per-language information about // the resource being constructed. func (mod *modContext) getConstructorResourceInfo(resourceTypeName, tok string) map[string]propertyType { dctx := mod.docGenContext docLangHelper := dctx.getLanguageDocHelper("yaml") resourceMap := make(map[string]propertyType) resourceDisplayName := resourceTypeName for _, lang := range dctx.supportedLanguages { // Use the module to package lookup to transform the module name to its normalized package name. modName := mod.getLanguageModuleName(lang) // Reset the type name back to the display name. resourceTypeName = resourceDisplayName switch lang { case "nodejs", "go", "python", "java": // Intentionally left blank. case "csharp": namespace := title(mod.pkg.Name(), lang) if ns, ok := dctx.csharpPkgInfo.Namespaces[mod.pkg.Name()]; ok { namespace = ns } if mod.mod == "" { resourceTypeName = fmt.Sprintf("Pulumi.%s.%s", namespace, resourceTypeName) break } resourceTypeName = fmt.Sprintf("Pulumi.%s.%s.%s", namespace, modName, resourceTypeName) case "yaml": def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) resourceMap[lang] = propertyType{ Name: resourceTypeName, DisplayName: docLangHelper.GetLanguageTypeString(def, mod.mod, &schema.ResourceType{Token: tok}, false), } continue default: panic(fmt.Errorf("cannot generate constructor info for unhandled language %q", lang)) } parts := strings.Split(resourceTypeName, ".") displayName := parts[len(parts)-1] resourceMap[lang] = propertyType{ Name: resourceDisplayName, DisplayName: displayName, } } return resourceMap } func (mod *modContext) getTSLookupParams(r *schema.Resource, stateParam string) []formalParam { dctx := mod.docGenContext docLangHelper := dctx.getLanguageDocHelper("nodejs") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "name", Type: propertyType{ Name: "string", }, }, { Name: "id", Type: propertyType{ Name: "Input<ID>", Link: docLangHelper.GetDocLinkForPulumiType(def, "ID"), }, }, { Name: "state", OptionalFlag: "?", Type: propertyType{ Name: stateParam, }, }, { Name: "opts", OptionalFlag: "?", Type: propertyType{ Name: "CustomResourceOptions", Link: docLangHelper.GetDocLinkForPulumiType(def, "CustomResourceOptions"), }, }, } } func (mod *modContext) getGoLookupParams(r *schema.Resource, stateParam string) []formalParam { dctx := mod.docGenContext docLangHelper := dctx.getLanguageDocHelper("go") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "ctx", OptionalFlag: "*", Type: propertyType{ Name: "Context", Link: docLangHelper.GetDocLinkForPulumiType(def, "Context"), }, }, { Name: "name", Type: propertyType{ Name: "string", }, }, { Name: "id", Type: propertyType{ Name: "IDInput", Link: docLangHelper.GetDocLinkForPulumiType(def, "IDInput"), }, }, { Name: "state", OptionalFlag: "*", Type: propertyType{ Name: stateParam, }, }, { Name: "opts", OptionalFlag: "...", Type: propertyType{ Name: "ResourceOption", Link: docLangHelper.GetDocLinkForPulumiType(def, "ResourceOption"), }, }, } } func (mod *modContext) getCSLookupParams(r *schema.Resource, stateParam string) []formalParam { dctx := mod.docGenContext docLangHelper := dctx.getLanguageDocHelper("csharp") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "name", Type: propertyType{ Name: "string", }, }, { Name: "id", Type: propertyType{ Name: "Input<string>", Link: docLangHelper.GetDocLinkForPulumiType(def, "Pulumi.Input"), }, }, { Name: "state", OptionalFlag: "?", Type: propertyType{ Name: stateParam, }, }, { Name: "opts", OptionalFlag: "?", DefaultValue: " = null", Type: propertyType{ Name: "CustomResourceOptions", Link: docLangHelper.GetDocLinkForPulumiType(def, "Pulumi.CustomResourceOptions"), }, }, } } func (mod *modContext) getJavaLookupParams(r *schema.Resource, stateParam string) []formalParam { dctx := mod.docGenContext docLangHelper := dctx.getLanguageDocHelper("java") def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) return []formalParam{ { Name: "name", Type: propertyType{ Name: "String", }, }, { Name: "id", Type: propertyType{ Name: "Output<String>", Link: docLangHelper.GetDocLinkForPulumiType(def, "Output"), }, }, { Name: "state", Type: propertyType{ Name: stateParam, }, }, { Name: "options", Type: propertyType{ Name: "CustomResourceOptions", Link: docLangHelper.GetDocLinkForPulumiType(def, "CustomResourceOptions"), }, }, } } func (mod *modContext) getPythonLookupParams(r *schema.Resource, stateParam string) []formalParam { dctx := mod.docGenContext // The input properties for a resource needs to be exploded as // individual constructor params. docLanguageHelper := dctx.getLanguageDocHelper("python") params := slice.Prealloc[formalParam](len(r.StateInputs.Properties)) for _, p := range r.StateInputs.Properties { def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) typ := docLanguageHelper.GetLanguageTypeString(def, mod.mod, codegen.PlainType(codegen.OptionalType(p)), true /*input*/) params = append(params, formalParam{ Name: python.PyName(p.Name), DefaultValue: " = None", Type: propertyType{ Name: typ, }, }) } return params } // genLookupParams generates a map of per-language way of rendering the formal parameters of the lookup function // used to lookup an existing resource. func (mod *modContext) genLookupParams(r *schema.Resource, stateParam string) map[string]string { dctx := mod.docGenContext lookupParams := make(map[string]string) if r.StateInputs == nil { return lookupParams } for _, lang := range dctx.supportedLanguages { var ( paramTemplate string params []formalParam ) b := &bytes.Buffer{} paramSeparatorTemplate := "param_separator" ps := paramSeparator{} switch lang { case "nodejs": params = mod.getTSLookupParams(r, stateParam) paramTemplate = "ts_formal_param" case "go": params = mod.getGoLookupParams(r, stateParam) paramTemplate = "go_formal_param" case "csharp": params = mod.getCSLookupParams(r, stateParam) paramTemplate = "csharp_formal_param" case "java": params = mod.getJavaLookupParams(r, stateParam) paramTemplate = "java_formal_param" case "python": params = mod.getPythonLookupParams(r, stateParam) paramTemplate = "py_formal_param" paramSeparatorTemplate = "py_param_separator" ps = paramSeparator{Indent: strings.Repeat(" ", len("def get("))} } n := len(params) for i, p := range params { if err := dctx.templates.ExecuteTemplate(b, paramTemplate, p); err != nil { panic(err) } if i != n-1 { if err := dctx.templates.ExecuteTemplate(b, paramSeparatorTemplate, ps); err != nil { panic(err) } } } lookupParams[lang] = b.String() } return lookupParams } // filterOutputProperties removes the input properties from the output properties list // (since input props are implicitly output props), returning only "output" props. func filterOutputProperties(inputProps []*schema.Property, props []*schema.Property) []*schema.Property { var outputProps []*schema.Property inputMap := make(map[string]bool, len(inputProps)) for _, p := range inputProps { inputMap[p.Name] = true } for _, p := range props { if _, found := inputMap[p.Name]; !found { outputProps = append(outputProps, p) } } return outputProps } func (mod *modContext) genResourceHeader(r *schema.Resource) header { resourceName := resourceName(r) var metaDescription string var titleTag string if mod.mod == "" { metaDescription = fmt.Sprintf("Documentation for the %s.%s resource "+ "with examples, input properties, output properties, "+ "lookup functions, and supporting types.", mod.pkg.Name(), resourceName) titleTag = fmt.Sprintf("%s.%s", mod.pkg.Name(), resourceName) } else { metaDescription = fmt.Sprintf("Documentation for the %s.%s.%s resource "+ "with examples, input properties, output properties, "+ "lookup functions, and supporting types.", mod.pkg.Name(), mod.mod, resourceName) titleTag = fmt.Sprintf("%s.%s.%s", mod.pkg.Name(), mod.mod, resourceName) } return header{ Title: resourceName, TitleTag: titleTag, MetaDesc: metaDescription, } } // genResource is the entrypoint for generating a doc for a resource // from its Pulumi schema. func (mod *modContext) genResource(r *schema.Resource) resourceDocArgs { dctx := mod.docGenContext // Create a resource module file into which all of this resource's types will go. name := resourceName(r) inputProps := make(map[string][]property) outputProps := make(map[string][]property) stateInputs := make(map[string][]property) var filteredOutputProps []*schema.Property // Provider resources do not have output properties, so there won't be anything to filter. if !r.IsProvider { filteredOutputProps = filterOutputProperties(r.InputProperties, r.Properties) } // All custom resources have an implicit `id` output property, that we must inject into the docs. if !r.IsComponent { filteredOutputProps = append(filteredOutputProps, &schema.Property{ Name: "id", Comment: "The provider-assigned unique ID for this managed resource.", Type: schema.StringType, }) } for _, lang := range dctx.supportedLanguages { inputProps[lang] = mod.getProperties(r.InputProperties, lang, true, false, r.IsProvider) outputProps[lang] = mod.getProperties(filteredOutputProps, lang, false, false, r.IsProvider) if r.IsProvider { continue } if r.StateInputs != nil { stateProps := mod.getProperties(r.StateInputs.Properties, lang, true, false, r.IsProvider) for i := 0; i < len(stateProps); i++ { id := "state_" + stateProps[i].ID stateProps[i].ID = id stateProps[i].Link = "#" + id } stateInputs[lang] = stateProps } } allOptionalInputs := true for _, prop := range r.InputProperties { // If at least one prop is required, then break. if prop.IsRequired() { allOptionalInputs = false break } } def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %s", mod.pkg.Name()) packageDetails := packageDetails{ DisplayName: getPackageDisplayName(def.Name), Repository: def.Repository, RepositoryName: getRepositoryName(def.Repository), License: def.License, Notes: def.Attribution, } renderedCtorParams, typedCtorParams := mod.genConstructors(r, allOptionalInputs) stateParam := name + "State" docInfo := dctx.decomposeDocstring(r.Comment) data := resourceDocArgs{ Header: mod.genResourceHeader(r), Tool: mod.tool, Comment: docInfo.description, DeprecationMessage: r.DeprecationMessage, ExamplesSection: docInfo.examples, ImportDocs: docInfo.importDetails, ConstructorParams: renderedCtorParams, ConstructorParamsTyped: typedCtorParams, ConstructorResource: mod.getConstructorResourceInfo(name, r.Token), ArgsRequired: !allOptionalInputs, InputProperties: inputProps, OutputProperties: outputProps, LookupParams: mod.genLookupParams(r, stateParam), StateInputs: stateInputs, StateParam: stateParam, NestedTypes: mod.genNestedTypes(r, true /*resourceType*/), Methods: mod.genMethods(r), PackageDetails: packageDetails, } return data } func (mod *modContext) getNestedTypes(t schema.Type, types nestedTypeUsageInfo, input bool) { switch t := t.(type) { case *schema.InputType: mod.getNestedTypes(t.ElementType, types, input) case *schema.OptionalType: mod.getNestedTypes(t.ElementType, types, input) case *schema.ArrayType: mod.getNestedTypes(t.ElementType, types, input) case *schema.MapType: mod.getNestedTypes(t.ElementType, types, input) case *schema.ObjectType: if types.contains(t.Token, input) { break } types.add(t.Token, input) for _, p := range t.Properties { mod.getNestedTypes(p.Type, types, input) } case *schema.EnumType: types.add(t.Token, false) case *schema.UnionType: for _, e := range t.ElementTypes { mod.getNestedTypes(e, types, input) } } } func (mod *modContext) getTypes(member interface{}, types nestedTypeUsageInfo) { glog.V(3).Infoln("getting nested types for module", mod.mod) switch t := member.(type) { case *schema.ObjectType: for _, p := range t.Properties { mod.getNestedTypes(p.Type, types, false) } case *schema.Resource: for _, p := range t.Properties { mod.getNestedTypes(p.Type, types, false) } for _, p := range t.InputProperties { mod.getNestedTypes(p.Type, types, true) } for _, m := range t.Methods { mod.getTypes(m.Function, types) } case *schema.Function: if t.Inputs != nil && !t.MultiArgumentInputs { mod.getNestedTypes(t.Inputs, types, true) } if t.ReturnType != nil { if objectType, ok := t.ReturnType.(*schema.ObjectType); ok && objectType != nil { mod.getNestedTypes(objectType, types, false) } } } } // getModuleFileName returns the file name to use for a module. func (mod *modContext) getModuleFileName() string { dctx := mod.docGenContext if !isKubernetesPackage(mod.pkg) { return mod.mod } // For k8s packages, use the Go-language info to get the file name // for the module. if override, ok := dctx.goPkgInfo.ModuleToPackage[mod.mod]; ok { return override } return mod.mod } // moduleConflictResolver holds module-level information for resolving naming conflicts. // It shares information with the top-level docGenContext // to ensure the same name is used across modules that reference each other. type moduleConflictResolver struct { dctx *docGenContext seen map[string]struct{} } func (dctx *docGenContext) newModuleConflictResolver() moduleConflictResolver { return moduleConflictResolver{ dctx: dctx, seen: map[string]struct{}{}, } } // getSafeName returns a documentation name for an item // that is unique within the module. // // if the item has already been resolved by any module, // the previously-resolved name is returned. func (r *moduleConflictResolver) getSafeName(name string, item interface{}) string { if safeName, ok := r.dctx.moduleConflictLinkMap[item]; ok { return safeName } var prefixes []string switch item.(type) { case *schema.Resource: prefixes = []string{"", "res-"} case *schema.Function: prefixes = []string{"", "fn-"} case *modContext: prefixes = []string{"", "mod-"} default: prefixes = []string{""} } for _, prefix := range prefixes { candidate := prefix + name if _, exists := r.seen[candidate]; exists { continue } r.seen[candidate] = struct{}{} r.dctx.moduleConflictLinkMap[item] = candidate return candidate } glog.Error("skipping unresolvable duplicate file name: ", name) return "" } func (mod *modContext) gen(fs codegen.Fs) error { glog.V(4).Infoln("genIndex for", mod.mod) modName := mod.getModuleFileName() conflictResolver := mod.docGenContext.newModuleConflictResolver() def, err := mod.pkg.Definition() contract.AssertNoErrorf(err, "failed to get definition for package %q", mod.pkg.Name()) modTitle := modName if modTitle == "" { // An empty string indicates that this is the root module. if def.DisplayName != "" { modTitle = def.DisplayName } else { modTitle = getPackageDisplayName(mod.pkg.Name()) } } // addFileTemplated executes template tmpl with data, // and adds a file $dirName/_index.md with the result. addFileTemplated := func(dirName, tmpl string, data interface{}) error { var buff bytes.Buffer if err := mod.docGenContext.templates.ExecuteTemplate(&buff, tmpl, data); err != nil { return err } p := path.Join(modName, dirName, "_index.md") fs.Add(p, buff.Bytes()) return nil } // If there are submodules, list them. modules := slice.Prealloc[indexEntry](len(mod.children)) for _, mod := range mod.children { modName := mod.getModuleFileName() displayName := modFilenameToDisplayName(modName) safeName := conflictResolver.getSafeName(displayName, mod) if safeName == "" { continue // unresolved conflict } modules = append(modules, indexEntry{ Link: getModuleLink(safeName), DisplayName: displayName, }) } sortIndexEntries(modules) // If there are resources in the root, list them. resources := slice.Prealloc[indexEntry](len(mod.resources)) for _, r := range mod.resources { title := resourceName(r) link := getResourceLink(title) link = conflictResolver.getSafeName(link, r) if link == "" { continue // unresolved conflict } data := mod.genResource(r) if err := addFileTemplated(link, "resource.tmpl", data); err != nil { return err } resources = append(resources, indexEntry{ Link: link + "/", DisplayName: title, }) } sortIndexEntries(resources) // If there are functions in the root, list them. functions := slice.Prealloc[indexEntry](len(mod.functions)) for _, f := range mod.functions { name := tokenToName(f.Token) link := getFunctionLink(name) link = conflictResolver.getSafeName(link, f) if link == "" { continue // unresolved conflict } data := mod.genFunction(f) if err := addFileTemplated(link, "function.tmpl", data); err != nil { return err } functions = append(functions, indexEntry{ Link: link + "/", DisplayName: strings.Title(name), }) } sortIndexEntries(functions) version := "" if mod.pkg.Version() != nil { version = mod.pkg.Version().String() } packageDetails := packageDetails{ DisplayName: getPackageDisplayName(def.Name), Repository: def.Repository, RepositoryName: getRepositoryName(def.Repository), License: def.License, Notes: def.Attribution, Version: version, } var modTitleTag string var packageDescription string // The same index.tmpl template is used for both top level package and module pages, if modules not present, // assume top level package index page when formatting title tags otherwise, if contains modules, assume modules // top level page when generating title tags. if len(modules) > 0 { modTitleTag = fmt.Sprintf("%s Package", getPackageDisplayName(modTitle)) } else { modTitleTag = fmt.Sprintf("%s.%s", mod.pkg.Name(), modTitle) packageDescription = fmt.Sprintf("Explore the resources and functions of the %s.%s module.", mod.pkg.Name(), modTitle) } // Generate the index file. idxData := indexData{ Tool: mod.tool, PackageDescription: packageDescription, Title: modTitle, TitleTag: modTitleTag, Resources: resources, Functions: functions, Modules: modules, PackageDetails: packageDetails, } // If this is the root module, write out the package description. if mod.mod == "" { idxData.PackageDescription = mod.pkg.Description() } return addFileTemplated("", "index.tmpl", idxData) } // indexEntry represents an individual entry on an index page. type indexEntry struct { Link string DisplayName string } // indexData represents the index file data to be rendered as _index.md. type indexData struct { Tool string Title string TitleTag string PackageDescription string Functions []indexEntry Resources []indexEntry Modules []indexEntry PackageDetails packageDetails } func sortIndexEntries(entries []indexEntry) { if len(entries) == 0 { return } sort.Slice(entries, func(i, j int) bool { return entries[i].DisplayName < entries[j].DisplayName }) } // getPackageDisplayName uses the title lookup map to look for a // display name for the given title. func getPackageDisplayName(title string) string { // If title not found in titleLookup map, default back to title given. if val, ok := titleLookup(title); ok { return val } return title } // getRepositoryName returns the repository name based on the repository's URL. func getRepositoryName(repoURL string) string { return strings.TrimPrefix(repoURL, "https://github.com/") } func (dctx *docGenContext) getMod( pkg schema.PackageReference, token string, tokenPkg schema.PackageReference, modules map[string]*modContext, tool string, add bool, ) *modContext { modName := pkg.TokenToModule(token) mod, ok := modules[modName] if !ok { mod = &modContext{ pkg: pkg, mod: modName, tool: tool, docGenContext: dctx, } if modName != "" && codegen.PkgEquals(tokenPkg, pkg) { parentName := path.Dir(modName) // If the parent name is blank, it means this is the package-level. if parentName == "." || parentName == "" { parentName = ":index:" } else { parentName = ":" + parentName + ":" } parent := dctx.getMod(pkg, parentName, tokenPkg, modules, tool, add) if add { parent.children = append(parent.children, mod) } } // Save the module only if we're adding and it's for the current package. // This way, modules for external packages are not saved. if add && tokenPkg == pkg { modules[modName] = mod } } return mod } func (dctx *docGenContext) generateModulesFromSchemaPackage(tool string, pkg *schema.Package) map[string]*modContext { // Group resources, types, and functions into modules. modules := map[string]*modContext{} // Decode language-specific info. if err := pkg.ImportLanguages(map[string]schema.Language{ "go": go_gen.Importer, "python": python.Importer, "csharp": dotnet.Importer, "nodejs": nodejs.Importer, }); err != nil { panic(err) } dctx.goPkgInfo, _ = pkg.Language["go"].(go_gen.GoPackageInfo) dctx.csharpPkgInfo, _ = pkg.Language["csharp"].(dotnet.CSharpPackageInfo) dctx.nodePkgInfo, _ = pkg.Language["nodejs"].(nodejs.NodePackageInfo) dctx.pythonPkgInfo, _ = pkg.Language["python"].(python.PackageInfo) goLangHelper := dctx.getLanguageDocHelper("go").(*go_gen.DocLanguageHelper) // Generate the Go package map info now, so we can use that to get the type string // names later. goLangHelper.GeneratePackagesMap(pkg, tool, dctx.goPkgInfo) csharpLangHelper := dctx.getLanguageDocHelper("csharp").(*dotnet.DocLanguageHelper) csharpLangHelper.Namespaces = dctx.csharpPkgInfo.Namespaces visitObjects := func(r *schema.Resource) { visitObjectTypes(r.InputProperties, func(t schema.Type) { switch T := t.(type) { case *schema.ObjectType: dctx.getMod(pkg.Reference(), T.Token, T.PackageReference, modules, tool, true).details(T).inputType = true } }) if r.StateInputs != nil { visitObjectTypes(r.StateInputs.Properties, func(t schema.Type) { switch T := t.(type) { case *schema.ObjectType: dctx.getMod(pkg.Reference(), T.Token, T.PackageReference, modules, tool, true).details(T).inputType = true } }) } } scanResource := func(r *schema.Resource) { mod := dctx.getMod(pkg.Reference(), r.Token, r.PackageReference, modules, tool, true) mod.resources = append(mod.resources, r) visitObjects(r) } scanK8SResource := func(r *schema.Resource) { mod := getKubernetesMod(pkg, r.Token, modules, tool) mod.resources = append(mod.resources, r) visitObjects(r) } glog.V(3).Infoln("scanning resources") if isKubernetesPackage(pkg.Reference()) { scanK8SResource(pkg.Provider) for _, r := range pkg.Resources { scanK8SResource(r) } } else { scanResource(pkg.Provider) for _, r := range pkg.Resources { scanResource(r) } } glog.V(3).Infoln("done scanning resources") for _, f := range pkg.Functions { if !f.IsMethod { mod := dctx.getMod(pkg.Reference(), f.Token, f.PackageReference, modules, tool, true) mod.functions = append(mod.functions, f) } } // Find nested types. for _, t := range pkg.Types { switch typ := t.(type) { case *schema.ObjectType: mod := dctx.getMod(pkg.Reference(), typ.Token, typ.PackageReference, modules, tool, false) if mod.details(typ).inputType { mod.inputTypes = append(mod.inputTypes, typ) } } } return modules } func (dctx *docGenContext) initialize(tool string, pkg *schema.Package) { dctx.templates = template.New("").Funcs(template.FuncMap{ "htmlSafe": func(html string) template.HTML { // Markdown fragments in the templates need to be rendered as-is, // so that html/template package doesn't try to inject data into it, // which will most certainly fail. //nolint:gosec return template.HTML(html) }, "markdownify": func(html string) template.HTML { // Convert a string of Markdown into HTML. var buf bytes.Buffer if err := goldmark.Convert([]byte(html), &buf); err != nil { glog.Fatalf("rendering Markdown: %v", err) } rendered := buf.String() // Trim surrounding <p></p> tags. result := strings.TrimSpace(rendered) result = strings.TrimPrefix(result, "<p>") result = strings.TrimSuffix(result, "</p>") // If there are still <p> tags, there are multiple paragraphs, // in which case use the original rendered string (untrimmed). if strings.Contains(result, "<p>") { result = rendered } //nolint:gosec return template.HTML(result) }, }) defer glog.Flush() if _, err := dctx.templates.ParseFS(packagedTemplates, "templates/*.tmpl"); err != nil { glog.Fatalf("initializing templates: %v", err) } // Generate the modules from the schema, and for every module // run the generator functions to generate markdown files. dctx.setModules(dctx.generateModulesFromSchemaPackage(tool, pkg)) } func (dctx *docGenContext) generatePackage(tool string, pkg *schema.Package) (map[string][]byte, error) { if dctx.modules() == nil { return nil, errors.New("must call Initialize before generating the docs package") } defer glog.Flush() glog.V(3).Infoln("generating package docs now...") files := codegen.Fs{} modules := []string{} modMap := dctx.modules() for k := range modMap { modules = append(modules, k) } sort.Strings(modules) for _, mod := range modules { if err := modMap[mod].gen(files); err != nil { return nil, err } } return files, nil } // GeneratePackageTree returns a navigable structure starting from the top-most module. func (dctx *docGenContext) generatePackageTree() ([]PackageTreeItem, error) { if dctx.modules() == nil { return nil, errors.New("must call Initialize before generating the docs package") } defer glog.Flush() var packageTree []PackageTreeItem // "" indicates the top-most module. if rootMod, ok := dctx.modules()[""]; ok { tree, err := generatePackageTree(*rootMod) if err != nil { glog.Errorf("Error generating the package tree for package: %v", err) } packageTree = tree } else { glog.Error("A root module entry was not found for the package. Cannot generate the package tree...") } return packageTree, nil } func visitObjectTypes(properties []*schema.Property, visitor func(t schema.Type)) { codegen.VisitTypeClosure(properties, func(t schema.Type) { switch st := t.(type) { case *schema.EnumType, *schema.ObjectType, *schema.ResourceType: visitor(st) } }) } // Export a default static context so as not to break external // consumers of this API; prefer *WithContext API internally to ensure // tests can run in parallel. var defaultContext = newDocGenContext() func Initialize(tool string, pkg *schema.Package) { defaultContext.initialize(tool, pkg) } // GeneratePackage generates docs for each resource given the Pulumi // schema. The returned map contains the filename with path as the key // and the contents as its value. func GeneratePackage(tool string, pkg *schema.Package) (map[string][]byte, error) { return defaultContext.generatePackage(tool, pkg) } // GeneratePackageTree returns a navigable structure starting from the top-most module. func GeneratePackageTree() ([]PackageTreeItem, error) { return defaultContext.generatePackageTree() }