mirror of https://github.com/pulumi/pulumi.git
1186 lines
35 KiB
Go
1186 lines
35 KiB
Go
// Copyright 2016-2020, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
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
|
|
}
|
|
|
|
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,
|
|
typechecker 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",
|
|
}
|
|
}
|
|
|
|
if typechecker != "" {
|
|
if options == nil {
|
|
options = map[string]interface{}{}
|
|
}
|
|
options["typechecker"] = typechecker
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a typechecker is given we need to list that in the requirements.txt as well
|
|
if typechecker != "" {
|
|
requirementsTxtLines = append(requirementsTxtLines, typechecker)
|
|
}
|
|
|
|
// 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 {
|
|
return typedDictEnabled(pkgInfo.InputTypes)
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// 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...))
|
|
}
|