package convert import ( "context" "fmt" "reflect" "sort" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/configs/configschema" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" ) func tftypeFromCtyType(in cty.Type) (tftypes.Type, error) { switch { case in.Equals(cty.String): return tftypes.String, nil case in.Equals(cty.Number): return tftypes.Number, nil case in.Equals(cty.Bool): return tftypes.Bool, nil case in.Equals(cty.DynamicPseudoType): return tftypes.DynamicPseudoType, nil case in.IsSetType(): elemType, err := tftypeFromCtyType(in.ElementType()) if err != nil { return nil, err } return tftypes.Set{ ElementType: elemType, }, nil case in.IsListType(): elemType, err := tftypeFromCtyType(in.ElementType()) if err != nil { return nil, err } return tftypes.List{ ElementType: elemType, }, nil case in.IsTupleType(): elemTypes := make([]tftypes.Type, 0, in.Length()) for _, typ := range in.TupleElementTypes() { elemType, err := tftypeFromCtyType(typ) if err != nil { return nil, err } elemTypes = append(elemTypes, elemType) } return tftypes.Tuple{ ElementTypes: elemTypes, }, nil case in.IsMapType(): elemType, err := tftypeFromCtyType(in.ElementType()) if err != nil { return nil, err } return tftypes.Map{ ElementType: elemType, }, nil case in.IsObjectType(): attrTypes := make(map[string]tftypes.Type) for key, typ := range in.AttributeTypes() { attrType, err := tftypeFromCtyType(typ) if err != nil { return nil, err } attrTypes[key] = attrType } return tftypes.Object{ AttributeTypes: attrTypes, }, nil } return nil, fmt.Errorf("unknown cty type %s", in.GoString()) } func ctyTypeFromTFType(in tftypes.Type) (cty.Type, error) { switch { case in.Is(tftypes.String): return cty.String, nil case in.Is(tftypes.Bool): return cty.Bool, nil case in.Is(tftypes.Number): return cty.Number, nil case in.Is(tftypes.DynamicPseudoType): return cty.DynamicPseudoType, nil case in.Is(tftypes.List{}): elemType, err := ctyTypeFromTFType(in.(tftypes.List).ElementType) if err != nil { return cty.Type{}, err } return cty.List(elemType), nil case in.Is(tftypes.Set{}): elemType, err := ctyTypeFromTFType(in.(tftypes.Set).ElementType) if err != nil { return cty.Type{}, err } return cty.Set(elemType), nil case in.Is(tftypes.Map{}): elemType, err := ctyTypeFromTFType(in.(tftypes.Map).ElementType) if err != nil { return cty.Type{}, err } return cty.Map(elemType), nil case in.Is(tftypes.Tuple{}): elemTypes := make([]cty.Type, 0, len(in.(tftypes.Tuple).ElementTypes)) for _, typ := range in.(tftypes.Tuple).ElementTypes { elemType, err := ctyTypeFromTFType(typ) if err != nil { return cty.Type{}, err } elemTypes = append(elemTypes, elemType) } return cty.Tuple(elemTypes), nil case in.Is(tftypes.Object{}): attrTypes := make(map[string]cty.Type, len(in.(tftypes.Object).AttributeTypes)) for k, v := range in.(tftypes.Object).AttributeTypes { attrType, err := ctyTypeFromTFType(v) if err != nil { return cty.Type{}, err } attrTypes[k] = attrType } return cty.Object(attrTypes), nil } return cty.Type{}, fmt.Errorf("unknown tftypes.Type %s", in) } // ConfigSchemaToProto takes a *configschema.Block and converts it to a // tfprotov5.SchemaBlock for a grpc response. func ConfigSchemaToProto(ctx context.Context, b *configschema.Block) *tfprotov5.SchemaBlock { block := &tfprotov5.SchemaBlock{ Description: b.Description, DescriptionKind: protoStringKind(ctx, b.DescriptionKind), Deprecated: b.Deprecated, } for _, name := range sortedKeys(b.Attributes) { a := b.Attributes[name] attr := &tfprotov5.SchemaAttribute{ Name: name, Description: a.Description, DescriptionKind: protoStringKind(ctx, a.DescriptionKind), Optional: a.Optional, Computed: a.Computed, Required: a.Required, Sensitive: a.Sensitive, Deprecated: a.Deprecated, } var err error attr.Type, err = tftypeFromCtyType(a.Type) if err != nil { panic(err) } block.Attributes = append(block.Attributes, attr) } for _, name := range sortedKeys(b.BlockTypes) { b := b.BlockTypes[name] block.BlockTypes = append(block.BlockTypes, protoSchemaNestedBlock(ctx, name, b)) } return block } func protoStringKind(ctx context.Context, k configschema.StringKind) tfprotov5.StringKind { switch k { default: logging.HelperSchemaTrace(ctx, fmt.Sprintf("Unexpected configschema.StringKind: %d", k)) return tfprotov5.StringKindPlain case configschema.StringPlain: return tfprotov5.StringKindPlain case configschema.StringMarkdown: return tfprotov5.StringKindMarkdown } } func protoSchemaNestedBlock(ctx context.Context, name string, b *configschema.NestedBlock) *tfprotov5.SchemaNestedBlock { var nesting tfprotov5.SchemaNestedBlockNestingMode switch b.Nesting { case configschema.NestingSingle: nesting = tfprotov5.SchemaNestedBlockNestingModeSingle case configschema.NestingGroup: nesting = tfprotov5.SchemaNestedBlockNestingModeGroup case configschema.NestingList: nesting = tfprotov5.SchemaNestedBlockNestingModeList case configschema.NestingSet: nesting = tfprotov5.SchemaNestedBlockNestingModeSet case configschema.NestingMap: nesting = tfprotov5.SchemaNestedBlockNestingModeMap default: nesting = tfprotov5.SchemaNestedBlockNestingModeInvalid } return &tfprotov5.SchemaNestedBlock{ TypeName: name, Block: ConfigSchemaToProto(ctx, &b.Block), Nesting: nesting, MinItems: int64(b.MinItems), MaxItems: int64(b.MaxItems), } } // ProtoToConfigSchema takes the GetSchema_Block from a grpc response and converts it // to a terraform *configschema.Block. func ProtoToConfigSchema(ctx context.Context, b *tfprotov5.SchemaBlock) *configschema.Block { block := &configschema.Block{ Attributes: make(map[string]*configschema.Attribute), BlockTypes: make(map[string]*configschema.NestedBlock), Description: b.Description, DescriptionKind: schemaStringKind(ctx, b.DescriptionKind), Deprecated: b.Deprecated, } for _, a := range b.Attributes { attr := &configschema.Attribute{ Description: a.Description, DescriptionKind: schemaStringKind(ctx, a.DescriptionKind), Required: a.Required, Optional: a.Optional, Computed: a.Computed, Sensitive: a.Sensitive, Deprecated: a.Deprecated, } var err error attr.Type, err = ctyTypeFromTFType(a.Type) if err != nil { panic(err) } block.Attributes[a.Name] = attr } for _, b := range b.BlockTypes { block.BlockTypes[b.TypeName] = schemaNestedBlock(ctx, b) } return block } func schemaStringKind(ctx context.Context, k tfprotov5.StringKind) configschema.StringKind { switch k { default: logging.HelperSchemaTrace(ctx, fmt.Sprintf("Unexpected tfprotov5.StringKind: %d", k)) return configschema.StringPlain case tfprotov5.StringKindPlain: return configschema.StringPlain case tfprotov5.StringKindMarkdown: return configschema.StringMarkdown } } func schemaNestedBlock(ctx context.Context, b *tfprotov5.SchemaNestedBlock) *configschema.NestedBlock { var nesting configschema.NestingMode switch b.Nesting { case tfprotov5.SchemaNestedBlockNestingModeSingle: nesting = configschema.NestingSingle case tfprotov5.SchemaNestedBlockNestingModeGroup: nesting = configschema.NestingGroup case tfprotov5.SchemaNestedBlockNestingModeList: nesting = configschema.NestingList case tfprotov5.SchemaNestedBlockNestingModeMap: nesting = configschema.NestingMap case tfprotov5.SchemaNestedBlockNestingModeSet: nesting = configschema.NestingSet default: // In all other cases we'll leave it as the zero value (invalid) and // let the caller validate it and deal with this. } nb := &configschema.NestedBlock{ Nesting: nesting, MinItems: int(b.MinItems), MaxItems: int(b.MaxItems), } nested := ProtoToConfigSchema(ctx, b.Block) nb.Block = *nested return nb } // sortedKeys returns the lexically sorted keys from the given map. This is // used to make schema conversions are deterministic. This panics if map keys // are not a string. func sortedKeys(m interface{}) []string { v := reflect.ValueOf(m) keys := make([]string, v.Len()) mapKeys := v.MapKeys() for i, k := range mapKeys { keys[i] = k.Interface().(string) } sort.Strings(keys) return keys }