pulumi/pkg/codegen/importer/hcl2.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

504 lines
14 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 importer
import (
"fmt"
"math"
"strings"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy/providers"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/zclconf/go-cty/cty"
)
// Null represents Pulumi HCL2's `null` variable.
var Null = &model.Variable{
Name: "null",
VariableType: model.NoneType,
}
// GenerateHCL2Definition generates a Pulumi HCL2 definition for a given resource.
func GenerateHCL2Definition(loader schema.Loader, state *resource.State, names NameTable) (*model.Block, error) {
// TODO: pull the package version from the resource's provider
pkg, err := loader.LoadPackage(string(state.Type.Package()), nil)
if err != nil {
return nil, err
}
r, ok := pkg.GetResource(string(state.Type))
if !ok {
return nil, fmt.Errorf("unknown resource type '%v'", r)
}
var items []model.BodyItem
for _, p := range r.InputProperties {
x, err := generatePropertyValue(p, state.Inputs[resource.PropertyKey(p.Name)])
if err != nil {
return nil, err
}
if x != nil {
items = append(items, &model.Attribute{
Name: p.Name,
Value: x,
})
}
}
resourceOptions, err := makeResourceOptions(state, names)
if err != nil {
return nil, err
}
if resourceOptions != nil {
items = append(items, resourceOptions)
}
typ, name := state.URN.Type(), state.URN.Name()
return &model.Block{
Tokens: syntax.NewBlockTokens("resource", string(name), string(typ)),
Type: "resource",
Labels: []string{string(name), string(typ)},
Body: &model.Body{
Items: items,
},
}, nil
}
func newVariableReference(name string) model.Expression {
return model.VariableReference(&model.Variable{
Name: name,
VariableType: model.DynamicType,
})
}
func appendResourceOption(block *model.Block, name string, value model.Expression) *model.Block {
if block == nil {
block = &model.Block{
Tokens: syntax.NewBlockTokens("options"),
Type: "options",
Body: &model.Body{},
}
}
block.Body.Items = append(block.Body.Items, &model.Attribute{
Tokens: syntax.NewAttributeTokens(name),
Name: name,
Value: value,
})
return block
}
func makeResourceOptions(state *resource.State, names NameTable) (*model.Block, error) {
var resourceOptions *model.Block
if state.Parent != "" && state.Parent.Type() != resource.RootStackType {
name, ok := names[state.Parent]
if !ok {
return nil, fmt.Errorf("no name for parent %v", state.Parent)
}
resourceOptions = appendResourceOption(resourceOptions, "parent", newVariableReference(name))
}
if state.Provider != "" {
ref, err := providers.ParseReference(state.Provider)
if err != nil {
return nil, fmt.Errorf("invalid provider reference %v: %w", state.Provider, err)
}
if !providers.IsDefaultProvider(ref.URN()) {
name, ok := names[ref.URN()]
if !ok {
return nil, fmt.Errorf("no name for provider %v", state.Parent)
}
resourceOptions = appendResourceOption(resourceOptions, "provider", newVariableReference(name))
}
}
if len(state.Dependencies) != 0 {
deps := make([]model.Expression, len(state.Dependencies))
for i, d := range state.Dependencies {
name, ok := names[d]
if !ok {
return nil, fmt.Errorf("no name for resource %v", d)
}
deps[i] = newVariableReference(name)
}
resourceOptions = appendResourceOption(resourceOptions, "dependsOn", &model.TupleConsExpression{
Tokens: syntax.NewTupleConsTokens(len(deps)),
Expressions: deps,
})
}
if state.Protect {
resourceOptions = appendResourceOption(resourceOptions, "protect", &model.LiteralValueExpression{
Tokens: syntax.NewLiteralValueTokens(cty.True),
Value: cty.True,
})
}
return resourceOptions, nil
}
// typeRank orders types by their simplicity.
func typeRank(t schema.Type) int {
switch t {
case schema.BoolType:
return 1
case schema.IntType:
return 2
case schema.NumberType:
return 3
case schema.StringType:
return 4
case schema.AssetType:
return 5
case schema.ArchiveType:
return 6
case schema.JSONType:
return 7
case schema.AnyType:
return 13
default:
switch t := t.(type) {
case *schema.TokenType:
return 8
case *schema.ArrayType:
return 9
case *schema.MapType:
return 10
case *schema.ObjectType:
return 11
case *schema.UnionType:
return 12
case *schema.InputType:
return typeRank(t.ElementType)
case *schema.OptionalType:
return typeRank(t.ElementType)
default:
return int(math.MaxInt32)
}
}
}
// simplerType returns true if T is simpler than U.
//
// The first-order ranking is:
//
// bool < int < number < string < archive < asset < json < token < array < map < object < union < any
//
// Additional rules apply to composite types of the same kind:
// - array(T) is simpler than array(U) if T is simpler than U
// - map(T) is simpler than map(U) if T is simpler than U
// - object({ ... }) is simpler than object({ ... }) if the former has a greater number of required properties that are
// simpler than the latter's required properties
// - union(...) is simpler than union(...) if the former's simplest element type is simpler than the latter's simplest
// element type
func simplerType(t, u schema.Type) bool {
tRank, uRank := typeRank(t), typeRank(u)
if tRank < uRank {
return true
} else if tRank > uRank {
return false
}
t, u = codegen.UnwrapType(t), codegen.UnwrapType(u)
// At this point we know that t and u have the same concrete type.
switch t := t.(type) {
case *schema.TokenType:
u := u.(*schema.TokenType)
if t.UnderlyingType != nil && u.UnderlyingType != nil {
return simplerType(t.UnderlyingType, u.UnderlyingType)
}
return false
case *schema.ArrayType:
return simplerType(t.ElementType, u.(*schema.ArrayType).ElementType)
case *schema.MapType:
return simplerType(t.ElementType, u.(*schema.MapType).ElementType)
case *schema.ObjectType:
// Count how many of T's required properties are simpler than U's required properties and vice versa.
uu := u.(*schema.ObjectType)
tscore, nt, uscore := 0, 0, 0
for _, p := range t.Properties {
if p.IsRequired() {
nt++
for _, q := range uu.Properties {
if q.IsRequired() {
if simplerType(p.Type, q.Type) {
tscore++
}
if simplerType(q.Type, p.Type) {
uscore++
}
}
}
}
}
// If the number of T's required properties that are simpler that U's required properties exceeds the number
// of U's required properties that are simpler than T's required properties, T is simpler.
if tscore > uscore {
return true
}
if tscore < uscore {
return false
}
// If the above counts are equal, T is simpler if it has fewer required properties.
nu := 0
for _, q := range uu.Properties {
if q.IsRequired() {
nu++
}
}
return nt < nu
case *schema.UnionType:
// Pick whichever has the simplest element type.
var simplestElementType schema.Type
for _, u := range u.(*schema.UnionType).ElementTypes {
if simplestElementType == nil || simplerType(u, simplestElementType) {
simplestElementType = u
}
}
for _, t := range t.ElementTypes {
if simplestElementType == nil || simplerType(t, simplestElementType) {
return true
}
}
return false
default:
return false
}
}
// zeroValue constructs a zero value of the given type.
func zeroValue(t schema.Type) model.Expression {
switch t := t.(type) {
case *schema.OptionalType:
return model.VariableReference(Null)
case *schema.InputType:
return zeroValue(t.ElementType)
case *schema.MapType:
return &model.ObjectConsExpression{}
case *schema.ArrayType:
return &model.TupleConsExpression{}
case *schema.UnionType:
// If there is a default type, create a value of that type.
if t.DefaultType != nil {
return zeroValue(t.DefaultType)
}
// Otherwise, pick the simplest type in the list.
var simplestType schema.Type
for _, t := range t.ElementTypes {
if simplestType == nil || simplerType(t, simplestType) {
simplestType = t
}
}
return zeroValue(simplestType)
case *schema.ObjectType:
var items []model.ObjectConsItem
for _, p := range t.Properties {
if p.IsRequired() {
items = append(items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{
Value: cty.StringVal(p.Name),
},
Value: zeroValue(p.Type),
})
}
}
return &model.ObjectConsExpression{Items: items}
case *schema.TokenType:
if t.UnderlyingType != nil {
return zeroValue(t.UnderlyingType)
}
return model.VariableReference(Null)
}
switch t {
case schema.BoolType:
x, err := generateValue(t, resource.NewBoolProperty(false))
contract.IgnoreError(err)
return x
case schema.IntType, schema.NumberType:
x, err := generateValue(t, resource.NewNumberProperty(0))
contract.IgnoreError(err)
return x
case schema.StringType:
x, err := generateValue(t, resource.NewStringProperty(""))
contract.IgnoreError(err)
return x
case schema.ArchiveType, schema.AssetType:
return model.VariableReference(Null)
case schema.JSONType, schema.AnyType:
return &model.ObjectConsExpression{}
default:
contract.Failf("unexpected schema type %v", t)
return nil
}
}
// generatePropertyValue generates the value for the given property. If the value is absent and the property is
// required, a zero value for the property's type is generated. If the value is absent and the property is not
// required, no value is generated (i.e. this function returns nil).
func generatePropertyValue(property *schema.Property, value resource.PropertyValue) (model.Expression, error) {
if !value.HasValue() {
if !property.IsRequired() {
return nil, nil
}
return zeroValue(property.Type), nil
}
return generateValue(property.Type, value)
}
// generateValue generates a value from the given property value. The given type may or may not match the shape of the
// given value.
func generateValue(typ schema.Type, value resource.PropertyValue) (model.Expression, error) {
typ = codegen.UnwrapType(typ)
switch {
case value.IsArchive():
return nil, fmt.Errorf("NYI: archives")
case value.IsArray():
elementType := schema.AnyType
if typ, ok := typ.(*schema.ArrayType); ok {
elementType = typ.ElementType
}
arr := value.ArrayValue()
exprs := make([]model.Expression, len(arr))
for i, v := range arr {
x, err := generateValue(elementType, v)
if err != nil {
return nil, err
}
exprs[i] = x
}
return &model.TupleConsExpression{
Tokens: syntax.NewTupleConsTokens(len(exprs)),
Expressions: exprs,
}, nil
case value.IsAsset():
return nil, fmt.Errorf("NYI: assets")
case value.IsBool():
return &model.LiteralValueExpression{
Value: cty.BoolVal(value.BoolValue()),
}, nil
case value.IsComputed() || value.IsOutput():
return nil, fmt.Errorf("cannot define computed values")
case value.IsNull():
return model.VariableReference(Null), nil
case value.IsNumber():
return &model.LiteralValueExpression{
Value: cty.NumberFloatVal(value.NumberValue()),
}, nil
case value.IsObject():
obj := value.ObjectValue()
items := make([]model.ObjectConsItem, 0, len(obj))
if objectType, ok := typ.(*schema.ObjectType); ok {
for _, p := range objectType.Properties {
x, err := generatePropertyValue(p, obj[resource.PropertyKey(p.Name)])
if err != nil {
return nil, err
}
if x != nil {
items = append(items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{
Value: cty.StringVal(p.Name),
},
Value: x,
})
}
}
} else {
elementType := schema.AnyType
if mapType, ok := typ.(*schema.MapType); ok {
elementType = mapType.ElementType
}
for _, k := range obj.StableKeys() {
// Ignore internal properties.
if strings.HasPrefix(string(k), "__") {
continue
}
x, err := generateValue(elementType, obj[k])
if err != nil {
return nil, err
}
// we need to take into account when we have a complex key - without this
// we will return key as an array as the / will show as 2 parts
propKey := string(k)
if strings.Contains(string(k), "/") {
propKey = fmt.Sprintf("%q", string(k))
}
items = append(items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{
Value: cty.StringVal(propKey),
},
Value: x,
})
}
}
return &model.ObjectConsExpression{
Tokens: syntax.NewObjectConsTokens(len(items)),
Items: items,
}, nil
case value.IsSecret():
arg, err := generateValue(typ, value.SecretValue().Element)
if err != nil {
return nil, err
}
return &model.FunctionCallExpression{
Name: "secret",
Signature: model.StaticFunctionSignature{
Parameters: []model.Parameter{{
Name: "value",
Type: arg.Type(),
}},
ReturnType: model.NewOutputType(arg.Type()),
},
Args: []model.Expression{arg},
}, nil
case value.IsString():
x := &model.TemplateExpression{
Parts: []model.Expression{
&model.LiteralValueExpression{
Value: cty.StringVal(value.StringValue()),
},
},
}
switch typ {
case schema.ArchiveType:
return &model.FunctionCallExpression{
Name: "fileArchive",
Args: []model.Expression{x},
}, nil
case schema.AssetType:
return &model.FunctionCallExpression{
Name: "fileAsset",
Args: []model.Expression{x},
}, nil
default:
return x, nil
}
default:
contract.Failf("unexpected property value %v", value)
return nil, nil
}
}