aeb6857f7f
The output side was already handled using the addionalSecretOutputs property. This change ensures both inputs and outputs are encrypted in the state.
1983 lines
58 KiB
Go
1983 lines
58 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 nodejs
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen"
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
)
|
|
|
|
type typeDetails struct {
|
|
outputType bool
|
|
inputType bool
|
|
argsType bool
|
|
plainType bool
|
|
}
|
|
|
|
func title(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
runes := []rune(s)
|
|
return string(append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...))
|
|
}
|
|
|
|
func camel(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
runes := []rune(s)
|
|
res := make([]rune, 0, len(runes))
|
|
for i, r := range runes {
|
|
if unicode.IsLower(r) {
|
|
res = append(res, runes[i:]...)
|
|
break
|
|
}
|
|
res = append(res, unicode.ToLower(r))
|
|
}
|
|
return string(res)
|
|
}
|
|
|
|
type modContext struct {
|
|
pkg *schema.Package
|
|
mod string
|
|
types []*schema.ObjectType
|
|
enums []*schema.EnumType
|
|
resources []*schema.Resource
|
|
functions []*schema.Function
|
|
typeDetails map[*schema.ObjectType]*typeDetails
|
|
children []*modContext
|
|
extraSourceFiles []string
|
|
tool string
|
|
|
|
// Name overrides set in NodeJSInfo
|
|
modToPkg map[string]string // Module name -> package name
|
|
compatibility string // Toggle compatibility mode for a specified target.
|
|
disableUnionOutputTypes bool // Disable unions in output types.
|
|
}
|
|
|
|
func (mod *modContext) String() string {
|
|
return mod.mod
|
|
}
|
|
|
|
func (mod *modContext) details(t *schema.ObjectType) *typeDetails {
|
|
details, ok := mod.typeDetails[t]
|
|
if !ok {
|
|
details = &typeDetails{}
|
|
if mod.typeDetails == nil {
|
|
mod.typeDetails = map[*schema.ObjectType]*typeDetails{}
|
|
}
|
|
mod.typeDetails[t] = details
|
|
}
|
|
return details
|
|
}
|
|
|
|
func (mod *modContext) tokenToModName(tok string) string {
|
|
components := strings.Split(tok, ":")
|
|
contract.Assertf(len(components) == 3, "malformed token %v", tok)
|
|
|
|
modName := mod.pkg.TokenToModule(tok)
|
|
if override, ok := mod.modToPkg[modName]; ok {
|
|
modName = override
|
|
}
|
|
|
|
if modName != "" {
|
|
modName = strings.Replace(modName, "/", ".", -1) + "."
|
|
}
|
|
|
|
return modName
|
|
}
|
|
|
|
func (mod *modContext) namingContext(pkg *schema.Package) (namingCtx *modContext, pkgName string, external bool) {
|
|
namingCtx = mod
|
|
if pkg != nil && pkg != mod.pkg {
|
|
external = true
|
|
pkgName = pkg.Name + "."
|
|
|
|
var info NodePackageInfo
|
|
contract.AssertNoError(pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}))
|
|
if v, ok := pkg.Language["nodejs"].(NodePackageInfo); ok {
|
|
info = v
|
|
}
|
|
namingCtx = &modContext{
|
|
pkg: pkg,
|
|
modToPkg: info.ModuleToPackage,
|
|
compatibility: info.Compatibility,
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (mod *modContext) objectType(pkg *schema.Package, tok string, input, args, enum bool) string {
|
|
root := "outputs."
|
|
if input {
|
|
root = "inputs."
|
|
}
|
|
|
|
namingCtx, pkgName, external := mod.namingContext(pkg)
|
|
if external {
|
|
root = "types.output."
|
|
if input {
|
|
root = "types.input."
|
|
}
|
|
}
|
|
|
|
modName, name := namingCtx.tokenToModName(tok), tokenToName(tok)
|
|
|
|
if enum {
|
|
return "enums." + modName + title(name)
|
|
}
|
|
|
|
if args && mod.compatibility != tfbridge20 && mod.compatibility != kubernetes20 {
|
|
name += "Args"
|
|
}
|
|
return pkgName + root + modName + title(name)
|
|
}
|
|
|
|
func (mod *modContext) resourceType(r *schema.ResourceType) string {
|
|
if strings.HasPrefix(r.Token, "pulumi:providers:") {
|
|
pkgName := strings.TrimPrefix(r.Token, "pulumi:providers:")
|
|
return fmt.Sprintf("%s.Provider", pkgName)
|
|
}
|
|
|
|
pkg := mod.pkg
|
|
if r.Resource != nil {
|
|
pkg = r.Resource.Package
|
|
}
|
|
namingCtx, pkgName, _ := mod.namingContext(pkg)
|
|
|
|
modName, name := namingCtx.tokenToModName(r.Token), tokenToName(r.Token)
|
|
|
|
return pkgName + modName + title(name)
|
|
}
|
|
|
|
func tokenToName(tok string) string {
|
|
components := strings.Split(tok, ":")
|
|
contract.Assertf(len(components) == 3, "malformed token %v", tok)
|
|
return title(components[2])
|
|
}
|
|
|
|
func resourceName(r *schema.Resource) string {
|
|
if r.IsProvider {
|
|
return "Provider"
|
|
}
|
|
return tokenToName(r.Token)
|
|
}
|
|
|
|
func (mod *modContext) resourceFileName(r *schema.Resource) string {
|
|
fileName := camel(resourceName(r)) + ".ts"
|
|
if mod.isReservedSourceFileName(fileName) {
|
|
fileName = camel(resourceName(r)) + "_.ts"
|
|
}
|
|
return fileName
|
|
}
|
|
|
|
func tokenToFunctionName(tok string) string {
|
|
return camel(tokenToName(tok))
|
|
}
|
|
|
|
func (mod *modContext) typeString(t schema.Type, input, wrapInput, args, optional bool, constValue interface{}) string {
|
|
var typ string
|
|
switch t := t.(type) {
|
|
case *schema.EnumType:
|
|
typ = mod.objectType(nil, t.Token, input, args, true)
|
|
case *schema.ArrayType:
|
|
typ = mod.typeString(t.ElementType, input, wrapInput, args, false, constValue) + "[]"
|
|
case *schema.MapType:
|
|
typ = fmt.Sprintf("{[key: string]: %v}", mod.typeString(t.ElementType, input, wrapInput, args, false, constValue))
|
|
case *schema.ObjectType:
|
|
typ = mod.objectType(t.Package, t.Token, input, args, false)
|
|
case *schema.ResourceType:
|
|
typ = mod.resourceType(t)
|
|
case *schema.TokenType:
|
|
typ = tokenToName(t.Token)
|
|
case *schema.UnionType:
|
|
if !input && mod.disableUnionOutputTypes {
|
|
if t.DefaultType != nil {
|
|
return mod.typeString(t.DefaultType, input, wrapInput, args, optional, constValue)
|
|
}
|
|
typ = "any"
|
|
} else {
|
|
var elements []string
|
|
for _, e := range t.ElementTypes {
|
|
t := mod.typeString(e, input, wrapInput, args, false, constValue)
|
|
if args && strings.HasPrefix(t, "pulumi.Input<") {
|
|
contract.Assert(t[len(t)-1] == '>')
|
|
// Strip off the leading `pulumi.Input<` and the trailing `>`
|
|
t = t[len("pulumi.Input<") : len(t)-1]
|
|
}
|
|
elements = append(elements, t)
|
|
}
|
|
typ = strings.Join(elements, " | ")
|
|
}
|
|
default:
|
|
switch t {
|
|
case schema.BoolType:
|
|
typ = "boolean"
|
|
case schema.IntType, schema.NumberType:
|
|
typ = "number"
|
|
case schema.StringType:
|
|
typ = "string"
|
|
case schema.ArchiveType:
|
|
typ = "pulumi.asset.Archive"
|
|
case schema.AssetType:
|
|
typ = "pulumi.asset.Asset | pulumi.asset.Archive"
|
|
case schema.JSONType:
|
|
fallthrough
|
|
case schema.AnyType:
|
|
typ = "any"
|
|
}
|
|
}
|
|
|
|
if constValue != nil && typ == "string" {
|
|
typ = fmt.Sprintf("%q", constValue.(string))
|
|
}
|
|
if wrapInput && typ != "any" {
|
|
typ = fmt.Sprintf("pulumi.Input<%s>", typ)
|
|
}
|
|
if optional {
|
|
return typ + " | undefined"
|
|
}
|
|
return typ
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func sanitizeComment(str string) string {
|
|
return strings.Replace(str, "*/", "*/", -1)
|
|
}
|
|
|
|
func printComment(w io.Writer, comment, deprecationMessage, indent string) {
|
|
if comment == "" && deprecationMessage == "" {
|
|
return
|
|
}
|
|
|
|
lines := strings.Split(sanitizeComment(comment), "\n")
|
|
for len(lines) > 0 && lines[len(lines)-1] == "" {
|
|
lines = lines[:len(lines)-1]
|
|
}
|
|
fmt.Fprintf(w, "%s/**\n", indent)
|
|
for _, l := range lines {
|
|
if l == "" {
|
|
fmt.Fprintf(w, "%s *\n", indent)
|
|
} else {
|
|
fmt.Fprintf(w, "%s * %s\n", indent, l)
|
|
}
|
|
}
|
|
if deprecationMessage != "" {
|
|
if len(lines) > 0 {
|
|
fmt.Fprintf(w, "%s *\n", indent)
|
|
}
|
|
fmt.Fprintf(w, "%s * @deprecated %s\n", indent, deprecationMessage)
|
|
}
|
|
fmt.Fprintf(w, "%s */\n", indent)
|
|
}
|
|
|
|
func (mod *modContext) genPlainType(w io.Writer, name, comment string, properties []*schema.Property, input, arg, readonly bool, level int) {
|
|
indent := strings.Repeat(" ", level)
|
|
|
|
printComment(w, comment, "", indent)
|
|
|
|
fmt.Fprintf(w, "%sexport interface %s {\n", indent, name)
|
|
for _, p := range properties {
|
|
printComment(w, p.Comment, p.DeprecationMessage, indent+" ")
|
|
|
|
prefix := ""
|
|
if readonly {
|
|
prefix = "readonly "
|
|
}
|
|
|
|
sigil := ""
|
|
if !p.IsRequired {
|
|
sigil = "?"
|
|
}
|
|
|
|
typ := mod.typeString(p.Type, input, arg && !p.IsPlain, arg && !p.IsPlain, false, p.ConstValue)
|
|
fmt.Fprintf(w, "%s %s%s%s: %s;\n", indent, prefix, p.Name, sigil, typ)
|
|
}
|
|
fmt.Fprintf(w, "%s}\n", indent)
|
|
}
|
|
|
|
func tsPrimitiveValue(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("%q", v.String()), nil
|
|
default:
|
|
return "", errors.Errorf("unsupported default value of type %T", value)
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) getConstValue(cv interface{}) (string, error) {
|
|
if cv == nil {
|
|
return "", nil
|
|
}
|
|
return tsPrimitiveValue(cv)
|
|
}
|
|
|
|
func (mod *modContext) getDefaultValue(dv *schema.DefaultValue, t schema.Type) (string, error) {
|
|
var val string
|
|
if dv.Value != nil {
|
|
v, err := tsPrimitiveValue(dv.Value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
val = v
|
|
}
|
|
|
|
if len(dv.Environment) != 0 {
|
|
getType := ""
|
|
switch t {
|
|
case schema.BoolType:
|
|
getType = "Boolean"
|
|
case schema.IntType, schema.NumberType:
|
|
getType = "Number"
|
|
}
|
|
|
|
envVars := fmt.Sprintf("%q", dv.Environment[0])
|
|
for _, e := range dv.Environment[1:] {
|
|
envVars += fmt.Sprintf(", %q", e)
|
|
}
|
|
|
|
cast := ""
|
|
if t != schema.StringType {
|
|
cast = "<any>"
|
|
}
|
|
|
|
getEnv := fmt.Sprintf("%sutilities.getEnv%s(%s)", cast, getType, envVars)
|
|
if val != "" {
|
|
val = fmt.Sprintf("(%s || %s)", getEnv, val)
|
|
} else {
|
|
val = getEnv
|
|
}
|
|
}
|
|
|
|
return val, nil
|
|
}
|
|
|
|
func (mod *modContext) genAlias(w io.Writer, alias *schema.Alias) {
|
|
fmt.Fprintf(w, "{ ")
|
|
|
|
var 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.Fprintf(w, ", ")
|
|
}
|
|
|
|
fmt.Fprintf(w, "%s", part)
|
|
}
|
|
|
|
fmt.Fprintf(w, " }")
|
|
}
|
|
|
|
func (mod *modContext) genResource(w io.Writer, r *schema.Resource) error {
|
|
// Create a resource module file into which all of this resource's types will go.
|
|
name := resourceName(r)
|
|
|
|
// Write the TypeDoc/JSDoc for the resource class
|
|
printComment(w, codegen.FilterExamples(r.Comment, "typescript"), r.DeprecationMessage, "")
|
|
|
|
var baseType, optionsType string
|
|
switch {
|
|
case r.IsComponent:
|
|
baseType, optionsType = "ComponentResource", "ComponentResourceOptions"
|
|
case r.IsProvider:
|
|
baseType, optionsType = "ProviderResource", "ResourceOptions"
|
|
default:
|
|
baseType, optionsType = "CustomResource", "CustomResourceOptions"
|
|
}
|
|
|
|
// Begin defining the class.
|
|
fmt.Fprintf(w, "export class %s extends pulumi.%s {\n", name, baseType)
|
|
|
|
// Emit a static factory to read instances of this resource unless this is a provider resource or ComponentResource.
|
|
stateType := name + "State"
|
|
if !r.IsProvider && !r.IsComponent {
|
|
fmt.Fprintf(w, " /**\n")
|
|
fmt.Fprintf(w, " * Get an existing %s resource's state with the given name, ID, and optional extra\n", name)
|
|
fmt.Fprintf(w, " * properties used to qualify the lookup.\n")
|
|
fmt.Fprintf(w, " *\n")
|
|
fmt.Fprintf(w, " * @param name The _unique_ name of the resulting resource.\n")
|
|
fmt.Fprintf(w, " * @param id The _unique_ provider ID of the resource to lookup.\n")
|
|
// TODO: Document id format: https://github.com/pulumi/pulumi/issues/4754
|
|
if r.StateInputs != nil {
|
|
fmt.Fprintf(w, " * @param state Any extra arguments used during the lookup.\n")
|
|
}
|
|
fmt.Fprintf(w, " * @param opts Optional settings to control the behavior of the CustomResource.\n")
|
|
fmt.Fprintf(w, " */\n")
|
|
|
|
stateParam, stateRef := "", "undefined as any, "
|
|
if r.StateInputs != nil {
|
|
stateParam, stateRef = fmt.Sprintf("state?: %s, ", stateType), "<any>state, "
|
|
}
|
|
|
|
fmt.Fprintf(w, " public static get(name: string, id: pulumi.Input<pulumi.ID>, %sopts?: pulumi.%s): %s {\n",
|
|
stateParam, optionsType, name)
|
|
if r.DeprecationMessage != "" && mod.compatibility != kubernetes20 {
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage)
|
|
}
|
|
fmt.Fprintf(w, " return new %s(name, %s{ ...opts, id: id });\n", name, stateRef)
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
pulumiType := r.Token
|
|
if r.IsProvider {
|
|
pulumiType = mod.pkg.Name
|
|
}
|
|
|
|
fmt.Fprintf(w, " /** @internal */\n")
|
|
fmt.Fprintf(w, " public static readonly __pulumiType = '%s';\n", pulumiType)
|
|
fmt.Fprintf(w, "\n")
|
|
fmt.Fprintf(w, " /**\n")
|
|
fmt.Fprintf(w, " * Returns true if the given object is an instance of %s. This is designed to work even\n", name)
|
|
fmt.Fprintf(w, " * when multiple copies of the Pulumi SDK have been loaded into the same process.\n")
|
|
fmt.Fprintf(w, " */\n")
|
|
fmt.Fprintf(w, " public static isInstance(obj: any): obj is %s {\n", name)
|
|
fmt.Fprintf(w, " if (obj === undefined || obj === null) {\n")
|
|
fmt.Fprintf(w, " return false;\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, " return obj['__pulumiType'] === %s.__pulumiType;\n", name)
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Emit all properties (using their output types).
|
|
// TODO[pulumi/pulumi#397]: represent sensitive types using a Secret<T> type.
|
|
ins := codegen.NewStringSet()
|
|
allOptionalInputs := true
|
|
for _, prop := range r.InputProperties {
|
|
ins.Add(prop.Name)
|
|
allOptionalInputs = allOptionalInputs && !prop.IsRequired
|
|
}
|
|
for _, prop := range r.Properties {
|
|
printComment(w, prop.Comment, prop.DeprecationMessage, " ")
|
|
|
|
// Make a little comment in the code so it's easy to pick out output properties.
|
|
var outcomment string
|
|
if !ins.Has(prop.Name) {
|
|
outcomment = "/*out*/ "
|
|
}
|
|
|
|
required := prop.IsRequired
|
|
if mod.compatibility == kubernetes20 {
|
|
required = true
|
|
}
|
|
fmt.Fprintf(w, " public %sreadonly %s!: pulumi.Output<%s>;\n", outcomment, prop.Name, mod.typeString(prop.Type, false, false, false, !required, prop.ConstValue))
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Now create a constructor that chains supercalls and stores into properties.
|
|
fmt.Fprintf(w, " /**\n")
|
|
fmt.Fprintf(w, " * Create a %s resource with the given unique name, arguments, and options.\n", name)
|
|
fmt.Fprintf(w, " *\n")
|
|
fmt.Fprintf(w, " * @param name The _unique_ name of the resource.\n")
|
|
fmt.Fprintf(w, " * @param args The arguments to use to populate this resource's properties.\n")
|
|
fmt.Fprintf(w, " * @param opts A bag of options that control this resource's behavior.\n")
|
|
fmt.Fprintf(w, " */\n")
|
|
|
|
// k8s provider "get" methods don't require args, so make args optional.
|
|
if mod.compatibility == kubernetes20 {
|
|
allOptionalInputs = true
|
|
}
|
|
|
|
// Write out callable constructor: We only emit a single public constructor, even though we use a private signature
|
|
// as well as part of the implementation of `.get`. This is complicated slightly by the fact that, if there is no
|
|
// args type, we will emit a constructor lacking that parameter.
|
|
var argsFlags string
|
|
if allOptionalInputs {
|
|
// If the number of required input properties was zero, we can make the args object optional.
|
|
argsFlags = "?"
|
|
}
|
|
argsType := name + "Args"
|
|
var trailingBrace string
|
|
switch {
|
|
case r.IsProvider, r.StateInputs == nil:
|
|
trailingBrace = " {"
|
|
default:
|
|
trailingBrace = ""
|
|
}
|
|
|
|
if r.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage)
|
|
}
|
|
fmt.Fprintf(w, " constructor(name: string, args%s: %s, opts?: pulumi.%s)%s\n", argsFlags, argsType,
|
|
optionsType, trailingBrace)
|
|
|
|
genInputProps := func() error {
|
|
for _, prop := range r.InputProperties {
|
|
if prop.IsRequired {
|
|
fmt.Fprintf(w, " if ((!args || args.%s === undefined) && !opts.urn) {\n", prop.Name)
|
|
fmt.Fprintf(w, " throw new Error(\"Missing required property '%s'\");\n", prop.Name)
|
|
fmt.Fprintf(w, " }\n")
|
|
}
|
|
}
|
|
for _, prop := range r.InputProperties {
|
|
var arg string
|
|
if prop.Secret {
|
|
arg = fmt.Sprintf("args?.%[1]s ? pulumi.secret(args.%[1]s) : undefined", prop.Name)
|
|
} else {
|
|
arg = fmt.Sprintf("args ? args.%[1]s : undefined", prop.Name)
|
|
}
|
|
|
|
prefix := " "
|
|
if prop.ConstValue != nil {
|
|
cv, err := mod.getConstValue(prop.ConstValue)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
arg = cv
|
|
} else {
|
|
if prop.DefaultValue != nil {
|
|
dv, err := mod.getDefaultValue(prop.DefaultValue, prop.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
arg = fmt.Sprintf("(%s) ?? %s", arg, dv)
|
|
}
|
|
|
|
// provider properties must be marshaled as JSON strings.
|
|
if r.IsProvider && !isStringType(prop.Type) {
|
|
arg = fmt.Sprintf("pulumi.output(%s).apply(JSON.stringify)", arg)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "%sinputs[\"%s\"] = %s;\n", prefix, prop.Name, arg)
|
|
}
|
|
|
|
for _, prop := range r.Properties {
|
|
prefix := " "
|
|
if !ins.Has(prop.Name) {
|
|
fmt.Fprintf(w, "%sinputs[\"%s\"] = undefined /*out*/;\n", prefix, prop.Name)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if !r.IsProvider {
|
|
if r.StateInputs != nil {
|
|
if r.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, " /** @deprecated %s */\n", r.DeprecationMessage)
|
|
}
|
|
|
|
// Now write out a general purpose constructor implementation that can handle the public signature as well as the
|
|
// signature to support construction via `.get`. And then emit the body preamble which will pluck out the
|
|
// conditional state into sensible variables using dynamic type tests.
|
|
fmt.Fprintf(w, " constructor(name: string, argsOrState?: %s | %s, opts?: pulumi.%s) {\n",
|
|
argsType, stateType, optionsType)
|
|
}
|
|
if r.DeprecationMessage != "" && mod.compatibility != kubernetes20 {
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, r.DeprecationMessage)
|
|
}
|
|
fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n")
|
|
fmt.Fprintf(w, " opts = opts || {};\n")
|
|
|
|
if r.StateInputs != nil {
|
|
// The lookup case:
|
|
fmt.Fprintf(w, " if (opts.id) {\n")
|
|
fmt.Fprintf(w, " const state = argsOrState as %[1]s | undefined;\n", stateType)
|
|
for _, prop := range r.StateInputs.Properties {
|
|
fmt.Fprintf(w, " inputs[\"%[1]s\"] = state ? state.%[1]s : undefined;\n", prop.Name)
|
|
}
|
|
// The creation case (with args):
|
|
fmt.Fprintf(w, " } else {\n")
|
|
fmt.Fprintf(w, " const args = argsOrState as %s | undefined;\n", argsType)
|
|
err := genInputProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// The creation case:
|
|
fmt.Fprintf(w, " if (!opts.id) {\n")
|
|
err := genInputProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The get case:
|
|
fmt.Fprintf(w, " } else {\n")
|
|
for _, prop := range r.Properties {
|
|
fmt.Fprintf(w, " inputs[\"%[1]s\"] = undefined /*out*/;\n", prop.Name)
|
|
}
|
|
}
|
|
} else {
|
|
fmt.Fprintf(w, " let inputs: pulumi.Inputs = {};\n")
|
|
fmt.Fprintf(w, " opts = opts || {};\n")
|
|
fmt.Fprintf(w, " {\n")
|
|
err := genInputProps()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var secretProps []string
|
|
for _, prop := range r.Properties {
|
|
if prop.Secret {
|
|
secretProps = append(secretProps, prop.Name)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, " }\n")
|
|
|
|
// If the caller didn't request a specific version, supply one using the version of this library.
|
|
fmt.Fprintf(w, " if (!opts.version) {\n")
|
|
fmt.Fprintf(w, " opts = pulumi.mergeOptions(opts, { version: utilities.getVersion()});\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
|
|
// Now invoke the super constructor with the type, name, and a property map.
|
|
if len(r.Aliases) > 0 {
|
|
fmt.Fprintf(w, " const aliasOpts = { aliases: [")
|
|
for i, alias := range r.Aliases {
|
|
if i > 0 {
|
|
fmt.Fprintf(w, ", ")
|
|
}
|
|
mod.genAlias(w, alias)
|
|
}
|
|
fmt.Fprintf(w, "] };\n")
|
|
fmt.Fprintf(w, " opts = pulumi.mergeOptions(opts, aliasOpts);\n")
|
|
}
|
|
|
|
if len(secretProps) > 0 {
|
|
fmt.Fprintf(w, ` const secretOpts = { additionalSecretOutputs: ["%s"] };`, strings.Join(secretProps, `", "`))
|
|
fmt.Fprintf(w, "\n opts = pulumi.mergeOptions(opts, secretOpts);\n")
|
|
}
|
|
|
|
// If it's a ComponentResource, set the remote option.
|
|
if r.IsComponent {
|
|
fmt.Fprintf(w, " super(%s.__pulumiType, name, inputs, opts, true /*remote*/);\n", name)
|
|
} else {
|
|
fmt.Fprintf(w, " super(%s.__pulumiType, name, inputs, opts);\n", name)
|
|
}
|
|
|
|
// Finish the class.
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, "}\n")
|
|
|
|
// Emit the state type for get methods.
|
|
if r.StateInputs != nil {
|
|
fmt.Fprintf(w, "\n")
|
|
mod.genPlainType(w, stateType, r.StateInputs.Comment, r.StateInputs.Properties, true, true, false, 0)
|
|
}
|
|
|
|
// Emit the argument type for construction.
|
|
fmt.Fprintf(w, "\n")
|
|
argsComment := fmt.Sprintf("The set of arguments for constructing a %s resource.", name)
|
|
mod.genPlainType(w, argsType, argsComment, r.InputProperties, true, true, false, 0)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mod *modContext) genFunction(w io.Writer, fun *schema.Function) {
|
|
name := tokenToFunctionName(fun.Token)
|
|
|
|
// Write the TypeDoc/JSDoc for the data source function.
|
|
printComment(w, codegen.FilterExamples(fun.Comment, "typescript"), "", "")
|
|
|
|
if fun.DeprecationMessage != "" {
|
|
fmt.Fprintf(w, "/** @deprecated %s */\n", fun.DeprecationMessage)
|
|
}
|
|
|
|
// Now, emit the function signature.
|
|
var argsig string
|
|
argsOptional := true
|
|
if fun.Inputs != nil {
|
|
for _, p := range fun.Inputs.Properties {
|
|
if p.IsRequired {
|
|
argsOptional = false
|
|
break
|
|
}
|
|
}
|
|
|
|
optFlag := ""
|
|
if argsOptional {
|
|
optFlag = "?"
|
|
}
|
|
argsig = fmt.Sprintf("args%s: %sArgs, ", optFlag, title(name))
|
|
}
|
|
var retty string
|
|
if fun.Outputs == nil {
|
|
retty = "void"
|
|
} else {
|
|
retty = title(name) + "Result"
|
|
}
|
|
fmt.Fprintf(w, "export function %s(%sopts?: pulumi.InvokeOptions): Promise<%s> {\n", name, argsig, retty)
|
|
if fun.DeprecationMessage != "" && mod.compatibility != kubernetes20 {
|
|
fmt.Fprintf(w, " pulumi.log.warn(\"%s is deprecated: %s\")\n", name, fun.DeprecationMessage)
|
|
}
|
|
|
|
// Zero initialize the args if empty and necessary.
|
|
if fun.Inputs != nil && argsOptional {
|
|
fmt.Fprintf(w, " args = args || {};\n")
|
|
}
|
|
|
|
// If the caller didn't request a specific version, supply one using the version of this library.
|
|
fmt.Fprintf(w, " if (!opts) {\n")
|
|
fmt.Fprintf(w, " opts = {}\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, "\n")
|
|
fmt.Fprintf(w, " if (!opts.version) {\n")
|
|
fmt.Fprintf(w, " opts.version = utilities.getVersion();\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
|
|
// Now simply invoke the runtime function with the arguments, returning the results.
|
|
fmt.Fprintf(w, " return pulumi.runtime.invoke(\"%s\", {\n", fun.Token)
|
|
if fun.Inputs != nil {
|
|
for _, p := range fun.Inputs.Properties {
|
|
// Pass the argument to the invocation.
|
|
fmt.Fprintf(w, " \"%[1]s\": args.%[1]s,\n", p.Name)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, " }, opts);\n")
|
|
fmt.Fprintf(w, "}\n")
|
|
|
|
// If there are argument and/or return types, emit them.
|
|
if fun.Inputs != nil {
|
|
fmt.Fprintf(w, "\n")
|
|
mod.genPlainType(w, title(name)+"Args", fun.Inputs.Comment, fun.Inputs.Properties, true, false, false, 0)
|
|
}
|
|
if fun.Outputs != nil {
|
|
fmt.Fprintf(w, "\n")
|
|
mod.genPlainType(w, title(name)+"Result", fun.Outputs.Comment, fun.Outputs.Properties, false, false, true, 0)
|
|
}
|
|
}
|
|
|
|
func visitObjectTypes(properties []*schema.Property, visitor func(*schema.ObjectType, bool)) {
|
|
codegen.VisitTypeClosure(properties, func(t codegen.Type) {
|
|
if o, ok := t.Type.(*schema.ObjectType); ok {
|
|
visitor(o, t.Plain)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (mod *modContext) genType(w io.Writer, obj *schema.ObjectType, input bool, level int) {
|
|
properties := obj.Properties
|
|
info, hasInfo := obj.Language["nodejs"]
|
|
if hasInfo {
|
|
var requiredProperties []string
|
|
if input {
|
|
requiredProperties = info.(NodeObjectInfo).RequiredInputs
|
|
} else {
|
|
requiredProperties = info.(NodeObjectInfo).RequiredOutputs
|
|
}
|
|
|
|
if requiredProperties != nil {
|
|
required := codegen.StringSet{}
|
|
for _, name := range requiredProperties {
|
|
required.Add(name)
|
|
}
|
|
|
|
properties = make([]*schema.Property, len(obj.Properties))
|
|
for i, p := range obj.Properties {
|
|
copy := *p
|
|
properties[i] = ©
|
|
properties[i].IsRequired = required.Has(p.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
name := tokenToName(obj.Token)
|
|
if mod.compatibility == tfbridge20 || mod.compatibility == kubernetes20 {
|
|
wrapInput := input && !mod.details(obj).plainType
|
|
mod.genPlainType(w, name, obj.Comment, properties, input, wrapInput, false, level)
|
|
return
|
|
}
|
|
|
|
if input {
|
|
if mod.details(obj).plainType {
|
|
mod.genPlainType(w, name, obj.Comment, properties, true, false, false, level)
|
|
}
|
|
if mod.details(obj).argsType {
|
|
mod.genPlainType(w, name+"Args", obj.Comment, properties, true, true, false, level)
|
|
}
|
|
return
|
|
}
|
|
|
|
mod.genPlainType(w, name, obj.Comment, properties, false, false, false, level)
|
|
}
|
|
|
|
func (mod *modContext) getTypeImports(t schema.Type, recurse bool, externalImports codegen.StringSet, imports map[string]codegen.StringSet, seen codegen.Set) bool {
|
|
if seen.Has(t) {
|
|
return false
|
|
}
|
|
seen.Add(t)
|
|
|
|
resourceOrTokenImport := func(tok string) bool {
|
|
modName, name, modPath := mod.pkg.TokenToModule(tok), tokenToName(tok), "./index"
|
|
if override, ok := mod.modToPkg[modName]; ok {
|
|
modName = override
|
|
}
|
|
if modName != mod.mod {
|
|
mp, err := filepath.Rel(mod.mod, modName)
|
|
contract.Assert(err == nil)
|
|
if path.Base(mp) == "." {
|
|
mp = path.Dir(mp)
|
|
}
|
|
modPath = filepath.ToSlash(mp)
|
|
}
|
|
if imports[modPath] == nil {
|
|
imports[modPath] = codegen.NewStringSet()
|
|
}
|
|
imports[modPath].Add(name)
|
|
return false
|
|
}
|
|
|
|
switch t := t.(type) {
|
|
case *schema.ArrayType:
|
|
return mod.getTypeImports(t.ElementType, recurse, externalImports, imports, seen)
|
|
case *schema.MapType:
|
|
return mod.getTypeImports(t.ElementType, recurse, externalImports, imports, seen)
|
|
case *schema.EnumType:
|
|
return true
|
|
case *schema.ObjectType:
|
|
// If it's from another package, add an import for the external package.
|
|
if t.Package != nil && t.Package != mod.pkg {
|
|
pkg := t.Package.Name
|
|
externalImports.Add(fmt.Sprintf("import * as %[1]s from \"@pulumi/%[1]s\";", pkg))
|
|
return false
|
|
}
|
|
|
|
for _, p := range t.Properties {
|
|
mod.getTypeImports(p.Type, recurse, externalImports, imports, seen)
|
|
}
|
|
return true
|
|
case *schema.ResourceType:
|
|
// If it's from another package, add an import for the external package.
|
|
if t.Resource != nil && t.Resource.Package != mod.pkg {
|
|
pkg := t.Resource.Package.Name
|
|
externalImports.Add(fmt.Sprintf("import * as %[1]s from \"@pulumi/%[1]s\";", pkg))
|
|
return false
|
|
}
|
|
|
|
return resourceOrTokenImport(t.Token)
|
|
case *schema.TokenType:
|
|
return resourceOrTokenImport(t.Token)
|
|
case *schema.UnionType:
|
|
needsTypes := false
|
|
for _, e := range t.ElementTypes {
|
|
needsTypes = mod.getTypeImports(e, recurse, externalImports, imports, seen) || needsTypes
|
|
}
|
|
return needsTypes
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) getImports(member interface{}, externalImports codegen.StringSet, imports map[string]codegen.StringSet) bool {
|
|
seen := codegen.Set{}
|
|
switch member := member.(type) {
|
|
case *schema.ObjectType:
|
|
needsTypes := false
|
|
for _, p := range member.Properties {
|
|
needsTypes = mod.getTypeImports(p.Type, true, externalImports, imports, seen) || needsTypes
|
|
}
|
|
return needsTypes
|
|
case *schema.ResourceType:
|
|
mod.getTypeImports(member, true, externalImports, imports, seen)
|
|
return false
|
|
case *schema.Resource:
|
|
needsTypes := false
|
|
for _, p := range member.Properties {
|
|
needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes
|
|
}
|
|
for _, p := range member.InputProperties {
|
|
needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes
|
|
}
|
|
return needsTypes
|
|
case *schema.Function:
|
|
needsTypes := false
|
|
if member.Inputs != nil {
|
|
needsTypes = mod.getTypeImports(member.Inputs, false, externalImports, imports, seen) || needsTypes
|
|
}
|
|
if member.Outputs != nil {
|
|
needsTypes = mod.getTypeImports(member.Outputs, false, externalImports, imports, seen) || needsTypes
|
|
}
|
|
return needsTypes
|
|
case []*schema.Property:
|
|
needsTypes := false
|
|
for _, p := range member {
|
|
needsTypes = mod.getTypeImports(p.Type, false, externalImports, imports, seen) || needsTypes
|
|
}
|
|
return needsTypes
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) genHeader(w io.Writer, imports []string, externalImports codegen.StringSet, importedTypes map[string]codegen.StringSet) {
|
|
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 len(imports) > 0 {
|
|
for _, i := range imports {
|
|
fmt.Fprintf(w, "%s\n", i)
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
if externalImports.Any() {
|
|
for _, i := range externalImports.SortedValues() {
|
|
fmt.Fprintf(w, "%s\n", i)
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
|
|
if len(importedTypes) > 0 {
|
|
var modules []string
|
|
for module := range importedTypes {
|
|
modules = append(modules, module)
|
|
}
|
|
sort.Strings(modules)
|
|
|
|
for _, module := range modules {
|
|
fmt.Fprintf(w, "import {")
|
|
for i, name := range importedTypes[module].SortedValues() {
|
|
if i > 0 {
|
|
fmt.Fprint(w, ", ")
|
|
}
|
|
fmt.Fprint(w, name)
|
|
}
|
|
fmt.Fprintf(w, "} from \"%s\";\n", module)
|
|
}
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
|
|
// configGetter returns the name of the config.get* method used for a configuration variable and the cast necessary
|
|
// for the result of the call, if any.
|
|
func (mod *modContext) configGetter(v *schema.Property) (string, string) {
|
|
if v.Type == schema.StringType {
|
|
return "get", ""
|
|
}
|
|
|
|
if tok, ok := v.Type.(*schema.TokenType); ok && tok.UnderlyingType == schema.StringType {
|
|
return "get", fmt.Sprintf("<%s>", mod.typeString(v.Type, false, false, false, false, nil))
|
|
}
|
|
|
|
// Only try to parse a JSON object if the config isn't a straight string.
|
|
return fmt.Sprintf("getObject<%s>", mod.typeString(v.Type, false, false, false, false, nil)), ""
|
|
}
|
|
|
|
func (mod *modContext) genConfig(w io.Writer, variables []*schema.Property) error {
|
|
externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{}
|
|
referencesNestedTypes := mod.getImports(variables, externalImports, imports)
|
|
|
|
mod.genHeader(w, mod.sdkImports(referencesNestedTypes, true), externalImports, imports)
|
|
|
|
// Create a config bag for the variables to pull from.
|
|
fmt.Fprintf(w, "let __config = new pulumi.Config(\"%v\");\n", mod.pkg.Name)
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
// Emit an entry for all config variables.
|
|
for _, p := range variables {
|
|
getfunc, cast := mod.configGetter(p)
|
|
|
|
printComment(w, p.Comment, "", "")
|
|
|
|
configFetch := fmt.Sprintf("%s__config.%s(\"%s\")", cast, getfunc, p.Name)
|
|
// TODO: handle ConstValues https://github.com/pulumi/pulumi/issues/4755
|
|
if p.DefaultValue != nil {
|
|
v, err := mod.getDefaultValue(p.DefaultValue, p.Type)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Note: this logic isn't quite correct, but already exists in all of the TF-based providers.
|
|
// Specifically, this doesn't work right if the first value is set to false but the default value
|
|
// is true.
|
|
configFetch += " || " + v
|
|
}
|
|
|
|
fmt.Fprintf(w, "export let %s: %s = %s;\n",
|
|
p.Name, mod.typeString(p.Type, false, false, false, true, nil), configFetch)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mod *modContext) getRelativePath() string {
|
|
rel, err := filepath.Rel(mod.mod, "")
|
|
contract.Assert(err == nil)
|
|
return path.Dir(filepath.ToSlash(rel))
|
|
}
|
|
|
|
func (mod *modContext) sdkImports(nested, utilities bool) []string {
|
|
imports := []string{"import * as pulumi from \"@pulumi/pulumi\";"}
|
|
|
|
relRoot := mod.getRelativePath()
|
|
if nested {
|
|
enumsImport := ""
|
|
containsEnums := mod.pkg.Language["nodejs"].(NodePackageInfo).ContainsEnums
|
|
if containsEnums {
|
|
enumsImport = ", enums"
|
|
}
|
|
imports = append(imports, fmt.Sprintf(`import { input as inputs, output as outputs%s } from "%s/types";`, enumsImport, relRoot))
|
|
}
|
|
|
|
if utilities {
|
|
imports = append(imports, fmt.Sprintf("import * as utilities from \"%s/utilities\";", relRoot))
|
|
}
|
|
|
|
return imports
|
|
}
|
|
|
|
func (mod *modContext) genTypes() (string, string) {
|
|
externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{}
|
|
for _, t := range mod.types {
|
|
mod.getImports(t, externalImports, imports)
|
|
}
|
|
|
|
inputs, outputs := &bytes.Buffer{}, &bytes.Buffer{}
|
|
|
|
mod.genHeader(inputs, mod.sdkImports(true, false), externalImports, imports)
|
|
mod.genHeader(outputs, mod.sdkImports(true, false), externalImports, imports)
|
|
|
|
// Build a namespace tree out of the types, then emit them.
|
|
namespaces := mod.getNamespaces()
|
|
mod.genNamespace(inputs, namespaces[""], true, 0)
|
|
mod.genNamespace(outputs, namespaces[""], false, 0)
|
|
|
|
return inputs.String(), outputs.String()
|
|
}
|
|
|
|
type namespace struct {
|
|
name string
|
|
types []*schema.ObjectType
|
|
enums []*schema.EnumType
|
|
children []*namespace
|
|
}
|
|
|
|
func (mod *modContext) getNamespaces() map[string]*namespace {
|
|
namespaces := map[string]*namespace{}
|
|
var getNamespace func(string) *namespace
|
|
getNamespace = func(mod string) *namespace {
|
|
ns, ok := namespaces[mod]
|
|
if !ok {
|
|
name := mod
|
|
if mod != "" {
|
|
name = path.Base(mod)
|
|
}
|
|
|
|
ns = &namespace{name: name}
|
|
if mod != "" {
|
|
parentMod := path.Dir(mod)
|
|
if parentMod == "." {
|
|
parentMod = ""
|
|
}
|
|
parent := getNamespace(parentMod)
|
|
parent.children = append(parent.children, ns)
|
|
}
|
|
|
|
namespaces[mod] = ns
|
|
}
|
|
return ns
|
|
}
|
|
|
|
for _, t := range mod.types {
|
|
modName := mod.pkg.TokenToModule(t.Token)
|
|
if override, ok := mod.modToPkg[modName]; ok {
|
|
modName = override
|
|
}
|
|
ns := getNamespace(modName)
|
|
ns.types = append(ns.types, t)
|
|
}
|
|
|
|
return namespaces
|
|
}
|
|
|
|
func (mod *modContext) genNamespace(w io.Writer, ns *namespace, input bool, level int) {
|
|
indent := strings.Repeat(" ", level)
|
|
|
|
sort.Slice(ns.types, func(i, j int) bool {
|
|
return tokenToName(ns.types[i].Token) < tokenToName(ns.types[j].Token)
|
|
})
|
|
sort.Slice(ns.enums, func(i, j int) bool {
|
|
return tokenToName(ns.enums[i].Token) < tokenToName(ns.enums[j].Token)
|
|
})
|
|
for i, t := range ns.types {
|
|
if input && mod.details(t).inputType || !input && mod.details(t).outputType {
|
|
mod.genType(w, t, input, level)
|
|
if i != len(ns.types)-1 {
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(ns.children, func(i, j int) bool {
|
|
return ns.children[i].name < ns.children[j].name
|
|
})
|
|
for i, child := range ns.children {
|
|
fmt.Fprintf(w, "%sexport namespace %s {\n", indent, child.name)
|
|
mod.genNamespace(w, child, input, level+1)
|
|
fmt.Fprintf(w, "%s}\n", indent)
|
|
if i != len(ns.children)-1 {
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) genEnum(w io.Writer, enum *schema.EnumType) error {
|
|
indent := " "
|
|
enumName := tokenToName(enum.Token)
|
|
fmt.Fprintf(w, "export const %s = {\n", enumName)
|
|
for _, e := range enum.Elements {
|
|
// If the enum doesn't have a name, set the value as the name.
|
|
if e.Name == "" {
|
|
e.Name = fmt.Sprintf("%v", e.Value)
|
|
}
|
|
safeName, err := makeSafeEnumName(e.Name, enumName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
e.Name = safeName
|
|
|
|
printComment(w, e.Comment, e.DeprecationMessage, indent)
|
|
fmt.Fprintf(w, "%s%s: ", indent, e.Name)
|
|
if val, ok := e.Value.(string); ok {
|
|
fmt.Fprintf(w, "%q,\n", val)
|
|
} else {
|
|
fmt.Fprintf(w, "%v,\n", e.Value)
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "} as const;\n")
|
|
fmt.Fprintf(w, "\n")
|
|
|
|
printComment(w, enum.Comment, "", "")
|
|
fmt.Fprintf(w, "export type %[1]s = (typeof %[1]s)[keyof typeof %[1]s];\n", enumName)
|
|
return nil
|
|
}
|
|
|
|
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) isReservedSourceFileName(name string) bool {
|
|
switch name {
|
|
case "index.ts":
|
|
return true
|
|
case "input.ts", "output.ts":
|
|
return len(mod.types) != 0
|
|
case "utilities.ts":
|
|
return mod.mod == ""
|
|
case "vars.ts":
|
|
return len(mod.pkg.Config) > 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (mod *modContext) gen(fs fs) error {
|
|
files := append([]string(nil), mod.extraSourceFiles...)
|
|
|
|
modDir := strings.ToLower(mod.mod)
|
|
|
|
addFile := func(name, contents string) {
|
|
p := path.Join(modDir, name)
|
|
files = append(files, p)
|
|
fs.add(p, []byte(contents))
|
|
}
|
|
|
|
// Utilities, config, readme
|
|
switch mod.mod {
|
|
case "":
|
|
buffer := &bytes.Buffer{}
|
|
mod.genHeader(buffer, nil, nil, nil)
|
|
fmt.Fprintf(buffer, "%s", utilitiesFile)
|
|
fs.add(path.Join(modDir, "utilities.ts"), buffer.Bytes())
|
|
|
|
// Ensure that the top-level (provider) module directory contains a README.md file.
|
|
readme := mod.pkg.Language["nodejs"].(NodePackageInfo).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(path.Join(modDir, "README.md"), []byte(readme))
|
|
case "config":
|
|
if len(mod.pkg.Config) > 0 {
|
|
buffer := &bytes.Buffer{}
|
|
if err := mod.genConfig(buffer, mod.pkg.Config); err != nil {
|
|
return err
|
|
}
|
|
addFile("vars.ts", buffer.String())
|
|
}
|
|
}
|
|
|
|
// Resources
|
|
for _, r := range mod.resources {
|
|
externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{}
|
|
referencesNestedTypes := mod.getImports(r, externalImports, imports)
|
|
|
|
buffer := &bytes.Buffer{}
|
|
mod.genHeader(buffer, mod.sdkImports(referencesNestedTypes, true), externalImports, imports)
|
|
|
|
if err := mod.genResource(buffer, r); err != nil {
|
|
return err
|
|
}
|
|
|
|
fileName := mod.resourceFileName(r)
|
|
addFile(fileName, buffer.String())
|
|
}
|
|
|
|
// Functions
|
|
for _, f := range mod.functions {
|
|
externalImports, imports := codegen.NewStringSet(), map[string]codegen.StringSet{}
|
|
referencesNestedTypes := mod.getImports(f, externalImports, imports)
|
|
|
|
buffer := &bytes.Buffer{}
|
|
mod.genHeader(buffer, mod.sdkImports(referencesNestedTypes, true), externalImports, imports)
|
|
|
|
mod.genFunction(buffer, f)
|
|
|
|
fileName := camel(tokenToName(f.Token)) + ".ts"
|
|
if mod.isReservedSourceFileName(fileName) {
|
|
fileName = camel(tokenToName(f.Token)) + "_.ts"
|
|
}
|
|
addFile(fileName, buffer.String())
|
|
}
|
|
|
|
if mod.hasEnums() {
|
|
buffer := &bytes.Buffer{}
|
|
mod.genHeader(buffer, []string{}, nil, nil)
|
|
|
|
err := mod.genEnums(buffer, mod.enums)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var fileName string
|
|
if modDir == "" {
|
|
fileName = "index.ts"
|
|
} else {
|
|
fileName = path.Join(modDir, "index.ts")
|
|
}
|
|
fileName = path.Join("types", "enums", fileName)
|
|
fs.add(fileName, buffer.Bytes())
|
|
}
|
|
|
|
// Nested types
|
|
if len(mod.types) > 0 {
|
|
input, output := mod.genTypes()
|
|
fs.add(path.Join(modDir, "input.ts"), []byte(input))
|
|
fs.add(path.Join(modDir, "output.ts"), []byte(output))
|
|
}
|
|
|
|
// Index
|
|
fs.add(path.Join(modDir, "index.ts"), []byte(mod.genIndex(files)))
|
|
return nil
|
|
}
|
|
|
|
func getChildMod(modName string) string {
|
|
child := strings.ToLower(modName)
|
|
// Extract version suffix from child modules. Nested versions will have their own index.ts file.
|
|
// Example: apps/v1beta1 -> v1beta1
|
|
parts := strings.SplitN(child, "/", 2)
|
|
if len(parts) == 2 {
|
|
child = parts[1]
|
|
}
|
|
return child
|
|
}
|
|
|
|
// genIndex emits an index module, optionally re-exporting other members or submodules.
|
|
func (mod *modContext) genIndex(exports []string) string {
|
|
w := &bytes.Buffer{}
|
|
|
|
var imports []string
|
|
// Include the SDK import if we'll be registering module resources.
|
|
if len(mod.resources) != 0 {
|
|
imports = mod.sdkImports(false /*nested*/, true /*utilities*/)
|
|
}
|
|
mod.genHeader(w, imports, nil, nil)
|
|
|
|
// Export anything flatly that is a direct export rather than sub-module.
|
|
if len(exports) > 0 {
|
|
modDir := strings.ToLower(mod.mod)
|
|
fmt.Fprintf(w, "// Export members:\n")
|
|
sort.Strings(exports)
|
|
for _, exp := range exports {
|
|
rel, err := filepath.Rel(modDir, exp)
|
|
contract.Assert(err == nil)
|
|
if path.Base(rel) == "." {
|
|
rel = path.Dir(rel)
|
|
}
|
|
fmt.Fprintf(w, "export * from \"./%s\";\n", strings.TrimSuffix(rel, ".ts"))
|
|
}
|
|
}
|
|
|
|
children := codegen.NewStringSet()
|
|
|
|
for _, mod := range mod.children {
|
|
child := getChildMod(mod.mod)
|
|
children.Add(child)
|
|
}
|
|
|
|
if len(mod.types) > 0 {
|
|
children.Add("input")
|
|
children.Add("output")
|
|
}
|
|
|
|
info, _ := mod.pkg.Language["nodejs"].(NodePackageInfo)
|
|
if info.ContainsEnums {
|
|
if mod.mod == "types" {
|
|
children.Add("enums")
|
|
} else if len(mod.enums) > 0 {
|
|
fmt.Fprintf(w, "\n")
|
|
fmt.Fprintf(w, "// Export enums:\n")
|
|
rel := mod.getRelativePath()
|
|
var filePath string
|
|
if mod.mod == "" {
|
|
filePath = ""
|
|
} else {
|
|
filePath = fmt.Sprintf("/%s", mod.mod)
|
|
}
|
|
fmt.Fprintf(w, "export * from \"%s/types/enums%s\";\n", rel, filePath)
|
|
}
|
|
}
|
|
|
|
// If there are submodules, export them.
|
|
if len(children) > 0 {
|
|
if len(exports) > 0 {
|
|
fmt.Fprintf(w, "\n")
|
|
}
|
|
fmt.Fprintf(w, "// Export sub-modules:\n")
|
|
|
|
sorted := children.SortedValues()
|
|
for _, mod := range sorted {
|
|
fmt.Fprintf(w, "import * as %[1]s from \"./%[1]s\";\n", mod)
|
|
}
|
|
|
|
printExports(w, sorted)
|
|
}
|
|
|
|
// If there are resources in this module, register the module with the runtime.
|
|
if len(mod.resources) != 0 {
|
|
mod.genResourceModule(w)
|
|
}
|
|
|
|
return w.String()
|
|
}
|
|
|
|
// genResourceModule generates a ResourceModule definition and the code to register an instance thereof with the
|
|
// Pulumi runtime. The generated ResourceModule supports the deserialization of resource references into fully-
|
|
// hydrated Resource instances. If this is the root module, this function also generates a ResourcePackage
|
|
// definition and its registration to support rehydrating providers.
|
|
func (mod *modContext) genResourceModule(w io.Writer) {
|
|
contract.Assert(len(mod.resources) != 0)
|
|
|
|
// Check for provider-only modules.
|
|
var provider *schema.Resource
|
|
if providerOnly := len(mod.resources) == 1 && mod.resources[0].IsProvider; providerOnly {
|
|
provider = mod.resources[0]
|
|
} else {
|
|
registrations, first := codegen.StringSet{}, true
|
|
for _, r := range mod.resources {
|
|
if r.IsProvider {
|
|
contract.Assert(provider == nil)
|
|
provider = r
|
|
continue
|
|
}
|
|
|
|
registrations.Add(mod.pkg.TokenToRuntimeModule(r.Token))
|
|
|
|
if first {
|
|
first = false
|
|
fmt.Fprintf(w, "\n// Import resources to register:\n")
|
|
}
|
|
fileName := strings.TrimSuffix(mod.resourceFileName(r), ".ts")
|
|
fmt.Fprintf(w, "import { %s } from \"./%s\";\n", resourceName(r), fileName)
|
|
}
|
|
|
|
fmt.Fprintf(w, "\nconst _module = {\n")
|
|
fmt.Fprintf(w, " version: utilities.getVersion(),\n")
|
|
fmt.Fprintf(w, " construct: (name: string, type: string, urn: string): pulumi.Resource => {\n")
|
|
fmt.Fprintf(w, " switch (type) {\n")
|
|
|
|
for _, r := range mod.resources {
|
|
if r.IsProvider {
|
|
continue
|
|
}
|
|
|
|
fmt.Fprintf(w, " case \"%v\":\n", r.Token)
|
|
fmt.Fprintf(w, " return new %v(name, <any>undefined, { urn })\n", resourceName(r))
|
|
}
|
|
|
|
fmt.Fprintf(w, " default:\n")
|
|
fmt.Fprintf(w, " throw new Error(`unknown resource type ${type}`);\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, " },\n")
|
|
fmt.Fprintf(w, "};\n")
|
|
for _, name := range registrations.SortedValues() {
|
|
fmt.Fprintf(w, "pulumi.runtime.registerResourceModule(\"%v\", \"%v\", _module)\n", mod.pkg.Name, name)
|
|
}
|
|
}
|
|
|
|
if provider != nil {
|
|
fmt.Fprintf(w, "\nimport { Provider } from \"./provider\";\n\n")
|
|
fmt.Fprintf(w, "pulumi.runtime.registerResourcePackage(\"%v\", {\n", mod.pkg.Name)
|
|
fmt.Fprintf(w, " version: utilities.getVersion(),\n")
|
|
fmt.Fprintf(w, " constructProvider: (name: string, type: string, urn: string): pulumi.ProviderResource => {\n")
|
|
fmt.Fprintf(w, " if (type !== \"%v\") {\n", provider.Token)
|
|
fmt.Fprintf(w, " throw new Error(`unknown provider type ${type}`);\n")
|
|
fmt.Fprintf(w, " }\n")
|
|
fmt.Fprintf(w, " return new Provider(name, <any>undefined, { urn });\n")
|
|
fmt.Fprintf(w, " },\n")
|
|
fmt.Fprintf(w, "});\n")
|
|
}
|
|
}
|
|
|
|
func printExports(w io.Writer, exports []string) {
|
|
fmt.Fprintf(w, "\n")
|
|
fmt.Fprintf(w, "export {\n")
|
|
for _, mod := range exports {
|
|
fmt.Fprintf(w, " %s,\n", mod)
|
|
}
|
|
fmt.Fprintf(w, "};\n")
|
|
}
|
|
|
|
func (mod *modContext) hasEnums() bool {
|
|
if mod.mod == "types" {
|
|
return false
|
|
}
|
|
if len(mod.enums) > 0 {
|
|
return true
|
|
}
|
|
if len(mod.children) > 0 {
|
|
for _, mod := range mod.children {
|
|
if mod.hasEnums() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (mod *modContext) genEnums(buffer *bytes.Buffer, enums []*schema.EnumType) error {
|
|
if len(mod.children) > 0 {
|
|
children := codegen.NewStringSet()
|
|
|
|
for _, mod := range mod.children {
|
|
child := getChildMod(mod.mod)
|
|
if mod.hasEnums() {
|
|
children.Add(child)
|
|
}
|
|
}
|
|
|
|
if len(children) > 0 {
|
|
fmt.Fprintf(buffer, "// Export sub-modules:\n")
|
|
|
|
sorted := children.SortedValues()
|
|
for _, mod := range sorted {
|
|
fmt.Fprintf(buffer, "import * as %[1]s from \"./%[1]s\";\n", mod)
|
|
}
|
|
printExports(buffer, sorted)
|
|
}
|
|
}
|
|
if len(enums) > 0 {
|
|
fmt.Fprintf(buffer, "\n")
|
|
for i, enum := range enums {
|
|
err := mod.genEnum(buffer, enum)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if i != len(enums)-1 {
|
|
fmt.Fprintf(buffer, "\n")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// genPackageMetadata generates all the non-code metadata required by a Pulumi package.
|
|
func genPackageMetadata(pkg *schema.Package, info NodePackageInfo, files fs) {
|
|
// The generator already emitted Pulumi.yaml, so that leaves two more files to write out:
|
|
// 1) package.json: minimal NPM package metadata
|
|
// 2) tsconfig.json: instructions for TypeScript compilation
|
|
files.add("package.json", []byte(genNPMPackageMetadata(pkg, info)))
|
|
files.add("tsconfig.json", []byte(genTypeScriptProjectFile(info, files)))
|
|
}
|
|
|
|
type npmPackage struct {
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
Description string `json:"description,omitempty"`
|
|
Keywords []string `json:"keywords,omitempty"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
Repository string `json:"repository,omitempty"`
|
|
License string `json:"license,omitempty"`
|
|
Scripts map[string]string `json:"scripts,omitempty"`
|
|
Dependencies map[string]string `json:"dependencies,omitempty"`
|
|
DevDependencies map[string]string `json:"devDependencies,omitempty"`
|
|
PeerDependencies map[string]string `json:"peerDependencies,omitempty"`
|
|
Resolutions map[string]string `json:"resolutions,omitempty"`
|
|
Pulumi npmPulumiManifest `json:"pulumi,omitempty"`
|
|
}
|
|
|
|
type npmPulumiManifest struct {
|
|
Resource bool `json:"resource,omitempty"`
|
|
PluginDownloadURL string `json:"pluginDownloadURL,omitempty"`
|
|
}
|
|
|
|
func genNPMPackageMetadata(pkg *schema.Package, info NodePackageInfo) string {
|
|
packageName := info.PackageName
|
|
if packageName == "" {
|
|
packageName = fmt.Sprintf("@pulumi/%s", pkg.Name)
|
|
}
|
|
|
|
devDependencies := map[string]string{}
|
|
if info.TypeScriptVersion != "" {
|
|
devDependencies["typescript"] = info.TypeScriptVersion
|
|
}
|
|
|
|
// Create info that will get serialized into an NPM package.json.
|
|
npminfo := npmPackage{
|
|
Name: packageName,
|
|
Version: "${VERSION}",
|
|
Description: info.PackageDescription,
|
|
Keywords: pkg.Keywords,
|
|
Homepage: pkg.Homepage,
|
|
Repository: pkg.Repository,
|
|
License: pkg.License,
|
|
// Ideally, this `scripts` section would include an install script that installs the provider, however, doing
|
|
// so causes problems when we try to restore package dependencies, since we must do an install for that. So
|
|
// we have another process that adds the install script when generating the package.json that we actually
|
|
// publish.
|
|
Scripts: map[string]string{
|
|
"build": "tsc",
|
|
},
|
|
DevDependencies: devDependencies,
|
|
Pulumi: npmPulumiManifest{
|
|
Resource: true,
|
|
PluginDownloadURL: pkg.PluginDownloadURL,
|
|
},
|
|
}
|
|
|
|
// Copy the overlay dependencies, if any.
|
|
for depk, depv := range info.Dependencies {
|
|
if npminfo.Dependencies == nil {
|
|
npminfo.Dependencies = make(map[string]string)
|
|
}
|
|
npminfo.Dependencies[depk] = depv
|
|
}
|
|
for depk, depv := range info.DevDependencies {
|
|
if npminfo.DevDependencies == nil {
|
|
npminfo.DevDependencies = make(map[string]string)
|
|
}
|
|
npminfo.DevDependencies[depk] = depv
|
|
}
|
|
for depk, depv := range info.PeerDependencies {
|
|
if npminfo.PeerDependencies == nil {
|
|
npminfo.PeerDependencies = make(map[string]string)
|
|
}
|
|
npminfo.PeerDependencies[depk] = depv
|
|
}
|
|
for resk, resv := range info.Resolutions {
|
|
if npminfo.Resolutions == nil {
|
|
npminfo.Resolutions = make(map[string]string)
|
|
}
|
|
npminfo.Resolutions[resk] = resv
|
|
}
|
|
|
|
// If there is no @pulumi/pulumi, add "latest" as a peer dependency (for npm linking style usage).
|
|
sdkPack := "@pulumi/pulumi"
|
|
if npminfo.Dependencies[sdkPack] == "" &&
|
|
npminfo.DevDependencies[sdkPack] == "" &&
|
|
npminfo.PeerDependencies[sdkPack] == "" {
|
|
if npminfo.PeerDependencies == nil {
|
|
npminfo.PeerDependencies = make(map[string]string)
|
|
}
|
|
npminfo.PeerDependencies["@pulumi/pulumi"] = "latest"
|
|
}
|
|
|
|
// Now write out the serialized form.
|
|
npmjson, err := json.MarshalIndent(npminfo, "", " ")
|
|
contract.Assert(err == nil)
|
|
return string(npmjson)
|
|
}
|
|
|
|
func genTypeScriptProjectFile(info NodePackageInfo, files fs) string {
|
|
w := &bytes.Buffer{}
|
|
|
|
fmt.Fprintf(w, `{
|
|
"compilerOptions": {
|
|
"outDir": "bin",
|
|
"target": "es2016",
|
|
"module": "commonjs",
|
|
"moduleResolution": "node",
|
|
"declaration": true,
|
|
"sourceMap": true,
|
|
"stripInternal": true,
|
|
"experimentalDecorators": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"strict": true
|
|
},
|
|
"files": [
|
|
`)
|
|
|
|
var tsFiles []string
|
|
for f := range files {
|
|
if path.Ext(f) == ".ts" {
|
|
tsFiles = append(tsFiles, f)
|
|
}
|
|
}
|
|
sort.Strings(tsFiles)
|
|
|
|
for i, file := range tsFiles {
|
|
var suffix string
|
|
if i != len(tsFiles)-1 {
|
|
suffix = ","
|
|
}
|
|
fmt.Fprintf(w, " \"%s\"%s\n", file, suffix)
|
|
}
|
|
fmt.Fprintf(w, ` ]
|
|
}
|
|
`)
|
|
return w.String()
|
|
}
|
|
|
|
// generateModuleContextMap groups resources, types, and functions into NodeJS packages.
|
|
func generateModuleContextMap(tool string, pkg *schema.Package, info NodePackageInfo,
|
|
extraFiles map[string][]byte) (map[string]*modContext, NodePackageInfo, error) {
|
|
|
|
// group resources, types, and functions into NodeJS packages
|
|
modules := map[string]*modContext{}
|
|
|
|
var getMod func(modName string) *modContext
|
|
getMod = func(modName string) *modContext {
|
|
if override, ok := info.ModuleToPackage[modName]; ok {
|
|
modName = override
|
|
}
|
|
mod, ok := modules[modName]
|
|
if !ok {
|
|
mod = &modContext{
|
|
pkg: pkg,
|
|
mod: modName,
|
|
tool: tool,
|
|
compatibility: info.Compatibility,
|
|
modToPkg: info.ModuleToPackage,
|
|
disableUnionOutputTypes: info.DisableUnionOutputTypes,
|
|
}
|
|
|
|
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 {
|
|
return getMod(pkg.TokenToModule(token))
|
|
}
|
|
|
|
// Create a temporary module for type information.
|
|
types := &modContext{}
|
|
|
|
// Create the config module if necessary.
|
|
if len(pkg.Config) > 0 &&
|
|
info.Compatibility != kubernetes20 { // k8s SDK doesn't use config.
|
|
_ = getMod("config")
|
|
}
|
|
|
|
visitObjectTypes(pkg.Config, func(t *schema.ObjectType, plain bool) {
|
|
types.details(t).outputType = true
|
|
})
|
|
|
|
scanResource := func(r *schema.Resource) {
|
|
mod := getModFromToken(r.Token)
|
|
mod.resources = append(mod.resources, r)
|
|
visitObjectTypes(r.Properties, func(t *schema.ObjectType, _ bool) {
|
|
types.details(t).outputType = true
|
|
})
|
|
visitObjectTypes(r.InputProperties, func(t *schema.ObjectType, plain bool) {
|
|
if r.IsProvider {
|
|
types.details(t).outputType = true
|
|
}
|
|
types.details(t).inputType = true
|
|
if plain {
|
|
types.details(t).plainType = true
|
|
} else {
|
|
types.details(t).argsType = true
|
|
}
|
|
})
|
|
if r.StateInputs != nil {
|
|
visitObjectTypes(r.StateInputs.Properties, func(t *schema.ObjectType, _ bool) {
|
|
types.details(t).inputType = true
|
|
types.details(t).argsType = true
|
|
})
|
|
}
|
|
}
|
|
|
|
scanResource(pkg.Provider)
|
|
for _, r := range pkg.Resources {
|
|
scanResource(r)
|
|
}
|
|
|
|
// Clear the input and outputs sets: we want the visitors below to touch the transitive closure of types reachable
|
|
// from function inputs and outputs, including types that have already been visited.
|
|
for _, f := range pkg.Functions {
|
|
mod := getModFromToken(f.Token)
|
|
mod.functions = append(mod.functions, f)
|
|
if f.Inputs != nil {
|
|
visitObjectTypes(f.Inputs.Properties, func(t *schema.ObjectType, _ bool) {
|
|
types.details(t).inputType = true
|
|
types.details(t).plainType = true
|
|
})
|
|
}
|
|
if f.Outputs != nil {
|
|
visitObjectTypes(f.Outputs.Properties, func(t *schema.ObjectType, _ bool) {
|
|
types.details(t).outputType = true
|
|
types.details(t).plainType = true
|
|
})
|
|
}
|
|
}
|
|
|
|
if _, ok := modules["types"]; ok {
|
|
return nil, info, errors.New("this provider has a `types` module which is reserved for input/output types")
|
|
}
|
|
|
|
// Create the types module.
|
|
for _, t := range pkg.Types {
|
|
switch typ := t.(type) {
|
|
case *schema.ObjectType:
|
|
types.types = append(types.types, typ)
|
|
case *schema.EnumType:
|
|
info.ContainsEnums = true
|
|
mod := getModFromToken(typ.Token)
|
|
mod.enums = append(mod.enums, typ)
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
if len(types.types) > 0 {
|
|
typeDetails, typeList := types.typeDetails, types.types
|
|
types = getMod("types")
|
|
types.typeDetails, types.types = typeDetails, typeList
|
|
}
|
|
|
|
// Add Typescript 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) != ".ts" {
|
|
continue
|
|
}
|
|
|
|
modName := path.Dir(p)
|
|
if modName == "/" || modName == "." {
|
|
modName = ""
|
|
}
|
|
mod := getMod(modName)
|
|
mod.extraSourceFiles = append(mod.extraSourceFiles, p)
|
|
}
|
|
|
|
return modules, info, nil
|
|
}
|
|
|
|
// LanguageResource holds information about a resource to be used by downstream codegen.
|
|
type LanguageResource struct {
|
|
*schema.Resource
|
|
|
|
Name string // The resource name (e.g., "FlowSchema")
|
|
Package string // The name of the package containing the resource definition (e.g., "flowcontrol.v1alpha1")
|
|
Properties []LanguageProperty // Properties of the resource
|
|
}
|
|
|
|
// LanguageProperty holds information about a resource property to be used by downstream codegen.
|
|
type LanguageProperty struct {
|
|
ConstValue string // If set, the constant value of the property (e.g., "flowcontrol.apiserver.k8s.io/v1alpha1")
|
|
Name string // The name of the property (e.g., "FlowSchemaSpec")
|
|
Package string // The package path containing the property definition (e.g., "outputs.flowcontrol.v1alpha1")
|
|
}
|
|
|
|
// LanguageResources returns a map of resources that can be used by downstream codegen. The map
|
|
// key is the resource schema token.
|
|
func LanguageResources(pkg *schema.Package) (map[string]LanguageResource, error) {
|
|
resources := map[string]LanguageResource{}
|
|
|
|
if err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
|
return nil, err
|
|
}
|
|
info, _ := pkg.Language["nodejs"].(NodePackageInfo)
|
|
|
|
modules, _, err := generateModuleContextMap("", pkg, info, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for modName, mod := range modules {
|
|
for _, r := range mod.resources {
|
|
packagePath := strings.Replace(modName, "/", ".", -1)
|
|
lr := LanguageResource{
|
|
Resource: r,
|
|
Name: resourceName(r),
|
|
Package: packagePath,
|
|
}
|
|
for _, p := range r.Properties {
|
|
lp := LanguageProperty{
|
|
Name: p.Name,
|
|
}
|
|
if p.ConstValue != nil {
|
|
lp.ConstValue = mod.typeString(p.Type, false, false, false, false, p.ConstValue)
|
|
} else {
|
|
lp.Package = mod.typeString(p.Type, false, false, false, false, nil)
|
|
}
|
|
lr.Properties = append(lr.Properties, lp)
|
|
}
|
|
resources[r.Token] = lr
|
|
}
|
|
}
|
|
|
|
return resources, nil
|
|
}
|
|
|
|
func GeneratePackage(tool string, pkg *schema.Package, extraFiles map[string][]byte) (map[string][]byte, error) {
|
|
// Decode node-specific info
|
|
if err := pkg.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
|
return nil, err
|
|
}
|
|
info, _ := pkg.Language["nodejs"].(NodePackageInfo)
|
|
|
|
modules, info, err := generateModuleContextMap(tool, pkg, info, extraFiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pkg.Language["nodejs"] = info
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// Finally emit the package metadata (NPM, TypeScript, and so on).
|
|
genPackageMetadata(pkg, info, files)
|
|
return files, nil
|
|
}
|
|
|
|
const utilitiesFile = `
|
|
export function getEnv(...vars: string[]): string | undefined {
|
|
for (const v of vars) {
|
|
const value = process.env[v];
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getEnvBoolean(...vars: string[]): boolean | undefined {
|
|
const s = getEnv(...vars);
|
|
if (s !== undefined) {
|
|
// 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 (["1", "t", "T", "true", "TRUE", "True"].find(v => v === s) !== undefined) {
|
|
return true;
|
|
}
|
|
if (["0", "f", "F", "false", "FALSE", "False"].find(v => v === s) !== undefined) {
|
|
return false;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getEnvNumber(...vars: string[]): number | undefined {
|
|
const s = getEnv(...vars);
|
|
if (s !== undefined) {
|
|
const f = parseFloat(s);
|
|
if (!isNaN(f)) {
|
|
return f;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function getVersion(): string {
|
|
let version = require('./package.json').version;
|
|
// Node allows for the version to be prefixed by a "v", while semver doesn't.
|
|
// If there is a v, strip it off.
|
|
if (version.indexOf('v') === 0) {
|
|
version = version.slice(1);
|
|
}
|
|
return version;
|
|
}
|
|
`
|