For the NodeJS k8s SDK, rather than falling back to default values using the || operator, use the nullish coalescing operator (??). This avoid situations where the primary value is set to false, and then is overridden by the default value.
1480 lines
47 KiB
Go
1480 lines
47 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.
|
|
|
|
// 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"
|
|
"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"
|
|
)
|
|
|
|
// Match k8s version suffix. Examples include "/v1beta1" and "/v1alpha2".
|
|
var k8sVersionSuffix = regexp.MustCompile(`/(v\d+((alpha|beta)\d+)?)$`)
|
|
|
|
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
|
|
extraSourceFiles []string
|
|
|
|
// Name overrides set in PackageInfo
|
|
modNameOverrides map[string]string // Optional overrides for Pulumi module names
|
|
compatibility string // Toggle compatibility mode for a specified target.
|
|
}
|
|
|
|
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 {
|
|
escaped := strings.ReplaceAll(l, `"""`, `\"\"\"`)
|
|
fmt.Fprintf(w, "%s%s\n", indent, escaped)
|
|
}
|
|
}
|
|
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)
|
|
relImport := relPathToRelImport(relRoot)
|
|
|
|
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", relImport)
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
|
|
func relPathToRelImport(relPath string) string {
|
|
// Convert relative path to relative import e.g. "../.." -> "..."
|
|
// https://realpython.com/absolute-vs-relative-python-imports/#relative-imports
|
|
relImport := "."
|
|
if relPath == "." {
|
|
return relImport
|
|
}
|
|
for _, component := range strings.Split(relPath, "/") {
|
|
if component == ".." {
|
|
relImport += "."
|
|
} else {
|
|
relImport += component
|
|
}
|
|
}
|
|
return relImport
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
// Utilities, config, readme
|
|
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())
|
|
|
|
// Ensure that the top-level (provider) module directory contains a README.md file.
|
|
readme := mod.pkg.Language["python"].(PackageInfo).Readme
|
|
if readme == "" {
|
|
readme = mod.pkg.Description
|
|
if readme != "" && readme[len(readme)-1] != '\n' {
|
|
readme += "\n"
|
|
}
|
|
if mod.pkg.Attribution != "" {
|
|
if len(readme) != 0 {
|
|
readme += "\n"
|
|
}
|
|
readme += mod.pkg.Attribution
|
|
}
|
|
if readme != "" && readme[len(readme)-1] != '\n' {
|
|
readme += "\n"
|
|
}
|
|
}
|
|
fs.add(filepath.Join(dir, "README.md"), []byte(readme))
|
|
|
|
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 {
|
|
child := mod.mod
|
|
if mod.compatibility == kubernetes20 {
|
|
// Extract version suffix from child modules. Nested versions will have their own __init__.py file.
|
|
// Example: apps/v1beta1 -> v1beta1
|
|
if match := k8sVersionSuffix.FindStringSubmatchIndex(child); len(match) != 0 {
|
|
child = child[match[2]:match[3]]
|
|
}
|
|
}
|
|
if i > 0 {
|
|
fmt.Fprintf(w, ", ")
|
|
}
|
|
fmt.Fprintf(w, "'%s'", PyName(child))
|
|
}
|
|
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)
|
|
|
|
if prop.DeprecationMessage != "" {
|
|
escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`)
|
|
fmt.Fprintf(w, " if %s is not None:\n", pname)
|
|
fmt.Fprintf(w, " warnings.warn(\"%s\", DeprecationWarning)\n", escaped)
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n\n", pname, escaped)
|
|
}
|
|
|
|
// 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 != "" {
|
|
escaped := strings.ReplaceAll(res.DeprecationMessage, `"`, `\"`)
|
|
fmt.Fprintf(w, "warnings.warn(\"%s\", DeprecationWarning)\n\n", escaped)
|
|
}
|
|
|
|
name := pyClassName(tokenToName(res.Token))
|
|
if res.IsProvider {
|
|
name = "Provider"
|
|
}
|
|
|
|
// Produce a class definition with optional """ comment.
|
|
fmt.Fprintf(w, "\nclass %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
|
|
|
|
// Exclude nested docs in kubernetes provider
|
|
if mod.compatibility != kubernetes20 {
|
|
seenTypes := codegen.Set{}
|
|
nested := nestedStructure(prop.Type, seenTypes)
|
|
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 != "" {
|
|
escaped := strings.ReplaceAll(res.DeprecationMessage, `"`, `\"`)
|
|
fmt.Fprintf(w, " warnings.warn(\"%s\", DeprecationWarning)\n\n", escaped)
|
|
}
|
|
// 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)
|
|
}
|
|
|
|
// Check that the property isn't deprecated
|
|
if prop.DeprecationMessage != "" {
|
|
escaped := strings.ReplaceAll(prop.DeprecationMessage, `"`, `\"`)
|
|
fmt.Fprintf(w, " if %s is not None:\n", pname)
|
|
fmt.Fprintf(w, " warnings.warn(\"%s\", DeprecationWarning)\n", escaped)
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", pname, escaped)
|
|
}
|
|
|
|
// 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\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 != "" {
|
|
escaped := strings.ReplaceAll(fun.DeprecationMessage, `"`, `\"`)
|
|
fmt.Fprintf(w, "warnings.warn(\"%s\", DeprecationWarning)\n", escaped)
|
|
}
|
|
|
|
// 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.FilterExamples(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\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\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\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()
|
|
}
|
|
|
|
// 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 buildCaseMappingTables(pkg *schema.Package, snakeCaseToCamelCase, camelCaseToSnakeCase map[string]string) {
|
|
for _, r := range pkg.Resources {
|
|
// Calculate casing tables. We do this up front because our docstring generator (which is run during
|
|
// genResource) requires them.
|
|
for _, prop := range r.Properties {
|
|
recordProperty(prop, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
|
}
|
|
for _, prop := range r.InputProperties {
|
|
recordProperty(prop, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
|
}
|
|
}
|
|
}
|
|
|
|
func recordProperty(prop *schema.Property, snakeCaseToCamelCase, camelCaseToSnakeCase map[string]string) {
|
|
mapCase := true
|
|
if python, ok := prop.Language["python"]; ok {
|
|
mapCase = python.(PropertyInfo).MapCase
|
|
}
|
|
if mapCase {
|
|
snakeCaseName := PyName(prop.Name)
|
|
if snakeCaseToCamelCase != nil {
|
|
snakeCaseToCamelCase[snakeCaseName] = prop.Name
|
|
}
|
|
if camelCaseToSnakeCase != nil {
|
|
camelCaseToSnakeCase[prop.Name] = snakeCaseName
|
|
}
|
|
}
|
|
|
|
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 == prop.Type {
|
|
continue
|
|
}
|
|
|
|
recordProperty(p, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.FilterExamples(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*/)
|
|
}
|
|
|
|
// Exclude nested docs in kubernetes provider
|
|
if mod.compatibility != kubernetes20 {
|
|
// 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.")
|
|
if res.StateInputs != nil {
|
|
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)
|
|
seenTypes := codegen.Set{}
|
|
for _, prop := range props {
|
|
nested := nestedStructure(prop.Type, seenTypes)
|
|
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, seen codegen.Set) []*nestedVariable {
|
|
if seen.Has(typ) {
|
|
return nil
|
|
}
|
|
seen.Add(typ)
|
|
switch typ := typ.(type) {
|
|
case *schema.ArrayType:
|
|
return nestedStructure(typ.ElementType, seen)
|
|
case *schema.ObjectType:
|
|
nested := make([]*nestedVariable, len(typ.Properties))
|
|
for i, p := range typ.Properties {
|
|
nested[i] = &nestedVariable{
|
|
prop: p,
|
|
nested: nestedStructure(p.Type, seen),
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
func generateModuleContextMap(tool string, pkg *schema.Package, info PackageInfo, extraFiles map[string][]byte) (map[string]*modContext, error) {
|
|
// Build case mapping tables
|
|
snakeCaseToCamelCase, camelCaseToSnakeCase := map[string]string{}, map[string]string{}
|
|
buildCaseMappingTables(pkg, snakeCaseToCamelCase, camelCaseToSnakeCase)
|
|
|
|
// group resources, types, and functions into modules
|
|
modules := map[string]*modContext{}
|
|
|
|
var getMod func(modName string) *modContext
|
|
getMod = func(modName string) *modContext {
|
|
mod, ok := modules[modName]
|
|
if !ok {
|
|
mod = &modContext{
|
|
pkg: pkg,
|
|
mod: modName,
|
|
tool: tool,
|
|
snakeCaseToCamelCase: snakeCaseToCamelCase,
|
|
camelCaseToSnakeCase: camelCaseToSnakeCase,
|
|
modNameOverrides: info.ModuleNameOverrides,
|
|
compatibility: info.Compatibility,
|
|
}
|
|
|
|
if modName != "" {
|
|
parentName := path.Dir(modName)
|
|
if parentName == "." {
|
|
parentName = ""
|
|
}
|
|
parent := getMod(parentName)
|
|
parent.children = append(parent.children, mod)
|
|
}
|
|
|
|
modules[modName] = mod
|
|
}
|
|
return mod
|
|
}
|
|
|
|
getModFromToken := func(token string) *modContext {
|
|
canonicalModName := pkg.TokenToModule(token)
|
|
modName := PyName(canonicalModName)
|
|
if override, ok := info.ModuleNameOverrides[canonicalModName]; ok {
|
|
modName = override
|
|
}
|
|
return getMod(modName)
|
|
}
|
|
|
|
// Create the config module if necessary.
|
|
if len(pkg.Config) > 0 &&
|
|
info.Compatibility != kubernetes20 { // k8s SDK doesn't use config.
|
|
_ = getMod("config")
|
|
}
|
|
|
|
scanResource := func(r *schema.Resource) {
|
|
mod := getModFromToken(r.Token)
|
|
mod.resources = append(mod.resources, r)
|
|
}
|
|
|
|
scanResource(pkg.Provider)
|
|
for _, r := range pkg.Resources {
|
|
scanResource(r)
|
|
}
|
|
|
|
for _, f := range pkg.Functions {
|
|
mod := getModFromToken(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")
|
|
}
|
|
|
|
// Add python source files to the corresponding modules. Note that we only add the file names; the contents are
|
|
// still laid out manually in GeneratePackage.
|
|
for p := range extraFiles {
|
|
if path.Ext(p) != ".py" {
|
|
continue
|
|
}
|
|
|
|
modName := path.Dir(p)
|
|
if modName == "/" || modName == "." {
|
|
modName = ""
|
|
}
|
|
mod := getMod(modName)
|
|
mod.extraSourceFiles = append(mod.extraSourceFiles, p)
|
|
}
|
|
|
|
return modules, nil
|
|
}
|
|
|
|
// LanguageResource is derived from the schema and can be used by downstream codegen.
|
|
type LanguageResource struct {
|
|
*schema.Resource
|
|
|
|
Name string // The resource name (e.g. Deployment)
|
|
Package string // The package name (e.g. pulumi_kubernetes.apps.v1)
|
|
}
|
|
|
|
// LanguageResources returns a map of resources that can be used by downstream codegen. The map
|
|
// key is the resource schema token.
|
|
func LanguageResources(tool string, pkg *schema.Package) (map[string]LanguageResource, error) {
|
|
resources := map[string]LanguageResource{}
|
|
|
|
if err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil {
|
|
return nil, err
|
|
}
|
|
info, _ := pkg.Language["python"].(PackageInfo)
|
|
|
|
modules, err := generateModuleContextMap(tool, pkg, info, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for modName, mod := range modules {
|
|
if modName == "" {
|
|
continue
|
|
}
|
|
for _, r := range mod.resources {
|
|
packagePath := strings.Replace(modName, "/", ".", -1)
|
|
lr := LanguageResource{
|
|
Resource: r,
|
|
Package: packagePath,
|
|
Name: pyClassName(tokenToName(r.Token)),
|
|
}
|
|
resources[r.Token] = lr
|
|
}
|
|
}
|
|
|
|
return resources, nil
|
|
}
|
|
|
|
func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) {
|
|
// Decode python-specific info
|
|
if err := pkg.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil {
|
|
return nil, err
|
|
}
|
|
info, _ := pkg.Language["python"].(PackageInfo)
|
|
|
|
modules, err := generateModuleContextMap(tool, pkg, info, extraFiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files := fs{}
|
|
for p, f := range extraFiles {
|
|
files.add(filepath.Join(pyPack(pkg.Name), 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)
|
|
`
|