// 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 ( "errors" "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/slice" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "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 := schema.LoadPackageReference(loader, string(state.Type.Package()), nil) if err != nil { return nil, err } r, ok, err := pkg.Resources().Get(string(state.Type)) if err != nil { return nil, fmt.Errorf("loading resource '%v': %w", state.Type, err) } if !ok { return nil, fmt.Errorf("unknown resource type '%v'", r) } var items []model.BodyItem name := state.URN.Name() // Check if _this_ urn is in the name table, if so we need to set logicalName and use the mapped name for // the resource block. if mappedName, ok := names[state.URN]; ok { items = append(items, &model.Attribute{ Name: "__logicalName", Value: &model.TemplateExpression{ Parts: []model.Expression{ &model.LiteralValueExpression{ Value: cty.StringVal(name), }, }, }, }) name = mappedName } 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 := string(state.URN.Type()) return &model.Block{ Tokens: syntax.NewBlockTokens("resource", name, typ), Type: "resource", Labels: []string{name, 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.QualifiedType() != 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.Provider) } 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) } // valueStructurallyTypedAs returns true if the given value is structurally typed as the given schema type. func valueStructurallyTypedAs(value resource.PropertyValue, schemaType schema.Type) bool { if union, ok := schemaType.(*schema.UnionType); ok { schemaType = reduceUnionType(union, value) } switch { case value.IsObject(): switch arg := schemaType.(type) { case *schema.ObjectType: schemaProperties := make(map[string]schema.Type) for _, schemaProperty := range arg.Properties { schemaProperties[schemaProperty.Name] = schemaProperty.Type } objectProperties := value.ObjectValue() // check that each property is present in the schema and that the value is structurally typed as well for propertyKey, propertyValue := range objectProperties { propertyValueSchema, ok := arg.Property(string(propertyKey)) if !ok { // unknown property return false } if !valueStructurallyTypedAs(propertyValue, propertyValueSchema.Type) { return false } } // check that all required properties from the schema are present in the object properties for _, schemaProperty := range arg.Properties { if schemaProperty.IsRequired() { if _, ok := objectProperties[resource.PropertyKey(schemaProperty.Name)]; !ok { // the required property was not present in the object return false } } } // all properties are present and structurally typed return true case *schema.UnionType: // make sure that at least of the union element types is structurally typed for _, unionElement := range arg.ElementTypes { if valueStructurallyTypedAs(value, unionElement) { return true } } } case value.IsString(): // basic case if schemaType == schema.StringType { return true } // for unions: check that at least of one of the element types is also a string // collapsing unions of unions as necessary recursively if union, ok := schemaType.(*schema.UnionType); ok { for _, elementType := range union.ElementTypes { if valueStructurallyTypedAs(value, elementType) { return true } } } case value.IsBool(): // basic case if schemaType == schema.BoolType { return true } // for unions: check that at least of one of the element types is also a bool // collapsing unions of unions as necessary recursively if union, ok := schemaType.(*schema.UnionType); ok { for _, elementType := range union.ElementTypes { if valueStructurallyTypedAs(value, elementType) { return true } } } case value.IsNumber(): // basic case if schemaType == schema.NumberType || schemaType == schema.IntType { return true } // for unions: check that at least of one of the element types is also a number // collapsing unions of unions as necessary recursively if union, ok := schemaType.(*schema.UnionType); ok { for _, elementType := range union.ElementTypes { if valueStructurallyTypedAs(value, elementType) { return true } } } case value.IsArray(): // basic case: check that each element in the array is structurally typed as the element type of the schenma array switch arg := schemaType.(type) { case *schema.ArrayType: for _, element := range value.ArrayValue() { if !valueStructurallyTypedAs(element, arg.ElementType) { return false } } // all elements are structurally typed return true case *schema.UnionType: // make sure that at least of the union element types is structurally typed for _, unionElement := range arg.ElementTypes { if valueStructurallyTypedAs(value, unionElement) { return true } } } } return false } // reduceUnionType reduces the given union type to a simpler type that potentially matches the value. // When the value type is primitive, choose the first element type of the union elements that is of the same type. // When the value is an object, use the discriminator to choose the element type. func reduceUnionType(schemaUnion *schema.UnionType, value resource.PropertyValue) schema.Type { switch { case value.IsObject(): // return the first element type that matches structurally fits the value findBestFitType := func() schema.Type { for _, t := range schemaUnion.ElementTypes { if valueStructurallyTypedAs(value, t) { return t } } // if we still couldn't find a type that fits the value return nil } // If the value is an object, use the discriminator to choose the element type. if schemaUnion.Discriminator == "" { return findBestFitType() } obj := value.ObjectValue() discriminatorValue, ok := obj[resource.PropertyKey(schemaUnion.Discriminator)] if !ok { // discriminator property is not present // return the first type that fits the value return findBestFitType() } if !discriminatorValue.IsString() { // discriminator property value is not a string, // so we can't select a type from the union mapping return findBestFitType() } correspondingTypeToken, ok := schemaUnion.Mapping[discriminatorValue.StringValue()] if !ok { // discriminator property value is not a key in the union mapping, return findBestFitType() } for _, elementType := range schemaUnion.ElementTypes { // found the type token // match it against the element type which should be an object elementTypeObject, ok := codegen.UnwrapType(elementType).(*schema.ObjectType) if ok { elementTypeToken, parseError := tokens.ParseTypeToken(elementTypeObject.Token) if parseError != nil { continue } foundTypeToken, parseError := tokens.ParseTypeToken(correspondingTypeToken) if parseError != nil { continue } typeName := string(elementTypeToken.Name()) foundTypeName := string(foundTypeToken.Name()) if typeName == foundTypeName { return elementTypeObject } } } default: for _, t := range schemaUnion.ElementTypes { if unionType, ok := t.(*schema.UnionType); ok { t = reduceUnionType(unionType, value) } if valueStructurallyTypedAs(value, t) { return t } } } // anything else, we don't know return nil } // 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) if unionType, ok := typ.(*schema.UnionType); ok { typ = reduceUnionType(unionType, value) } switch { case value.IsArchive(): return nil, errors.New("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, errors.New("NYI: assets") case value.IsBool(): return &model.LiteralValueExpression{ Value: cty.BoolVal(value.BoolValue()), }, nil case value.IsComputed() || value.IsOutput(): return nil, errors.New("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 := slice.Prealloc[model.ObjectConsItem](len(obj)) switch arg := typ.(type) { case *schema.ObjectType: for _, p := range arg.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, }) } } default: 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 } // Always quote the key in case it includes invalid identifier characters (like '/' or ':') 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 } }