// 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. package python import ( "bytes" "fmt" "io" "os" "path" "path/filepath" "slices" "sort" "strings" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2" "github.com/pulumi/pulumi/pkg/v3/codegen" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format" "github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax" "github.com/pulumi/pulumi/pkg/v3/codegen/pcl" "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/pulumi/pulumi/sdk/v3/go/common/encoding" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" ) const stackRefQualifiedName = "pulumi.StackReference" type generator struct { // The formatter to use when generating code. *format.Formatter program *pcl.Program diagnostics hcl.Diagnostics configCreated bool quotes map[model.Expression]string isComponent bool // insideTypedDict is used to track if the generator is currently inside a TypedDict so that // nested TypedDicts can be handled correctly. insideTypedDict bool } func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) { pcl.MapProvidersAsResources(program) g, err := newGenerator(program) if err != nil { return nil, nil, err } // Linearize the nodes into an order appropriate for procedural code generation. nodes := pcl.Linearize(program) // Creating a list to store and later print helper methods if they turn out to be needed preambleHelperMethods := codegen.NewStringSet() var main bytes.Buffer g.genPreamble(&main, program, preambleHelperMethods) for _, n := range nodes { g.genNode(&main, n) } files := map[string][]byte{ "__main__.py": main.Bytes(), } for componentDir, component := range program.CollectComponents() { componentFilename := strings.ReplaceAll(filepath.Base(componentDir), "-", "_") componentName := component.DeclarationName() componentGenerator, err := newGenerator(component.Program) if err != nil { return files, componentGenerator.diagnostics, err } // mark the generator to target components componentGenerator.isComponent = true componentPreambleMethods := codegen.NewStringSet() var componentBuffer bytes.Buffer // generate imports for the component componentGenerator.genPreamble(&componentBuffer, component.Program, componentPreambleMethods) componentGenerator.genComponentDefinition(&componentBuffer, component, componentName) files[componentFilename+".py"] = componentBuffer.Bytes() } return files, g.diagnostics, nil } func componentInputElementType(pclType model.Type) string { switch pclType { case model.BoolType: return "bool" case model.IntType: return "int" case model.NumberType: return "float" case model.StringType: return "str" default: switch pclType := pclType.(type) { case *model.ListType: elementType := componentInputElementType(pclType.ElementType) return fmt.Sprintf("list[%s]", elementType) case *model.MapType: elementType := componentInputElementType(pclType.ElementType) return fmt.Sprintf("Dict[str, %s]", elementType) // reduce option(T) to just T // the TypedDict has total=False which means all properties are optional by default case *model.UnionType: if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[0] == model.NoneType { return componentInputElementType(pclType.ElementTypes[1]) } else if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[1] == model.NoneType { return componentInputElementType(pclType.ElementTypes[0]) } else { return "Any" } default: return "Any" } } } // collectObjectTypedConfigVariables returns the object types in config variables need to be emitted // as classes. func collectObjectTypedConfigVariables(component *pcl.Component) map[string]*model.ObjectType { objectTypes := map[string]*model.ObjectType{} for _, config := range component.Program.ConfigVariables() { switch configType := config.Type().(type) { case *model.ObjectType: objectTypes[config.Name()] = configType case *model.ListType: switch elementType := configType.ElementType.(type) { case *model.ObjectType: objectTypes[config.Name()] = elementType } case *model.MapType: switch elementType := configType.ElementType.(type) { case *model.ObjectType: objectTypes[config.Name()] = elementType } } } return objectTypes } func (g *generator) genComponentDefinition(w io.Writer, component *pcl.Component, componentName string) { configVars := component.Program.ConfigVariables() hasAnyInputVariables := len(configVars) > 0 if hasAnyInputVariables { objectTypedConfigs := collectObjectTypedConfigVariables(component) variableNames := pcl.SortedStringKeys(objectTypedConfigs) // generate resource args for this component for _, variableName := range variableNames { objectType := objectTypedConfigs[variableName] objectTypeName := title(variableName) g.Fprintf(w, "class %s(TypedDict, total=False):\n", objectTypeName) g.Indented(func() { propertyNames := pcl.SortedStringKeys(objectType.Properties) for _, propertyName := range propertyNames { propertyType := objectType.Properties[propertyName] inputType := componentInputElementType(propertyType) g.Fprintf(w, "%s%s: Input[%s]\n", g.Indent, propertyName, inputType) } }) g.Fgen(w, "\n") } // emit args class g.Fgenf(w, "class %sArgs(TypedDict, total=False):\n", componentName) g.Indented(func() { // define constructor args for _, configVar := range configVars { argName := configVar.Name() argType := componentInputElementType(configVar.Type()) switch configType := configVar.Type().(type) { case *model.ObjectType: // for objects of type T, generate T as is argType = title(configVar.Name()) case *model.ListType: // for list(T) where T is an object type, generate List[T] switch configType.ElementType.(type) { case *model.ObjectType: objectTypeName := title(configVar.Name()) argType = fmt.Sprintf("list(%s)", objectTypeName) } case *model.MapType: // for map(T) where T is an object type, generate Dict[str, T] switch configType.ElementType.(type) { case *model.ObjectType: objectTypeName := title(configVar.Name()) argType = fmt.Sprintf("Dict[str, %s]", objectTypeName) } } argType = fmt.Sprintf("Input[%s]", argType) g.Fgenf(w, "%s%s: %s", g.Indent, argName, argType) g.Fgen(w, "\n") } }) g.Fgen(w, "\n") } componentToken := "components:index:" + componentName g.Fgenf(w, "class %s(pulumi.ComponentResource):\n", componentName) g.Indented(func() { if hasAnyInputVariables { g.Fgenf(w, "%sdef __init__(self, name: str, args: %s, opts:Optional[pulumi.ResourceOptions] = None):\n", g.Indent, componentName+"Args") g.Fgenf(w, "%s%ssuper().__init__(\"%s\", name, args, opts)\n", g.Indent, g.Indent, componentToken) } else { g.Fgenf(w, "%sdef __init__(self, name: str, opts: Optional[pulumi.ResourceOptions] = None):\n", g.Indent) g.Fgenf(w, "%s%ssuper().__init__(\"%s\", name, {}, opts)\n", g.Indent, g.Indent, componentToken) } g.Fgen(w, "\n") g.Indented(func() { for _, node := range pcl.Linearize(component.Program) { switch node := node.(type) { case *pcl.LocalVariable: g.genLocalVariable(w, node) g.Fgen(w, "\n") case *pcl.Component: // set options { parent = self } for the component resource // where "self" is a reference to the component resource itself if node.Options == nil { node.Options = &pcl.ResourceOptions{} } if node.Options.Parent == nil { node.Options.Parent = model.ConstantReference(&model.Constant{ Name: "self", }) } g.genComponent(w, node) g.Fgen(w, "\n") case *pcl.Resource: // set options { parent = self } for the component resource // where "self" is a reference to the component resource itself if node.Options == nil { node.Options = &pcl.ResourceOptions{} } if node.Options.Parent == nil { node.Options.Parent = model.ConstantReference(&model.Constant{ Name: "self", }) } g.genResource(w, node) g.Fgen(w, "\n") } } outputVars := component.Program.OutputVariables() for _, output := range outputVars { g.Fgenf(w, "%sself.%s = %v\n", g.Indent, output.Name(), output.Value) } if len(outputVars) == 0 { g.Fgenf(w, "%sself.register_outputs()\n", g.Indent) } else { g.Fgenf(w, "%sself.register_outputs({\n", g.Indent) g.Indented(func() { for index, output := range outputVars { g.Fgenf(w, "%s'%s': %v", g.Indent, output.Name(), output.Value) if index != len(outputVars)-1 { g.Fgen(w, ", ") } g.Fgen(w, "\n") } }) g.Fgenf(w, "%s})", g.Indent) } }) }) } func GenerateProject( directory string, project workspace.Project, program *pcl.Program, localDependencies map[string]string, ) error { files, diagnostics, err := GenerateProgram(program) if err != nil { return err } if diagnostics.HasErrors() { return diagnostics } // Check the project for "main" as that changes where we write out files and some relative paths. rootDirectory := directory if project.Main != "" { directory = filepath.Join(rootDirectory, project.Main) // mkdir -p the subdirectory err = os.MkdirAll(directory, 0o700) if err != nil { return fmt.Errorf("create main directory: %w", err) } } var options map[string]interface{} if _, ok := localDependencies["pulumi"]; ok { options = map[string]interface{}{ "virtualenv": "venv", } } // Set the runtime to "python" then marshal to Pulumi.yaml project.Runtime = workspace.NewProjectRuntimeInfo("python", options) projectBytes, err := encoding.YAML.Marshal(project) if err != nil { return err } // Build a requirements.txt based on the packages used by program requirementsTxtLines := []string{} if path, ok := localDependencies["pulumi"]; ok { requirementsTxtLines = append(requirementsTxtLines, path) } else { requirementsTxtLines = append(requirementsTxtLines, "pulumi>=3.0.0,<4.0.0") } // For each package add a PackageReference line // find references from the main/entry program and programs of components packages, err := program.CollectNestedPackageSnapshots() if err != nil { return err } for _, p := range packages { if p.Name == "pulumi" { continue } if path, ok := localDependencies[p.Name]; ok { requirementsTxtLines = append(requirementsTxtLines, path) } else { if err := p.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil { return err } packageName := "pulumi-" + p.Name if langInfo, found := p.Language["python"]; found { pyInfo, ok := langInfo.(PackageInfo) if ok && pyInfo.PackageName != "" { packageName = pyInfo.PackageName } } if p.Version != nil { requirementsTxtLines = append(requirementsTxtLines, fmt.Sprintf("%s==%s", packageName, p.Version.String())) } else { requirementsTxtLines = append(requirementsTxtLines, packageName) } } } // We want the requirements.txt files we generate to be stable, so we sort the // lines before obtaining the bytes. slices.Sort(requirementsTxtLines) files["requirements.txt"] = []byte(strings.Join(requirementsTxtLines, "\n") + "\n") // Add the language specific .gitignore files[".gitignore"] = []byte(`*.pyc venv/`) for filename, data := range files { outPath := path.Join(directory, filename) err := os.WriteFile(outPath, data, 0o600) if err != nil { return fmt.Errorf("could not write output program: %w", err) } } // Write out the Pulumi.yaml err = os.WriteFile(path.Join(rootDirectory, "Pulumi.yaml"), projectBytes, 0o600) if err != nil { return fmt.Errorf("write Pulumi.yaml: %w", err) } return nil } func newGenerator(program *pcl.Program) (*generator, error) { // Import Python-specific schema info. packages, err := program.PackageSnapshots() if err != nil { return nil, err } for _, p := range packages { if err := p.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil { return nil, err } } g := &generator{ program: program, quotes: map[model.Expression]string{}, } g.Formatter = format.NewFormatter(g) return g, nil } // genLeadingTrivia generates the list of leading trivia associated with a given token. func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) { // TODO(pdg): whitespace for _, t := range token.LeadingTrivia { if c, ok := t.(syntax.Comment); ok { g.genComment(w, c) } } } // genTrailingTrivia generates the list of trailing trivia associated with a given token. func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) { // TODO(pdg): whitespace for _, t := range token.TrailingTrivia { if c, ok := t.(syntax.Comment); ok { g.genComment(w, c) } } } // genTrivia generates the list of trivia associated with a given token. func (g *generator) genTrivia(w io.Writer, token syntax.Token) { g.genLeadingTrivia(w, token) g.genTrailingTrivia(w, token) } // genComment generates a comment into the output. func (g *generator) genComment(w io.Writer, comment syntax.Comment) { for _, l := range comment.Lines { g.Fgenf(w, "%s#%s\n", g.Indent, l) } } // rewriteApplyLambdaBody rewrites the body of a lambda where it rewrites the usage of lambda variables // into an index expression of a dictionary. for example lambda arg `value` will become <argsParamName>["value"] func rewriteApplyLambdaBody(applyLambda *model.AnonymousFunctionExpression, argsParamName string) model.Expression { rewriter := func(expr model.Expression) (model.Expression, hcl.Diagnostics) { switch expr := expr.(type) { case *model.ScopeTraversalExpression: if len(expr.Parts) == 1 { // check whether this expression is traversing a lambda arg // rewrite arg into argsParamName["argName"] for _, param := range applyLambda.Signature.Parameters { if param.Name == expr.RootName { return &model.IndexExpression{ Collection: model.VariableReference(&model.Variable{ Name: argsParamName, }), Key: &model.LiteralValueExpression{ Value: cty.StringVal(fmt.Sprintf("'%s'", param.Name)), }, }, nil } } } } return expr, nil } rewrittenBody, _ := model.VisitExpression(applyLambda.Body, model.IdentityVisitor, rewriter) return rewrittenBody } func (g *generator) genPreamble(w io.Writer, program *pcl.Program, preambleHelperMethods codegen.StringSet) { // Print the pulumi import at the top. g.Fprintln(w, "import pulumi") // Accumulate other imports for the various providers. Don't emit them yet, as we need to sort them later on. type Import struct { // Use an "import ${KEY} as ${.Pkg}" ImportAs bool // Only relevant for when ImportAs=true Pkg string } importSet := map[string]Import{} for _, n := range program.Nodes { if r, isResource := n.(*pcl.Resource); isResource { pcl.FixupPulumiPackageTokens(r) pkg, _, _, _ := r.DecomposeToken() if pkg == "pulumi" { continue } packageName := "pulumi_" + makeValidIdentifier(pkg) if r.Schema != nil && r.Schema.PackageReference != nil { pkg, err := r.Schema.PackageReference.Definition() if err == nil { if pkgInfo, ok := pkg.Language["python"].(PackageInfo); ok && pkgInfo.PackageName != "" { packageName = pkgInfo.PackageName } } } importSet[packageName] = Import{ImportAs: true, Pkg: makeValidIdentifier(pkg)} } diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) { if call, ok := n.(*model.FunctionCallExpression); ok { if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" { for _, importPackage := range i { importAs := strings.HasPrefix(importPackage, "pulumi_") var maybePkg string if importAs { maybePkg = importPackage[len("pulumi_"):] } importSet[importPackage] = Import{ ImportAs: importAs, Pkg: maybePkg, } } } if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok { preambleHelperMethods.Add(helperMethodBody) } } return n, nil }) contract.Assertf(len(diags) == 0, "unexpected diagnostics reported: %v", diags) } var imports []string importSetNames := codegen.NewStringSet() for k := range importSet { importSetNames.Add(k) } for _, pkg := range importSetNames.SortedValues() { if pkg == "pulumi" { continue } control := importSet[pkg] if control.ImportAs { imports = append(imports, fmt.Sprintf("import %s as %s", pkg, EnsureKeywordSafe(control.Pkg))) } else { imports = append(imports, "import "+pkg) } } if g.isComponent { // add typing information imports = append(imports, "from typing import Optional, Dict, TypedDict, Any") imports = append(imports, "from pulumi import Input") } seenComponentImports := map[string]bool{} for _, node := range program.Nodes { if component, ok := node.(*pcl.Component); ok { componentPath := strings.ReplaceAll(filepath.Base(component.DirPath()), "-", "_") componentName := component.DeclarationName() pathAndName := componentPath + "-" + componentName if _, ok := seenComponentImports[pathAndName]; !ok { imports = append(imports, fmt.Sprintf("from %s import %s", componentPath, componentName)) seenComponentImports[pathAndName] = true } } } // Now sort the imports and emit them. sort.Strings(imports) for _, i := range imports { g.Fprintln(w, i) } g.Fprint(w, "\n") // If we collected any helper methods that should be added, write them just before the main func for _, preambleHelperMethodBody := range preambleHelperMethods.SortedValues() { g.Fprintf(w, "%s\n\n", preambleHelperMethodBody) } } func (g *generator) genNode(w io.Writer, n pcl.Node) { switch n := n.(type) { case *pcl.Resource: g.genResource(w, n) case *pcl.ConfigVariable: g.genConfigVariable(w, n) case *pcl.LocalVariable: g.genLocalVariable(w, n) case *pcl.OutputVariable: g.genOutputVariable(w, n) case *pcl.Component: g.genComponent(w, n) } } func tokenToQualifiedName(pkg, module, member string) string { components := strings.Split(module, "/") for i, component := range components { components[i] = PyName(component) } module = strings.Join(components, ".") if module != "" { module = "." + module } return fmt.Sprintf("%s%s.%s", PyName(pkg), module, title(member)) } // resourceTypeName computes the qualified name of a python resource. func resourceTypeName(r *pcl.Resource) (string, hcl.Diagnostics) { // Compute the resource type from the Pulumi type token. pkg, module, member, diagnostics := r.DecomposeToken() pcl.FixupPulumiPackageTokens(r) // Normalize module. if r.Schema != nil { pkg, err := r.Schema.PackageReference.Definition() if err != nil { diagnostics = append(diagnostics, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "unable to bind schema for resource", Detail: err.Error(), Subject: r.Definition.Syntax.DefRange().Ptr(), }) } else { err = pkg.ImportLanguages(map[string]schema.Language{"python": Importer}) contract.AssertNoErrorf(err, "failed to import python language plugin for package %s", pkg.Name) if lang, ok := pkg.Language["python"]; ok { if pkgInfo, ok := lang.(PackageInfo); ok { if m, ok := pkgInfo.ModuleNameOverrides[module]; ok { module = m } } } } } return tokenToQualifiedName(pkg, module, member), diagnostics } func (g *generator) typedDictEnabled(expr model.Expression, typ model.Type) bool { schemaType, ok := pcl.GetSchemaForType(typ) if !ok { return false } schemaType = codegen.UnwrapType(schemaType) objType, ok := schemaType.(*schema.ObjectType) if !ok { return false } pkg, err := objType.PackageReference.Definition() contract.AssertNoErrorf(err, "error loading definition for package %q", objType.PackageReference.Name()) if lang, ok := pkg.Language["python"]; ok { if pkgInfo, ok := lang.(PackageInfo); ok { if typedDictEnabled(pkgInfo.InputTypes) { return true } } } return false } // argumentTypeName computes the Python argument class name for the given expression and model type. func (g *generator) argumentTypeName(expr model.Expression, destType model.Type) string { schemaType, ok := pcl.GetSchemaForType(destType) if !ok { return "" } schemaType = codegen.UnwrapType(schemaType) objType, ok := schemaType.(*schema.ObjectType) if !ok { return "" } token := objType.Token tokenRange := expr.SyntaxNode().Range() // Example: aws, s3/BucketLogging, BucketLogging, []Diagnostics pkgName, module, member, diagnostics := pcl.DecomposeToken(token, tokenRange) contract.Assertf(len(diagnostics) == 0, "unexpected diagnostics reported: %v", diagnostics) modName := objType.PackageReference.TokenToModule(token) // Normalize module. pkg, err := objType.PackageReference.Definition() contract.AssertNoErrorf(err, "error loading definition for package %q", objType.PackageReference.Name()) if lang, ok := pkg.Language["python"]; ok { if pkgInfo, ok := lang.(PackageInfo); ok { if m, ok := pkgInfo.ModuleNameOverrides[module]; ok { modName = m } } } return tokenToQualifiedName(pkgName, modName, member) + "Args" } // makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name // and the count variable name, if any. func (g *generator) makeResourceName(baseName, count string) string { if count == "" { if g.isComponent { return fmt.Sprintf(`f"{name}-%s"`, baseName) } return fmt.Sprintf(`"%s"`, baseName) } if g.isComponent { return fmt.Sprintf(`f"{name}-%s-{%s}"`, baseName, count) } return fmt.Sprintf(`f"%s-{%s}"`, baseName, count) } func (g *generator) lowerResourceOptions(opts *pcl.ResourceOptions) (*model.Block, []*quoteTemp) { if opts == nil { return nil, nil } var block *model.Block var temps []*quoteTemp appendOption := func(name string, value model.Expression) { if block == nil { block = &model.Block{ Type: "options", Body: &model.Body{}, } } value, valueTemps := g.lowerExpression(value, value.Type()) temps = append(temps, valueTemps...) block.Body.Items = append(block.Body.Items, &model.Attribute{ Tokens: syntax.NewAttributeTokens(name), Name: name, Value: value, }) } if opts.Parent != nil { appendOption("parent", opts.Parent) } if opts.Provider != nil { appendOption("provider", opts.Provider) } if opts.DependsOn != nil { appendOption("depends_on", opts.DependsOn) } if opts.Protect != nil { appendOption("protect", opts.Protect) } if opts.RetainOnDelete != nil { appendOption("retain_on_delete", opts.RetainOnDelete) } if opts.IgnoreChanges != nil { appendOption("ignore_changes", opts.IgnoreChanges) } if opts.DeletedWith != nil { appendOption("deleted_with", opts.DeletedWith) } return block, temps } func (g *generator) genResourceOptions(w io.Writer, block *model.Block, hasInputs bool) { if block == nil { return } prefix := " " if hasInputs { prefix = "\n" + g.Indent } g.Fprintf(w, ",%sopts = pulumi.ResourceOptions(", prefix) g.Indented(func() { for i, item := range block.Body.Items { if i > 0 { g.Fprintf(w, ",\n%s", g.Indent) } attr := item.(*model.Attribute) g.Fgenf(w, "%s=%v", attr.Name, attr.Value) } }) g.Fprint(w, ")") } // genResourceDeclaration handles the generation of instantiations resources. func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) { qualifiedMemberName, diagnostics := resourceTypeName(r) g.diagnostics = append(g.diagnostics, diagnostics...) optionsBag, temps := g.lowerResourceOptions(r.Options) name := r.LogicalName() nameVar := PyName(r.Name()) if needsDefinition { g.genTrivia(w, r.Definition.Tokens.GetType("")) for _, l := range r.Definition.Tokens.Labels { g.genTrivia(w, l) } g.genTrivia(w, r.Definition.Tokens.GetOpenBrace()) } if r.Schema != nil { for _, input := range r.Inputs { destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: input.Name}) g.diagnostics = append(g.diagnostics, diagnostics...) value, valueTemps := g.lowerExpression(input.Value, destType.(model.Type)) temps = append(temps, valueTemps...) input.Value = value } } g.genTemps(w, temps) instantiate := func(resName string) { g.Fgenf(w, "%s(%s", qualifiedMemberName, resName) indenter := func(f func()) { f() } if len(r.Inputs) > 1 { indenter = g.Indented } indenter(func() { for _, attr := range r.Inputs { propertyName := InitParamName(attr.Name) // special case: pulumi.StackReference requires `stack_name` instead of `name` if qualifiedMemberName == stackRefQualifiedName && propertyName == "name" { propertyName = "stack_name" } if len(r.Inputs) == 1 { g.Fgenf(w, ", %s=%.v", propertyName, attr.Value) } else { g.Fgenf(w, ",\n%s%s=%.v", g.Indent, propertyName, attr.Value) } } g.genResourceOptions(w, optionsBag, len(r.Inputs) != 0) }) g.Fprint(w, ")") } if r.Options != nil && r.Options.Range != nil { rangeExpr := r.Options.Range rangeType := r.Options.Range.Type() if model.ContainsOutputs(rangeType) { loweredRangeExpr, rangeExprTemps := g.lowerExpression(rangeExpr, rangeType) if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion { g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar) } else { g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar) } localFuncName := "create_" + PyName(r.LogicalName()) // Generate a local definition which actually creates the resources g.Fgenf(w, "def %s(range_body):\n", localFuncName) g.Indented(func() { r.Options.Range = model.VariableReference(&model.Variable{ Name: "range_body", VariableType: model.ResolveOutputs(rangeExpr.Type()), }) g.genResourceDeclaration(w, r, false) g.Fgen(w, "\n") }) g.genTemps(w, rangeExprTemps) switch expr := loweredRangeExpr.(type) { case *model.FunctionCallExpression: if expr.Name == pcl.IntrinsicApply { applyArgs, applyLambda := pcl.ParseApplyCall(expr) // Step 1: generate the apply function call: if len(applyArgs) == 1 { // If we only have a single output, just generate a normal `.apply` g.Fgenf(w, "%v.apply(", applyArgs[0]) } else { // Otherwise, generate a call to `pulumi.Output.all([]).apply()`. g.Fgen(w, "pulumi.Output.all(\n") g.Indented(func() { for i, arg := range applyArgs { argName := applyLambda.Signature.Parameters[i].Name g.Fgenf(w, "%s%s=%v", g.Indent, argName, arg) if i < len(applyArgs)-1 { g.Fgen(w, ",") } g.Fgen(w, "\n") } }) g.Fgen(w, ").apply(") } // Step 2: apply lambda function arguments g.Fgen(w, "lambda resolved_outputs:") // Step 3: The function body is where the resources are generated: // The function body is also a non-output value so we rewrite the range of // the resource declaration to this non-output value rewrittenLambdaBody := rewriteApplyLambdaBody(applyLambda, "resolved_outputs") g.Fgenf(w, " %s(%.v))\n", localFuncName, rewrittenLambdaBody) return } // If we have anything else that returns output, just generate a normal `.apply` g.Fgenf(w, "%.20v.apply(%s)\n", loweredRangeExpr, localFuncName) return case *model.ForExpression: // A list generator that contains outputs looks like list(output(T)) // when we pass that list into `Output.all` it returns a list with a single element, // that element is another list of all resolved items // that is why we index the resolved outputs at 0 g.Fgenf(w, "pulumi.Output.all(%v).apply(lambda resolved_outputs: %s(resolved_outputs[0]))\n", rangeExpr, localFuncName) return case *model.TupleConsExpression: // A list that contains outputs looks like list(output(T)) // ideally we want this to be output(list(T)) and then call apply: // so we call pulumi.all to lift the elements of the list, then call apply g.Fgen(w, "pulumi.Output.all(\n") g.Indented(func() { for i, item := range expr.Expressions { g.Fgenf(w, "%s%v", g.Indent, item) if i < len(expr.Expressions)-1 { g.Fgenf(w, ",") } g.Fgen(w, "\n") } }) g.Fgenf(w, ").apply(%s)\n", localFuncName) return default: // If we have anything else that returns output, just generate a normal `.apply` g.Fgenf(w, "%v.apply(%s)\n", rangeExpr, localFuncName) return } } if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion { if needsDefinition { g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar) } g.Fgenf(w, "%sif %.v:\n", g.Indent, rangeExpr) g.Indented(func() { g.Fprintf(w, "%s%s = ", g.Indent, nameVar) instantiate(g.makeResourceName(name, "")) g.Fprint(w, "\n") }) } else { if needsDefinition { g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar) } resKey := "key" if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion { g.Fgenf(w, "%sfor range in [{\"value\": i} for i in range(0, %.v)]:\n", g.Indent, rangeExpr) resKey = "value" } else { g.Fgenf(w, "%sfor range in [{\"key\": k, \"value\": v} for [k, v] in enumerate(%.v)]:\n", g.Indent, rangeExpr) } resName := g.makeResourceName(name, fmt.Sprintf("range['%s']", resKey)) g.Indented(func() { g.Fgenf(w, "%s%s.append(", g.Indent, nameVar) instantiate(resName) g.Fprint(w, ")\n") }) } } else { g.Fgenf(w, "%s%s = ", g.Indent, nameVar) instantiate(g.makeResourceName(name, "")) g.Fprint(w, "\n") } g.genTrivia(w, r.Definition.Tokens.GetCloseBrace()) } // genResource handles the generation of instantiations of resources. func (g *generator) genResource(w io.Writer, r *pcl.Resource) { g.genResourceDeclaration(w, r, true) } // genComponent handles the generation of instantiations of non-builtin resources. func (g *generator) genComponent(w io.Writer, r *pcl.Component) { componentName := r.DeclarationName() optionsBag, temps := g.lowerResourceOptions(r.Options) name := r.LogicalName() nameVar := PyName(r.Name()) g.genTrivia(w, r.Definition.Tokens.GetType("")) for _, l := range r.Definition.Tokens.Labels { g.genTrivia(w, l) } g.genTrivia(w, r.Definition.Tokens.GetOpenBrace()) for _, input := range r.Inputs { value, valueTemps := g.lowerExpression(input.Value, input.Value.Type()) temps = append(temps, valueTemps...) input.Value = value } g.genTemps(w, temps) hasInputVariables := len(r.Program.ConfigVariables()) > 0 instantiate := func(resName string) { if hasInputVariables { g.Fgenf(w, "%s(%s, {\n", componentName, resName) } else { g.Fgenf(w, "%s(%s", componentName, resName) } indenter := func(f func()) { f() } if len(r.Inputs) > 1 { indenter = g.Indented } indenter(func() { for index, attr := range r.Inputs { propertyName := attr.Name if len(r.Inputs) == 1 { g.Fgenf(w, "'%s': %.v", propertyName, attr.Value) } else { g.Fgenf(w, "%s'%s': %.v", g.Indent, propertyName, attr.Value) } if index != len(r.Inputs)-1 { // add comma after each input when that property is not the last g.Fgen(w, ", ") if len(r.Inputs) > 1 { g.Fgen(w, "\n") } } } g.genResourceOptions(w, optionsBag, len(r.Inputs) != 0) }) if hasInputVariables { g.Fgenf(w, "%s})", g.Indent) } else { g.Fgen(w, ")") } } if r.Options != nil && r.Options.Range != nil { rangeExpr := r.Options.Range if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion { g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar) g.Fgenf(w, "%sif %.v:\n", g.Indent, rangeExpr) g.Indented(func() { g.Fprintf(w, "%s%s = ", g.Indent, nameVar) instantiate(g.makeResourceName(name, "")) g.Fprint(w, "\n") }) } else { g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar) resKey := "key" if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion { g.Fgenf(w, "%sfor range in [{\"value\": i} for i in range(0, %.v)]:\n", g.Indent, rangeExpr) resKey = "value" } else { g.Fgenf(w, "%sfor range in [{\"key\": k, \"value\": v} for [k, v] in enumerate(%.v)]:\n", g.Indent, rangeExpr) } resName := g.makeResourceName(name, fmt.Sprintf("range['%s']", resKey)) g.Indented(func() { g.Fgenf(w, "%s%s.append(", g.Indent, nameVar) instantiate(resName) g.Fprint(w, ")\n") }) } } else { g.Fgenf(w, "%s%s = ", g.Indent, nameVar) instantiate(g.makeResourceName(name, "")) g.Fprint(w, "\n") } g.genTrivia(w, r.Definition.Tokens.GetCloseBrace()) } func (g *generator) genTemps(w io.Writer, temps []*quoteTemp) { for _, t := range temps { // TODO(pdg): trivia g.Fgenf(w, "%s%s = %.v\n", g.Indent, t.Name, t.Value) } } func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) { // TODO(pdg): trivia if !g.configCreated { g.Fprintf(w, "%sconfig = pulumi.Config()\n", g.Indent) g.configCreated = true } getType := "_object" switch v.Type() { case model.StringType: getType = "" case model.NumberType: getType = "_float" case model.IntType: getType = "_int" case model.BoolType: getType = "_bool" } getOrRequire := "get" if v.DefaultValue == nil { getOrRequire = "require" } var defaultValue model.Expression var temps []*quoteTemp if v.DefaultValue != nil { defaultValue, temps = g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()) } g.genTemps(w, temps) if v.Description != "" { for _, line := range strings.Split(v.Description, "\n") { g.Fgenf(w, "%s# %s\n", g.Indent, line) } } name := PyName(v.Name()) g.Fgenf(w, "%s%s = config.%s%s(\"%s\")\n", g.Indent, name, getOrRequire, getType, v.LogicalName()) if defaultValue != nil { g.Fgenf(w, "%sif %s is None:\n", g.Indent, name) g.Indented(func() { g.Fgenf(w, "%s%s = %.v\n", g.Indent, name, defaultValue) }) } } func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) { value, temps := g.lowerExpression(v.Definition.Value, v.Type()) g.genTemps(w, temps) g.genTrivia(w, v.Definition.Tokens.Name) g.Fgenf(w, "%s%s = %.v\n", g.Indent, PyName(v.Name()), value) } func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) { value, temps := g.lowerExpression(v.Value, v.Type()) g.genTemps(w, temps) // TODO(pdg): trivia g.Fgenf(w, "%spulumi.export(\"%s\", %.v)\n", g.Indent, v.LogicalName(), value) } func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) { message := "not yet implemented: " + fmt.Sprintf(reason, vs...) g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: message, Detail: message, }) g.Fgenf(w, "(lambda: raise Exception(%q))()", fmt.Sprintf(reason, vs...)) }