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