| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package convert | |
| import ( | |
| "encoding/json" | |
| "reflect" | |
| "sort" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/providers" | |
| proto "github.com/hashicorp/terraform/internal/tfplugin5" | |
| "github.com/zclconf/go-cty/cty" | |
| ) | |
| // ConfigSchemaToProto takes a *configschema.Block and converts it to a | |
| // proto.Schema_Block for a grpc response. | |
| func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block { | |
| block := &proto.Schema_Block{ | |
| Description: b.Description, | |
| DescriptionKind: protoStringKind(b.DescriptionKind), | |
| Deprecated: b.Deprecated, | |
| DeprecationMessage: b.DeprecationMessage, | |
| } | |
| for _, name := range sortedKeys(b.Attributes) { | |
| a := b.Attributes[name] | |
| attr := &proto.Schema_Attribute{ | |
| Name: name, | |
| Description: a.Description, | |
| DescriptionKind: protoStringKind(a.DescriptionKind), | |
| Optional: a.Optional, | |
| Computed: a.Computed, | |
| Required: a.Required, | |
| Sensitive: a.Sensitive, | |
| Deprecated: a.Deprecated, | |
| DeprecationMessage: a.DeprecationMessage, | |
| WriteOnly: a.WriteOnly, | |
| } | |
| ty, err := json.Marshal(a.Type) | |
| if err != nil { | |
| panic(err) | |
| } | |
| attr.Type = ty | |
| block.Attributes = append(block.Attributes, attr) | |
| } | |
| for _, name := range sortedKeys(b.BlockTypes) { | |
| b := b.BlockTypes[name] | |
| block.BlockTypes = append(block.BlockTypes, protoSchemaNestedBlock(name, b)) | |
| } | |
| return block | |
| } | |
| func protoStringKind(k configschema.StringKind) proto.StringKind { | |
| switch k { | |
| default: | |
| return proto.StringKind_PLAIN | |
| case configschema.StringMarkdown: | |
| return proto.StringKind_MARKDOWN | |
| } | |
| } | |
| func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Schema_NestedBlock { | |
| var nesting proto.Schema_NestedBlock_NestingMode | |
| switch b.Nesting { | |
| case configschema.NestingSingle: | |
| nesting = proto.Schema_NestedBlock_SINGLE | |
| case configschema.NestingGroup: | |
| nesting = proto.Schema_NestedBlock_GROUP | |
| case configschema.NestingList: | |
| nesting = proto.Schema_NestedBlock_LIST | |
| case configschema.NestingSet: | |
| nesting = proto.Schema_NestedBlock_SET | |
| case configschema.NestingMap: | |
| nesting = proto.Schema_NestedBlock_MAP | |
| default: | |
| nesting = proto.Schema_NestedBlock_INVALID | |
| } | |
| return &proto.Schema_NestedBlock{ | |
| TypeName: name, | |
| Block: ConfigSchemaToProto(&b.Block), | |
| Nesting: nesting, | |
| MinItems: int64(b.MinItems), | |
| MaxItems: int64(b.MaxItems), | |
| } | |
| } | |
| // ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema. | |
| // It takes an optional resource identity schema for resources that support identity. | |
| func ProtoToProviderSchema(s *proto.Schema, id *proto.ResourceIdentitySchema) providers.Schema { | |
| schema := providers.Schema{ | |
| Version: s.Version, | |
| Body: ProtoToConfigSchema(s.Block), | |
| } | |
| if id != nil { | |
| schema.IdentityVersion = id.Version | |
| schema.Identity = ProtoToIdentitySchema(id.IdentityAttributes) | |
| } | |
| return schema | |
| } | |
| func ProtoToActionSchema(s *proto.ActionSchema) providers.ActionSchema { | |
| return providers.ActionSchema{ | |
| ConfigSchema: ProtoToConfigSchema(s.Schema.Block), | |
| } | |
| } | |
| func ProtoToListSchema(s *proto.Schema) providers.Schema { | |
| listSchema := ProtoToProviderSchema(s, nil) | |
| itemCount := 0 | |
| // check if the provider has set some attributes/blocks as required. | |
| // When yes, then we set minItem = 1, which | |
| // validates that the configuration contains a "config" block. | |
| for _, attrS := range listSchema.Body.Attributes { | |
| if attrS.Required { | |
| itemCount = 1 | |
| break | |
| } | |
| } | |
| for _, block := range listSchema.Body.BlockTypes { | |
| if block.MinItems > 0 { | |
| itemCount = 1 | |
| break | |
| } | |
| } | |
| return providers.Schema{ | |
| Version: s.Version, | |
| Body: &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| "data": { | |
| Type: cty.DynamicPseudoType, | |
| Computed: true, | |
| }, | |
| }, | |
| BlockTypes: map[string]*configschema.NestedBlock{ | |
| "config": { | |
| Block: *listSchema.Body, | |
| Nesting: configschema.NestingSingle, | |
| MinItems: itemCount, | |
| MaxItems: itemCount, | |
| }, | |
| }, | |
| }, | |
| } | |
| } | |
| // ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it | |
| // to a terraform *configschema.Block. | |
| func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block { | |
| block := &configschema.Block{ | |
| Attributes: make(map[string]*configschema.Attribute), | |
| BlockTypes: make(map[string]*configschema.NestedBlock), | |
| Description: b.Description, | |
| DescriptionKind: schemaStringKind(b.DescriptionKind), | |
| Deprecated: b.Deprecated, | |
| DeprecationMessage: b.DeprecationMessage, | |
| } | |
| for _, a := range b.Attributes { | |
| attr := &configschema.Attribute{ | |
| Description: a.Description, | |
| DescriptionKind: schemaStringKind(a.DescriptionKind), | |
| Required: a.Required, | |
| Optional: a.Optional, | |
| Computed: a.Computed, | |
| Sensitive: a.Sensitive, | |
| Deprecated: a.Deprecated, | |
| DeprecationMessage: a.DeprecationMessage, | |
| WriteOnly: a.WriteOnly, | |
| } | |
| if err := json.Unmarshal(a.Type, &attr.Type); err != nil { | |
| panic(err) | |
| } | |
| block.Attributes[a.Name] = attr | |
| } | |
| for _, b := range b.BlockTypes { | |
| block.BlockTypes[b.TypeName] = schemaNestedBlock(b) | |
| } | |
| return block | |
| } | |
| func schemaStringKind(k proto.StringKind) configschema.StringKind { | |
| switch k { | |
| default: | |
| return configschema.StringPlain | |
| case proto.StringKind_MARKDOWN: | |
| return configschema.StringMarkdown | |
| } | |
| } | |
| func schemaNestedBlock(b *proto.Schema_NestedBlock) *configschema.NestedBlock { | |
| var nesting configschema.NestingMode | |
| switch b.Nesting { | |
| case proto.Schema_NestedBlock_SINGLE: | |
| nesting = configschema.NestingSingle | |
| case proto.Schema_NestedBlock_GROUP: | |
| nesting = configschema.NestingGroup | |
| case proto.Schema_NestedBlock_LIST: | |
| nesting = configschema.NestingList | |
| case proto.Schema_NestedBlock_MAP: | |
| nesting = configschema.NestingMap | |
| case proto.Schema_NestedBlock_SET: | |
| 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(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 | |
| } | |
| func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object { | |
| obj := &configschema.Object{ | |
| Attributes: make(map[string]*configschema.Attribute), | |
| Nesting: configschema.NestingSingle, | |
| } | |
| for _, a := range attributes { | |
| attr := &configschema.Attribute{ | |
| Description: a.Description, | |
| Required: a.RequiredForImport, | |
| Optional: a.OptionalForImport, | |
| } | |
| if err := json.Unmarshal(a.Type, &attr.Type); err != nil { | |
| panic(err) | |
| } | |
| obj.Attributes[a.Name] = attr | |
| } | |
| return obj | |
| } | |
| func ResourceIdentitySchemaToProto(b providers.IdentitySchema) *proto.ResourceIdentitySchema { | |
| attrs := []*proto.ResourceIdentitySchema_IdentityAttribute{} | |
| for _, name := range sortedKeys(b.Body.Attributes) { | |
| a := b.Body.Attributes[name] | |
| attr := &proto.ResourceIdentitySchema_IdentityAttribute{ | |
| Name: name, | |
| Description: a.Description, | |
| RequiredForImport: a.Required, | |
| OptionalForImport: a.Optional, | |
| } | |
| ty, err := json.Marshal(a.Type) | |
| if err != nil { | |
| panic(err) | |
| } | |
| attr.Type = ty | |
| attrs = append(attrs, attr) | |
| } | |
| return &proto.ResourceIdentitySchema{ | |
| Version: b.Version, | |
| IdentityAttributes: attrs, | |
| } | |
| } |