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