pulumi/pkg/codegen/python/gen_program.go

1103 lines
33 KiB
Go
Raw Normal View History

// 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"
"sort"
"strings"
2023-05-02 14:15:22 +00:00
"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"
)
2022-08-19 17:27:05 +00:00
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
}
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)
2023-11-14 23:17:01 +00:00
g.Fprintf(w, "calass %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
2023-11-14 23:17:01 +00:00
g.Fgenf(w, "calass %sArrgs(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 := fmt.Sprintf("components:index:%s", componentName)
2023-11-14 23:17:01 +00:00
g.Fgenf(w, "calass %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,
2023-11-14 23:17:01 +00:00
fmt.Sprintf("%sArrgs", componentName))
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
}
// Set the runtime to "python" then marshal to Pulumi.yaml
project.Runtime = workspace.NewProjectRuntimeInfo("python", nil)
projectBytes, err := encoding.YAML.Marshal(project)
if err != nil {
return err
}
files["Pulumi.yaml"] = projectBytes
// Build a requirements.txt based on the packages used by program
var requirementsTxt bytes.Buffer
requirementsTxt.WriteString("pulumi>=3.0.0,<4.0.0\n")
// 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 {
2022-08-19 17:27:05 +00:00
if p.Name == "pulumi" {
continue
}
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 {
fmt.Fprintf(&requirementsTxt, "%s==%s\n", packageName, p.Version.String())
} else {
fmt.Fprintf(&requirementsTxt, "%s\n", packageName)
}
}
files["requirements.txt"] = requirementsTxt.Bytes()
// Add the language specific .gitignore
files[".gitignore"] = []byte(`*.pyc
venv/`)
for filename, data := range files {
outPath := path.Join(directory, filename)
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
err := os.WriteFile(outPath, data, 0o600)
if err != nil {
return fmt.Errorf("could not write output program: %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 {
2022-08-19 17:27:05 +00:00
pcl.FixupPulumiPackageTokens(r)
pkg, _, _, _ := r.DecomposeToken()
2022-08-19 17:27:05 +00:00
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 info, ok := pkg.Language["python"].(PackageInfo); ok && info.PackageName != "" {
packageName = info.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,
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
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, fmt.Sprintf("import %s", pkg))
}
}
if g.isComponent {
// add typing information
imports = append(imports, "from typing import Optional, Dict, TypedDict, Any")
imports = append(imports, "from pulumi import Input")
}
for _, node := range program.Nodes {
if component, ok := node.(*pcl.Component); ok {
componentPath := strings.ReplaceAll(filepath.Base(component.DirPath()), "-", "_")
componentName := component.DeclarationName()
Add install command (#13081) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/10990. This also includes a couple of fixes for python to get the `TestLanguageConvertComponentSmoke` test working for it. Firstly it fixes InstallDependencies to not create a venv if no venv path is set. The language host was trying to install a venv to the same directory as the program itself (that is not nested under "venv" or the like). But then because the runtime option wasn't set the execution wasn't using that created venv anyway. Secondly the imports for components are not relative. We're not in a proper module when running a python program so relative imports to side-by-side component folders don't work. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-10-25 16:03:02 +00:00
imports = append(imports, fmt.Sprintf("from %s import %s", componentPath, componentName))
}
}
// 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()
2022-08-19 17:27:05 +00:00
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 {
pkgInfo := lang.(PackageInfo)
if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
module = m
}
}
}
}
return tokenToQualifiedName(pkg, module, member), diagnostics
}
// 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 {
pkgInfo := lang.(PackageInfo)
if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
modName = m
}
}
2023-11-14 23:17:01 +00:00
return tokenToQualifiedName(pkgName, modName, member) + "Arrgs"
}
// 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)
}
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)
2022-08-19 17:27:05 +00:00
// 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 := fmt.Sprintf("create_%s", 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())
2022-11-02 19:39:57 +00:00
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)
// TODO(pdg): trivia
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 := fmt.Sprintf("not yet implemented: %s", 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...))
}