github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/codegen/go/gen_program.go (about) 1 package gen 2 3 import ( 4 "bytes" 5 "fmt" 6 gofmt "go/format" 7 "io" 8 "io/ioutil" 9 "path" 10 "strings" 11 "sync" 12 13 "github.com/hashicorp/hcl/v2" 14 "github.com/iancoleman/strcase" 15 16 "github.com/pulumi/pulumi/pkg/v3/codegen" 17 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" 18 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format" 19 "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" 20 "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" 21 "github.com/pulumi/pulumi/pkg/v3/codegen/schema" 22 "github.com/pulumi/pulumi/sdk/v3/go/common/encoding" 23 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 24 "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" 25 ) 26 27 const ( 28 IndexToken = "index" 29 fromBase64Fn = "fromBase64" 30 ) 31 32 type generator struct { 33 // The formatter to use when generating code. 34 *format.Formatter 35 program *pcl.Program 36 packages map[string]*schema.Package 37 contexts map[string]map[string]*pkgContext 38 diagnostics hcl.Diagnostics 39 spills *spills 40 jsonTempSpiller *jsonSpiller 41 ternaryTempSpiller *tempSpiller 42 readDirTempSpiller *readDirSpiller 43 splatSpiller *splatSpiller 44 optionalSpiller *optionalSpiller 45 scopeTraversalRoots codegen.StringSet 46 arrayHelpers map[string]*promptToInputArrayHelper 47 isErrAssigned bool 48 tmpVarCount int 49 configCreated bool 50 externalCache *Cache 51 52 // User-configurable options 53 assignResourcesToVariables bool // Assign resource to a new variable instead of _. 54 } 55 56 // GenerateProgramOptions are used to configure optional generator behavior. 57 type GenerateProgramOptions struct { 58 AssignResourcesToVariables bool // Assign resource to a new variable instead of _. 59 ExternalCache *Cache 60 } 61 62 func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) { 63 pcl.MapProvidersAsResources(program) 64 return GenerateProgramWithOptions(program, GenerateProgramOptions{}) 65 } 66 67 func GenerateProgramWithOptions(program *pcl.Program, opts GenerateProgramOptions) ( 68 map[string][]byte, hcl.Diagnostics, error) { 69 packages, contexts := map[string]*schema.Package{}, map[string]map[string]*pkgContext{} 70 packageDefs, err := programPackageDefs(program) 71 if err != nil { 72 return nil, nil, err 73 } 74 75 if opts.ExternalCache == nil { 76 opts.ExternalCache = globalCache 77 } 78 79 for _, pkg := range packageDefs { 80 packages[pkg.Name], contexts[pkg.Name] = pkg, getPackages("tool", pkg, opts.ExternalCache) 81 } 82 83 g := &generator{ 84 program: program, 85 packages: packages, 86 contexts: contexts, 87 spills: &spills{counts: map[string]int{}}, 88 jsonTempSpiller: &jsonSpiller{}, 89 ternaryTempSpiller: &tempSpiller{}, 90 readDirTempSpiller: &readDirSpiller{}, 91 splatSpiller: &splatSpiller{}, 92 optionalSpiller: &optionalSpiller{}, 93 scopeTraversalRoots: codegen.NewStringSet(), 94 arrayHelpers: make(map[string]*promptToInputArrayHelper), 95 externalCache: opts.ExternalCache, 96 } 97 98 // Apply any generate options. 99 g.assignResourcesToVariables = opts.AssignResourcesToVariables 100 101 g.Formatter = format.NewFormatter(g) 102 103 // We must collect imports once before lowering, and once after. 104 // This allows us to avoid complexity of traversing apply expressions for things like JSON 105 // but still have access to types provided by __convert intrinsics after lowering. 106 pulumiImports := codegen.NewStringSet() 107 stdImports := codegen.NewStringSet() 108 preambleHelperMethods := codegen.NewStringSet() 109 g.collectImports(program, stdImports, pulumiImports, preambleHelperMethods) 110 111 // Linearize the nodes into an order appropriate for procedural code generation. 112 nodes := pcl.Linearize(program) 113 114 var progPostamble bytes.Buffer 115 for _, n := range nodes { 116 g.collectScopeRoots(n) 117 } 118 119 for _, n := range nodes { 120 g.genNode(&progPostamble, n) 121 } 122 123 g.genPostamble(&progPostamble, nodes) 124 125 // We must generate the program first and the preamble second and finally cat the two together. 126 // This is because nested object/tuple cons expressions can require imports that aren't 127 // present in resource declarations or invokes alone. Expressions are lowered when the program is generated 128 // and this must happen first so we can access types via __convert intrinsics. 129 var index bytes.Buffer 130 g.genPreamble(&index, program, stdImports, pulumiImports, preambleHelperMethods) 131 index.Write(progPostamble.Bytes()) 132 133 // Run Go formatter on the code before saving to disk 134 formattedSource, err := gofmt.Source(index.Bytes()) 135 if err != nil { 136 return nil, g.diagnostics, fmt.Errorf("invalid Go source code:\n\n%s: %w", index.String(), err) 137 } 138 139 files := map[string][]byte{ 140 "main.go": formattedSource, 141 } 142 return files, g.diagnostics, nil 143 } 144 145 func GenerateProject(directory string, project workspace.Project, program *pcl.Program) error { 146 files, diagnostics, err := GenerateProgram(program) 147 if err != nil { 148 return err 149 } 150 if diagnostics.HasErrors() { 151 return diagnostics 152 } 153 154 // Set the runtime to "go" then marshal to Pulumi.yaml 155 project.Runtime = workspace.NewProjectRuntimeInfo("go", nil) 156 projectBytes, err := encoding.YAML.Marshal(project) 157 if err != nil { 158 return err 159 } 160 files["Pulumi.yaml"] = projectBytes 161 162 // Build a go.mod based on the packages used by program 163 var gomod bytes.Buffer 164 gomod.WriteString("module " + project.Name.String() + "\n") 165 gomod.WriteString(` 166 go 1.17 167 168 require ( 169 github.com/pulumi/pulumi/sdk/v3 v3.30.0 170 `) 171 172 // For each package add a PackageReference line 173 packages, err := programPackageDefs(program) 174 if err != nil { 175 return err 176 } 177 for _, p := range packages { 178 if p.Name == "pulumi" { 179 continue 180 } 181 if err := p.ImportLanguages(map[string]schema.Language{"go": Importer}); err != nil { 182 return err 183 } 184 185 if p.Version != nil && p.Version.Major <= 0 { 186 // Let `go mod tidy` resolve pre-1.0 and non-module package versions on InstallDependencies, 187 // as it better handles the way we use `importBasePath`. What we need is a `modulePath`. `go 188 // get` handles these cases, which are not parseable, they depend on retrieving the target 189 // repository and downloading it to disk. 190 // 191 // Here are two cases, first the parseable case: 192 // 193 // * go get github.com/pulumi/pulumi-aws/sdk/v5/go/aws@v5.3.0 194 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ module path 195 // ~~ major version 196 // ~~~~~~ package path - can be any number of path parts 197 // ~~~~~~ version 198 // 199 // Here, we can cut on the major version. 200 201 // * go get github.com/pulumi/pulumi-aws-native/sdk/go/aws@v0.16.0 202 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ module path 203 // ~~~~~~ package path - can be any number of path parts 204 // ~~~~~~~ version 205 // 206 // Here we cannot cut on the major version, as it isn't present. The only way to resolve this 207 // package is to pull the repo. 208 // 209 // Fortunately for these pre-1.0 releases, `go mod tidy` on the generated repo will at least 210 // add the module based on the import generated in the .go files, but it will always get the 211 // latest version. 212 213 if info, ok := p.Language["go"]; ok { 214 if info, ok := info.(GoPackageInfo); ok && info.ModulePath != "" { 215 gomod.WriteString(fmt.Sprintf(" %s v%s\n", info.ModulePath, p.Version.String())) 216 } 217 } 218 continue 219 } 220 221 // Relatively safe default, this works for Pulumi provider packages: 222 vPath := "" 223 if p.Version != nil && p.Version.Major > 1 { 224 vPath = fmt.Sprintf("/v%d", p.Version.Major) 225 } 226 packageName := fmt.Sprintf("github.com/pulumi/pulumi-%s/sdk%s/go/%s", p.Name, vPath, p.Name) 227 if langInfo, found := p.Language["go"]; found { 228 goInfo, ok := langInfo.(GoPackageInfo) 229 if ok && goInfo.ImportBasePath != "" { 230 separatorIndex := strings.Index(goInfo.ImportBasePath, vPath) 231 if separatorIndex < 0 { 232 packageName = "" 233 } else { 234 modulePrefix := goInfo.ImportBasePath[:separatorIndex] 235 packageName = fmt.Sprintf("%s%s", modulePrefix, vPath) 236 } 237 } 238 } 239 240 if packageName != "" { 241 gomod.WriteString(fmt.Sprintf(" %s v%s\n", packageName, p.Version.String())) 242 } 243 } 244 245 gomod.WriteString(")") 246 247 files["go.mod"] = gomod.Bytes() 248 249 for filename, data := range files { 250 outPath := path.Join(directory, filename) 251 err := ioutil.WriteFile(outPath, data, 0600) 252 if err != nil { 253 return fmt.Errorf("could not write output program: %w", err) 254 } 255 } 256 257 return nil 258 } 259 260 var packageContexts sync.Map 261 262 func getPackages(tool string, pkg *schema.Package, cache *Cache) map[string]*pkgContext { 263 if v, ok := packageContexts.Load(pkg); ok { 264 return v.(map[string]*pkgContext) 265 } 266 267 if err := pkg.ImportLanguages(map[string]schema.Language{"go": Importer}); err != nil { 268 return nil 269 } 270 271 var goPkgInfo GoPackageInfo 272 if goInfo, ok := pkg.Language["go"].(GoPackageInfo); ok { 273 goPkgInfo = goInfo 274 } 275 v := generatePackageContextMap(tool, pkg, goPkgInfo, cache) 276 packageContexts.Store(pkg, v) 277 return v 278 } 279 280 func (g *generator) collectScopeRoots(n pcl.Node) { 281 diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) { 282 if st, ok := n.(*model.ScopeTraversalExpression); ok { 283 g.scopeTraversalRoots.Add(st.RootName) 284 } 285 return n, nil 286 }) 287 contract.Assert(len(diags) == 0) 288 } 289 290 // genPreamble generates package decl, imports, and opens the main func 291 func (g *generator) genPreamble(w io.Writer, program *pcl.Program, stdImports, pulumiImports, 292 preambleHelperMethods codegen.StringSet) { 293 g.Fprint(w, "package main\n\n") 294 g.Fprintf(w, "import (\n") 295 296 g.collectImports(program, stdImports, pulumiImports, preambleHelperMethods) 297 for _, imp := range stdImports.SortedValues() { 298 g.Fprintf(w, "\"%s\"\n", imp) 299 } 300 301 g.Fprintf(w, "\n") 302 g.Fprintf(w, "\"github.com/pulumi/pulumi/sdk/v3/go/pulumi\"\n") 303 304 for _, imp := range pulumiImports.SortedValues() { 305 g.Fprintf(w, "%s\n", imp) 306 } 307 308 g.Fprintf(w, ")\n") 309 310 // If we collected any helper methods that should be added, write them just before the main func 311 for _, preambleHelperMethodBody := range preambleHelperMethods.SortedValues() { 312 g.Fprintf(w, "%s\n\n", preambleHelperMethodBody) 313 } 314 315 g.Fprintf(w, "func main() {\n") 316 g.Fprintf(w, "pulumi.Run(func(ctx *pulumi.Context) error {\n") 317 } 318 319 func (g *generator) collectTypeImports(program *pcl.Program, t schema.Type, imports codegen.StringSet) { 320 var token string 321 switch t := t.(type) { 322 case *schema.InputType: 323 g.collectTypeImports(program, t.ElementType, imports) 324 return 325 case *schema.OptionalType: 326 g.collectTypeImports(program, t.ElementType, imports) 327 return 328 case *schema.ArrayType: 329 g.collectTypeImports(program, t.ElementType, imports) 330 return 331 case *schema.MapType: 332 g.collectTypeImports(program, t.ElementType, imports) 333 return 334 case *schema.UnionType: 335 for _, t := range t.ElementTypes { 336 g.collectTypeImports(program, t, imports) 337 } 338 return 339 case *schema.ObjectType: 340 token = t.Token 341 case *schema.EnumType: 342 token = t.Token 343 case *schema.TokenType: 344 token = t.Token 345 case *schema.ResourceType: 346 token = t.Token 347 } 348 if token == "" { 349 return 350 } 351 352 var tokenRange hcl.Range 353 pkg, mod, name, _ := pcl.DecomposeToken(token, tokenRange) 354 vPath, err := g.getVersionPath(program, pkg) 355 if err != nil { 356 panic(err) 357 } 358 imports.Add(g.getPulumiImport(pkg, vPath, mod, name)) 359 } 360 361 // collect Imports returns two sets of packages imported by the program, std lib packages and pulumi packages 362 func (g *generator) collectImports( 363 program *pcl.Program, 364 stdImports, 365 pulumiImports, 366 preambleHelperMethods codegen.StringSet) (codegen.StringSet, codegen.StringSet, codegen.StringSet) { 367 // Accumulate import statements for the various providers 368 for _, n := range program.Nodes { 369 if r, isResource := n.(*pcl.Resource); isResource { 370 pcl.FixupPulumiPackageTokens(r) 371 pkg, mod, name, _ := r.DecomposeToken() 372 if pkg == "pulumi" { 373 if mod == "providers" { 374 pkg = name 375 mod = "" 376 } else if mod == "" { 377 continue 378 } 379 380 } 381 vPath, err := g.getVersionPath(program, pkg) 382 if err != nil { 383 panic(err) 384 } 385 386 pulumiImports.Add(g.getPulumiImport(pkg, vPath, mod, name)) 387 } 388 if _, isConfigVar := n.(*pcl.ConfigVariable); isConfigVar { 389 pulumiImports.Add("\"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config\"") 390 } 391 392 diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) { 393 if call, ok := n.(*model.FunctionCallExpression); ok { 394 if call.Name == pcl.Invoke { 395 tokenArg := call.Args[0] 396 token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString() 397 tokenRange := tokenArg.SyntaxNode().Range() 398 pkg, mod, name, diagnostics := pcl.DecomposeToken(token, tokenRange) 399 400 contract.Assert(len(diagnostics) == 0) 401 402 vPath, err := g.getVersionPath(program, pkg) 403 if err != nil { 404 panic(err) 405 } 406 pulumiImports.Add(g.getPulumiImport(pkg, vPath, mod, name)) 407 } else if call.Name == pcl.IntrinsicConvert { 408 g.collectConvertImports(program, call, pulumiImports) 409 } 410 411 // Checking to see if this function call deserves its own dedicated helper method in the preamble 412 if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name); ok { 413 preambleHelperMethods.Add(helperMethodBody) 414 } 415 } 416 return n, nil 417 }) 418 contract.Assert(len(diags) == 0) 419 420 diags = n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) { 421 if call, ok := n.(*model.FunctionCallExpression); ok { 422 for _, fnPkg := range g.genFunctionPackages(call) { 423 stdImports.Add(fnPkg) 424 } 425 } 426 if t, ok := n.(*model.TemplateExpression); ok { 427 if len(t.Parts) > 1 { 428 stdImports.Add("fmt") 429 } 430 } 431 return n, nil 432 }) 433 contract.Assert(len(diags) == 0) 434 } 435 436 return stdImports, pulumiImports, preambleHelperMethods 437 } 438 439 func (g *generator) collectConvertImports( 440 program *pcl.Program, 441 call *model.FunctionCallExpression, 442 pulumiImports codegen.StringSet) { 443 if schemaType, ok := pcl.GetSchemaForType(call.Type()); ok { 444 // Sometimes code for a `__convert` call does not 445 // really use the import of the result type. In such 446 // cases it is important not to generate a 447 // non-compiling unused import. Detect some of these 448 // cases here. 449 // 450 // Fully solving this is deferred for later: 451 // TODO[pulumi/pulumi#8324]. 452 if expr, ok := call.Args[0].(*model.TemplateExpression); ok { 453 if lit, ok := expr.Parts[0].(*model.LiteralValueExpression); ok && 454 model.StringType.AssignableFrom(lit.Type()) && 455 call.Type().AssignableFrom(lit.Type()) { 456 return 457 } 458 } 459 g.collectTypeImports(program, schemaType, pulumiImports) 460 } 461 } 462 463 func (g *generator) getVersionPath(program *pcl.Program, pkg string) (string, error) { 464 for _, p := range program.PackageReferences() { 465 if p.Name() == pkg { 466 if ver := p.Version(); ver != nil && ver.Major > 1 { 467 return fmt.Sprintf("/v%d", ver.Major), nil 468 } 469 return "", nil 470 } 471 } 472 473 return "", fmt.Errorf("could not find package version information for pkg: %s", pkg) 474 475 } 476 477 func (g *generator) getPkgContext(pkg, mod string) (*pkgContext, bool) { 478 p, ok := g.contexts[pkg] 479 if !ok { 480 return nil, false 481 } 482 m, ok := p[mod] 483 return m, ok 484 } 485 486 func (g *generator) getGoPackageInfo(pkg string) (GoPackageInfo, bool) { 487 p, ok := g.packages[pkg] 488 if !ok { 489 return GoPackageInfo{}, false 490 } 491 info, ok := p.Language["go"].(GoPackageInfo) 492 return info, ok 493 } 494 495 func (g *generator) getPulumiImport(pkg, vPath, mod, name string) string { 496 info, _ := g.getGoPackageInfo(pkg) 497 if m, ok := info.ModuleToPackage[mod]; ok { 498 mod = m 499 } 500 501 imp := fmt.Sprintf("github.com/pulumi/pulumi-%s/sdk%s/go/%s/%s", pkg, vPath, pkg, mod) 502 // namespaceless invokes "aws:index:..." 503 if mod == "" { 504 imp = fmt.Sprintf("github.com/pulumi/pulumi-%s/sdk%s/go/%s", pkg, vPath, pkg) 505 } 506 507 // All providers don't follow the sdk/go/<package> scheme. Allow ImportBasePath as 508 // a means to override this assumption. 509 if info.ImportBasePath != "" { 510 if mod != "" { 511 imp = fmt.Sprintf("%s/%s", info.ImportBasePath, mod) 512 } else { 513 imp = info.ImportBasePath 514 } 515 } 516 517 if alias, ok := info.PackageImportAliases[imp]; ok { 518 return fmt.Sprintf("%s %q", alias, imp) 519 } 520 521 modSplit := strings.Split(mod, "/") 522 // account for mods like "eks/ClusterVpcConfig" index... 523 if len(modSplit) > 1 { 524 if modSplit[0] == "" || modSplit[0] == IndexToken { 525 imp = fmt.Sprintf("github.com/pulumi/pulumi-%s/sdk%s/go/%s", pkg, vPath, pkg) 526 if info.ImportBasePath != "" { 527 imp = info.ImportBasePath 528 } 529 } else { 530 imp = fmt.Sprintf("github.com/pulumi/pulumi-%s/sdk%s/go/%s/%s", pkg, vPath, pkg, modSplit[0]) 531 if info.ImportBasePath != "" { 532 imp = fmt.Sprintf("%s/%s", info.ImportBasePath, modSplit[0]) 533 } 534 } 535 } 536 return fmt.Sprintf("%q", imp) 537 } 538 539 // genPostamble closes the method 540 func (g *generator) genPostamble(w io.Writer, nodes []pcl.Node) { 541 542 g.Fprint(w, "return nil\n") 543 g.Fprintf(w, "})\n") 544 g.Fprintf(w, "}\n") 545 546 g.genHelpers(w) 547 } 548 549 func (g *generator) genHelpers(w io.Writer) { 550 for _, v := range g.arrayHelpers { 551 v.generateHelperMethod(w) 552 } 553 } 554 555 func (g *generator) genNode(w io.Writer, n pcl.Node) { 556 switch n := n.(type) { 557 case *pcl.Resource: 558 g.genResource(w, n) 559 case *pcl.OutputVariable: 560 g.genOutputAssignment(w, n) 561 case *pcl.ConfigVariable: 562 g.genConfigVariable(w, n) 563 case *pcl.LocalVariable: 564 g.genLocalVariable(w, n) 565 } 566 } 567 568 var resourceType = model.NewOpaqueType("pulumi.Resource") 569 570 func (g *generator) lowerResourceOptions(opts *pcl.ResourceOptions) (*model.Block, []interface{}) { 571 if opts == nil { 572 return nil, nil 573 } 574 575 var block *model.Block 576 var temps []interface{} 577 appendOption := func(name string, value model.Expression, destType model.Type) { 578 if block == nil { 579 block = &model.Block{ 580 Type: "options", 581 Body: &model.Body{}, 582 } 583 } 584 585 value, valueTemps := g.lowerExpression(value, destType) 586 temps = append(temps, valueTemps...) 587 588 block.Body.Items = append(block.Body.Items, &model.Attribute{ 589 Tokens: syntax.NewAttributeTokens(name), 590 Name: name, 591 Value: value, 592 }) 593 } 594 595 if opts.Parent != nil { 596 appendOption("Parent", opts.Parent, model.DynamicType) 597 } 598 if opts.Provider != nil { 599 appendOption("Provider", opts.Provider, model.DynamicType) 600 } 601 if opts.DependsOn != nil { 602 appendOption("DependsOn", opts.DependsOn, model.NewListType(resourceType)) 603 } 604 if opts.Protect != nil { 605 appendOption("Protect", opts.Protect, model.BoolType) 606 } 607 if opts.IgnoreChanges != nil { 608 appendOption("IgnoreChanges", opts.IgnoreChanges, model.NewListType(model.StringType)) 609 } 610 611 return block, temps 612 } 613 614 func (g *generator) genResourceOptions(w io.Writer, block *model.Block) { 615 if block == nil { 616 return 617 } 618 619 for _, item := range block.Body.Items { 620 attr := item.(*model.Attribute) 621 g.Fgenf(w, ", pulumi.%s(%v)", attr.Name, attr.Value) 622 } 623 } 624 625 func (g *generator) genResource(w io.Writer, r *pcl.Resource) { 626 627 resName, resNameVar := r.LogicalName(), makeValidIdentifier(r.Name()) 628 pkg, mod, typ, _ := r.DecomposeToken() 629 originalMod := mod 630 if pkg == "pulumi" && mod == "providers" { 631 pkg = typ 632 mod = "" 633 typ = "Provider" 634 } 635 if mod == "" || strings.HasPrefix(mod, "/") || strings.HasPrefix(mod, "index/") { 636 originalMod = mod 637 mod = pkg 638 } 639 640 // Compute resource options 641 options, temps := g.lowerResourceOptions(r.Options) 642 g.genTemps(w, temps) 643 644 // Add conversions to input properties 645 for _, input := range r.Inputs { 646 destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: input.Name}) 647 g.diagnostics = append(g.diagnostics, diagnostics...) 648 expr, temps := g.lowerExpression(input.Value, destType.(model.Type)) 649 input.Value = expr 650 g.genTemps(w, temps) 651 } 652 653 modOrAlias := g.getModOrAlias(pkg, mod, originalMod) 654 655 instantiate := func(varName, resourceName string, w io.Writer) { 656 if g.scopeTraversalRoots.Has(varName) || strings.HasPrefix(varName, "__") { 657 g.Fgenf(w, "%s, err := %s.New%s(ctx, %s, ", varName, modOrAlias, typ, resourceName) 658 } else { 659 assignment := ":=" 660 if g.isErrAssigned { 661 assignment = "=" 662 } 663 if g.assignResourcesToVariables { 664 g.Fgenf(w, "%s, err := %s.New%s(ctx, %s, ", 665 strcase.ToLowerCamel(resourceName), modOrAlias, typ, resourceName) 666 } else { 667 g.Fgenf(w, "_, err %s %s.New%s(ctx, %s, ", assignment, modOrAlias, typ, resourceName) 668 } 669 } 670 g.isErrAssigned = true 671 672 if len(r.Inputs) > 0 { 673 g.Fgenf(w, "&%s.%sArgs{\n", modOrAlias, typ) 674 for _, attr := range r.Inputs { 675 g.Fgenf(w, "%s: ", strings.Title(attr.Name)) 676 g.Fgenf(w, "%.v,\n", attr.Value) 677 678 } 679 g.Fprint(w, "}") 680 } else { 681 g.Fprint(w, "nil") 682 } 683 g.genResourceOptions(w, options) 684 g.Fprint(w, ")\n") 685 g.Fgenf(w, "if err != nil {\n") 686 g.Fgenf(w, "return err\n") 687 g.Fgenf(w, "}\n") 688 } 689 690 if r.Options != nil && r.Options.Range != nil { 691 rangeType := model.ResolveOutputs(r.Options.Range.Type()) 692 rangeExpr, temps := g.lowerExpression(r.Options.Range, rangeType) 693 g.genTemps(w, temps) 694 695 g.Fgenf(w, "var %s []*%s.%s\n", resNameVar, modOrAlias, typ) 696 697 // ahead of range statement declaration generate the resource instantiation 698 // to detect and removed unused k,v variables 699 var buf bytes.Buffer 700 instantiate("__res", fmt.Sprintf(`fmt.Sprintf("%s-%%v", key0)`, resName), &buf) 701 instantiation := buf.String() 702 isValUsed := strings.Contains(instantiation, "val0") 703 valVar := "_" 704 if isValUsed { 705 valVar = "val0" 706 } 707 if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion { 708 g.Fgenf(w, "for index := 0; index < %.v; index++ {\n", rangeExpr) 709 g.Indented(func() { 710 g.Fgenf(w, "%skey0 := index\n", g.Indent) 711 g.Fgenf(w, "%s%s := index\n", g.Indent, valVar) 712 }) 713 } else { 714 g.Fgenf(w, "for key0, %s := range %.v {\n", valVar, rangeExpr) 715 } 716 717 g.Fgen(w, instantiation) 718 g.Fgenf(w, "%[1]s = append(%[1]s, __res)\n", resNameVar) 719 g.Fgenf(w, "}\n") 720 721 } else { 722 instantiate(resNameVar, fmt.Sprintf("%q", resName), w) 723 } 724 725 } 726 727 func (g *generator) genOutputAssignment(w io.Writer, v *pcl.OutputVariable) { 728 expr, temps := g.lowerExpression(v.Value, v.Type()) 729 g.genTemps(w, temps) 730 g.Fgenf(w, "ctx.Export(%q, %.3v)\n", v.LogicalName(), expr) 731 } 732 func (g *generator) genTemps(w io.Writer, temps []interface{}) { 733 singleReturn := "" 734 g.genTempsMultiReturn(w, temps, singleReturn) 735 } 736 737 func (g *generator) genTempsMultiReturn(w io.Writer, temps []interface{}, zeroValueType string) { 738 genZeroValueDecl := false 739 740 if zeroValueType != "" { 741 for _, t := range temps { 742 switch t.(type) { 743 case *spillTemp, *jsonTemp, *readDirTemp: 744 genZeroValueDecl = true 745 default: 746 } 747 } 748 if genZeroValueDecl { 749 // TODO add entropy to var name 750 // currently only used inside anonymous functions (no scope collisions) 751 g.Fgenf(w, "var _zero %s\n", zeroValueType) 752 } 753 754 } 755 756 for _, t := range temps { 757 switch t := t.(type) { 758 case *ternaryTemp: 759 // TODO derive from ambient context 760 isInput := false 761 g.Fgenf(w, "var %s %s\n", t.Name, g.argumentTypeName(t.Value.TrueResult, t.Type(), isInput)) 762 g.Fgenf(w, "if %.v {\n", t.Value.Condition) 763 g.Fgenf(w, "%s = %.v\n", t.Name, t.Value.TrueResult) 764 g.Fgenf(w, "} else {\n") 765 g.Fgenf(w, "%s = %.v\n", t.Name, t.Value.FalseResult) 766 g.Fgenf(w, "}\n") 767 case *spillTemp: 768 bytesVar := fmt.Sprintf("tmp%s", strings.ToUpper(t.Variable.Name)) 769 g.Fgenf(w, "%s, err := json.Marshal(", bytesVar) 770 args := t.Value.(*model.FunctionCallExpression).Args[0] 771 g.Fgenf(w, "%.v)\n", args) 772 g.Fgenf(w, "if err != nil {\n") 773 if genZeroValueDecl { 774 g.Fgenf(w, "return _zero, err\n") 775 } else { 776 g.Fgenf(w, "return err\n") 777 } 778 g.Fgenf(w, "}\n") 779 g.Fgenf(w, "%s := string(%s)\n", t.Variable.Name, bytesVar) 780 g.isErrAssigned = true 781 case *readDirTemp: 782 tmpSuffix := strings.Split(t.Name, "files")[1] 783 g.Fgenf(w, "%s, err := os.ReadDir(%.v)\n", t.Name, t.Value.Args[0]) 784 g.Fgenf(w, "if err != nil {\n") 785 if genZeroValueDecl { 786 g.Fgenf(w, "return _zero, err\n") 787 } else { 788 g.Fgenf(w, "return err\n") 789 } 790 g.Fgenf(w, "}\n") 791 namesVar := fmt.Sprintf("fileNames%s", tmpSuffix) 792 g.Fgenf(w, "%s := make([]string, len(%s))\n", namesVar, t.Name) 793 iVar := fmt.Sprintf("key%s", tmpSuffix) 794 valVar := fmt.Sprintf("val%s", tmpSuffix) 795 g.Fgenf(w, "for %s, %s := range %s {\n", iVar, valVar, t.Name) 796 g.Fgenf(w, "%s[%s] = %s.Name()\n", namesVar, iVar, valVar) 797 g.Fgenf(w, "}\n") 798 g.isErrAssigned = true 799 case *splatTemp: 800 argTyp := g.argumentTypeName(t.Value.Each, t.Value.Each.Type(), false) 801 if strings.Contains(argTyp, ".") { 802 g.Fgenf(w, "var %s %sArray\n", t.Name, argTyp) 803 } else { 804 g.Fgenf(w, "var %s []%s\n", t.Name, argTyp) 805 } 806 g.Fgenf(w, "for _, val0 := range %.v {\n", t.Value.Source) 807 g.Fgenf(w, "%s = append(%s, %.v)\n", t.Name, t.Name, t.Value.Each) 808 g.Fgenf(w, "}\n") 809 case *optionalTemp: 810 g.Fgenf(w, "%s := %.v\n", t.Name, t.Value) 811 default: 812 contract.Failf("unexpected temp type: %v", t) 813 } 814 } 815 } 816 817 func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) { 818 expr, temps := g.lowerExpression(v.Definition.Value, v.Type()) 819 g.genTemps(w, temps) 820 name := makeValidIdentifier(v.Name()) 821 assignment := ":=" 822 if !g.scopeTraversalRoots.Has(v.Name()) { 823 name = "_" 824 if g.isErrAssigned { 825 assignment = "=" 826 } 827 } 828 if name == "_" { 829 assignment = "=" 830 } 831 switch expr := expr.(type) { 832 case *model.FunctionCallExpression: 833 switch expr.Name { 834 case pcl.Invoke: 835 // OutputVersionedInvoke does not return an error 836 noError, _, _ := pcl.RecognizeOutputVersionedInvoke(expr) 837 if noError { 838 if name == "_" { 839 assignment = "=" 840 } 841 g.Fgenf(w, "%s %s %.3v;\n", name, assignment, expr) 842 } else { 843 g.Fgenf(w, "%s, err %s %.3v;\n", name, assignment, expr) 844 g.isErrAssigned = true 845 g.Fgenf(w, "if err != nil {\n") 846 g.Fgenf(w, "return err\n") 847 g.Fgenf(w, "}\n") 848 } 849 case pcl.IntrinsicApply: 850 if name == "_" { 851 assignment = "=" 852 } 853 g.Fgenf(w, "%s %s ", name, assignment) 854 g.genApply(w, expr) 855 g.Fgenf(w, "\n") 856 case "join", "mimeType", 857 "fileArchive", "remoteArchive", "assetArchive", 858 "fileAsset", "stringAsset", "remoteAsset", 859 "toBase64": 860 if name == "_" { 861 assignment = "=" 862 } 863 g.Fgenf(w, "%s %s %.3v;\n", name, assignment, expr) 864 case fromBase64Fn: 865 tmpVar := fmt.Sprintf("%s%d", "tmpVar", g.tmpVarCount) 866 g.Fgenf(w, "%s, _ %s %.3v;\n", tmpVar, assignment, expr) 867 if name == "_" { 868 assignment = ":=" 869 } 870 g.Fgenf(w, "%s %s string(%s)\n", name, assignment, tmpVar) 871 g.tmpVarCount++ 872 g.isErrAssigned = true 873 default: 874 g.Fgenf(w, "%s := %.3v;\n", name, expr) 875 } 876 default: 877 g.Fgenf(w, "%s := %.3v;\n", name, expr) 878 879 } 880 } 881 882 func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) { 883 if !g.configCreated { 884 g.Fprint(w, "cfg := config.New(ctx, \"\")\n") 885 g.configCreated = true 886 } 887 888 getType := "" 889 switch v.Type() { 890 case model.StringType: // Already default 891 case model.NumberType: 892 getType = "Float" 893 case model.IntType: 894 getType = "Int" 895 case model.BoolType: 896 getType = "Boolean" 897 case model.DynamicType: 898 getType = "Object" 899 } 900 901 getOrRequire := "Get" 902 if v.DefaultValue == nil { 903 getOrRequire = "Require" 904 } 905 906 name := makeValidIdentifier(v.Name()) 907 if v.DefaultValue == nil { 908 g.Fgenf(w, "%s := cfg.%s%s(\"%s\")\n", name, getOrRequire, getType, v.LogicalName()) 909 } else { 910 expr, temps := g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()) 911 g.genTemps(w, temps) 912 switch expr := expr.(type) { 913 case *model.FunctionCallExpression: 914 switch expr.Name { 915 case pcl.Invoke: 916 g.Fgenf(w, "%s, err := %.3v;\n", name, expr) 917 g.isErrAssigned = true 918 g.Fgenf(w, "if err != nil {\n") 919 g.Fgenf(w, "return err\n") 920 g.Fgenf(w, "}\n") 921 } 922 default: 923 switch v.Type() { 924 // Go will default to interpreting integers (i.e. 3) as ints, even if the config is Number 925 case model.NumberType: 926 g.Fgenf(w, "%s := float64(%.3v);\n", name, expr) 927 default: 928 g.Fgenf(w, "%s := %.3v;\n", name, expr) 929 } 930 } 931 switch v.Type() { 932 case model.StringType: 933 g.Fgenf(w, "if param := cfg.Get(\"%s\"); param != \"\"{\n", v.LogicalName()) 934 case model.NumberType: 935 g.Fgenf(w, "if param := cfg.GetFloat64(\"%s\"); param != 0 {\n", v.LogicalName()) 936 case model.IntType: 937 g.Fgenf(w, "if param := cfg.GetInt(\"%s\"); param != 0 {\n", v.LogicalName()) 938 case model.BoolType: 939 g.Fgenf(w, "if param := cfg.GetBool(\"%s\"); param {\n", v.LogicalName()) 940 default: 941 g.Fgenf(w, "if param := cfg.GetBool(\"%s\"); param != nil {\n", v.LogicalName()) 942 } 943 g.Fgenf(w, "%s = param\n", name) 944 g.Fgen(w, "}\n") 945 } 946 } 947 948 // nolint: lll 949 // useLookupInvokeForm takes a token for an invoke and determines whether to use the 950 // .Get or .Lookup form. The Go SDK has collisions in .Get methods that require renaming. 951 // For instance, gen.go creates a resource getter for AWS VPCs that collides with a function: 952 // GetVPC resource getter: https://github.com/pulumi/pulumi-aws/blob/7835df354694e2f9f23371602a9febebc6b45be8/sdk/go/aws/ec2/getVpc.go#L15 953 // LookupVPC function: https://github.com/pulumi/pulumi-aws/blob/7835df354694e2f9f23371602a9febebc6b45be8/sdk/go/aws/ec2/getVpc.go#L15 954 // Given that the naming here is not consisten, we must reverse the process from gen.go. 955 func (g *generator) useLookupInvokeForm(token string) bool { 956 pkg, module, member, _ := pcl.DecomposeToken(token, *new(hcl.Range)) 957 modSplit := strings.Split(module, "/") 958 mod := modSplit[0] 959 fn := Title(member) 960 if mod == IndexToken && len(modSplit) >= 2 { 961 // e.g. "aws:index/getPartition:getPartition" where module is "index/getPartition" 962 mod = "" 963 fn = Title(modSplit[1]) 964 } else { 965 // e.g. for type "ec2/getVpc:getVpcArgs" 966 if _, has := g.contexts[pkg][mod]; !has { 967 mod = module 968 } 969 } 970 fnLookup := "Lookup" + fn[3:] 971 pkgContext, has := g.contexts[pkg][mod] 972 if has && pkgContext.names.Has(fnLookup) { 973 return true 974 } 975 return false 976 } 977 978 // getModOrAlias attempts to reconstruct the import statement and check if the imported package 979 // is aliased, returning that alias if available. 980 func (g *generator) getModOrAlias(pkg, mod, originalMod string) string { 981 info, ok := g.getGoPackageInfo(pkg) 982 if !ok { 983 return mod 984 } 985 if m, ok := info.ModuleToPackage[mod]; ok { 986 mod = m 987 } 988 989 imp := fmt.Sprintf("%s/%s", info.ImportBasePath, mod) 990 if alias, ok := info.PackageImportAliases[imp]; ok { 991 return alias 992 } else if info.ImportBasePath != "" { 993 if originalMod == "" || originalMod == IndexToken { 994 return path.Base(info.ImportBasePath) 995 } 996 } 997 mod = strings.Split(mod, "/")[0] 998 return mod 999 } 1000 1001 // Go needs complete package definitions in order to properly resolve names. 1002 // 1003 // TODO: naming decisions should really be encoded statically so that they can be decided locally. 1004 func programPackageDefs(program *pcl.Program) ([]*schema.Package, error) { 1005 refs := program.PackageReferences() 1006 defs := make([]*schema.Package, len(refs)) 1007 for i, ref := range refs { 1008 def, err := ref.Definition() 1009 if err != nil { 1010 return nil, err 1011 } 1012 defs[i] = def 1013 } 1014 return defs, nil 1015 }