pulumi/pkg/codegen/dotnet/gen_program.go
Pat Gavlin 69ba47cff2
[codegen/*] Add support for resource options. (#4925)
The PCL binder has supported resource options for some time, but these
options haven't been used or processed by the various code generators.
These options--particularly the parent and provider options0--are
critical for import codegen. These changes implement the basic set of
options, and add a note about fleshing out the rest as necessary.

One component of these changes is a new rewriter that rewrites property
references into property paths that are understood by the Pulumi engine.
This rewriter is used to preprocess the contents of the `ignoreChanges`
resource option.

These changes also hack around a weakness in the HCL2 type system:
In Go, references to resources should be typed as `hcl2.ResourceType`.
Unfortunately, this breaks the existing collection semantics associated
with resources. Because of this, the Go code generator does not have
enough information to know that it should generate a `[]pulumi.Resource`
for lists of resources. These changes hack around that limitation using
a Go-specific opaque type and some hardcoded comparisons in
`argumentTypeName`.

Fixes #4923.
2020-06-29 16:33:52 -07:00

518 lines
15 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 dotnet
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v2/codegen"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/model/format"
"github.com/pulumi/pulumi/pkg/v2/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v2/codegen/schema"
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
)
type generator struct {
// The formatter to use when generating code.
*format.Formatter
program *hcl2.Program
// C# namespace map per package.
namespaces map[string]map[string]string
// Type names per invoke function token.
functionArgs map[string]string
// Whether awaits are needed, and therefore an async Initialize method should be declared.
asyncInit bool
configCreated bool
diagnostics hcl.Diagnostics
}
func GenerateProgram(program *hcl2.Program) (map[string][]byte, hcl.Diagnostics, error) {
// Linearize the nodes into an order appropriate for procedural code generation.
nodes := hcl2.Linearize(program)
// Import C#-specific schema info.
namespaces := make(map[string]map[string]string)
functionArgs := make(map[string]string)
for _, p := range program.Packages() {
if err := p.ImportLanguages(map[string]schema.Language{"csharp": Importer}); err != nil {
return make(map[string][]byte), nil, err
}
packageNamespaces := p.Language["csharp"].(CSharpPackageInfo).Namespaces
namespaces[p.Name] = packageNamespaces
for _, f := range p.Functions {
if f.Inputs != nil {
functionArgs[f.Inputs.Token] = f.Token
}
}
}
g := &generator{
program: program,
namespaces: namespaces,
functionArgs: functionArgs,
}
g.Formatter = format.NewFormatter(g)
for _, n := range nodes {
if r, ok := n.(*hcl2.Resource); ok && requiresAsyncInit(r) {
g.asyncInit = true
break
}
}
var index bytes.Buffer
g.genPreamble(&index, program)
g.Indented(func() {
// Emit async Initialize if needed
if g.asyncInit {
g.genInitialize(&index, nodes)
}
g.Indented(func() {
for _, n := range nodes {
g.genNode(&index, n)
}
})
})
g.genPostamble(&index, nodes)
files := map[string][]byte{
"MyStack.cs": index.Bytes(),
}
return files, g.diagnostics, nil
}
// genTrivia generates the list of trivia associated with a given token.
func (g *generator) genTrivia(w io.Writer, token syntax.Token) {
for _, t := range token.LeadingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
for _, t := range token.TrailingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
}
// 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)
}
}
// genPreamble generates using statements, class definition and constructor.
func (g *generator) genPreamble(w io.Writer, program *hcl2.Program) {
// Accumulate other using statements for the various providers and packages. Don't emit them yet, as we need
// to sort them later on.
systemUsings := codegen.NewStringSet()
pulumiUsings := codegen.NewStringSet()
for _, n := range program.Nodes {
if r, isResource := n.(*hcl2.Resource); isResource {
pkg, _, _, _ := r.DecomposeToken()
if pkg != "pulumi" {
namespace := namespaceName(g.namespaces[pkg], pkg)
pulumiUsings.Add(fmt.Sprintf("%s = Pulumi.%[1]s", namespace))
}
if r.Options != nil && r.Options.Range != nil {
systemUsings.Add("System.Collections.Generic")
}
}
diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
if call, ok := n.(*model.FunctionCallExpression); ok {
for _, i := range g.genFunctionUsings(call) {
if strings.HasPrefix(i, "System") {
systemUsings.Add(i)
} else {
pulumiUsings.Add(i)
}
}
}
if _, ok := n.(*model.SplatExpression); ok {
systemUsings.Add("System.Linq")
}
return n, nil
})
contract.Assert(len(diags) == 0)
}
if g.asyncInit {
systemUsings.Add("System.Threading.Tasks")
}
for _, pkg := range systemUsings.SortedValues() {
g.Fprintf(w, "using %v;\n", pkg)
}
g.Fprintln(w, `using Pulumi;`)
for _, pkg := range pulumiUsings.SortedValues() {
g.Fprintf(w, "using %v;\n", pkg)
}
g.Fprint(w, "\n")
// Emit Stack class signature
g.Fprint(w, "class MyStack : Stack\n")
g.Fprint(w, "{\n")
g.Fprint(w, " public MyStack()\n")
g.Fprint(w, " {\n")
}
// genInitialize generates the declaration and the call to the async Initialize method, and also fills stack
// outputs from the initialization result.
func (g *generator) genInitialize(w io.Writer, nodes []hcl2.Node) {
g.Indented(func() {
g.Fgenf(w, "%svar dict = Output.Create(Initialize());\n", g.Indent)
for _, n := range nodes {
switch n := n.(type) {
case *hcl2.OutputVariable:
g.Fprintf(w, "%sthis.%s = dict.Apply(dict => dict[\"%s\"]);\n", g.Indent,
propertyName(n.Name()), makeValidIdentifier(n.Name()))
}
}
})
g.Fgenf(w, "%s}\n\n", g.Indent)
g.Fgenf(w, "%sprivate async Task<IDictionary<string, Output<string>>> Initialize()\n", g.Indent)
g.Fgenf(w, "%s{\n", g.Indent)
}
// genPostamble closes the method and the class and declares stack output statements.
func (g *generator) genPostamble(w io.Writer, nodes []hcl2.Node) {
g.Indented(func() {
// Return outputs from Initialize if needed
if g.asyncInit {
// Emit stack output dictionary
g.Indented(func() {
g.Fprintf(w, "\n%sreturn new Dictionary<string, Output<string>>\n%[1]s{\n", g.Indent)
g.Indented(func() {
for _, n := range nodes {
switch n := n.(type) {
case *hcl2.OutputVariable:
g.Fgenf(w, "%s{ \"%s\", %[2]s },\n", g.Indent, n.Name())
}
}
})
g.Fprintf(w, "%s};\n", g.Indent)
})
}
// Close class constructor or Initialize
g.Fprintf(w, "%s}\n\n", g.Indent)
// Emit stack output properties
for _, n := range nodes {
switch n := n.(type) {
case *hcl2.OutputVariable:
g.genOutputProperty(w, n)
}
}
})
g.Fprint(w, "}\n")
}
func (g *generator) genNode(w io.Writer, n hcl2.Node) {
switch n := n.(type) {
case *hcl2.Resource:
g.genResource(w, n)
case *hcl2.ConfigVariable:
g.genConfigVariable(w, n)
case *hcl2.LocalVariable:
g.genLocalVariable(w, n)
case *hcl2.OutputVariable:
g.genOutputAssignment(w, n)
}
}
// requiresAsyncInit returns true if the program requires awaits in the code, and therefore an asynchronous
// method must be declared.
func requiresAsyncInit(r *hcl2.Resource) bool {
if r.Options == nil || r.Options.Range == nil {
return false
}
return model.ContainsPromises(r.Options.Range.Type())
}
// resourceTypeName computes the C# class name for the given resource.
func (g *generator) resourceTypeName(r *hcl2.Resource) string {
// Compute the resource type from the Pulumi type token.
pkg, module, member, diags := r.DecomposeToken()
contract.Assert(len(diags) == 0)
if pkg == "pulumi" && module == "providers" {
pkg, module, member = member, "", "Provider"
}
namespaces := g.namespaces[pkg]
namespaceKey := strings.Replace(module, "/", ".", -1)
rootNamespace := namespaceName(namespaces, pkg)
namespace := namespaceName(namespaces, namespaceKey)
if namespace != "" {
namespace = "." + namespace
}
qualifiedMemberName := fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
return qualifiedMemberName
}
// functionName computes the C# namespace and class name for the given function token.
func (g *generator) functionName(tokenArg model.Expression) (string, string) {
token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString()
tokenRange := tokenArg.SyntaxNode().Range()
// Compute the resource type from the Pulumi type token.
pkg, module, member, diags := hcl2.DecomposeToken(token, tokenRange)
contract.Assert(len(diags) == 0)
namespaces := g.namespaces[pkg]
namespaceKey := strings.Replace(module, "/", ".", -1)
rootNamespace := namespaceName(namespaces, pkg)
namespace := namespaceName(namespaces, namespaceKey)
if namespace != "" {
namespace = "." + namespace
}
return rootNamespace, fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
}
// argumentTypeName computes the C# argument class name for the given expression and model type.
func (g *generator) argumentTypeName(expr model.Expression, destType model.Type) string {
schemaType, ok := hcl2.GetSchemaForType(destType.(model.Type))
if !ok {
return ""
}
objType, ok := schemaType.(*schema.ObjectType)
if !ok {
return ""
}
token := objType.Token
tokenRange := expr.SyntaxNode().Range()
qualifier := "Inputs"
if f, ok := g.functionArgs[token]; ok {
token = f
qualifier = ""
}
pkg, module, member, diags := hcl2.DecomposeToken(token, tokenRange)
contract.Assert(len(diags) == 0)
namespaces := g.namespaces[pkg]
namespaceKey := strings.Split(module, "/")[0]
rootNamespace := namespaceName(namespaces, pkg)
namespace := namespaceName(namespaces, namespaceKey)
if strings.ToLower(namespace) == "index" {
namespace = ""
}
if namespace != "" {
namespace = "." + namespace
}
if qualifier != "" {
namespace = namespace + "." + qualifier
}
member = member + "Args"
return fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
}
// 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 == "" {
return fmt.Sprintf(`"%s"`, baseName)
}
return fmt.Sprintf("$\"%s-{%s}\"", baseName, count)
}
func (g *generator) genResourceOptions(opts *hcl2.ResourceOptions) string {
if opts == nil {
return ""
}
var result bytes.Buffer
appendOption := func(name string, value model.Expression) {
if result.Len() == 0 {
_, err := fmt.Fprintf(&result, ", new CustomResourceOptions\n%s{", g.Indent)
g.Indent += " "
contract.IgnoreError(err)
}
g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
}
if opts.Parent != nil {
appendOption("Parent", opts.Parent)
}
if opts.Provider != nil {
appendOption("Provider", opts.Provider)
}
if opts.DependsOn != nil {
appendOption("DependsOn", opts.DependsOn)
}
if opts.Protect != nil {
appendOption("Protect", opts.Protect)
}
if opts.IgnoreChanges != nil {
appendOption("IgnoreChanges", opts.IgnoreChanges)
}
if result.Len() != 0 {
g.Indent = g.Indent[:len(g.Indent)-4]
_, err := fmt.Fprintf(&result, "\n%s}", g.Indent)
contract.IgnoreError(err)
}
return result.String()
}
// genResource handles the generation of instantiations of non-builtin resources.
func (g *generator) genResource(w io.Writer, r *hcl2.Resource) {
qualifiedMemberName := g.resourceTypeName(r)
// Add conversions to input properties
for _, input := range r.Inputs {
destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: input.Name})
g.diagnostics = append(g.diagnostics, diagnostics...)
input.Value = g.lowerExpression(input.Value, destType.(model.Type))
}
name := r.Name()
variableName := makeValidIdentifier(name)
g.genTrivia(w, r.Definition.Tokens.GetType(""))
for _, l := range r.Definition.Tokens.GetLabels(nil) {
g.genTrivia(w, l)
}
g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())
instantiate := func(resName string) {
g.Fgenf(w, "new %s(%s, new %[1]sArgs\n", qualifiedMemberName, resName)
g.Fgenf(w, "%s{\n", g.Indent)
g.Indented(func() {
for _, attr := range r.Inputs {
g.Fgenf(w, "%s%s =", g.Indent, propertyName(attr.Name))
g.Fgenf(w, " %.v,\n", attr.Value)
}
})
g.Fgenf(w, "%s}%s)", g.Indent, g.genResourceOptions(r.Options))
}
if r.Options != nil && r.Options.Range != nil {
rangeType := model.ResolveOutputs(r.Options.Range.Type())
rangeExpr := g.lowerExpression(r.Options.Range, rangeType)
g.Fgenf(w, "%svar %s = new List<%s>();\n", g.Indent, variableName, qualifiedMemberName)
resKey := "Key"
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
g.Fgenf(w, "%sfor (var rangeIndex = 0; rangeIndex < %.12o; rangeIndex++)\n", g.Indent, rangeExpr)
g.Fgenf(w, "%s{\n", g.Indent)
g.Fgenf(w, "%s var range = new { Value = rangeIndex };\n", g.Indent)
resKey = "Value"
} else {
rangeExpr := &model.FunctionCallExpression{
Name: "entries",
Args: []model.Expression{rangeExpr},
}
g.Fgenf(w, "%sforeach (var range in %.v)\n", g.Indent, rangeExpr)
g.Fgenf(w, "%s{\n", g.Indent)
}
resName := g.makeResourceName(name, "range."+resKey)
g.Indented(func() {
g.Fgenf(w, "%s%s.Add(", g.Indent, variableName)
instantiate(resName)
g.Fgenf(w, ");\n")
})
g.Fgenf(w, "%s}\n", g.Indent)
} else {
g.Fgenf(w, "%svar %s = ", g.Indent, variableName)
instantiate(g.makeResourceName(name, ""))
g.Fgenf(w, ";\n")
}
g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
}
func (g *generator) genConfigVariable(w io.Writer, v *hcl2.ConfigVariable) {
if !g.configCreated {
g.Fprintf(w, "%svar config = new Config();\n", g.Indent)
g.configCreated = true
}
getType := "Object<dynamic>"
switch v.Type() {
case model.StringType:
getType = ""
case model.NumberType, model.IntType:
getType = "Number"
case model.BoolType:
getType = "Boolean"
}
getOrRequire := "Get"
if v.DefaultValue == nil {
getOrRequire = "Require"
}
g.Fgenf(w, "%[1]svar %[2]s = config.%[3]s%[4]s(\"%[2]s\")", g.Indent, v.Name(), getOrRequire, getType)
if v.DefaultValue != nil {
g.Fgenf(w, " ?? %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()))
}
g.Fgenf(w, ";\n")
}
func (g *generator) genLocalVariable(w io.Writer, v *hcl2.LocalVariable) {
// TODO(pdg): trivia
expr := g.lowerExpression(v.Definition.Value, v.Type())
g.Fgenf(w, "%svar %s = %.3v;\n", g.Indent, makeValidIdentifier(v.Name()), expr)
}
func (g *generator) genOutputAssignment(w io.Writer, v *hcl2.OutputVariable) {
if g.asyncInit {
g.Fgenf(w, "%svar %s", g.Indent, makeValidIdentifier(v.Name()))
} else {
g.Fgenf(w, "%sthis.%s", g.Indent, propertyName(v.Name()))
}
g.Fgenf(w, " = %.3v;\n", g.lowerExpression(v.Value, v.Type()))
}
func (g *generator) genOutputProperty(w io.Writer, v *hcl2.OutputVariable) {
// TODO(pdg): trivia
g.Fgenf(w, "%s[Output(\"%s\")]\n", g.Indent, v.Name())
// TODO(msh): derive the element type of the Output from the type of its value.
g.Fgenf(w, "%spublic Output<string> %s { get; set; }\n", g.Indent, propertyName(v.Name()))
}
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, "\"TODO: %s\"", fmt.Sprintf(reason, vs...))
}