* Make `async:true` the default for `invoke` calls (#3750) * Switch away from native grpc impl. (#3728) * Remove usage of the 'deasync' library from @pulumi/pulumi. (#3752) * Only retry as long as we get unavailable back. Anything else continues. (#3769) * Handle all errors for now. (#3781) * Do not assume --yes was present when using pulumi in non-interactive mode (#3793) * Upgrade all paths for sdk and pkg to v2 * Backport C# invoke classes and other recent gen changes (#4288) Adjust C# generation * Replace IDeployment with a sealed class (#4318) Replace IDeployment with a sealed class * .NET: default to args subtype rather than Args.Empty (#4320) * Adding system namespace for Dotnet code gen This is required for using Obsolute attributes for deprecations ``` Iam/InstanceProfile.cs(142,10): error CS0246: The type or namespace name 'ObsoleteAttribute' could not be found (are you missing a using directive or an assembly reference?) [/Users/stack72/code/go/src/github.com/pulumi/pulumi-aws/sdk/dotnet/Pulumi.Aws.csproj] Iam/InstanceProfile.cs(142,10): error CS0246: The type or namespace name 'Obsolete' could not be found (are you missing a using directive or an assembly reference?) [/Users/stack72/code/go/src/github.com/pulumi/pulumi-aws/sdk/dotnet/Pulumi.Aws.csproj] ``` * Fix the nullability of config type properties in C# codegen (#4379)
1341 lines
42 KiB
Go
1341 lines
42 KiB
Go
// Copyright 2016-2018, 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.
|
|
|
|
// Pulling out some of the repeated strings tokens into constants would harm readability, so we just ignore the
|
|
// goconst linter's warning.
|
|
//
|
|
// nolint: lll, goconst
|
|
package python
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/blang/semver"
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen"
|
|
"github.com/pulumi/pulumi/pkg/v2/codegen/schema"
|
|
"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
|
|
)
|
|
|
|
type stringSet map[string]struct{}
|
|
|
|
func (ss stringSet) add(s string) {
|
|
ss[s] = struct{}{}
|
|
}
|
|
|
|
func (ss stringSet) has(s string) bool {
|
|
_, ok := ss[s]
|
|
return ok
|
|
}
|
|
|
|
func title(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
runes := []rune(s)
|
|
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
|
|
}
|
|
|
|
type modContext struct {
|
|
pkg *schema.Package
|
|
mod string
|
|
resources []*schema.Resource
|
|
functions []*schema.Function
|
|
children []*modContext
|
|
snakeCaseToCamelCase map[string]string
|
|
camelCaseToSnakeCase map[string]string
|
|
tool string
|
|
}
|
|
|
|
func tokenToName(tok string) string {
|
|
components := strings.Split(tok, ":")
|
|
contract.Assertf(len(components) == 3, "malformed token %v", tok)
|
|
return title(components[2])
|
|
}
|
|
|
|
func printComment(w io.Writer, comment string, indent string) {
|
|
lines := strings.Split(comment, "\n")
|
|
for len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
if len(lines) == 0 {
|
|
return
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s\"\"\"\n", indent)
|
|
for _, l := range lines {
|
|
if l == "" {
|
|
fmt.Fprintf(w, "\n")
|
|
} else {
|
|
fmt.Fprintf(w, "%s%s\n", indent, l)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "%s\"\"\"\n", indent)
|
|
}
|
|
|
|
func (mod *modContext) genHeader(w io.Writer, needsSDK bool) {
|
|
// Set the encoding to UTF-8, in case the comments contain non-ASCII characters.
|
|
fmt.Fprintf(w, "# coding=utf-8\n")
|
|
|
|
// Emit a standard warning header ("do not edit", etc).
|
|
fmt.Fprintf(w, "# *** WARNING: this file was generated by %v. ***\n", mod.tool)
|
|
fmt.Fprintf(w, "# *** Do not edit by hand unless you're certain you know what you are doing! ***\n\n")
|
|
|
|
// If needed, emit the standard Pulumi SDK import statement.
|
|
if needsSDK {
|
|
rel, err := filepath.Rel(mod.mod, "")
|
|
contract.Assert(err == nil)
|
|
relRoot := path.Dir(rel)
|
|
|
|
fmt.Fprintf(w, "import json\n")
|
|
fmt.Fprintf(w, "import warnings\n")
|
|
fmt.Fprintf(w, "import pulumi\n")
|
|
fmt.Fprintf(w, "import pulumi.runtime\n")
|
|
fmt.Fprintf(w, "from typing import Union\n")
|
|
fmt.Fprintf(w, "from %s import utilities, tables\n", relRoot)
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
|
|
type fs map[string][]byte
|
|
|
|
func (fs fs) add(path string, contents []byte) {
|
|
_, has := fs[path]
|
|
contract.Assertf(!has, "duplicate file: %s", path)
|
|
fs[path] = contents
|
|
}
|
|
|
|
func (mod *modContext) gen(fs fs) error {
|
|
dir := path.Join(pyPack(mod.pkg.Name), mod.mod)
|
|
|
|
var exports []string
|
|
for p := range fs {
|
|
d := path.Dir(p)
|
|
if d == "." {
|
|
d = ""
|
|
}
|
|
if d == dir {
|
|
exports = append(exports, strings.TrimSuffix(path.Base(p), ".py"))
|
|
}
|
|
}
|
|
|
|
addFile := func(name, contents string) {
|
|
p := path.Join(dir, name)
|
|
exports = append(exports, name[:len(name)-len(".py")])
|
|
fs.add(p, []byte(contents))
|
|
}
|
|
|
|
// Ensure that the target module directory contains a README.md file.
|
|
if mod.mod != "" {
|
|
readme := mod.pkg.Description
|
|
if readme != "" && readme[len(readme)-1] != '\n' {
|
|
readme += "\n"
|
|
}
|
|
fs.add(filepath.Join(dir, "README.md"), []byte(readme))
|
|
}
|
|
|
|
// Utilities, config
|
|
switch mod.mod {
|
|
case "":
|
|
buffer := &bytes.Buffer{}
|
|
mod.genHeader(buffer, false)
|
|
fmt.Fprintf(buffer, "%s", utilitiesFile)
|
|
fs.add(filepath.Join(dir, "utilities.py"), buffer.Bytes())
|
|
|
|
case "config":
|
|
if len(mod.pkg.Config) > 0 {
|
|
vars, err := mod.genConfig(mod.pkg.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addFile("vars.py", vars)
|
|
}
|
|
}
|
|
|
|
// Resources
|
|
for _, r := range mod.resources {
|
|
res, err := mod.genResource(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := PyName(tokenToName(r.Token))
|
|
if r.IsProvider {
|
|
name = "provider"
|
|
}
|
|
addFile(name+".py", res)
|
|
}
|
|
|
|
// Functions
|
|
for _, f := range mod.functions {
|
|
fun, err := mod.genFunction(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
addFile(PyName(tokenToName(f.Token))+".py", fun)
|
|
}
|
|
|
|
// Index
|
|
fs.add(path.Join(dir, "__init__.py"), []byte(mod.genInit(exports)))
|
|
return nil
|
|
}
|
|
|
|
// genInit emits an __init__.py module, optionally re-exporting other members or submodules.
|
|
func (mod *modContext) genInit(exports []string) string {
|
|
w := &bytes.Buffer{}
|
|
mod.genHeader(w, false)
|
|
|
|
// If there are subpackages, export them in the __all__ variable.
|
|
if len(mod.children) > 0 {
|
|
sort.Slice(mod.children, func(i, j int) bool {
|
|
return PyName(mod.children[i].mod) < PyName(mod.children[j].mod)
|
|
})
|
|
|
|
fmt.Fprintf(w, "import importlib\n")
|
|
fmt.Fprintf(w, "# Make subpackages available:\n")
|
|
fmt.Fprintf(w, "__all__ = [")
|
|
for i, mod := range mod.children {
|
|
if i > 0 {
|
|
fmt.Fprintf(w, ", ")
|
|
}
|
|
fmt.Fprintf(w, "'%s'", PyName(mod.mod))
|
|
}
|
|
fmt.Fprintf(w, "]\n")
|
|
fmt.Fprintf(w, "for pkg in __all__:\n")
|
|
fmt.Fprintf(w, " if pkg != 'config':\n")
|
|
fmt.Fprintf(w, " importlib.import_module(f'{__name__}.{pkg}')\n")
|
|
}
|
|
|
|
// Now, import anything to export flatly that is a direct export rather than sub-module.
|
|
if len(exports) > 0 {
|
|
if len(mod.children) > 0 {
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
sort.Slice(exports, func(i, j int) bool {
|
|
return PyName(exports[i]) < PyName(exports[j])
|
|
})
|
|
|
|
fmt.Fprintf(w, "# Export this package's modules as members:\n")
|
|
for _, exp := range exports {
|
|
fmt.Fprintf(w, "from .%s import *\n", PyName(exp))
|
|
}
|
|
}
|
|
|
|
return w.String()
|
|
}
|
|
|
|
// emitConfigVariables emits all config vaiables in the given module, returning the resulting file.
|
|
func (mod *modContext) genConfig(variables []*schema.Property) (string, error) {
|
|
w := &bytes.Buffer{}
|
|
mod.genHeader(w, true)
|
|
|
|
// Create a config bag for the variables to pull from.
|
|
fmt.Fprintf(w, "__config__ = pulumi.Config('%s')\n", mod.pkg.Name)
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Emit an entry for all config variables.
|
|
for _, p := range variables {
|
|
configFetch := fmt.Sprintf("__config__.get('%s')", p.Name)
|
|
if p.DefaultValue != nil {
|
|
v, err := getDefaultValue(p.DefaultValue, p.Type)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
configFetch += " or " + v
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s = %s\n", PyName(p.Name), configFetch)
|
|
printComment(w, p.Comment, "")
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
return w.String(), nil
|
|
}
|
|
|
|
func (mod *modContext) genAwaitableType(w io.Writer, obj *schema.ObjectType) string {
|
|
baseName := pyClassName(tokenToName(obj.Token))
|
|
|
|
// Produce a class definition with optional """ comment.
|
|
fmt.Fprintf(w, "class %s:\n", baseName)
|
|
printComment(w, obj.Comment, " ")
|
|
|
|
// Now generate an initializer with properties for all inputs.
|
|
fmt.Fprintf(w, " def __init__(__self__")
|
|
for _, prop := range obj.Properties {
|
|
fmt.Fprintf(w, ", %s=None", PyName(prop.Name))
|
|
}
|
|
fmt.Fprintf(w, "):\n")
|
|
for _, prop := range obj.Properties {
|
|
// Check that required arguments are present. Also check that types are as expected.
|
|
pname := PyName(prop.Name)
|
|
ptype := pyType(prop.Type)
|
|
fmt.Fprintf(w, " if %s and not isinstance(%s, %s):\n", pname, pname, ptype)
|
|
fmt.Fprintf(w, " raise TypeError(\"Expected argument '%s' to be a %s\")\n", pname, ptype)
|
|
|
|
// Now perform the assignment, and follow it with a """ doc comment if there was one found.
|
|
fmt.Fprintf(w, " __self__.%[1]s = %[1]s\n", pname)
|
|
printComment(w, prop.Comment, " ")
|
|
}
|
|
|
|
awaitableName := "Awaitable" + baseName
|
|
|
|
// Produce an awaitable subclass.
|
|
fmt.Fprintf(w, "class %s(%s):\n", awaitableName, baseName)
|
|
|
|
// Emit __await__ and __iter__ in order to make this type awaitable.
|
|
//
|
|
// Note that we need __await__ to be an iterator, but we only want it to return one value. As such, we use
|
|
// `if False: yield` to construct this.
|
|
//
|
|
// We also need the result of __await__ to be a plain, non-awaitable value. We achieve this by returning a new
|
|
// instance of the base class.
|
|
fmt.Fprintf(w, " # pylint: disable=using-constant-test\n")
|
|
fmt.Fprintf(w, " def __await__(self):\n")
|
|
fmt.Fprintf(w, " if False:\n")
|
|
fmt.Fprintf(w, " yield self\n")
|
|
fmt.Fprintf(w, " return %s(\n", baseName)
|
|
for i, prop := range obj.Properties {
|
|
if i > 0 {
|
|
fmt.Fprintf(w, ",\n")
|
|
}
|
|
pname := PyName(prop.Name)
|
|
fmt.Fprintf(w, " %s=self.%s", pname, pname)
|
|
}
|
|
fmt.Fprintf(w, ")\n")
|
|
|
|
return awaitableName
|
|
}
|
|
|
|
func (mod *modContext) genResource(res *schema.Resource) (string, error) {
|
|
w := &bytes.Buffer{}
|
|
mod.genHeader(w, true)
|
|
|
|
baseType := "pulumi.CustomResource"
|
|
if res.IsProvider {
|
|
baseType = "pulumi.ProviderResource"
|
|
}
|
|
|
|
if !res.IsProvider && res.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, "warnings.warn(\"%s\", DeprecationWarning)\n", res.DeprecationMessage)
|
|
}
|
|
|
|
name := pyClassName(tokenToName(res.Token))
|
|
if res.IsProvider {
|
|
name = "Provider"
|
|
}
|
|
|
|
// Produce a class definition with optional """ comment.
|
|
fmt.Fprintf(w, "class %s(%s):\n", name, baseType)
|
|
for _, prop := range res.Properties {
|
|
name := PyName(prop.Name)
|
|
ty := pyType(prop.Type)
|
|
fmt.Fprintf(w, " %s: pulumi.Output[%s]\n", name, ty)
|
|
if prop.Comment != "" {
|
|
doc := prop.Comment
|
|
|
|
nested := nestedStructure(prop.Type)
|
|
if len(nested) > 0 {
|
|
doc = fmt.Sprintf("%s\n", doc)
|
|
|
|
b := &bytes.Buffer{}
|
|
mod.genNestedStructureBullets(b, nested, " ", false /*wrapInput*/)
|
|
doc = fmt.Sprintf("%s%s", doc, b.String())
|
|
}
|
|
|
|
printComment(w, doc, " ")
|
|
}
|
|
}
|
|
|
|
if res.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, " warnings.warn(\"%s\", DeprecationWarning)\n", res.DeprecationMessage)
|
|
}
|
|
// Now generate an initializer with arguments for all input properties.
|
|
fmt.Fprintf(w, " def __init__(__self__, resource_name, opts=None")
|
|
|
|
// If there's an argument type, emit it.
|
|
for _, prop := range res.InputProperties {
|
|
fmt.Fprintf(w, ", %s=None", PyName(prop.Name))
|
|
}
|
|
|
|
// Old versions of TFGen emitted parameters named __name__ and __opts__. In order to preserve backwards
|
|
// compatibility, we still emit them, but we don't emit documentation for them.
|
|
fmt.Fprintf(w, ", __props__=None, __name__=None, __opts__=None):\n")
|
|
mod.genInitDocstring(w, res)
|
|
if res.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, res.DeprecationMessage)
|
|
}
|
|
fmt.Fprintf(w, " if __name__ is not None:\n")
|
|
fmt.Fprintf(w, " warnings.warn(\"explicit use of __name__ is deprecated\", DeprecationWarning)\n")
|
|
fmt.Fprintf(w, " resource_name = __name__\n")
|
|
fmt.Fprintf(w, " if __opts__ is not None:\n")
|
|
fmt.Fprintf(w, " warnings.warn(\"explicit use of __opts__ is deprecated, use 'opts' instead\", DeprecationWarning)\n")
|
|
fmt.Fprintf(w, " opts = __opts__\n")
|
|
fmt.Fprintf(w, " if opts is None:\n")
|
|
fmt.Fprintf(w, " opts = pulumi.ResourceOptions()\n")
|
|
fmt.Fprintf(w, " if not isinstance(opts, pulumi.ResourceOptions):\n")
|
|
fmt.Fprintf(w, " raise TypeError('Expected resource options to be a ResourceOptions instance')\n")
|
|
fmt.Fprintf(w, " if opts.version is None:\n")
|
|
fmt.Fprintf(w, " opts.version = utilities.get_version()\n")
|
|
fmt.Fprintf(w, " if opts.id is None:\n")
|
|
fmt.Fprintf(w, " if __props__ is not None:\n")
|
|
fmt.Fprintf(w, " raise TypeError(")
|
|
fmt.Fprintf(w, "'__props__ is only valid when passed in combination with a valid opts.id to get an existing resource')\n")
|
|
fmt.Fprintf(w, " __props__ = dict()\n\n")
|
|
fmt.Fprintf(w, "")
|
|
|
|
ins := stringSet{}
|
|
for _, prop := range res.InputProperties {
|
|
pname := PyName(prop.Name)
|
|
|
|
// Fill in computed defaults for arguments.
|
|
if prop.DefaultValue != nil {
|
|
dv, err := getDefaultValue(prop.DefaultValue, prop.Type)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fmt.Fprintf(w, " if %s is None:\n", pname)
|
|
fmt.Fprintf(w, " %s = %s\n", pname, dv)
|
|
}
|
|
|
|
// Check that required arguments are present.
|
|
if prop.IsRequired {
|
|
fmt.Fprintf(w, " if %s is None:\n", pname)
|
|
fmt.Fprintf(w, " raise TypeError(\"Missing required property '%s'\")\n", pname)
|
|
}
|
|
|
|
// And add it to the dictionary.
|
|
arg := pname
|
|
|
|
// If this resource is a provider then, regardless of the schema of the underlying provider
|
|
// type, we must project all properties as strings. For all properties that are not strings,
|
|
// we'll marshal them to JSON and use the JSON string as a string input.
|
|
//
|
|
// Note the use of the `json` package here - we must import it at the top of the file so
|
|
// that we can use it.
|
|
if res.IsProvider && !isStringType(prop.Type) {
|
|
arg = fmt.Sprintf("pulumi.Output.from_input(%s).apply(json.dumps) if %s is not None else None", arg, arg)
|
|
}
|
|
fmt.Fprintf(w, " __props__['%s'] = %s\n", pname, arg)
|
|
|
|
ins.add(prop.Name)
|
|
}
|
|
|
|
for _, prop := range res.Properties {
|
|
// Default any pure output properties to None. This ensures they are available as properties, even if
|
|
// they don't ever get assigned a real value, and get documentation if available.
|
|
if !ins.has(prop.Name) {
|
|
fmt.Fprintf(w, " __props__['%s'] = None\n", PyName(prop.Name))
|
|
}
|
|
}
|
|
|
|
if len(res.Aliases) > 0 {
|
|
fmt.Fprintf(w, ` alias_opts = pulumi.ResourceOptions(aliases=[`)
|
|
|
|
for i, alias := range res.Aliases {
|
|
if i > 0 {
|
|
fmt.Fprintf(w, ", ")
|
|
}
|
|
mod.writeAlias(w, alias)
|
|
}
|
|
|
|
fmt.Fprintf(w, "])\n")
|
|
fmt.Fprintf(w, " opts = pulumi.ResourceOptions.merge(opts, alias_opts)\n")
|
|
}
|
|
|
|
// Finally, chain to the base constructor, which will actually register the resource.
|
|
tok := res.Token
|
|
if res.IsProvider {
|
|
tok = mod.pkg.Name
|
|
}
|
|
fmt.Fprintf(w, " super(%s, __self__).__init__(\n", name)
|
|
fmt.Fprintf(w, " '%s',\n", tok)
|
|
fmt.Fprintf(w, " resource_name,\n")
|
|
fmt.Fprintf(w, " __props__,\n")
|
|
fmt.Fprintf(w, " opts)\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
if !res.IsProvider {
|
|
fmt.Fprintf(w, " @staticmethod\n")
|
|
fmt.Fprintf(w, " def get(resource_name, id, opts=None")
|
|
|
|
if res.StateInputs != nil {
|
|
for _, prop := range res.StateInputs.Properties {
|
|
fmt.Fprintf(w, ", %[1]s=None", PyName(prop.Name))
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "):\n")
|
|
mod.genGetDocstring(w, res)
|
|
fmt.Fprintf(w,
|
|
" opts = pulumi.ResourceOptions.merge(opts, pulumi.ResourceOptions(id=id))\n")
|
|
fmt.Fprintf(w, "\n")
|
|
fmt.Fprintf(w, " __props__ = dict()\n\n")
|
|
|
|
if res.StateInputs != nil {
|
|
for _, prop := range res.StateInputs.Properties {
|
|
fmt.Fprintf(w, " __props__[\"%[1]s\"] = %[1]s\n", PyName(prop.Name))
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(w, " return %s(resource_name, opts=opts, __props__=__props__)\n", name)
|
|
}
|
|
|
|
// Override translate_{input|output}_property on each resource to translate between snake case and
|
|
// camel case when interacting with tfbridge.
|
|
fmt.Fprintf(w,
|
|
` def translate_output_property(self, prop):
|
|
return tables._CAMEL_TO_SNAKE_CASE_TABLE.get(prop) or prop
|
|
|
|
def translate_input_property(self, prop):
|
|
return tables._SNAKE_TO_CAMEL_CASE_TABLE.get(prop) or prop
|
|
|
|
`)
|
|
|
|
return w.String(), nil
|
|
}
|
|
|
|
func (mod *modContext) writeAlias(w io.Writer, alias *schema.Alias) {
|
|
fmt.Fprint(w, "pulumi.Alias(")
|
|
parts := []string{}
|
|
if alias.Name != nil {
|
|
parts = append(parts, fmt.Sprintf("name=\"%v\"", *alias.Name))
|
|
}
|
|
if alias.Project != nil {
|
|
parts = append(parts, fmt.Sprintf("project=\"%v\"", *alias.Project))
|
|
}
|
|
if alias.Type != nil {
|
|
parts = append(parts, fmt.Sprintf("type_=\"%v\"", *alias.Type))
|
|
}
|
|
|
|
for i, part := range parts {
|
|
if i > 0 {
|
|
fmt.Fprint(w, ", ")
|
|
}
|
|
fmt.Fprint(w, part)
|
|
}
|
|
fmt.Fprint(w, ")")
|
|
}
|
|
|
|
func (mod *modContext) genFunction(fun *schema.Function) (string, error) {
|
|
w := &bytes.Buffer{}
|
|
mod.genHeader(w, true)
|
|
|
|
name := PyName(tokenToName(fun.Token))
|
|
|
|
if fun.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, "warnings.warn(\"%s\", DeprecationWarning)\n", fun.DeprecationMessage)
|
|
}
|
|
|
|
// If there is a return type, emit it.
|
|
retTypeName := ""
|
|
var rets []*schema.Property
|
|
if fun.Outputs != nil {
|
|
retTypeName, rets = mod.genAwaitableType(w, fun.Outputs), fun.Outputs.Properties
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
var args []*schema.Property
|
|
if fun.Inputs != nil {
|
|
args = fun.Inputs.Properties
|
|
}
|
|
|
|
// Write out the function signature.
|
|
fmt.Fprintf(w, "def %s(", name)
|
|
for _, arg := range args {
|
|
fmt.Fprintf(w, "%s=None,", PyName(arg.Name))
|
|
}
|
|
fmt.Fprintf(w, "opts=None")
|
|
fmt.Fprintf(w, "):\n")
|
|
|
|
// If this func has documentation, write it at the top of the docstring, otherwise use a generic comment.
|
|
docs := &bytes.Buffer{}
|
|
if fun.Comment != "" {
|
|
fmt.Fprintln(docs, codegen.StripNonRelevantExamples(fun.Comment, "python"))
|
|
} else {
|
|
fmt.Fprintln(docs, "Use this data source to access information about an existing resource.")
|
|
}
|
|
if len(args) > 0 {
|
|
fmt.Fprintln(docs, "")
|
|
for _, arg := range args {
|
|
mod.genPropDocstring(docs, arg, false /*wrapInputs*/)
|
|
}
|
|
|
|
// Nested structures are typed as `dict` so we include some extra documentation for these structures.
|
|
mod.genNestedStructuresDocstring(docs, args, false /*wrapInput*/)
|
|
}
|
|
printComment(w, docs.String(), " ")
|
|
|
|
if fun.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, fun.DeprecationMessage)
|
|
}
|
|
|
|
// Copy the function arguments into a dictionary.
|
|
fmt.Fprintf(w, " __args__ = dict()\n\n")
|
|
fmt.Fprintf(w, "\n")
|
|
for _, arg := range args {
|
|
// TODO: args validation.
|
|
fmt.Fprintf(w, " __args__['%s'] = %s\n", arg.Name, PyName(arg.Name))
|
|
}
|
|
|
|
// If the caller explicitly specified a version, use it, otherwise inject this package's version.
|
|
fmt.Fprintf(w, " if opts is None:\n")
|
|
fmt.Fprintf(w, " opts = pulumi.InvokeOptions()\n")
|
|
fmt.Fprintf(w, " if opts.version is None:\n")
|
|
fmt.Fprintf(w, " opts.version = utilities.get_version()\n")
|
|
|
|
// Now simply invoke the runtime function with the arguments.
|
|
fmt.Fprintf(w, " __ret__ = pulumi.runtime.invoke('%s', __args__, opts=opts).value\n", fun.Token)
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// And copy the results to an object, if there are indeed any expected returns.
|
|
if fun.Outputs != nil {
|
|
fmt.Fprintf(w, " return %s(\n", retTypeName)
|
|
for i, ret := range rets {
|
|
fmt.Fprintf(w, " %s=__ret__.get('%s')", PyName(ret.Name), ret.Name)
|
|
if i == len(rets)-1 {
|
|
fmt.Fprintf(w, ")\n")
|
|
} else {
|
|
fmt.Fprintf(w, ",\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
return w.String(), nil
|
|
}
|
|
|
|
var requirementRegex = regexp.MustCompile(`^>=([^,]+),<[^,]+$`)
|
|
var pep440AlphaRegex = regexp.MustCompile(`^(\d+\.\d+\.\d)+a(\d+)$`)
|
|
var pep440BetaRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)b(\d+)$`)
|
|
var pep440RCRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)rc(\d+)$`)
|
|
var pep440DevRegex = regexp.MustCompile(`^(\d+\.\d+\.\d+)\.dev(\d+)$`)
|
|
|
|
var oldestAllowedPulumi = semver.Version{
|
|
Major: 0,
|
|
Minor: 17,
|
|
Patch: 28,
|
|
}
|
|
|
|
func sanitizePackageDescription(description string) string {
|
|
lines := strings.SplitN(description, "\n", 2)
|
|
if len(lines) > 0 {
|
|
return lines[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// genPackageMetadata generates all the non-code metadata required by a Pulumi package.
|
|
func genPackageMetadata(tool string, pkg *schema.Package, requires map[string]string) (string, error) {
|
|
w := &bytes.Buffer{}
|
|
(&modContext{tool: tool}).genHeader(w, false)
|
|
|
|
// Now create a standard Python package from the metadata.
|
|
fmt.Fprintf(w, "import errno\n")
|
|
fmt.Fprintf(w, "from setuptools import setup, find_packages\n")
|
|
fmt.Fprintf(w, "from setuptools.command.install import install\n")
|
|
fmt.Fprintf(w, "from subprocess import check_call\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Create a command that will install the Pulumi plugin for this resource provider.
|
|
fmt.Fprintf(w, "class InstallPluginCommand(install):\n")
|
|
fmt.Fprintf(w, " def run(self):\n")
|
|
fmt.Fprintf(w, " install.run(self)\n")
|
|
fmt.Fprintf(w, " try:\n")
|
|
fmt.Fprintf(w, " check_call(['pulumi', 'plugin', 'install', 'resource', '%s', '${PLUGIN_VERSION}'])\n", pkg.Name)
|
|
fmt.Fprintf(w, " except OSError as error:\n")
|
|
fmt.Fprintf(w, " if error.errno == errno.ENOENT:\n")
|
|
fmt.Fprintf(w, " print(\"\"\"\n")
|
|
fmt.Fprintf(w, " There was an error installing the %s resource provider plugin.\n", pkg.Name)
|
|
fmt.Fprintf(w, " It looks like `pulumi` is not installed on your system.\n")
|
|
fmt.Fprintf(w, " Please visit https://pulumi.com/ to install the Pulumi CLI.\n")
|
|
fmt.Fprintf(w, " You may try manually installing the plugin by running\n")
|
|
fmt.Fprintf(w, " `pulumi plugin install resource %s ${PLUGIN_VERSION}`\n", pkg.Name)
|
|
fmt.Fprintf(w, " \"\"\")\n")
|
|
fmt.Fprintf(w, " else:\n")
|
|
fmt.Fprintf(w, " raise\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Generate a readme method which will load README.rst, we use this to fill out the
|
|
// long_description field in the setup call.
|
|
fmt.Fprintf(w, "def readme():\n")
|
|
fmt.Fprintf(w, " with open('README.md', encoding='utf-8') as f:\n")
|
|
fmt.Fprintf(w, " return f.read()\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Finally, the actual setup part.
|
|
fmt.Fprintf(w, "setup(name='%s',\n", pyPack(pkg.Name))
|
|
fmt.Fprintf(w, " version='${VERSION}',\n")
|
|
if pkg.Description != "" {
|
|
fmt.Fprintf(w, " description=%q,\n", sanitizePackageDescription(pkg.Description))
|
|
}
|
|
fmt.Fprintf(w, " long_description=readme(),\n")
|
|
fmt.Fprintf(w, " long_description_content_type='text/markdown',\n")
|
|
fmt.Fprintf(w, " cmdclass={\n")
|
|
fmt.Fprintf(w, " 'install': InstallPluginCommand,\n")
|
|
fmt.Fprintf(w, " },\n")
|
|
if pkg.Keywords != nil {
|
|
fmt.Fprintf(w, " keywords='")
|
|
for i, kw := range pkg.Keywords {
|
|
if i > 0 {
|
|
fmt.Fprint(w, " ")
|
|
}
|
|
fmt.Fprint(w, kw)
|
|
}
|
|
fmt.Fprintf(w, "',\n")
|
|
}
|
|
if pkg.Homepage != "" {
|
|
fmt.Fprintf(w, " url='%s',\n", pkg.Homepage)
|
|
}
|
|
if pkg.Repository != "" {
|
|
fmt.Fprintf(w, " project_urls={\n")
|
|
fmt.Fprintf(w, " 'Repository': '%s'\n", pkg.Repository)
|
|
fmt.Fprintf(w, " },\n")
|
|
}
|
|
if pkg.License != "" {
|
|
fmt.Fprintf(w, " license='%s',\n", pkg.License)
|
|
}
|
|
fmt.Fprintf(w, " packages=find_packages(),\n")
|
|
|
|
// Publish type metadata: PEP 561
|
|
fmt.Fprintf(w, " package_data={\n")
|
|
fmt.Fprintf(w, " '%s': [\n", pyPack(pkg.Name))
|
|
fmt.Fprintf(w, " 'py.typed'\n")
|
|
fmt.Fprintf(w, " ]\n")
|
|
fmt.Fprintf(w, " },\n")
|
|
|
|
// Ensure that the Pulumi SDK has an entry if not specified. If the SDK _is_ specified, ensure
|
|
// that it specifies an acceptable version range.
|
|
if pulumiReq, ok := requires["pulumi"]; ok {
|
|
// We expect a specific pattern of ">=version,<version" here.
|
|
matches := requirementRegex.FindStringSubmatch(pulumiReq)
|
|
if len(matches) != 2 {
|
|
return "", errors.Errorf("invalid requirement specifier \"%s\"; expected \">=version1,<version2\"", pulumiReq)
|
|
}
|
|
|
|
lowerBound, err := pep440VersionToSemver(matches[1])
|
|
if err != nil {
|
|
return "", errors.Errorf("invalid version for lower bound: %v", err)
|
|
}
|
|
if lowerBound.LT(oldestAllowedPulumi) {
|
|
return "", errors.Errorf("lower version bound must be at least %v", oldestAllowedPulumi)
|
|
}
|
|
} else {
|
|
if requires == nil {
|
|
requires = map[string]string{}
|
|
}
|
|
requires["pulumi"] = ""
|
|
}
|
|
|
|
// Sort the entries so they are deterministic.
|
|
reqNames := []string{
|
|
"semver>=2.8.1",
|
|
"parver>=0.2.1",
|
|
}
|
|
for req := range requires {
|
|
reqNames = append(reqNames, req)
|
|
}
|
|
sort.Strings(reqNames)
|
|
|
|
fmt.Fprintf(w, " install_requires=[\n")
|
|
for i, req := range reqNames {
|
|
var comma string
|
|
if i < len(reqNames)-1 {
|
|
comma = ","
|
|
}
|
|
fmt.Fprintf(w, " '%s%s'%s\n", req, requires[req], comma)
|
|
}
|
|
fmt.Fprintf(w, " ],\n")
|
|
|
|
fmt.Fprintf(w, " zip_safe=False)\n")
|
|
return w.String(), nil
|
|
}
|
|
|
|
func pep440VersionToSemver(v string) (semver.Version, error) {
|
|
switch {
|
|
case pep440AlphaRegex.MatchString(v):
|
|
parts := pep440AlphaRegex.FindStringSubmatch(v)
|
|
v = parts[1] + "-alpha." + parts[2]
|
|
case pep440BetaRegex.MatchString(v):
|
|
parts := pep440BetaRegex.FindStringSubmatch(v)
|
|
v = parts[1] + "-beta." + parts[2]
|
|
case pep440RCRegex.MatchString(v):
|
|
parts := pep440RCRegex.FindStringSubmatch(v)
|
|
v = parts[1] + "-rc." + parts[2]
|
|
case pep440DevRegex.MatchString(v):
|
|
parts := pep440DevRegex.FindStringSubmatch(v)
|
|
v = parts[1] + "-dev." + parts[2]
|
|
}
|
|
|
|
return semver.ParseTolerant(v)
|
|
}
|
|
|
|
// Emits property conversion tables for all properties recorded using `recordProperty`. The two tables emitted here are
|
|
// used to convert to and from snake case and camel case.
|
|
func (mod *modContext) genPropertyConversionTables() string {
|
|
w := &bytes.Buffer{}
|
|
mod.genHeader(w, false)
|
|
|
|
var allKeys []string
|
|
for key := range mod.snakeCaseToCamelCase {
|
|
allKeys = append(allKeys, key)
|
|
}
|
|
sort.Strings(allKeys)
|
|
|
|
fmt.Fprintf(w, "_SNAKE_TO_CAMEL_CASE_TABLE = {\n")
|
|
for _, key := range allKeys {
|
|
value := mod.snakeCaseToCamelCase[key]
|
|
if key != value {
|
|
fmt.Fprintf(w, " %q: %q,\n", key, value)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "}\n")
|
|
fmt.Fprintf(w, "\n_CAMEL_TO_SNAKE_CASE_TABLE = {\n")
|
|
for _, value := range allKeys {
|
|
key := mod.snakeCaseToCamelCase[value]
|
|
if key != value {
|
|
fmt.Fprintf(w, " %q: %q,\n", key, value)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "}\n")
|
|
return w.String()
|
|
}
|
|
|
|
type propertyInfo struct {
|
|
MapCase bool `json:"mapCase,omitempty"`
|
|
}
|
|
|
|
// recordProperty records the given property's name and member names. For each property name contained in the given
|
|
// property, the name is converted to snake case and recorded in the snake case to camel case table.
|
|
//
|
|
// Once all resources have been emitted, the table is written out to a format usable for implementations of
|
|
// translate_input_property and translate_output_property.
|
|
func (mod *modContext) recordProperty(prop *schema.Property) error {
|
|
mapCase := true
|
|
if python, ok := prop.Language["python"]; ok {
|
|
var info propertyInfo
|
|
if err := json.Unmarshal([]byte(python), &info); err != nil {
|
|
return errors.Wrap(err, "decoding python property info")
|
|
}
|
|
mapCase = info.MapCase
|
|
}
|
|
|
|
if mapCase {
|
|
snakeCaseName := PyName(prop.Name)
|
|
mod.snakeCaseToCamelCase[snakeCaseName] = prop.Name
|
|
mod.camelCaseToSnakeCase[prop.Name] = snakeCaseName
|
|
}
|
|
|
|
propType := prop.Type.String()
|
|
if obj, ok := prop.Type.(*schema.ObjectType); ok {
|
|
for _, p := range obj.Properties {
|
|
// Skip the nested type's property if the property's type is the same
|
|
// as that of the nested type itself.
|
|
// For example, the JSONSchemaProps type in Kubernetes has properties whose
|
|
// type is itself. This can lead to infinite recursion.
|
|
if p.Type.String() == propType {
|
|
continue
|
|
}
|
|
|
|
if err := mod.recordProperty(p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// genInitDocstring emits the docstring for the __init__ method of the given resource type.
|
|
//
|
|
// Sphinx (the documentation generator that we use to generate Python docs) does not draw a
|
|
// distinction between documentation comments on the class itself and documentation comments on the
|
|
// __init__ method of a class. The docs repo instructs Sphinx to concatenate the two together, which
|
|
// means that we don't need to emit docstrings on the class at all as long as the __init__ docstring
|
|
// is good enough.
|
|
//
|
|
// The docstring we generate here describes both the class itself and the arguments to the class's
|
|
// constructor. The format of the docstring is in "Sphinx form":
|
|
// 1. Parameters are introduced using the syntax ":param <type> <name>: <comment>". Sphinx parses this and uses it
|
|
// to populate the list of parameters for this function.
|
|
// 2. The doc string of parameters is expected to be indented to the same indentation as the type of the parameter.
|
|
// Sphinx will complain and make mistakes if this is not the case.
|
|
// 3. The doc string can't have random newlines in it, or Sphinx will complain.
|
|
//
|
|
// This function does the best it can to navigate these constraints and produce a docstring that
|
|
// Sphinx can make sense of.
|
|
func (mod *modContext) genInitDocstring(w io.Writer, res *schema.Resource) {
|
|
// b contains the full text of the docstring, without the leading and trailing triple quotes.
|
|
b := &bytes.Buffer{}
|
|
|
|
// If this resource has documentation, write it at the top of the docstring, otherwise use a generic comment.
|
|
if res.Comment != "" {
|
|
fmt.Fprintln(b, codegen.StripNonRelevantExamples(res.Comment, "python"))
|
|
} else {
|
|
fmt.Fprintf(b, "Create a %s resource with the given unique name, props, and options.\n", tokenToName(res.Token))
|
|
}
|
|
|
|
// All resources have a resource_name parameter and opts parameter.
|
|
fmt.Fprintln(b, ":param str resource_name: The name of the resource.")
|
|
fmt.Fprintln(b, ":param pulumi.ResourceOptions opts: Options for the resource.")
|
|
for _, prop := range res.InputProperties {
|
|
mod.genPropDocstring(b, prop, true /*wrapInput*/)
|
|
}
|
|
|
|
// Nested structures are typed as `dict` so we include some extra documentation for these structures.
|
|
mod.genNestedStructuresDocstring(b, res.InputProperties, true /*wrapInput*/)
|
|
|
|
// printComment handles the prefix and triple quotes.
|
|
printComment(w, b.String(), " ")
|
|
}
|
|
|
|
func (mod *modContext) genGetDocstring(w io.Writer, res *schema.Resource) {
|
|
// "buf" contains the full text of the docstring, without the leading and trailing triple quotes.
|
|
b := &bytes.Buffer{}
|
|
|
|
fmt.Fprintf(b, "Get an existing %s resource's state with the given name, id, and optional extra\n"+
|
|
"properties used to qualify the lookup.\n", tokenToName(res.Token))
|
|
fmt.Fprintln(b, "")
|
|
|
|
fmt.Fprintln(b, ":param str resource_name: The unique name of the resulting resource.")
|
|
fmt.Fprintln(b, ":param str id: The unique provider ID of the resource to lookup.")
|
|
fmt.Fprintln(b, ":param pulumi.ResourceOptions opts: Options for the resource.")
|
|
for _, prop := range res.StateInputs.Properties {
|
|
mod.genPropDocstring(b, prop, true /*wrapInput*/)
|
|
}
|
|
|
|
// Nested structures are typed as `dict` so we include some extra documentation for these structures.
|
|
mod.genNestedStructuresDocstring(b, res.StateInputs.Properties, true /*wrapInput*/)
|
|
|
|
// printComment handles the prefix and triple quotes.
|
|
printComment(w, b.String(), " ")
|
|
}
|
|
|
|
func (mod *modContext) genPropDocstring(w io.Writer, prop *schema.Property, wrapInput bool) {
|
|
if prop.Comment == "" {
|
|
return
|
|
}
|
|
|
|
name := PyName(prop.Name)
|
|
ty := pyType(prop.Type)
|
|
if wrapInput {
|
|
ty = fmt.Sprintf("pulumi.Input[%s]", ty)
|
|
}
|
|
|
|
// If this property has some documentation associated with it, we need to split it so that it is indented
|
|
// in a way that Sphinx can understand.
|
|
lines := strings.Split(prop.Comment, "\n")
|
|
for len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
for i, docLine := range lines {
|
|
// If it's the first line, print the :param header.
|
|
if i == 0 {
|
|
fmt.Fprintf(w, ":param %s %s: %s\n", ty, name, docLine)
|
|
} else {
|
|
// Otherwise, print out enough padding to align with the first char of the type.
|
|
fmt.Fprintf(w, " %s\n", docLine)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) genNestedStructuresDocstring(w io.Writer, props []*schema.Property, wrapInput bool) {
|
|
var names []string
|
|
nestedMap := make(map[string][]*nestedVariable)
|
|
for _, prop := range props {
|
|
nested := nestedStructure(prop.Type)
|
|
if len(nested) > 0 {
|
|
nestedMap[prop.Name] = nested
|
|
names = append(names, prop.Name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
nested := nestedMap[name]
|
|
fmt.Fprintf(w, "\nThe **%s** object supports the following:\n\n", PyName(name))
|
|
mod.genNestedStructureBullets(w, nested, " ", wrapInput)
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) genNestedStructureBullets(w io.Writer, nested []*nestedVariable, indent string, wrapInput bool) {
|
|
contract.Assert(len(nested) > 0)
|
|
for i, nes := range nested {
|
|
name := nes.prop.Name
|
|
if snake, ok := mod.camelCaseToSnakeCase[name]; ok {
|
|
name = snake
|
|
}
|
|
|
|
typ := pyType(nes.prop.Type)
|
|
if wrapInput {
|
|
typ = fmt.Sprintf("pulumi.Input[%s]", typ)
|
|
}
|
|
|
|
docPrefix := " - "
|
|
if nes.prop.Comment == "" {
|
|
// If there's no doc available, just write a new line.
|
|
docPrefix = "\n"
|
|
}
|
|
fmt.Fprintf(w, "%s* `%s` (`%s`)%s", indent, name, typ, docPrefix)
|
|
|
|
// If this property has some documentation associated with it, we need to split it so that it is indented
|
|
// in a way that Sphinx can understand.
|
|
lines := strings.Split(nes.prop.Comment, "\n")
|
|
for len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
for j, docLine := range lines {
|
|
// If it's the first line, print it.
|
|
if j == 0 {
|
|
fmt.Fprintln(w, docLine)
|
|
} else {
|
|
// Otherwise, pad with a couple spaces so the text is aligned with the bullet text above.
|
|
fmt.Fprintf(w, "%s %s\n", indent, docLine)
|
|
}
|
|
}
|
|
|
|
if len(nes.nested) > 0 {
|
|
mod.genNestedStructureBullets(w, nes.nested, indent+" ", wrapInput)
|
|
if i < len(nested)-1 {
|
|
fmt.Fprintln(w, "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type nestedVariable struct {
|
|
prop *schema.Property
|
|
nested []*nestedVariable
|
|
}
|
|
|
|
func nestedStructure(typ schema.Type) []*nestedVariable {
|
|
switch typ := typ.(type) {
|
|
case *schema.ArrayType:
|
|
return nestedStructure(typ.ElementType)
|
|
case *schema.ObjectType:
|
|
nested := make([]*nestedVariable, len(typ.Properties))
|
|
for i, p := range typ.Properties {
|
|
nested[i] = &nestedVariable{
|
|
prop: p,
|
|
nested: nestedStructure(p.Type),
|
|
}
|
|
}
|
|
return nested
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// pyType returns the expected runtime type for the given variable. Of course, being a dynamic language, this
|
|
// check is not exhaustive, but it should be good enough to catch 80% of the cases early on.
|
|
func pyType(typ schema.Type) string {
|
|
switch typ := typ.(type) {
|
|
case *schema.ArrayType:
|
|
return "list"
|
|
case *schema.MapType, *schema.ObjectType, *schema.UnionType:
|
|
return "dict"
|
|
case *schema.TokenType:
|
|
if typ.UnderlyingType != nil {
|
|
return pyType(typ.UnderlyingType)
|
|
}
|
|
return "dict"
|
|
default:
|
|
switch typ {
|
|
case schema.BoolType:
|
|
return "bool"
|
|
case schema.IntType, schema.NumberType:
|
|
return "float"
|
|
case schema.StringType:
|
|
return "str"
|
|
case schema.ArchiveType:
|
|
return "pulumi.Archive"
|
|
case schema.AssetType:
|
|
return "Union[pulumi.Asset, pulumi.Archive]"
|
|
default:
|
|
return "dict"
|
|
}
|
|
}
|
|
}
|
|
|
|
func isStringType(t schema.Type) bool {
|
|
for tt, ok := t.(*schema.TokenType); ok; tt, ok = t.(*schema.TokenType) {
|
|
t = tt.UnderlyingType
|
|
}
|
|
|
|
return t == schema.StringType
|
|
}
|
|
|
|
// pyPack returns the suggested package name for the given string.
|
|
func pyPack(s string) string {
|
|
return "pulumi_" + s
|
|
}
|
|
|
|
// pyClassName turns a raw name into one that is suitable as a Python class name.
|
|
func pyClassName(name string) string {
|
|
return EnsureKeywordSafe(name)
|
|
}
|
|
|
|
func getPrimitiveValue(value interface{}) (string, error) {
|
|
v := reflect.ValueOf(value)
|
|
if v.Kind() == reflect.Interface {
|
|
v = v.Elem()
|
|
}
|
|
|
|
switch v.Kind() {
|
|
case reflect.Bool:
|
|
if v.Bool() {
|
|
return "True", nil
|
|
}
|
|
return "False", nil
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
|
|
return strconv.FormatInt(v.Int(), 10), nil
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
|
return strconv.FormatUint(v.Uint(), 10), nil
|
|
case reflect.Float32, reflect.Float64:
|
|
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
|
|
case reflect.String:
|
|
return fmt.Sprintf("'%s'", v.String()), nil
|
|
default:
|
|
return "", errors.Errorf("unsupported default value of type %T", value)
|
|
}
|
|
}
|
|
|
|
func getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) {
|
|
defaultValue := ""
|
|
if dv.Value != nil {
|
|
v, err := getPrimitiveValue(dv.Value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defaultValue = v
|
|
}
|
|
|
|
if len(dv.Environment) > 0 {
|
|
envFunc := "utilities.get_env"
|
|
switch t {
|
|
case schema.BoolType:
|
|
envFunc = "utilities.get_env_bool"
|
|
case schema.IntType:
|
|
envFunc = "utilities.get_env_int"
|
|
case schema.NumberType:
|
|
envFunc = "utilities.get_env_float"
|
|
}
|
|
|
|
envVars := fmt.Sprintf("'%s'", dv.Environment[0])
|
|
for _, e := range dv.Environment[1:] {
|
|
envVars += fmt.Sprintf(", '%s'", e)
|
|
}
|
|
if defaultValue == "" {
|
|
defaultValue = fmt.Sprintf("%s(%s)", envFunc, envVars)
|
|
} else {
|
|
defaultValue = fmt.Sprintf("(%s(%s) or %s)", envFunc, envVars, defaultValue)
|
|
}
|
|
}
|
|
|
|
return defaultValue, nil
|
|
}
|
|
|
|
type pythonPackageInfo struct {
|
|
Requires map[string]string `json:"requires,omitempty"`
|
|
}
|
|
|
|
func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) {
|
|
// Decode node-specific info
|
|
var info pythonPackageInfo
|
|
if python, ok := pkg.Language["python"]; ok {
|
|
if err := json.Unmarshal([]byte(python), &info); err != nil {
|
|
return nil, errors.Wrap(err, "decoding python package info")
|
|
}
|
|
}
|
|
|
|
// group resources, types, and functions into Go packages
|
|
modules := map[string]*modContext{}
|
|
snakeCaseToCamelCase, camelCaseToSnakeCase := map[string]string{}, map[string]string{}
|
|
|
|
var getMod func(token string) *modContext
|
|
getMod = func(token string) *modContext {
|
|
modName := pkg.TokenToModule(token)
|
|
mod, ok := modules[modName]
|
|
if !ok {
|
|
mod = &modContext{
|
|
pkg: pkg,
|
|
mod: modName,
|
|
tool: tool,
|
|
snakeCaseToCamelCase: snakeCaseToCamelCase,
|
|
camelCaseToSnakeCase: camelCaseToSnakeCase,
|
|
}
|
|
|
|
if modName != "" {
|
|
parentName := path.Dir(modName)
|
|
if parentName == "." || parentName == "" {
|
|
parentName = ":index:"
|
|
}
|
|
parent := getMod(parentName)
|
|
parent.children = append(parent.children, mod)
|
|
}
|
|
|
|
modules[modName] = mod
|
|
}
|
|
return mod
|
|
}
|
|
|
|
// Create the config module if necessary.
|
|
if len(pkg.Config) > 0 {
|
|
_ = getMod(":config/config:")
|
|
}
|
|
|
|
scanResource := func(r *schema.Resource) error {
|
|
mod := getMod(r.Token)
|
|
mod.resources = append(mod.resources, r)
|
|
|
|
// Calculate casing tables. We do this up front because our docstring generator (which is run during
|
|
// genResource) requires them.
|
|
for _, prop := range r.Properties {
|
|
if err := mod.recordProperty(prop); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, prop := range r.InputProperties {
|
|
if err := mod.recordProperty(prop); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err := scanResource(pkg.Provider); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, r := range pkg.Resources {
|
|
if err := scanResource(r); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, f := range pkg.Functions {
|
|
mod := getMod(f.Token)
|
|
mod.functions = append(mod.functions, f)
|
|
}
|
|
|
|
if _, ok := modules["types"]; ok {
|
|
return nil, errors.New("this provider has a `types` module which is reserved for input/output types")
|
|
}
|
|
|
|
files := fs{}
|
|
for p, f := range extraFiles {
|
|
files.add(p, f)
|
|
|
|
}
|
|
for _, mod := range modules {
|
|
if err := mod.gen(files); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Emit casing tables.
|
|
files.add(filepath.Join(pyPack(pkg.Name), "tables.py"), []byte(modules[""].genPropertyConversionTables()))
|
|
|
|
// Finally emit the package metadata (setup.py).
|
|
setup, err := genPackageMetadata(tool, pkg, info.Requires)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
files.add("setup.py", []byte(setup))
|
|
|
|
return files, nil
|
|
}
|
|
|
|
const utilitiesFile = `
|
|
import os
|
|
import pkg_resources
|
|
|
|
from semver import VersionInfo as SemverVersion
|
|
from parver import Version as PEP440Version
|
|
|
|
def get_env(*args):
|
|
for v in args:
|
|
value = os.getenv(v)
|
|
if value is not None:
|
|
return value
|
|
return None
|
|
|
|
def get_env_bool(*args):
|
|
str = get_env(*args)
|
|
if str is not None:
|
|
# NOTE: these values are taken from https://golang.org/src/strconv/atob.go?s=351:391#L1, which is what
|
|
# Terraform uses internally when parsing boolean values.
|
|
if str in ["1", "t", "T", "true", "TRUE", "True"]:
|
|
return True
|
|
if str in ["0", "f", "F", "false", "FALSE", "False"]:
|
|
return False
|
|
return None
|
|
|
|
def get_env_int(*args):
|
|
str = get_env(*args)
|
|
if str is not None:
|
|
try:
|
|
return int(str)
|
|
except:
|
|
return None
|
|
return None
|
|
|
|
def get_env_float(*args):
|
|
str = get_env(*args)
|
|
if str is not None:
|
|
try:
|
|
return float(str)
|
|
except:
|
|
return None
|
|
return None
|
|
|
|
def get_version():
|
|
# __name__ is set to the fully-qualified name of the current module, In our case, it will be
|
|
# <some module>.utilities. <some module> is the module we want to query the version for.
|
|
root_package, *rest = __name__.split('.')
|
|
|
|
# pkg_resources uses setuptools to inspect the set of installed packages. We use it here to ask
|
|
# for the currently installed version of the root package (i.e. us) and get its version.
|
|
|
|
# Unfortunately, PEP440 and semver differ slightly in incompatible ways. The Pulumi engine expects
|
|
# to receive a valid semver string when receiving requests from the language host, so it's our
|
|
# responsibility as the library to convert our own PEP440 version into a valid semver string.
|
|
|
|
pep440_version_string = pkg_resources.require(root_package)[0].version
|
|
pep440_version = PEP440Version.parse(pep440_version_string)
|
|
(major, minor, patch) = pep440_version.release
|
|
prerelease = None
|
|
if pep440_version.pre_tag == 'a':
|
|
prerelease = f"alpha.{pep440_version.pre}"
|
|
elif pep440_version.pre_tag == 'b':
|
|
prerelease = f"beta.{pep440_version.pre}"
|
|
elif pep440_version.pre_tag == 'rc':
|
|
prerelease = f"rc.{pep440_version.pre}"
|
|
elif pep440_version.dev is not None:
|
|
prerelease = f"dev.{pep440_version.dev}"
|
|
|
|
# The only significant difference between PEP440 and semver as it pertains to us is that PEP440 has explicit support
|
|
# for dev builds, while semver encodes them as "prerelease" versions. In order to bridge between the two, we convert
|
|
# our dev build version into a prerelease tag. This matches what all of our other packages do when constructing
|
|
# their own semver string.
|
|
semver_version = SemverVersion(major=major, minor=minor, patch=patch, prerelease=prerelease)
|
|
return str(semver_version)
|
|
`
|