pulumi/pkg/codegen/schema/schema.go
Pat Gavlin 7b1d6ec1ac
Reify Input and Optional types in the schema type system. (#7059)
These changes support arbitrary combinations of input + plain types
within a schema. Handling plain types at the property level was not
sufficient to support such combinations. Reifying these types
required updating quite a bit of code. This is likely to have caused
some temporary complications, but should eventually lead to
substantial simplification in the SDK and program code generators.

With the new design, input and optional types are explicit in the schema
type system. Optionals will only appear at the outermost level of a type
(i.e. Input<Optional<>>, Array<Optional<>>, etc. will not occur). In
addition to explicit input types, each object type now has a "plain"
shape and an "input" shape. The former uses only plain types; the latter
uses input shapes wherever a plain type is not specified. Plain types
are indicated in the schema by setting the "plain" property of a type spec
to true.
2021-06-24 09:17:55 -07:00

1980 lines
62 KiB
Go

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package schema
import (
"encoding/json"
"fmt"
"math"
"net/url"
"os"
"path"
"regexp"
"sort"
"strings"
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// TODO:
// - Providerless packages
// Type represents a datatype in the Pulumi Schema. Types created by this package are identical if they are
// equal values.
type Type interface {
String() string
isType()
}
type primitiveType int
const (
boolType primitiveType = 1
intType primitiveType = 2
numberType primitiveType = 3
stringType primitiveType = 4
archiveType primitiveType = 5
assetType primitiveType = 6
anyType primitiveType = 7
jsonType primitiveType = 8
)
//nolint: goconst
func (t primitiveType) String() string {
switch t {
case boolType:
return "boolean"
case intType:
return "integer"
case numberType:
return "number"
case stringType:
return "string"
case archiveType:
return "pulumi:pulumi:Archive"
case assetType:
return "pulumi:pulumi:Asset"
case jsonType:
fallthrough
case anyType:
return "pulumi:pulumi:Any"
default:
panic("unknown primitive type")
}
}
func (primitiveType) isType() {}
// IsPrimitiveType returns true if the given Type is a primitive type. The primitive types are bool, int, number,
// string, archive, asset, and any.
func IsPrimitiveType(t Type) bool {
_, ok := plainType(t).(primitiveType)
return ok
}
var (
// BoolType represents the set of boolean values.
BoolType Type = boolType
// IntType represents the set of 32-bit integer values.
IntType Type = intType
// NumberType represents the set of IEEE754 double-precision values.
NumberType Type = numberType
// StringType represents the set of UTF-8 string values.
StringType Type = stringType
// ArchiveType represents the set of Pulumi Archive values.
ArchiveType Type = archiveType
// AssetType represents the set of Pulumi Asset values.
AssetType Type = assetType
// JSONType represents the set of JSON-encoded values.
JSONType Type = jsonType
// AnyType represents the complete set of values.
AnyType Type = anyType
)
// MapType represents maps from strings to particular element types.
type MapType struct {
// ElementType is the element type of the map.
ElementType Type
}
func (t *MapType) String() string {
return fmt.Sprintf("Map<%v>", t.ElementType)
}
func (*MapType) isType() {}
// ArrayType represents arrays of particular element types.
type ArrayType struct {
// ElementType is the element type of the array.
ElementType Type
}
func (t *ArrayType) String() string {
return fmt.Sprintf("Array<%v>", t.ElementType)
}
func (*ArrayType) isType() {}
// EnumType represents an enum.
type EnumType struct {
// Package is the type's package.
Package *Package
// Token is the type's Pulumi type token.
Token string
// Comment is the description of the type, if any.
Comment string
// Elements are the predefined enum values.
Elements []*Enum
// ElementType is the underlying type for the enum.
ElementType Type
}
// Enum contains information about an enum.
type Enum struct {
// Value is the value of the enum.
Value interface{}
// Comment is the description for the enum value.
Comment string
// Name for the enum.
Name string
// DeprecationMessage indicates whether or not the value is deprecated.
DeprecationMessage string
}
func (t *EnumType) String() string {
return t.Token
}
func (*EnumType) isType() {}
// UnionType represents values that may be any one of a specified set of types.
type UnionType struct {
// ElementTypes are the allowable types for the union type.
ElementTypes []Type
// DefaultType is the default type, if any, for the union type. This can be used by targets that do not support
// unions, or in positions where unions are not appropriate.
DefaultType Type
// Discriminator informs the consumer of an alternative schema based on the value associated with it.
Discriminator string
// Mapping is an optional object to hold mappings between payload values and schema names or references.
Mapping map[string]string
}
func (t *UnionType) String() string {
elements := make([]string, len(t.ElementTypes)+1)
for i, e := range t.ElementTypes {
elements[i] = e.String()
}
def := "default="
if t.DefaultType != nil {
def += t.DefaultType.String()
}
elements[len(elements)-1] = def
return fmt.Sprintf("Union<%v>", strings.Join(elements, ", "))
}
func (*UnionType) isType() {}
// ObjectType represents schematized maps from strings to particular types.
type ObjectType struct {
// Package is the package that defines the resource.
Package *Package
// Token is the type's Pulumi type token.
Token string
// Comment is the description of the type, if any.
Comment string
// Properties is the list of the type's properties.
Properties []*Property
// Language specifies additional language-specific data about the object type.
Language map[string]interface{}
// InputShape is the input shape for this object. Only valid if IsPlainShape returns true.
InputShape *ObjectType
// PlainShape is the plain shape for this object. Only valid if IsInputShape returns true.
PlainShape *ObjectType
properties map[string]*Property
}
// IsPlainShape returns true if this object type is the plain shape of a (plain, input)
// pair. The plain shape of an object does not contain *InputType values and only
// references other plain shapes.
func (t *ObjectType) IsPlainShape() bool {
return t.PlainShape == nil
}
// IsInputShape returns true if this object type is the plain shape of a (plain, input)
// pair. The input shape of an object may contain *InputType values and may
// reference other input shapes.
func (t *ObjectType) IsInputShape() bool {
return t.PlainShape != nil
}
func (t *ObjectType) Property(name string) (*Property, bool) {
if t.properties == nil && len(t.Properties) > 0 {
t.properties = make(map[string]*Property)
for _, p := range t.Properties {
t.properties[p.Name] = p
}
}
p, ok := t.properties[name]
return p, ok
}
func (t *ObjectType) String() string {
if t.PlainShape != nil {
return t.Token + "•Input"
}
return t.Token
}
func (*ObjectType) isType() {}
type ResourceType struct {
// Token is the type's Pulumi type token.
Token string
// Resource is the type's underlying resource.
Resource *Resource
}
func (t *ResourceType) String() string {
return t.Token
}
func (t *ResourceType) isType() {}
// TokenType represents an opaque type that is referred to only by its token. A TokenType may have an underlying type
// that can be used in place of the token.
type TokenType struct {
// Token is the type's Pulumi type token.
Token string
// Underlying type is the type's underlying type, if any.
UnderlyingType Type
}
func (t *TokenType) String() string {
return t.Token
}
func (*TokenType) isType() {}
// InputType represents a type that accepts either a prompt value or an output value.
type InputType struct {
// ElementType is the element type of the input.
ElementType Type
}
func (t *InputType) String() string {
return fmt.Sprintf("Input<%v>", t.ElementType)
}
func (*InputType) isType() {}
// OptionalType represents a type that accepts an optional value.
type OptionalType struct {
// ElementType is the element type of the input.
ElementType Type
}
func (t *OptionalType) String() string {
return fmt.Sprintf("Optional<%v>", t.ElementType)
}
func (*OptionalType) isType() {}
// DefaultValue describes a default value for a property.
type DefaultValue struct {
// Value specifies a static default value, if any. This value must be representable in the Pulumi schema type
// system, and its type must be assignable to that of the property to which the default applies.
Value interface{}
// Environment specifies a set of environment variables to probe for a default value.
Environment []string
// Language specifies additional language-specific data about the default value.
Language map[string]interface{}
}
// Property describes an object or resource property.
type Property struct {
// Name is the name of the property.
Name string
// Comment is the description of the property, if any.
Comment string
// Type is the type of the property.
Type Type
// ConstValue is the constant value for the property, if any.
ConstValue interface{}
// DefaultValue is the default value for the property, if any.
DefaultValue *DefaultValue
// DeprecationMessage indicates whether or not the property is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the property.
Language map[string]interface{}
// Secret is true if the property is secret (default false).
Secret bool
}
// IsRequired returns true if this property is required (i.e. its type is not Optional).
func (p *Property) IsRequired() bool {
_, optional := p.Type.(*OptionalType)
return !optional
}
// Alias describes an alias for a Pulumi resource.
type Alias struct {
// Name is the "name" portion of the alias, if any.
Name *string
// Project is the "project" portion of the alias, if any.
Project *string
// Type is the "type" portion of the alias, if any.
Type *string
}
// Resource describes a Pulumi resource.
type Resource struct {
// Package is the package that defines the resource.
Package *Package
// Token is the resource's Pulumi type token.
Token string
// Comment is the description of the resource, if any.
Comment string
// IsProvider is true if the resource is a provider resource.
IsProvider bool
// InputProperties is the list of the resource's input properties.
InputProperties []*Property
// Properties is the list of the resource's output properties. This should be a superset of the input properties.
Properties []*Property
// StateInputs is the set of inputs used to get an existing resource, if any.
StateInputs *ObjectType
// Aliases is the list of aliases for the resource.
Aliases []*Alias
// DeprecationMessage indicates whether or not the resource is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the resource.
Language map[string]interface{}
// IsComponent indicates whether the resource is a ComponentResource.
IsComponent bool
// Methods is the list of methods for the resource.
Methods []*Method
}
type Method struct {
// Name is the name of the method.
Name string
// Function is the function definition for the method.
Function *Function
}
// Function describes a Pulumi function.
type Function struct {
// Package is the package that defines the function.
Package *Package
// Token is the function's Pulumi type token.
Token string
// Comment is the description of the function, if any.
Comment string
// Inputs is the bag of input values for the function, if any.
Inputs *ObjectType
// Outputs is the bag of output values for the function, if any.
Outputs *ObjectType
// DeprecationMessage indicates whether or not the function is deprecated.
DeprecationMessage string
// Language specifies additional language-specific data about the function.
Language map[string]interface{}
// IsMethod indicates whether the function is a method of a resource.
IsMethod bool
}
// Package describes a Pulumi package.
type Package struct {
moduleFormat *regexp.Regexp
// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes". "random")
Name string
// Version is the version of the package.
Version *semver.Version
// Description is the description of the package.
Description string
// Keywords is the list of keywords that are associated with the package, if any.
Keywords []string
// Homepage is the package's homepage.
Homepage string
// License indicates which license is used for the package's contents.
License string
// Attribution allows freeform text attribution of derived work, if needed.
Attribution string
// Repository is the URL at which the source for the package can be found.
Repository string
// LogoURL is the URL for the package's logo, if any.
LogoURL string
// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
PluginDownloadURL string
// Types is the list of non-resource types defined by the package.
Types []Type
// Config is the set of configuration properties defined by the package.
Config []*Property
// Provider is the resource provider for the package, if any.
Provider *Resource
// Resources is the list of resource types defined by the package.
Resources []*Resource
// Functions is the list of functions defined by the package.
Functions []*Function
// Language specifies additional language-specific data about the package.
Language map[string]interface{}
resourceTable map[string]*Resource
resourceTypeTable map[string]*ResourceType
functionTable map[string]*Function
typeTable map[string]Type
}
// Language provides hooks for importing language-specific metadata in a package.
type Language interface {
// ImportDefaultSpec decodes language-specific metadata associated with a DefaultValue.
ImportDefaultSpec(def *DefaultValue, bytes json.RawMessage) (interface{}, error)
// ImportPropertySpec decodes language-specific metadata associated with a Property.
ImportPropertySpec(property *Property, bytes json.RawMessage) (interface{}, error)
// ImportObjectTypeSpec decodes language-specific metadata associated with a ObjectType.
ImportObjectTypeSpec(object *ObjectType, bytes json.RawMessage) (interface{}, error)
// ImportResourceSpec decodes language-specific metadata associated with a Resource.
ImportResourceSpec(resource *Resource, bytes json.RawMessage) (interface{}, error)
// ImportFunctionSpec decodes language-specific metadata associated with a Function.
ImportFunctionSpec(function *Function, bytes json.RawMessage) (interface{}, error)
// ImportPackageSpec decodes language-specific metadata associated with a Package.
ImportPackageSpec(pkg *Package, bytes json.RawMessage) (interface{}, error)
}
func sortedLanguageNames(metadata map[string]interface{}) []string {
names := make([]string, 0, len(metadata))
for lang := range metadata {
names = append(names, lang)
}
sort.Strings(names)
return names
}
func importDefaultLanguages(def *DefaultValue, languages map[string]Language) error {
for _, name := range sortedLanguageNames(def.Language) {
val := def.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportDefaultSpec(def, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
def.Language[name] = val
}
}
}
return nil
}
func importPropertyLanguages(property *Property, languages map[string]Language) error {
if property.DefaultValue != nil {
if err := importDefaultLanguages(property.DefaultValue, languages); err != nil {
return errors.Wrapf(err, "importing default value")
}
}
for _, name := range sortedLanguageNames(property.Language) {
val := property.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportPropertySpec(property, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
property.Language[name] = val
}
}
}
return nil
}
func importObjectTypeLanguages(object *ObjectType, languages map[string]Language) error {
for _, property := range object.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return errors.Wrapf(err, "importing property %v", property.Name)
}
}
for _, name := range sortedLanguageNames(object.Language) {
val := object.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportObjectTypeSpec(object, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
object.Language[name] = val
}
}
}
return nil
}
func importResourceLanguages(resource *Resource, languages map[string]Language) error {
for _, property := range resource.InputProperties {
if err := importPropertyLanguages(property, languages); err != nil {
return errors.Wrapf(err, "importing input property %v", property.Name)
}
}
for _, property := range resource.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return errors.Wrapf(err, "importing property %v", property.Name)
}
}
if resource.StateInputs != nil {
for _, property := range resource.StateInputs.Properties {
if err := importPropertyLanguages(property, languages); err != nil {
return errors.Wrapf(err, "importing state input property %v", property.Name)
}
}
}
for _, name := range sortedLanguageNames(resource.Language) {
val := resource.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportResourceSpec(resource, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
resource.Language[name] = val
}
}
}
return nil
}
func importFunctionLanguages(function *Function, languages map[string]Language) error {
if function.Inputs != nil {
if err := importObjectTypeLanguages(function.Inputs, languages); err != nil {
return errors.Wrapf(err, "importing inputs")
}
}
if function.Outputs != nil {
if err := importObjectTypeLanguages(function.Outputs, languages); err != nil {
return errors.Wrapf(err, "importing outputs")
}
}
for _, name := range sortedLanguageNames(function.Language) {
val := function.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportFunctionSpec(function, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
function.Language[name] = val
}
}
}
return nil
}
func (pkg *Package) ImportLanguages(languages map[string]Language) error {
if len(languages) == 0 {
return nil
}
for _, t := range pkg.Types {
if object, ok := t.(*ObjectType); ok {
if err := importObjectTypeLanguages(object, languages); err != nil {
return errors.Wrapf(err, "importing object type %v", object.Token)
}
}
}
for _, config := range pkg.Config {
if err := importPropertyLanguages(config, languages); err != nil {
return errors.Wrapf(err, "importing configuration property %v", config.Name)
}
}
if pkg.Provider != nil {
if err := importResourceLanguages(pkg.Provider, languages); err != nil {
return errors.Wrapf(err, "importing provider")
}
}
for _, resource := range pkg.Resources {
if err := importResourceLanguages(resource, languages); err != nil {
return errors.Wrapf(err, "importing resource %v", resource.Token)
}
}
for _, function := range pkg.Functions {
if err := importFunctionLanguages(function, languages); err != nil {
return errors.Wrapf(err, "importing function %v", function.Token)
}
}
for _, name := range sortedLanguageNames(pkg.Language) {
val := pkg.Language[name]
if raw, ok := val.(json.RawMessage); ok {
if lang, ok := languages[name]; ok {
val, err := lang.ImportPackageSpec(pkg, raw)
if err != nil {
return errors.Wrapf(err, "importing %v metadata", name)
}
pkg.Language[name] = val
}
}
}
return nil
}
func (pkg *Package) TokenToModule(tok string) string {
// token := pkg ":" module ":" member
components := strings.Split(tok, ":")
if len(components) != 3 {
return ""
}
switch components[1] {
case "providers":
return ""
default:
matches := pkg.moduleFormat.FindStringSubmatch(components[1])
if len(matches) < 2 || strings.HasPrefix(matches[1], "index") {
return ""
}
return matches[1]
}
}
func (pkg *Package) TokenToRuntimeModule(tok string) string {
// token := pkg ":" module ":" member
components := strings.Split(tok, ":")
if len(components) != 3 {
return ""
}
return components[1]
}
func (pkg *Package) GetResource(token string) (*Resource, bool) {
r, ok := pkg.resourceTable[token]
return r, ok
}
func (pkg *Package) GetFunction(token string) (*Function, bool) {
f, ok := pkg.functionTable[token]
return f, ok
}
func (pkg *Package) GetResourceType(token string) (*ResourceType, bool) {
t, ok := pkg.resourceTypeTable[token]
return t, ok
}
func (pkg *Package) GetType(token string) (Type, bool) {
t, ok := pkg.typeTable[token]
return t, ok
}
// TypeSpec is the serializable form of a reference to a type.
type TypeSpec struct {
// Type is the primitive or composite type, if any. May be "bool", "integer", "number", "string", "array", or
// "object".
Type string `json:"type,omitempty"`
// Ref is a reference to a type in this or another document. For example, the built-in Archive, Asset, and Any
// types are referenced as "pulumi.json#/Archive", "pulumi.json#/Asset", and "pulumi.json#/Any", respectively.
// A type from this document is referenced as "#/types/pulumi:type:token".
// A type from another document is referenced as "path#/types/pulumi:type:token", where path is of the form:
// "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
// A resource from this document is referenced as "#/resources/pulumi:type:token".
// A resource from another document is referenced as "path#/resources/pulumi:type:token", where path is of the form:
// "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
Ref string `json:"$ref,omitempty"`
// AdditionalProperties, if set, describes the element type of an "object" (i.e. a string -> value map).
AdditionalProperties *TypeSpec `json:"additionalProperties,omitempty"`
// Items, if set, describes the element type of an array.
Items *TypeSpec `json:"items,omitempty"`
// OneOf indicates that values of the type may be one of any of the listed types.
OneOf []TypeSpec `json:"oneOf,omitempty"`
// Discriminator informs the consumer of an alternative schema based on the value associated with it.
Discriminator *DiscriminatorSpec `json:"discriminator,omitempty"`
// Plain indicates that when used as an input, this type does not accept eventual values.
Plain bool `json:"plain,omitempty"`
}
// DiscriminatorSpec informs the consumer of an alternative schema based on the value associated with it.
type DiscriminatorSpec struct {
// PropertyName is the name of the property in the payload that will hold the discriminator value.
PropertyName string `json:"propertyName"`
// Mapping is an optional object to hold mappings between payload values and schema names or references.
Mapping map[string]string `json:"mapping,omitempty"`
}
// DefaultSpec is the serializable form of extra information about the default value for a property.
type DefaultSpec struct {
// Environment specifies a set of environment variables to probe for a default value.
Environment []string `json:"environment,omitempty"`
// Language specifies additional language-specific data about the default value.
Language map[string]json.RawMessage `json:"language,omitempty"`
}
// PropertySpec is the serializable form of an object or resource property.
type PropertySpec struct {
TypeSpec
// Description is the description of the property, if any.
Description string `json:"description,omitempty"`
// Const is the constant value for the property, if any. The type of the value must be assignable to the type of
// the property.
Const interface{} `json:"const,omitempty"`
// Default is the default value for the property, if any. The type of the value must be assignable to the type of
// the property.
Default interface{} `json:"default,omitempty"`
// DefaultInfo contains additional information about the property's default value, if any.
DefaultInfo *DefaultSpec `json:"defaultInfo,omitempty"`
// DeprecationMessage indicates whether or not the property is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty"`
// Language specifies additional language-specific data about the property.
Language map[string]json.RawMessage `json:"language,omitempty"`
// Secret specifies if the property is secret (default false).
Secret bool `json:"secret,omitempty"`
}
// ObjectTypeSpec is the serializable form of an object type.
type ObjectTypeSpec struct {
// Description is the description of the type, if any.
Description string `json:"description,omitempty"`
// Properties, if present, is a map from property name to PropertySpec that describes the type's properties.
Properties map[string]PropertySpec `json:"properties,omitempty"`
// Type must be "object" if this is an object type, or the underlying type for an enum.
Type string `json:"type,omitempty"`
// Required, if present, is a list of the names of an object type's required properties. These properties must be set
// for inputs and will always be set for outputs.
Required []string `json:"required,omitempty"`
// Plain, was a list of the names of an object type's plain properties. This property is ignored: instead, property
// types should be marked as plain where necessary.
Plain []string `json:"plain,omitempty"`
// Language specifies additional language-specific data about the type.
Language map[string]json.RawMessage `json:"language,omitempty"`
}
// ComplexTypeSpec is the serializable form of an object or enum type.
type ComplexTypeSpec struct {
ObjectTypeSpec
// Enum, if present, is the list of possible values for an enum type.
Enum []*EnumValueSpec `json:"enum,omitempty"`
}
// EnumValuesSpec is the serializable form of the values metadata associated with an enum type.
type EnumValueSpec struct {
// Name, if present, overrides the name of the enum value that would usually be derived from the value.
Name string `json:"name,omitempty"`
// Description of the enum value.
Description string `json:"description,omitempty"`
// Value is the enum value itself.
Value interface{} `json:"value"`
// DeprecationMessage indicates whether or not the value is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty"`
}
// AliasSpec is the serializable form of an alias description.
type AliasSpec struct {
// Name is the name portion of the alias, if any.
Name *string `json:"name,omitempty"`
// Project is the project portion of the alias, if any.
Project *string `json:"project,omitempty"`
// Type is the type portion of the alias, if any.
Type *string `json:"type,omitempty"`
}
// ResourceSpec is the serializable form of a resource description.
type ResourceSpec struct {
ObjectTypeSpec
// InputProperties is a map from property name to PropertySpec that describes the resource's input properties.
InputProperties map[string]PropertySpec `json:"inputProperties,omitempty"`
// RequiredInputs is a list of the names of the resource's required input properties.
RequiredInputs []string `json:"requiredInputs,omitempty"`
// PlainInputs was a list of the names of the resource's plain input properties. This property is ignored:
// instead, property types should be marked as plain where necessary.
PlainInputs []string `json:"plainInputs,omitempty"`
// StateInputs is an optional ObjectTypeSpec that describes additional inputs that mau be necessary to get an
// existing resource. If this is unset, only an ID is necessary.
StateInputs *ObjectTypeSpec `json:"stateInputs,omitempty"`
// Aliases is the list of aliases for the resource.
Aliases []AliasSpec `json:"aliases,omitempty"`
// DeprecationMessage indicates whether or not the resource is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty"`
// Language specifies additional language-specific data about the resource.
Language map[string]json.RawMessage `json:"language,omitempty"`
// IsComponent indicates whether the resource is a ComponentResource.
IsComponent bool `json:"isComponent,omitempty"`
// Methods maps method names to functions in this schema.
Methods map[string]string `json:"methods,omitempty"`
}
// FunctionSpec is the serializable form of a function description.
type FunctionSpec struct {
// Description is the description of the function, if any.
Description string `json:"description,omitempty"`
// Inputs is the bag of input values for the function, if any.
Inputs *ObjectTypeSpec `json:"inputs,omitempty"`
// Outputs is the bag of output values for the function, if any.
Outputs *ObjectTypeSpec `json:"outputs,omitempty"`
// DeprecationMessage indicates whether or not the function is deprecated.
DeprecationMessage string `json:"deprecationMessage,omitempty"`
// Language specifies additional language-specific data about the function.
Language map[string]json.RawMessage `json:"language,omitempty"`
}
// ConfigSpec is the serializable description of a package's configuration variables.
type ConfigSpec struct {
// Variables is a map from variable name to PropertySpec that describes a package's configuration variables.
Variables map[string]PropertySpec `json:"variables,omitempty"`
// Required is a list of the names of the package's required configuration variables.
Required []string `json:"defaults,omitempty"`
}
// MetadataSpec contains information for the importer about this package.
type MetadataSpec struct {
// ModuleFormat is a regex that is used by the importer to extract a module name from the module portion of a
// type token. Packages that use the module format "namespace1/namespace2/.../namespaceN" do not need to specify
// a format. The regex must define one capturing group that contains the module name, which must be formatted as
// "namespace1/namespace2/...namespaceN".
ModuleFormat string `json:"moduleFormat,omitempty"`
}
// PackageSpec is the serializable description of a Pulumi package.
type PackageSpec struct {
// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes", "random")
Name string `json:"name"`
// Version is the version of the package. The version must be valid semver.
Version string `json:"version,omitempty"`
// Description is the description of the package.
Description string `json:"description,omitempty"`
// Keywords is the list of keywords that are associated with the package, if any.
Keywords []string `json:"keywords,omitempty"`
// Homepage is the package's homepage.
Homepage string `json:"homepage,omitempty"`
// License indicates which license is used for the package's contents.
License string `json:"license,omitempty"`
// Attribution allows freeform text attribution of derived work, if needed.
Attribution string `json:"attribution,omitempty"`
// Repository is the URL at which the source for the package can be found.
Repository string `json:"repository,omitempty"`
// LogoURL is the URL for the package's logo, if any.
LogoURL string `json:"logoUrl,omitempty"`
// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
PluginDownloadURL string `json:"pluginDownloadURL,omitempty"`
// Meta contains information for the importer about this package.
Meta *MetadataSpec `json:"meta,omitempty"`
// Config describes the set of configuration variables defined by this package.
Config ConfigSpec `json:"config"`
// Types is a map from type token to ComplexTypeSpec that describes the set of complex types (ie. object, enum)
// defined by this package.
Types map[string]ComplexTypeSpec `json:"types,omitempty"`
// Provider describes the provider type for this package.
Provider ResourceSpec `json:"provider"`
// Resources is a map from type token to ResourceSpec that describes the set of resources defined by this package.
Resources map[string]ResourceSpec `json:"resources,omitempty"`
// Functions is a map from token to FunctionSpec that describes the set of functions defined by this package.
Functions map[string]FunctionSpec `json:"functions,omitempty"`
// Language specifies additional language-specific data about the package.
Language map[string]json.RawMessage `json:"language,omitempty"`
}
// importSpec converts a serializable PackageSpec into a Package. This function includes a loader parameter which
// works as a singleton -- if it is nil, a new loader is instantiated, else the provided loader is used. This avoids
// breaking downstream consumers of ImportSpec while allowing us to extend schema support to external packages.
func importSpec(spec PackageSpec, languages map[string]Language, loader Loader) (*Package, error) {
// Parse the version, if any.
var version *semver.Version
if spec.Version != "" {
v, err := semver.ParseTolerant(spec.Version)
if err != nil {
return nil, errors.Wrap(err, "parsing package version")
}
version = &v
}
// Parse the module format, if any.
moduleFormat := "(.*)"
if spec.Meta != nil && spec.Meta.ModuleFormat != "" {
moduleFormat = spec.Meta.ModuleFormat
}
moduleFormatRegexp, err := regexp.Compile(moduleFormat)
if err != nil {
return nil, errors.Wrap(err, "compiling module format regexp")
}
pkg := &Package{}
// We want to use the same loader instance for all referenced packages, so only instantiate the loader if the
// reference is nil.
if loader == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
ctx, err := plugin.NewContext(nil, nil, nil, nil, cwd, nil, false, nil)
if err != nil {
return nil, err
}
defer contract.IgnoreClose(ctx)
loader = NewPluginLoader(ctx.Host)
}
types, err := bindTypes(pkg, spec.Types, loader)
if err != nil {
return nil, errors.Wrap(err, "binding types")
}
config, err := bindConfig(spec.Config, types)
if err != nil {
return nil, errors.Wrap(err, "binding config")
}
functions, functionTable, err := bindFunctions(spec.Functions, types)
if err != nil {
return nil, errors.Wrap(err, "binding functions")
}
provider, err := bindProvider(spec.Name, spec.Provider, types, functionTable)
if err != nil {
return nil, errors.Wrap(err, "binding provider")
}
resources, resourceTable, err := bindResources(spec.Resources, types, functionTable)
if err != nil {
return nil, errors.Wrap(err, "binding resources")
}
// Build the type list.
var typeList []Type
for _, t := range types.resources {
typeList = append(typeList, t)
}
for _, t := range types.objects {
// t is a plain shape: add it and its coresponding input shape to the type list.
typeList = append(typeList, t)
typeList = append(typeList, t.InputShape)
}
for _, t := range types.arrays {
typeList = append(typeList, t)
}
for _, t := range types.maps {
typeList = append(typeList, t)
}
for _, t := range types.unions {
typeList = append(typeList, t)
}
for _, t := range types.tokens {
typeList = append(typeList, t)
}
for _, t := range types.enums {
typeList = append(typeList, t)
}
sort.Slice(typeList, func(i, j int) bool {
return typeList[i].String() < typeList[j].String()
})
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
*pkg = Package{
moduleFormat: moduleFormatRegexp,
Name: spec.Name,
Version: version,
Description: spec.Description,
Keywords: spec.Keywords,
Homepage: spec.Homepage,
License: spec.License,
Attribution: spec.Attribution,
Repository: spec.Repository,
PluginDownloadURL: spec.PluginDownloadURL,
Config: config,
Types: typeList,
Provider: provider,
Resources: resources,
Functions: functions,
Language: language,
resourceTable: resourceTable,
functionTable: functionTable,
typeTable: types.named,
resourceTypeTable: types.resources,
}
if err := pkg.ImportLanguages(languages); err != nil {
return nil, err
}
return pkg, nil
}
// ImportSpec converts a serializable PackageSpec into a Package.
func ImportSpec(spec PackageSpec, languages map[string]Language) (*Package, error) {
// Call the internal implementation that includes a loader parameter.
return importSpec(spec, languages, nil)
}
// types facilitates interning (only storing a single reference to an object) during schema processing. The fields
// correspond to fields in the schema, and are populated during the binding process.
type types struct {
pkg *Package
loader Loader
resources map[string]*ResourceType
objects map[string]*ObjectType
arrays map[Type]*ArrayType
maps map[Type]*MapType
unions map[string]*UnionType
tokens map[string]*TokenType
enums map[string]*EnumType
named map[string]Type // objects and enums
inputs map[Type]*InputType
optionals map[Type]*OptionalType
}
func (t *types) bindPrimitiveType(name string) (Type, error) {
switch name {
case "boolean":
return BoolType, nil
case "integer":
return IntType, nil
case "number":
return NumberType, nil
case "string":
return StringType, nil
default:
return nil, errors.Errorf("unknown primitive type %v", name)
}
}
// typeSpecRef contains the parsed fields from a type spec reference.
type typeSpecRef struct {
URL *url.URL // The parsed URL
Package string // The package component of the schema ref
Version *semver.Version // The version component of the schema ref
Kind string // The kind of reference: 'resources', 'types', or 'provider'
Token string // The type token
}
const (
resourcesRef = "resources"
typesRef = "types"
providerRef = "provider"
)
// Regex used to parse external schema paths. This is declared at the package scope to avoid repeated recompilation.
var refPathRegex = regexp.MustCompile(`^/?(?P<package>\w+)/(?P<version>v[^/]*)/schema\.json$`)
func (t *types) parseTypeSpecRef(ref string) (typeSpecRef, error) {
parsedURL, err := url.Parse(ref)
if err != nil {
return typeSpecRef{}, errors.Wrapf(err, "failed to parse ref URL: %s", ref)
}
// Parse the package name and version if the URL contains a path. If there is no path--if the URL is just a
// fragment--then the reference refers to the package being bound.
pkgName, pkgVersion := t.pkg.Name, t.pkg.Version
if len(parsedURL.Path) > 0 {
path, err := url.PathUnescape(parsedURL.Path)
if err != nil {
return typeSpecRef{}, errors.Wrapf(err, "failed to unescape path: %s", parsedURL.Path)
}
pathMatch := refPathRegex.FindStringSubmatch(path)
if len(pathMatch) != 3 {
return typeSpecRef{}, fmt.Errorf("failed to parse path: %s", path)
}
pkg, versionToken := pathMatch[1], pathMatch[2]
version, err := semver.ParseTolerant(versionToken)
if err != nil {
return typeSpecRef{}, errors.Wrapf(err, "failed to parse package version: %s", versionToken)
}
pkgName, pkgVersion = pkg, &version
}
// Parse the fragment into a reference kind and token. The fragment is in one of two forms:
// 1. #/provider
// 2. #/(resources|types)/some:type:token
//
// Unfortunately, early code generators were lax and emitted unescaped backslashes in the type token, so we can't
// just split on "/".
fragment := path.Clean(parsedURL.EscapedFragment())
if path.IsAbs(fragment) {
fragment = fragment[1:]
}
kind, token := "", ""
slash := strings.Index(fragment, "/")
if slash == -1 {
kind = fragment
} else {
kind, token = fragment[:slash], fragment[slash+1:]
}
switch kind {
case "provider":
if token != "" {
return typeSpecRef{}, fmt.Errorf("invalid provider reference '%v'", ref)
}
token = "pulumi:providers:" + pkgName
case "resources", "types":
token, err = url.PathUnescape(token)
if err != nil {
return typeSpecRef{}, errors.Wrapf(err, "failed to unescape token: %s", token)
}
default:
return typeSpecRef{}, fmt.Errorf("invalid type reference '%v'", ref)
}
return typeSpecRef{
URL: parsedURL,
Package: pkgName,
Version: pkgVersion,
Kind: kind,
Token: token,
}, nil
}
func versionEquals(a, b *semver.Version) bool {
// We treat "nil" as "unconstrained".
if a == nil || b == nil {
return true
}
return a.Equals(*b)
}
func (t *types) newInputType(elementType Type) Type {
if _, ok := elementType.(*InputType); ok {
return elementType
}
typ, ok := t.inputs[elementType]
if !ok {
typ = &InputType{ElementType: elementType}
t.inputs[elementType] = typ
}
return typ
}
func (t *types) newOptionalType(elementType Type) Type {
if _, ok := elementType.(*OptionalType); ok {
return elementType
}
typ, ok := t.optionals[elementType]
if !ok {
typ = &OptionalType{ElementType: elementType}
t.optionals[elementType] = typ
}
return typ
}
func (t *types) newMapType(elementType Type) Type {
typ, ok := t.maps[elementType]
if !ok {
typ = &MapType{ElementType: elementType}
t.maps[elementType] = typ
}
return typ
}
func (t *types) newArrayType(elementType Type) Type {
typ, ok := t.arrays[elementType]
if !ok {
typ = &ArrayType{ElementType: elementType}
t.arrays[elementType] = typ
}
return typ
}
func (t *types) newUnionType(
elements []Type, defaultType Type, discriminator string, mapping map[string]string) *UnionType {
union := &UnionType{
ElementTypes: elements,
DefaultType: defaultType,
Discriminator: discriminator,
Mapping: mapping,
}
if typ, ok := t.unions[union.String()]; ok {
return typ
}
t.unions[union.String()] = union
return union
}
func (t *types) bindTypeSpecRef(spec TypeSpec, inputShape bool) (Type, error) {
// Explicitly handle built-in types so that we don't have to handle this type of path during ref parsing.
switch spec.Ref {
case "pulumi.json#/Archive":
return ArchiveType, nil
case "pulumi.json#/Asset":
return AssetType, nil
case "pulumi.json#/Json":
return JSONType, nil
case "pulumi.json#/Any":
return AnyType, nil
}
ref, err := t.parseTypeSpecRef(spec.Ref)
if err != nil {
return nil, err
}
// If this is a reference to an external sch
referencesExternalSchema := ref.Package != t.pkg.Name || !versionEquals(ref.Version, t.pkg.Version)
if referencesExternalSchema {
pkg, err := t.loader.LoadPackage(ref.Package, ref.Version)
if err != nil {
return nil, errors.Wrapf(err, "resolving package %v", ref.URL)
}
switch ref.Kind {
case typesRef:
typ, ok := pkg.GetType(ref.Token)
if !ok {
return nil, fmt.Errorf("type %v not found in package %v", ref.Token, ref.Package)
}
if obj, ok := typ.(*ObjectType); ok && inputShape {
typ = obj.InputShape
}
return typ, nil
case resourcesRef, providerRef:
typ, ok := pkg.GetResourceType(ref.Token)
if !ok {
return nil, fmt.Errorf("resource type %v not found in package %v", ref.Token, ref.Package)
}
return typ, nil
}
}
switch ref.Kind {
case typesRef:
if typ, ok := t.objects[ref.Token]; ok {
if inputShape {
return typ.InputShape, nil
}
return typ, nil
}
if typ, ok := t.enums[ref.Token]; ok {
return typ, nil
}
typ, ok := t.tokens[ref.Token]
if !ok {
typ = &TokenType{Token: ref.Token}
if spec.Type != "" {
ut, err := t.bindPrimitiveType(spec.Type)
if err != nil {
return nil, err
}
typ.UnderlyingType = ut
}
t.tokens[ref.Token] = typ
}
return typ, nil
case resourcesRef, providerRef:
typ, ok := t.resources[ref.Token]
if !ok {
typ = &ResourceType{Token: ref.Token}
t.resources[ref.Token] = typ
}
return typ, nil
default:
return nil, errors.Errorf("failed to parse ref %s", spec.Ref)
}
}
func (t *types) bindType(spec TypeSpec, inputShape bool) (result Type, err error) {
if inputShape && !spec.Plain {
defer func() {
result = t.newInputType(result)
}()
}
if spec.Ref != "" {
return t.bindTypeSpecRef(spec, inputShape)
}
if spec.OneOf != nil {
if len(spec.OneOf) < 2 {
return nil, errors.New("oneOf should list at least two types")
}
var defaultType Type
if spec.Type != "" {
dt, err := t.bindPrimitiveType(spec.Type)
if err != nil {
return nil, err
}
defaultType = dt
}
elements := make([]Type, len(spec.OneOf))
for i, spec := range spec.OneOf {
e, err := t.bindType(spec, inputShape)
if err != nil {
return nil, err
}
elements[i] = e
}
var discriminator string
var mapping map[string]string
if spec.Discriminator != nil {
discriminator = spec.Discriminator.PropertyName
mapping = spec.Discriminator.Mapping
}
return t.newUnionType(elements, defaultType, discriminator, mapping), nil
}
// nolint: goconst
switch spec.Type {
case "boolean", "integer", "number", "string":
return t.bindPrimitiveType(spec.Type)
case "array":
if spec.Items == nil {
return nil, errors.Errorf("missing \"items\" property in type spec")
}
elementType, err := t.bindType(*spec.Items, inputShape)
if err != nil {
return nil, err
}
return t.newArrayType(elementType), nil
case "object":
elementType := StringType
if spec.AdditionalProperties != nil {
et, err := t.bindType(*spec.AdditionalProperties, inputShape)
if err != nil {
return nil, err
}
elementType = et
}
return t.newMapType(elementType), nil
default:
return nil, errors.Errorf("unknown type kind %v", spec.Type)
}
}
func plainType(typ Type) Type {
for {
switch t := typ.(type) {
case *InputType:
typ = t.ElementType
case *OptionalType:
typ = t.ElementType
case *ObjectType:
if t.PlainShape == nil {
return t
}
typ = t.PlainShape
default:
return t
}
}
}
func bindConstValue(value interface{}, typ Type) (interface{}, error) {
if value == nil {
return nil, nil
}
switch plainType(typ) {
case BoolType:
if _, ok := value.(bool); !ok {
return nil, errors.Errorf("invalid constant of type %T for boolean property", value)
}
case IntType:
v, ok := value.(float64)
if !ok {
return nil, errors.Errorf("invalid constant of type %T for integer property", value)
}
if math.Trunc(v) != v || v < math.MinInt32 || v > math.MaxInt32 {
return nil, errors.Errorf("invalid constant of type number for integer property")
}
value = int32(v)
case NumberType:
if _, ok := value.(float64); !ok {
return nil, errors.Errorf("invalid constant of type %T for number property", value)
}
case StringType:
if _, ok := value.(string); !ok {
return nil, errors.Errorf("invalid constant of type %T for string property", value)
}
default:
return nil, errors.Errorf("constant values may only be provided for boolean, integer, number, and string properties")
}
return value, nil
}
func bindDefaultValue(value interface{}, spec *DefaultSpec, typ Type) (*DefaultValue, error) {
if value == nil && spec == nil {
return nil, nil
}
if value != nil {
typ = plainType(typ)
switch typ := typ.(type) {
case *UnionType:
if typ.DefaultType != nil {
return bindDefaultValue(value, spec, typ.DefaultType)
}
for _, elementType := range typ.ElementTypes {
v, err := bindDefaultValue(value, spec, elementType)
if err == nil {
return v, nil
}
}
case *EnumType:
return bindDefaultValue(value, spec, typ.ElementType)
}
switch typ {
case BoolType:
if _, ok := value.(bool); !ok {
return nil, errors.Errorf("invalid default of type %T for boolean property", value)
}
case IntType:
v, ok := value.(float64)
if !ok {
return nil, errors.Errorf("invalid default of type %T for integer property", value)
}
if math.Trunc(v) != v || v < math.MinInt32 || v > math.MaxInt32 {
return nil, errors.Errorf("invalid default of type number for integer property")
}
value = int32(v)
case NumberType:
if _, ok := value.(float64); !ok {
return nil, errors.Errorf("invalid default of type %T for number property", value)
}
case StringType:
if _, ok := value.(string); !ok {
return nil, errors.Errorf("invalid default of type %T for string property", value)
}
default:
return nil, errors.Errorf("default values may only be provided for boolean, integer, number, and string properties")
}
}
dv := &DefaultValue{Value: value}
if spec != nil {
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
dv.Environment, dv.Language = spec.Environment, language
}
return dv, nil
}
// bindProperties binds the map of property specs and list of required properties into a sorted list of properties and
// a lookup table.
func (t *types) bindProperties(properties map[string]PropertySpec,
required []string, inputShape bool) ([]*Property, map[string]*Property, error) {
// Bind property types and constant or default values.
propertyMap := map[string]*Property{}
var result []*Property
for name, spec := range properties {
typ, err := t.bindType(spec.TypeSpec, inputShape)
if err != nil {
return nil, nil, errors.Wrapf(err, "error binding type for property %q", name)
}
cv, err := bindConstValue(spec.Const, typ)
if err != nil {
return nil, nil, errors.Wrapf(err, "error binding constant value for property %q", name)
}
dv, err := bindDefaultValue(spec.Default, spec.DefaultInfo, typ)
if err != nil {
return nil, nil, errors.Wrapf(err, "error binding default value for property %q", name)
}
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
p := &Property{
Name: name,
Comment: spec.Description,
Type: t.newOptionalType(typ),
ConstValue: cv,
DefaultValue: dv,
DeprecationMessage: spec.DeprecationMessage,
Language: language,
Secret: spec.Secret,
}
propertyMap[name], result = p, append(result, p)
}
// Compute required properties.
for _, name := range required {
p, ok := propertyMap[name]
if !ok {
return nil, nil, errors.Errorf("unknown required property %q", name)
}
p.Type = p.Type.(*OptionalType).ElementType
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, propertyMap, nil
}
func (t *types) bindObjectTypeDetails(obj *ObjectType, token string, spec ObjectTypeSpec) error {
if len(spec.Plain) > 0 {
return errors.New("plain has been removed; the property type must be marked as plain instead")
}
properties, propertyMap, err := t.bindProperties(spec.Properties, spec.Required, false)
if err != nil {
return err
}
inputProperties, inputPropertyMap, err := t.bindProperties(spec.Properties, spec.Required, true)
if err != nil {
return err
}
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
obj.Package = t.pkg
obj.Token = token
obj.Comment = spec.Description
obj.Language = language
obj.Properties = properties
obj.properties = propertyMap
obj.InputShape.Package = t.pkg
obj.InputShape.Token = token
obj.InputShape.Comment = spec.Description
obj.InputShape.Language = language
obj.InputShape.Properties = inputProperties
obj.InputShape.properties = inputPropertyMap
return nil
}
func (t *types) bindObjectType(token string, spec ObjectTypeSpec) (*ObjectType, error) {
obj := &ObjectType{}
obj.InputShape = &ObjectType{PlainShape: obj}
if err := t.bindObjectTypeDetails(obj, token, spec); err != nil {
return nil, err
}
return obj, nil
}
func (t *types) bindResourceTypeDetails(obj *ResourceType, token string) error {
obj.Token = token
return nil
}
func (t *types) bindResourceType(token string) (*ResourceType, error) {
r := &ResourceType{}
if err := t.bindResourceTypeDetails(r, token); err != nil {
return nil, err
}
return r, nil
}
func (t *types) bindEnumTypeDetails(enum *EnumType, token string, spec ComplexTypeSpec) error {
typ, err := t.bindPrimitiveType(spec.Type)
if err != nil {
return err
}
values, err := t.bindEnumValues(spec.Enum, typ)
if err != nil {
return err
}
enum.Package = t.pkg
enum.Token = token
enum.Elements = values
enum.ElementType = typ
enum.Comment = spec.Description
return nil
}
func (t *types) bindEnumValues(values []*EnumValueSpec, typ Type) ([]*Enum, error) {
var enums []*Enum
errorMessage := func(val interface{}, expectedType string) error {
return fmt.Errorf("cannot assign enum value of type '%T' to enum of type '%s'", val, expectedType)
}
for _, spec := range values {
switch typ {
case StringType:
if _, ok := spec.Value.(string); !ok {
return nil, errorMessage(spec.Value, typ.String())
}
case IntType:
v, ok := spec.Value.(float64)
if !ok {
return nil, errorMessage(spec.Value, typ.String())
}
if math.Trunc(v) != v || v < math.MinInt32 || v > math.MaxInt32 {
return nil, errors.Errorf("cannot assign enum value of type 'number' to enum of type 'integer'")
}
spec.Value = int32(v)
case NumberType:
if _, ok := spec.Value.(float64); !ok {
return nil, errorMessage(spec.Value, typ.String())
}
case BoolType:
if _, ok := spec.Value.(bool); !ok {
return nil, errorMessage(spec.Value, typ.String())
}
default:
return nil, fmt.Errorf("enum values may only be of string, integer, number or boolean types")
}
enum := &Enum{
Value: spec.Value,
Comment: spec.Description,
Name: spec.Name,
DeprecationMessage: spec.DeprecationMessage,
}
enums = append(enums, enum)
}
return enums, nil
}
func (t *types) bindEnumType(token string, spec ComplexTypeSpec) (*EnumType, error) {
enum := &EnumType{}
if err := t.bindEnumTypeDetails(enum, token, spec); err != nil {
return nil, err
}
return enum, nil
}
func bindTypes(pkg *Package, complexTypes map[string]ComplexTypeSpec, loader Loader) (*types, error) {
typs := &types{
pkg: pkg,
loader: loader,
resources: map[string]*ResourceType{},
objects: map[string]*ObjectType{},
arrays: map[Type]*ArrayType{},
maps: map[Type]*MapType{},
unions: map[string]*UnionType{},
tokens: map[string]*TokenType{},
enums: map[string]*EnumType{},
named: map[string]Type{},
inputs: map[Type]*InputType{},
optionals: map[Type]*OptionalType{},
}
// Declare object and enum types before processing properties.
for token, spec := range complexTypes {
if spec.Type == "object" {
// It's important that we set the token here. This package interns types so that they can be equality-compared
// for identity. Types are interned based on their string representation, and the string representation of an
// object type is its token. While this doesn't affect object types directly, it breaks the interning of types
// that reference object types (e.g. arrays, maps, unions)
typ := &ObjectType{Token: token}
typ.InputShape = &ObjectType{Token: token, PlainShape: typ}
typs.objects[token] = typ
typs.named[token] = typ
} else if len(spec.Enum) > 0 {
typ := &EnumType{Token: token}
typs.enums[token] = typ
typs.named[token] = typ
// Bind enums before object types because object type generation depends on enum values to be present.
if err := typs.bindEnumTypeDetails(typs.enums[token], token, spec); err != nil {
return nil, errors.Wrapf(err, "failed to bind type %s", token)
}
}
}
// Process resources.
for _, r := range pkg.Resources {
typs.resources[r.Token] = &ResourceType{Token: r.Token}
}
// Process object types.
for token, spec := range complexTypes {
if spec.Type == "object" {
if err := typs.bindObjectTypeDetails(
typs.objects[token], token, spec.ObjectTypeSpec); err != nil {
return nil, errors.Wrapf(err, "failed to bind type %s", token)
}
}
}
return typs, nil
}
func bindMethods(resourceToken string, methods map[string]string,
functionTable map[string]*Function) ([]*Method, error) {
names := make([]string, 0, len(methods))
for name := range methods {
names = append(names, name)
}
sort.Strings(names)
result := make([]*Method, 0, len(methods))
for _, name := range names {
token := methods[name]
function, ok := functionTable[token]
if !ok {
return nil, errors.Errorf("unknown function %s for method %s", token, name)
}
if function.IsMethod {
return nil, errors.Errorf("function %s for method %s is already a method", token, name)
}
idx := strings.LastIndex(function.Token, "/")
if idx == -1 || function.Token[:idx] != resourceToken {
return nil, errors.Errorf("invalid function token format %s for method %s", token, name)
}
if function.Inputs == nil || function.Inputs.Properties == nil || len(function.Inputs.Properties) == 0 ||
function.Inputs.Properties[0].Name != "__self__" {
return nil, errors.Errorf("function %s for method %s is missing __self__ parameter", token, name)
}
function.IsMethod = true
result = append(result, &Method{
Name: name,
Function: function,
})
}
return result, nil
}
func bindConfig(spec ConfigSpec, types *types) ([]*Property, error) {
properties, _, err := types.bindProperties(spec.Variables, spec.Required, false)
return properties, err
}
func bindResource(token string, spec ResourceSpec, types *types,
functionTable map[string]*Function) (*Resource, error) {
if len(spec.Plain) > 0 {
return nil, errors.New("plain has been removed; property types must be marked as plain instead")
}
if len(spec.PlainInputs) > 0 {
return nil, errors.New("plainInputs has been removed; individual property types must be marked as plain instead")
}
properties, _, err := types.bindProperties(spec.Properties, spec.Required, false)
if err != nil {
return nil, errors.Wrap(err, "failed to bind properties")
}
inputProperties, _, err := types.bindProperties(spec.InputProperties, spec.RequiredInputs, true)
if err != nil {
return nil, errors.Wrap(err, "failed to bind properties")
}
methods, err := bindMethods(token, spec.Methods, functionTable)
if err != nil {
return nil, errors.Wrap(err, "failed to bind methods")
}
for _, method := range methods {
if _, ok := spec.Properties[method.Name]; ok {
return nil, errors.Errorf("property and method have the same name %s", method.Name)
}
}
var stateInputs *ObjectType
if spec.StateInputs != nil {
si, err := types.bindObjectType(token+"Args", *spec.StateInputs)
if err != nil {
return nil, errors.Wrap(err, "error binding inputs")
}
stateInputs = si.InputShape
}
var aliases []*Alias
for _, a := range spec.Aliases {
aliases = append(aliases, &Alias{Name: a.Name, Project: a.Project, Type: a.Type})
}
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
return &Resource{
Package: types.pkg,
Token: token,
Comment: spec.Description,
InputProperties: inputProperties,
Properties: properties,
StateInputs: stateInputs,
Aliases: aliases,
DeprecationMessage: spec.DeprecationMessage,
Language: language,
IsComponent: spec.IsComponent,
Methods: methods,
}, nil
}
func bindProvider(pkgName string, spec ResourceSpec, types *types,
functionTable map[string]*Function) (*Resource, error) {
res, err := bindResource("pulumi:providers:"+pkgName, spec, types, functionTable)
if err != nil {
return nil, errors.Wrap(err, "error binding provider")
}
res.IsProvider = true
// Since non-primitive provider configuration is currently JSON serialized, we can't handle it without
// modifying the path by which it's looked up. As a temporary workaround to enable access to config which
// values which are primitives, we'll simply remove any properties for the provider resource which are not
// strings, or types with an underlying type of string, before we generate the provider code.
var stringProperties []*Property
for _, prop := range res.Properties {
typ := plainType(prop.Type)
if tokenType, isTokenType := typ.(*TokenType); isTokenType {
if tokenType.UnderlyingType != stringType {
continue
}
} else {
if typ != stringType {
continue
}
}
stringProperties = append(stringProperties, prop)
}
res.Properties = stringProperties
types.resources[res.Token] = &ResourceType{
Token: res.Token,
Resource: res,
}
return res, nil
}
func bindResources(specs map[string]ResourceSpec, types *types,
functionTable map[string]*Function) ([]*Resource, map[string]*Resource, error) {
resourceTable := map[string]*Resource{}
var resources []*Resource
for token, spec := range specs {
res, err := bindResource(token, spec, types, functionTable)
if err != nil {
return nil, nil, errors.Wrapf(err, "error binding resource %v", token)
}
resourceTable[token] = res
if rt, ok := types.resources[token]; ok {
if rt.Resource == nil {
rt.Resource = res
}
} else {
types.resources[token] = &ResourceType{
Token: res.Token,
Resource: res,
}
}
resources = append(resources, res)
}
sort.Slice(resources, func(i, j int) bool {
return resources[i].Token < resources[j].Token
})
return resources, resourceTable, nil
}
func bindFunction(token string, spec FunctionSpec, types *types) (*Function, error) {
var inputs *ObjectType
if spec.Inputs != nil {
ins, err := types.bindObjectType(token+"Args", *spec.Inputs)
if err != nil {
return nil, errors.Wrap(err, "error binding inputs")
}
inputs = ins
}
var outputs *ObjectType
if spec.Outputs != nil {
outs, err := types.bindObjectType(token+"Result", *spec.Outputs)
if err != nil {
return nil, errors.Wrap(err, "error binding inputs")
}
outputs = outs
}
language := make(map[string]interface{})
for name, raw := range spec.Language {
language[name] = raw
}
return &Function{
Package: types.pkg,
Token: token,
Comment: spec.Description,
Inputs: inputs,
Outputs: outputs,
DeprecationMessage: spec.DeprecationMessage,
Language: language,
}, nil
}
func bindFunctions(specs map[string]FunctionSpec, types *types) ([]*Function, map[string]*Function, error) {
functionTable := map[string]*Function{}
var functions []*Function
for token, spec := range specs {
f, err := bindFunction(token, spec, types)
if err != nil {
return nil, nil, errors.Wrapf(err, "error binding function %v", token)
}
functionTable[token] = f
functions = append(functions, f)
}
sort.Slice(functions, func(i, j int) bool {
return functions[i].Token < functions[j].Token
})
return functions, functionTable, nil
}