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