github.com/crossplane/upjet@v1.3.0/pkg/types/conversion/tfjson/tfjson.go (about) 1 // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io> 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package tfjson 6 7 import ( 8 tfjson "github.com/hashicorp/terraform-json" 9 schemav2 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 "github.com/pkg/errors" 11 "github.com/zclconf/go-cty/cty" 12 ) 13 14 // GetV2ResourceMap converts input resource schemas with 15 // "terraform-json" representation to terraform-plugin-sdk representation which 16 // is what Upjet expects today. 17 // 18 // What we are trying to achieve here is to convert a lower level 19 // representation of resource schema map, e.g. output of `terraform providers schema -json` 20 // to plugin sdk representation. This is mostly the opposite of what the 21 // following method is doing: https://github.com/hashicorp/terraform-plugin-sdk/blob/7e0a333644f1971a936995677b7a106140a0659f/helper/schema/core_schema.go#L43 22 // 23 // Ideally, we should not rely on plugin SDK types in Upjet at all but only 24 // work with types in https://github.com/hashicorp/terraform-json which is 25 // there exactly for this purpose, an external representation of Terraform 26 // schemas. This conversion aims to be an intermediate step for that ultimate 27 // goal. 28 func GetV2ResourceMap(resourceSchemas map[string]*tfjson.Schema) map[string]*schemav2.Resource { 29 v2map := make(map[string]*schemav2.Resource, len(resourceSchemas)) 30 for k, v := range resourceSchemas { 31 v2map[k] = v2ResourceFromTFJSONSchema(v) 32 } 33 return v2map 34 } 35 36 func v2ResourceFromTFJSONSchema(s *tfjson.Schema) *schemav2.Resource { 37 v2Res := &schemav2.Resource{SchemaVersion: int(s.Version)} 38 if s.Block == nil { 39 return v2Res 40 } 41 42 toSchemaMap := make(map[string]*schemav2.Schema, len(s.Block.Attributes)+len(s.Block.NestedBlocks)) 43 44 for k, v := range s.Block.Attributes { 45 toSchemaMap[k] = tfJSONAttributeToV2Schema(v) 46 } 47 for k, v := range s.Block.NestedBlocks { 48 // CRUD timeouts are not part of the generated MR API, 49 // they cannot be dynamically configured and they are determined by either 50 // the underlying Terraform resource configuration or the upjet resource 51 // configuration. Please also see config.Resource.OperationTimeouts. 52 if k == schemav2.TimeoutsConfigKey { 53 continue 54 } 55 toSchemaMap[k] = tfJSONBlockTypeToV2Schema(v) 56 } 57 58 v2Res.Schema = toSchemaMap 59 v2Res.Description = s.Block.Description 60 v2Res.DeprecationMessage = deprecatedMessage(s.Block.Deprecated) 61 return v2Res 62 } 63 64 func tfJSONAttributeToV2Schema(attr *tfjson.SchemaAttribute) *schemav2.Schema { 65 v2sch := &schemav2.Schema{ 66 Optional: attr.Optional, 67 Required: attr.Required, 68 Description: attr.Description, 69 Computed: attr.Computed, 70 Deprecated: deprecatedMessage(attr.Deprecated), 71 Sensitive: attr.Sensitive, 72 } 73 if err := schemaV2TypeFromCtyType(attr.AttributeType, v2sch); err != nil { 74 panic(err) 75 } 76 return v2sch 77 } 78 79 func tfJSONBlockTypeToV2Schema(nb *tfjson.SchemaBlockType) *schemav2.Schema { //nolint:gocyclo 80 v2sch := &schemav2.Schema{ 81 MinItems: int(nb.MinItems), 82 MaxItems: int(nb.MaxItems), 83 } 84 // Note(turkenh): Schema representation returned by the cli for block types 85 // does not have optional or computed fields. So, we are trying to infer 86 // those fields by doing the opposite of what is done here: 87 // https://github.com/hashicorp/terraform-plugin-sdk/blob/6461ac6e9044a44157c4e2c8aec0f1ab7efc2055/helper/schema/core_schema.go#L204 88 v2sch.Computed = false 89 v2sch.Optional = false 90 if nb.MinItems == 0 { 91 v2sch.Optional = true 92 } 93 if nb.MinItems == 0 && nb.MaxItems == 0 { 94 v2sch.Computed = true 95 } 96 97 switch nb.NestingMode { //nolint:exhaustive 98 case tfjson.SchemaNestingModeSet: 99 v2sch.Type = schemav2.TypeSet 100 case tfjson.SchemaNestingModeList: 101 v2sch.Type = schemav2.TypeList 102 case tfjson.SchemaNestingModeMap: 103 v2sch.Type = schemav2.TypeMap 104 case tfjson.SchemaNestingModeSingle: 105 v2sch.Type = schemav2.TypeList 106 v2sch.MinItems = 0 107 v2sch.Required = hasRequiredChild(nb) 108 v2sch.Optional = !v2sch.Required 109 if v2sch.Required { 110 v2sch.MinItems = 1 111 } 112 v2sch.MaxItems = 1 113 default: 114 panic("unhandled nesting mode: " + nb.NestingMode) 115 } 116 117 if nb.Block == nil { 118 return v2sch 119 } 120 121 v2sch.Description = nb.Block.Description 122 v2sch.Deprecated = deprecatedMessage(nb.Block.Deprecated) 123 124 res := &schemav2.Resource{} 125 res.Schema = make(map[string]*schemav2.Schema, len(nb.Block.Attributes)+len(nb.Block.NestedBlocks)) 126 for key, attr := range nb.Block.Attributes { 127 res.Schema[key] = tfJSONAttributeToV2Schema(attr) 128 } 129 for key, block := range nb.Block.NestedBlocks { 130 // Please note that unlike the resource-level CRUD timeout configuration 131 // blocks (as mentioned above), we will generate the timeouts parameters 132 // for any nested configuration blocks, *if they exist*. 133 // We can prevent them here, but they are different than the resource's 134 // top-level CRUD timeouts, so we have opted to generate them. 135 res.Schema[key] = tfJSONBlockTypeToV2Schema(block) 136 } 137 v2sch.Elem = res 138 return v2sch 139 } 140 141 // checks whether the given tfjson.SchemaBlockType has any required children. 142 // Children which are themselves blocks (nested blocks) are 143 // checked recursively. 144 func hasRequiredChild(nb *tfjson.SchemaBlockType) bool { 145 if nb.Block == nil { 146 return false 147 } 148 for _, a := range nb.Block.Attributes { 149 if a == nil { 150 continue 151 } 152 if a.Required { 153 return true 154 } 155 } 156 for _, b := range nb.Block.NestedBlocks { 157 if b == nil { 158 continue 159 } 160 if hasRequiredChild(b) { 161 return true 162 } 163 } 164 return false 165 } 166 167 func schemaV2TypeFromCtyType(typ cty.Type, schema *schemav2.Schema) error { //nolint:gocyclo 168 configMode := schemav2.SchemaConfigModeAuto 169 170 switch { 171 case typ.IsPrimitiveType(): 172 schema.Type = primitiveToV2SchemaType(typ) 173 case typ.IsCollectionType(): 174 var elemType any 175 et := typ.ElementType() 176 switch { 177 case et.IsPrimitiveType(): 178 elemType = &schemav2.Schema{ 179 Type: primitiveToV2SchemaType(et), 180 Computed: schema.Computed, 181 Optional: schema.Optional, 182 } 183 case et.IsCollectionType(): 184 elemType = &schemav2.Schema{ 185 Type: collectionToV2SchemaType(et), 186 Computed: schema.Computed, 187 Optional: schema.Optional, 188 } 189 if err := schemaV2TypeFromCtyType(et, elemType.(*schemav2.Schema)); err != nil { 190 return err 191 } 192 case et.IsObjectType(): 193 configMode = schemav2.SchemaConfigModeAttr 194 res := &schemav2.Resource{} 195 res.Schema = make(map[string]*schemav2.Schema, len(et.AttributeTypes())) 196 for key, attrTyp := range et.AttributeTypes() { 197 sch := &schemav2.Schema{ 198 Computed: schema.Computed, 199 Optional: schema.Optional, 200 } 201 if et.AttributeOptional(key) { 202 sch.Optional = true 203 } 204 205 if err := schemaV2TypeFromCtyType(attrTyp, sch); err != nil { 206 return err 207 } 208 res.Schema[key] = sch 209 } 210 elemType = res 211 default: 212 return errors.Errorf("unexpected cty.Type %s", typ.GoString()) 213 } 214 schema.ConfigMode = configMode 215 schema.Type = collectionToV2SchemaType(typ) 216 schema.Elem = elemType 217 case typ.IsTupleType(): 218 return errors.New("cannot convert cty TupleType to schema v2 type") 219 case typ.Equals(cty.DynamicPseudoType): 220 return errors.New("cannot convert cty DynamicPseudoType to schema v2 type") 221 } 222 223 return nil 224 } 225 226 func primitiveToV2SchemaType(typ cty.Type) schemav2.ValueType { 227 switch { 228 case typ.Equals(cty.String): 229 return schemav2.TypeString 230 case typ.Equals(cty.Number): 231 // TODO(turkenh): Figure out handling floats with IntOrString on type 232 // builder side 233 return schemav2.TypeFloat 234 case typ.Equals(cty.Bool): 235 return schemav2.TypeBool 236 } 237 return schemav2.TypeInvalid 238 } 239 240 func collectionToV2SchemaType(typ cty.Type) schemav2.ValueType { 241 switch { 242 case typ.IsSetType(): 243 return schemav2.TypeSet 244 case typ.IsListType(): 245 return schemav2.TypeList 246 case typ.IsMapType(): 247 return schemav2.TypeMap 248 } 249 return schemav2.TypeInvalid 250 } 251 252 func deprecatedMessage(deprecated bool) string { 253 if deprecated { 254 return "deprecated" 255 } 256 return "" 257 }