github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/core_schema.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package schema 5 6 import ( 7 "fmt" 8 9 "github.com/terramate-io/tf/configs/configschema" 10 "github.com/zclconf/go-cty/cty" 11 ) 12 13 // The functions and methods in this file are concerned with the conversion 14 // of this package's schema model into the slightly-lower-level schema model 15 // used by Terraform core for configuration parsing. 16 17 // CoreConfigSchema lowers the receiver to the schema model expected by 18 // Terraform core. 19 // 20 // This lower-level model has fewer features than the schema in this package, 21 // describing only the basic structure of configuration and state values we 22 // expect. The full schemaMap from this package is still required for full 23 // validation, handling of default values, etc. 24 // 25 // This method presumes a schema that passes InternalValidate, and so may 26 // panic or produce an invalid result if given an invalid schemaMap. 27 func (m schemaMap) CoreConfigSchema() *configschema.Block { 28 if len(m) == 0 { 29 // We return an actual (empty) object here, rather than a nil, 30 // because a nil result would mean that we don't have a schema at 31 // all, rather than that we have an empty one. 32 return &configschema.Block{} 33 } 34 35 ret := &configschema.Block{ 36 Attributes: map[string]*configschema.Attribute{}, 37 BlockTypes: map[string]*configschema.NestedBlock{}, 38 } 39 40 for name, schema := range m { 41 if schema.Elem == nil { 42 ret.Attributes[name] = schema.coreConfigSchemaAttribute() 43 continue 44 } 45 if schema.Type == TypeMap { 46 // For TypeMap in particular, it isn't valid for Elem to be a 47 // *Resource (since that would be ambiguous in flatmap) and 48 // so Elem is treated as a TypeString schema if so. This matches 49 // how the field readers treat this situation, for compatibility 50 // with configurations targeting Terraform 0.11 and earlier. 51 if _, isResource := schema.Elem.(*Resource); isResource { 52 sch := *schema // shallow copy 53 sch.Elem = &Schema{ 54 Type: TypeString, 55 } 56 ret.Attributes[name] = sch.coreConfigSchemaAttribute() 57 continue 58 } 59 } 60 switch schema.ConfigMode { 61 case SchemaConfigModeAttr: 62 ret.Attributes[name] = schema.coreConfigSchemaAttribute() 63 case SchemaConfigModeBlock: 64 ret.BlockTypes[name] = schema.coreConfigSchemaBlock() 65 default: // SchemaConfigModeAuto, or any other invalid value 66 if schema.Computed && !schema.Optional { 67 // Computed-only schemas are always handled as attributes, 68 // because they never appear in configuration. 69 ret.Attributes[name] = schema.coreConfigSchemaAttribute() 70 continue 71 } 72 switch schema.Elem.(type) { 73 case *Schema, ValueType: 74 ret.Attributes[name] = schema.coreConfigSchemaAttribute() 75 case *Resource: 76 ret.BlockTypes[name] = schema.coreConfigSchemaBlock() 77 default: 78 // Should never happen for a valid schema 79 panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", schema.Elem)) 80 } 81 } 82 } 83 84 return ret 85 } 86 87 // coreConfigSchemaAttribute prepares a configschema.Attribute representation 88 // of a schema. This is appropriate only for primitives or collections whose 89 // Elem is an instance of Schema. Use coreConfigSchemaBlock for collections 90 // whose elem is a whole resource. 91 func (s *Schema) coreConfigSchemaAttribute() *configschema.Attribute { 92 // The Schema.DefaultFunc capability adds some extra weirdness here since 93 // it can be combined with "Required: true" to create a situation where 94 // required-ness is conditional. Terraform Core doesn't share this concept, 95 // so we must sniff for this possibility here and conditionally turn 96 // off the "Required" flag if it looks like the DefaultFunc is going 97 // to provide a value. 98 // This is not 100% true to the original interface of DefaultFunc but 99 // works well enough for the EnvDefaultFunc and MultiEnvDefaultFunc 100 // situations, which are the main cases we care about. 101 // 102 // Note that this also has a consequence for commands that return schema 103 // information for documentation purposes: running those for certain 104 // providers will produce different results depending on which environment 105 // variables are set. We accept that weirdness in order to keep this 106 // interface to core otherwise simple. 107 reqd := s.Required 108 opt := s.Optional 109 if reqd && s.DefaultFunc != nil { 110 v, err := s.DefaultFunc() 111 // We can't report errors from here, so we'll instead just force 112 // "Required" to false and let the provider try calling its 113 // DefaultFunc again during the validate step, where it can then 114 // return the error. 115 if err != nil || (err == nil && v != nil) { 116 reqd = false 117 opt = true 118 } 119 } 120 121 return &configschema.Attribute{ 122 Type: s.coreConfigSchemaType(), 123 Optional: opt, 124 Required: reqd, 125 Computed: s.Computed, 126 Sensitive: s.Sensitive, 127 Description: s.Description, 128 } 129 } 130 131 // coreConfigSchemaBlock prepares a configschema.NestedBlock representation of 132 // a schema. This is appropriate only for collections whose Elem is an instance 133 // of Resource, and will panic otherwise. 134 func (s *Schema) coreConfigSchemaBlock() *configschema.NestedBlock { 135 ret := &configschema.NestedBlock{} 136 if nested := s.Elem.(*Resource).coreConfigSchema(); nested != nil { 137 ret.Block = *nested 138 } 139 switch s.Type { 140 case TypeList: 141 ret.Nesting = configschema.NestingList 142 case TypeSet: 143 ret.Nesting = configschema.NestingSet 144 case TypeMap: 145 ret.Nesting = configschema.NestingMap 146 default: 147 // Should never happen for a valid schema 148 panic(fmt.Errorf("invalid s.Type %s for s.Elem being resource", s.Type)) 149 } 150 151 ret.MinItems = s.MinItems 152 ret.MaxItems = s.MaxItems 153 154 if s.Required && s.MinItems == 0 { 155 // configschema doesn't have a "required" representation for nested 156 // blocks, but we can fake it by requiring at least one item. 157 ret.MinItems = 1 158 } 159 if s.Optional && s.MinItems > 0 { 160 // Historically helper/schema would ignore MinItems if Optional were 161 // set, so we must mimic this behavior here to ensure that providers 162 // relying on that undocumented behavior can continue to operate as 163 // they did before. 164 ret.MinItems = 0 165 } 166 if s.Computed && !s.Optional { 167 // MinItems/MaxItems are meaningless for computed nested blocks, since 168 // they are never set by the user anyway. This ensures that we'll never 169 // generate weird errors about them. 170 ret.MinItems = 0 171 ret.MaxItems = 0 172 } 173 174 return ret 175 } 176 177 // coreConfigSchemaType determines the core config schema type that corresponds 178 // to a particular schema's type. 179 func (s *Schema) coreConfigSchemaType() cty.Type { 180 switch s.Type { 181 case TypeString: 182 return cty.String 183 case TypeBool: 184 return cty.Bool 185 case TypeInt, TypeFloat: 186 // configschema doesn't distinguish int and float, so helper/schema 187 // will deal with this as an additional validation step after 188 // configuration has been parsed and decoded. 189 return cty.Number 190 case TypeList, TypeSet, TypeMap: 191 var elemType cty.Type 192 switch set := s.Elem.(type) { 193 case *Schema: 194 elemType = set.coreConfigSchemaType() 195 case ValueType: 196 // This represents a mistake in the provider code, but it's a 197 // common one so we'll just shim it. 198 elemType = (&Schema{Type: set}).coreConfigSchemaType() 199 case *Resource: 200 // By default we construct a NestedBlock in this case, but this 201 // behavior is selected either for computed-only schemas or 202 // when ConfigMode is explicitly SchemaConfigModeBlock. 203 // See schemaMap.CoreConfigSchema for the exact rules. 204 elemType = set.coreConfigSchema().ImpliedType() 205 default: 206 if set != nil { 207 // Should never happen for a valid schema 208 panic(fmt.Errorf("invalid Schema.Elem %#v; need *Schema or *Resource", s.Elem)) 209 } 210 // Some pre-existing schemas assume string as default, so we need 211 // to be compatible with them. 212 elemType = cty.String 213 } 214 switch s.Type { 215 case TypeList: 216 return cty.List(elemType) 217 case TypeSet: 218 return cty.Set(elemType) 219 case TypeMap: 220 return cty.Map(elemType) 221 default: 222 // can never get here in practice, due to the case we're inside 223 panic("invalid collection type") 224 } 225 default: 226 // should never happen for a valid schema 227 panic(fmt.Errorf("invalid Schema.Type %s", s.Type)) 228 } 229 } 230 231 // CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema on 232 // the resource's schema. CoreConfigSchema adds the implicitly required "id" 233 // attribute for top level resources if it doesn't exist. 234 func (r *Resource) CoreConfigSchema() *configschema.Block { 235 block := r.coreConfigSchema() 236 237 if block.Attributes == nil { 238 block.Attributes = map[string]*configschema.Attribute{} 239 } 240 241 // Add the implicitly required "id" field if it doesn't exist 242 if block.Attributes["id"] == nil { 243 block.Attributes["id"] = &configschema.Attribute{ 244 Type: cty.String, 245 Optional: true, 246 Computed: true, 247 } 248 } 249 250 _, timeoutsAttr := block.Attributes[TimeoutsConfigKey] 251 _, timeoutsBlock := block.BlockTypes[TimeoutsConfigKey] 252 253 // Insert configured timeout values into the schema, as long as the schema 254 // didn't define anything else by that name. 255 if r.Timeouts != nil && !timeoutsAttr && !timeoutsBlock { 256 timeouts := configschema.Block{ 257 Attributes: map[string]*configschema.Attribute{}, 258 } 259 260 if r.Timeouts.Create != nil { 261 timeouts.Attributes[TimeoutCreate] = &configschema.Attribute{ 262 Type: cty.String, 263 Optional: true, 264 } 265 } 266 267 if r.Timeouts.Read != nil { 268 timeouts.Attributes[TimeoutRead] = &configschema.Attribute{ 269 Type: cty.String, 270 Optional: true, 271 } 272 } 273 274 if r.Timeouts.Update != nil { 275 timeouts.Attributes[TimeoutUpdate] = &configschema.Attribute{ 276 Type: cty.String, 277 Optional: true, 278 } 279 } 280 281 if r.Timeouts.Delete != nil { 282 timeouts.Attributes[TimeoutDelete] = &configschema.Attribute{ 283 Type: cty.String, 284 Optional: true, 285 } 286 } 287 288 if r.Timeouts.Default != nil { 289 timeouts.Attributes[TimeoutDefault] = &configschema.Attribute{ 290 Type: cty.String, 291 Optional: true, 292 } 293 } 294 295 block.BlockTypes[TimeoutsConfigKey] = &configschema.NestedBlock{ 296 Nesting: configschema.NestingSingle, 297 Block: timeouts, 298 } 299 } 300 301 return block 302 } 303 304 func (r *Resource) coreConfigSchema() *configschema.Block { 305 return schemaMap(r.Schema).CoreConfigSchema() 306 } 307 308 // CoreConfigSchema is a convenient shortcut for calling CoreConfigSchema 309 // on the backends's schema. 310 func (r *Backend) CoreConfigSchema() *configschema.Block { 311 return schemaMap(r.Schema).CoreConfigSchema() 312 }