github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/schema.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 // schema is a high-level framework for easily writing new providers 5 // for Terraform. Usage of schema is recommended over attempting to write 6 // to the low-level plugin interfaces manually. 7 // 8 // schema breaks down provider creation into simple CRUD operations for 9 // resources. The logic of diffing, destroying before creating, updating 10 // or creating, etc. is all handled by the framework. The plugin author 11 // only needs to implement a configuration schema and the CRUD operations and 12 // everything else is meant to just work. 13 // 14 // A good starting point is to view the Provider structure. 15 package schema 16 17 import ( 18 "context" 19 "fmt" 20 "os" 21 "reflect" 22 "regexp" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 28 "github.com/mitchellh/copystructure" 29 "github.com/mitchellh/mapstructure" 30 "github.com/terramate-io/tf/configs/hcl2shim" 31 "github.com/terramate-io/tf/legacy/terraform" 32 ) 33 34 // Name of ENV variable which (if not empty) prefers panic over error 35 const PanicOnErr = "TF_SCHEMA_PANIC_ON_ERROR" 36 37 // type used for schema package context keys 38 type contextKey string 39 40 var ( 41 protoVersionMu sync.Mutex 42 protoVersion5 = false 43 ) 44 45 func isProto5() bool { 46 protoVersionMu.Lock() 47 defer protoVersionMu.Unlock() 48 return protoVersion5 49 50 } 51 52 // SetProto5 enables a feature flag for any internal changes required required 53 // to work with the new plugin protocol. This should not be called by 54 // provider. 55 func SetProto5() { 56 protoVersionMu.Lock() 57 defer protoVersionMu.Unlock() 58 protoVersion5 = true 59 } 60 61 // Schema is used to describe the structure of a value. 62 // 63 // Read the documentation of the struct elements for important details. 64 type Schema struct { 65 // Type is the type of the value and must be one of the ValueType values. 66 // 67 // This type not only determines what type is expected/valid in configuring 68 // this value, but also what type is returned when ResourceData.Get is 69 // called. The types returned by Get are: 70 // 71 // TypeBool - bool 72 // TypeInt - int 73 // TypeFloat - float64 74 // TypeString - string 75 // TypeList - []interface{} 76 // TypeMap - map[string]interface{} 77 // TypeSet - *schema.Set 78 // 79 Type ValueType 80 81 // ConfigMode allows for overriding the default behaviors for mapping 82 // schema entries onto configuration constructs. 83 // 84 // By default, the Elem field is used to choose whether a particular 85 // schema is represented in configuration as an attribute or as a nested 86 // block; if Elem is a *schema.Resource then it's a block and it's an 87 // attribute otherwise. 88 // 89 // If Elem is *schema.Resource then setting ConfigMode to 90 // SchemaConfigModeAttr will force it to be represented in configuration 91 // as an attribute, which means that the Computed flag can be used to 92 // provide default elements when the argument isn't set at all, while still 93 // allowing the user to force zero elements by explicitly assigning an 94 // empty list. 95 // 96 // When Computed is set without Optional, the attribute is not settable 97 // in configuration at all and so SchemaConfigModeAttr is the automatic 98 // behavior, and SchemaConfigModeBlock is not permitted. 99 ConfigMode SchemaConfigMode 100 101 // If one of these is set, then this item can come from the configuration. 102 // Both cannot be set. If Optional is set, the value is optional. If 103 // Required is set, the value is required. 104 // 105 // One of these must be set if the value is not computed. That is: 106 // value either comes from the config, is computed, or is both. 107 Optional bool 108 Required bool 109 110 // If this is non-nil, the provided function will be used during diff 111 // of this field. If this is nil, a default diff for the type of the 112 // schema will be used. 113 // 114 // This allows comparison based on something other than primitive, list 115 // or map equality - for example SSH public keys may be considered 116 // equivalent regardless of trailing whitespace. 117 DiffSuppressFunc SchemaDiffSuppressFunc 118 119 // If this is non-nil, then this will be a default value that is used 120 // when this item is not set in the configuration. 121 // 122 // DefaultFunc can be specified to compute a dynamic default. 123 // Only one of Default or DefaultFunc can be set. If DefaultFunc is 124 // used then its return value should be stable to avoid generating 125 // confusing/perpetual diffs. 126 // 127 // Changing either Default or the return value of DefaultFunc can be 128 // a breaking change, especially if the attribute in question has 129 // ForceNew set. If a default needs to change to align with changing 130 // assumptions in an upstream API then it may be necessary to also use 131 // the MigrateState function on the resource to change the state to match, 132 // or have the Read function adjust the state value to align with the 133 // new default. 134 // 135 // If Required is true above, then Default cannot be set. DefaultFunc 136 // can be set with Required. If the DefaultFunc returns nil, then there 137 // will be no default and the user will be asked to fill it in. 138 // 139 // If either of these is set, then the user won't be asked for input 140 // for this key if the default is not nil. 141 Default interface{} 142 DefaultFunc SchemaDefaultFunc 143 144 // Description is used as the description for docs or asking for user 145 // input. It should be relatively short (a few sentences max) and should 146 // be formatted to fit a CLI. 147 Description string 148 149 // InputDefault is the default value to use for when inputs are requested. 150 // This differs from Default in that if Default is set, no input is 151 // asked for. If Input is asked, this will be the default value offered. 152 InputDefault string 153 154 // The fields below relate to diffs. 155 // 156 // If Computed is true, then the result of this value is computed 157 // (unless specified by config) on creation. 158 // 159 // If ForceNew is true, then a change in this resource necessitates 160 // the creation of a new resource. 161 // 162 // StateFunc is a function called to change the value of this before 163 // storing it in the state (and likewise before comparing for diffs). 164 // The use for this is for example with large strings, you may want 165 // to simply store the hash of it. 166 Computed bool 167 ForceNew bool 168 StateFunc SchemaStateFunc 169 170 // The following fields are only set for a TypeList, TypeSet, or TypeMap. 171 // 172 // Elem represents the element type. For a TypeMap, it must be a *Schema 173 // with a Type that is one of the primitives: TypeString, TypeBool, 174 // TypeInt, or TypeFloat. Otherwise it may be either a *Schema or a 175 // *Resource. If it is *Schema, the element type is just a simple value. 176 // If it is *Resource, the element type is a complex structure, 177 // potentially managed via its own CRUD actions on the API. 178 Elem interface{} 179 180 // The following fields are only set for a TypeList or TypeSet. 181 // 182 // MaxItems defines a maximum amount of items that can exist within a 183 // TypeSet or TypeList. Specific use cases would be if a TypeSet is being 184 // used to wrap a complex structure, however more than one instance would 185 // cause instability. 186 // 187 // MinItems defines a minimum amount of items that can exist within a 188 // TypeSet or TypeList. Specific use cases would be if a TypeSet is being 189 // used to wrap a complex structure, however less than one instance would 190 // cause instability. 191 // 192 // If the field Optional is set to true then MinItems is ignored and thus 193 // effectively zero. 194 MaxItems int 195 MinItems int 196 197 // PromoteSingle originally allowed for a single element to be assigned 198 // where a primitive list was expected, but this no longer works from 199 // Terraform v0.12 onwards (Terraform Core will require a list to be set 200 // regardless of what this is set to) and so only applies to Terraform v0.11 201 // and earlier, and so should be used only to retain this functionality 202 // for those still using v0.11 with a provider that formerly used this. 203 PromoteSingle bool 204 205 // The following fields are only valid for a TypeSet type. 206 // 207 // Set defines a function to determine the unique ID of an item so that 208 // a proper set can be built. 209 Set SchemaSetFunc 210 211 // ComputedWhen is a set of queries on the configuration. Whenever any 212 // of these things is changed, it will require a recompute (this requires 213 // that Computed is set to true). 214 // 215 // NOTE: This currently does not work. 216 ComputedWhen []string 217 218 // ConflictsWith is a set of schema keys that conflict with this schema. 219 // This will only check that they're set in the _config_. This will not 220 // raise an error for a malfunctioning resource that sets a conflicting 221 // key. 222 ConflictsWith []string 223 224 // When Deprecated is set, this attribute is deprecated. 225 // 226 // A deprecated field still works, but will probably stop working in near 227 // future. This string is the message shown to the user with instructions on 228 // how to address the deprecation. 229 Deprecated string 230 231 // When Removed is set, this attribute has been removed from the schema 232 // 233 // Removed attributes can be left in the Schema to generate informative error 234 // messages for the user when they show up in resource configurations. 235 // This string is the message shown to the user with instructions on 236 // what do to about the removed attribute. 237 Removed string 238 239 // ValidateFunc allows individual fields to define arbitrary validation 240 // logic. It is yielded the provided config value as an interface{} that is 241 // guaranteed to be of the proper Schema type, and it can yield warnings or 242 // errors based on inspection of that value. 243 // 244 // ValidateFunc is honored only when the schema's Type is set to TypeInt, 245 // TypeFloat, TypeString, TypeBool, or TypeMap. It is ignored for all other types. 246 ValidateFunc SchemaValidateFunc 247 248 // Sensitive ensures that the attribute's value does not get displayed in 249 // logs or regular output. It should be used for passwords or other 250 // secret fields. Future versions of Terraform may encrypt these 251 // values. 252 Sensitive bool 253 } 254 255 // SchemaConfigMode is used to influence how a schema item is mapped into a 256 // corresponding configuration construct, using the ConfigMode field of 257 // Schema. 258 type SchemaConfigMode int 259 260 const ( 261 SchemaConfigModeAuto SchemaConfigMode = iota 262 SchemaConfigModeAttr 263 SchemaConfigModeBlock 264 ) 265 266 // SchemaDiffSuppressFunc is a function which can be used to determine 267 // whether a detected diff on a schema element is "valid" or not, and 268 // suppress it from the plan if necessary. 269 // 270 // Return true if the diff should be suppressed, false to retain it. 271 type SchemaDiffSuppressFunc func(k, old, new string, d *ResourceData) bool 272 273 // SchemaDefaultFunc is a function called to return a default value for 274 // a field. 275 type SchemaDefaultFunc func() (interface{}, error) 276 277 // EnvDefaultFunc is a helper function that returns the value of the 278 // given environment variable, if one exists, or the default value 279 // otherwise. 280 func EnvDefaultFunc(k string, dv interface{}) SchemaDefaultFunc { 281 return func() (interface{}, error) { 282 if v := os.Getenv(k); v != "" { 283 return v, nil 284 } 285 286 return dv, nil 287 } 288 } 289 290 // MultiEnvDefaultFunc is a helper function that returns the value of the first 291 // environment variable in the given list that returns a non-empty value. If 292 // none of the environment variables return a value, the default value is 293 // returned. 294 func MultiEnvDefaultFunc(ks []string, dv interface{}) SchemaDefaultFunc { 295 return func() (interface{}, error) { 296 for _, k := range ks { 297 if v := os.Getenv(k); v != "" { 298 return v, nil 299 } 300 } 301 return dv, nil 302 } 303 } 304 305 // SchemaSetFunc is a function that must return a unique ID for the given 306 // element. This unique ID is used to store the element in a hash. 307 type SchemaSetFunc func(interface{}) int 308 309 // SchemaStateFunc is a function used to convert some type to a string 310 // to be stored in the state. 311 type SchemaStateFunc func(interface{}) string 312 313 // SchemaValidateFunc is a function used to validate a single field in the 314 // schema. 315 type SchemaValidateFunc func(interface{}, string) ([]string, []error) 316 317 func (s *Schema) GoString() string { 318 return fmt.Sprintf("*%#v", *s) 319 } 320 321 // Returns a default value for this schema by either reading Default or 322 // evaluating DefaultFunc. If neither of these are defined, returns nil. 323 func (s *Schema) DefaultValue() (interface{}, error) { 324 if s.Default != nil { 325 return s.Default, nil 326 } 327 328 if s.DefaultFunc != nil { 329 defaultValue, err := s.DefaultFunc() 330 if err != nil { 331 return nil, fmt.Errorf("error loading default: %s", err) 332 } 333 return defaultValue, nil 334 } 335 336 return nil, nil 337 } 338 339 // Returns a zero value for the schema. 340 func (s *Schema) ZeroValue() interface{} { 341 // If it's a set then we'll do a bit of extra work to provide the 342 // right hashing function in our empty value. 343 if s.Type == TypeSet { 344 setFunc := s.Set 345 if setFunc == nil { 346 // Default set function uses the schema to hash the whole value 347 elem := s.Elem 348 switch t := elem.(type) { 349 case *Schema: 350 setFunc = HashSchema(t) 351 case *Resource: 352 setFunc = HashResource(t) 353 default: 354 panic("invalid set element type") 355 } 356 } 357 return &Set{F: setFunc} 358 } else { 359 return s.Type.Zero() 360 } 361 } 362 363 func (s *Schema) finalizeDiff(d *terraform.ResourceAttrDiff, customized bool) *terraform.ResourceAttrDiff { 364 if d == nil { 365 return d 366 } 367 368 if s.Type == TypeBool { 369 normalizeBoolString := func(s string) string { 370 switch s { 371 case "0": 372 return "false" 373 case "1": 374 return "true" 375 } 376 return s 377 } 378 d.Old = normalizeBoolString(d.Old) 379 d.New = normalizeBoolString(d.New) 380 } 381 382 if s.Computed && !d.NewRemoved && d.New == "" { 383 // Computed attribute without a new value set 384 d.NewComputed = true 385 } 386 387 if s.ForceNew { 388 // ForceNew, mark that this field is requiring new under the 389 // following conditions, explained below: 390 // 391 // * Old != New - There is a change in value. This field 392 // is therefore causing a new resource. 393 // 394 // * NewComputed - This field is being computed, hence a 395 // potential change in value, mark as causing a new resource. 396 d.RequiresNew = d.Old != d.New || d.NewComputed 397 } 398 399 if d.NewRemoved { 400 return d 401 } 402 403 if s.Computed { 404 // FIXME: This is where the customized bool from getChange finally 405 // comes into play. It allows the previously incorrect behavior 406 // of an empty string being used as "unset" when the value is 407 // computed. This should be removed once we can properly 408 // represent an unset/nil value from the configuration. 409 if !customized { 410 if d.Old != "" && d.New == "" { 411 // This is a computed value with an old value set already, 412 // just let it go. 413 return nil 414 } 415 } 416 417 if d.New == "" && !d.NewComputed { 418 // Computed attribute without a new value set 419 d.NewComputed = true 420 } 421 } 422 423 if s.Sensitive { 424 // Set the Sensitive flag so output is hidden in the UI 425 d.Sensitive = true 426 } 427 428 return d 429 } 430 431 // InternalMap is used to aid in the transition to the new schema types and 432 // protocol. The name is not meant to convey any usefulness, as this is not to 433 // be used directly by any providers. 434 type InternalMap = schemaMap 435 436 // schemaMap is a wrapper that adds nice functions on top of schemas. 437 type schemaMap map[string]*Schema 438 439 func (m schemaMap) panicOnError() bool { 440 if os.Getenv(PanicOnErr) != "" { 441 return true 442 } 443 return false 444 } 445 446 // Data returns a ResourceData for the given schema, state, and diff. 447 // 448 // The diff is optional. 449 func (m schemaMap) Data( 450 s *terraform.InstanceState, 451 d *terraform.InstanceDiff) (*ResourceData, error) { 452 return &ResourceData{ 453 schema: m, 454 state: s, 455 diff: d, 456 panicOnError: m.panicOnError(), 457 }, nil 458 } 459 460 // DeepCopy returns a copy of this schemaMap. The copy can be safely modified 461 // without affecting the original. 462 func (m *schemaMap) DeepCopy() schemaMap { 463 copy, err := copystructure.Config{Lock: true}.Copy(m) 464 if err != nil { 465 panic(err) 466 } 467 return *copy.(*schemaMap) 468 } 469 470 // Diff returns the diff for a resource given the schema map, 471 // state, and configuration. 472 func (m schemaMap) Diff( 473 s *terraform.InstanceState, 474 c *terraform.ResourceConfig, 475 customizeDiff CustomizeDiffFunc, 476 meta interface{}, 477 handleRequiresNew bool) (*terraform.InstanceDiff, error) { 478 result := new(terraform.InstanceDiff) 479 result.Attributes = make(map[string]*terraform.ResourceAttrDiff) 480 481 // Make sure to mark if the resource is tainted 482 if s != nil { 483 result.DestroyTainted = s.Tainted 484 } 485 486 d := &ResourceData{ 487 schema: m, 488 state: s, 489 config: c, 490 panicOnError: m.panicOnError(), 491 } 492 493 for k, schema := range m { 494 err := m.diff(k, schema, result, d, false) 495 if err != nil { 496 return nil, err 497 } 498 } 499 500 // Remove any nil diffs just to keep things clean 501 for k, v := range result.Attributes { 502 if v == nil { 503 delete(result.Attributes, k) 504 } 505 } 506 507 // If this is a non-destroy diff, call any custom diff logic that has been 508 // defined. 509 if !result.DestroyTainted && customizeDiff != nil { 510 mc := m.DeepCopy() 511 rd := newResourceDiff(mc, c, s, result) 512 if err := customizeDiff(rd, meta); err != nil { 513 return nil, err 514 } 515 for _, k := range rd.UpdatedKeys() { 516 err := m.diff(k, mc[k], result, rd, false) 517 if err != nil { 518 return nil, err 519 } 520 } 521 } 522 523 if handleRequiresNew { 524 // If the diff requires a new resource, then we recompute the diff 525 // so we have the complete new resource diff, and preserve the 526 // RequiresNew fields where necessary so the user knows exactly what 527 // caused that. 528 if result.RequiresNew() { 529 // Create the new diff 530 result2 := new(terraform.InstanceDiff) 531 result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) 532 533 // Preserve the DestroyTainted flag 534 result2.DestroyTainted = result.DestroyTainted 535 536 // Reset the data to not contain state. We have to call init() 537 // again in order to reset the FieldReaders. 538 d.state = nil 539 d.init() 540 541 // Perform the diff again 542 for k, schema := range m { 543 err := m.diff(k, schema, result2, d, false) 544 if err != nil { 545 return nil, err 546 } 547 } 548 549 // Re-run customization 550 if !result2.DestroyTainted && customizeDiff != nil { 551 mc := m.DeepCopy() 552 rd := newResourceDiff(mc, c, d.state, result2) 553 if err := customizeDiff(rd, meta); err != nil { 554 return nil, err 555 } 556 for _, k := range rd.UpdatedKeys() { 557 err := m.diff(k, mc[k], result2, rd, false) 558 if err != nil { 559 return nil, err 560 } 561 } 562 } 563 564 // Force all the fields to not force a new since we know what we 565 // want to force new. 566 for k, attr := range result2.Attributes { 567 if attr == nil { 568 continue 569 } 570 571 if attr.RequiresNew { 572 attr.RequiresNew = false 573 } 574 575 if s != nil { 576 attr.Old = s.Attributes[k] 577 } 578 } 579 580 // Now copy in all the requires new diffs... 581 for k, attr := range result.Attributes { 582 if attr == nil { 583 continue 584 } 585 586 newAttr, ok := result2.Attributes[k] 587 if !ok { 588 newAttr = attr 589 } 590 591 if attr.RequiresNew { 592 newAttr.RequiresNew = true 593 } 594 595 result2.Attributes[k] = newAttr 596 } 597 598 // And set the diff! 599 result = result2 600 } 601 602 } 603 604 // Go through and detect all of the ComputedWhens now that we've 605 // finished the diff. 606 // TODO 607 608 if result.Empty() { 609 // If we don't have any diff elements, just return nil 610 return nil, nil 611 } 612 613 return result, nil 614 } 615 616 // Input implements the terraform.ResourceProvider method by asking 617 // for input for required configuration keys that don't have a value. 618 func (m schemaMap) Input( 619 input terraform.UIInput, 620 c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { 621 keys := make([]string, 0, len(m)) 622 for k, _ := range m { 623 keys = append(keys, k) 624 } 625 sort.Strings(keys) 626 627 for _, k := range keys { 628 v := m[k] 629 630 // Skip things that don't require config, if that is even valid 631 // for a provider schema. 632 // Required XOR Optional must always be true to validate, so we only 633 // need to check one. 634 if v.Optional { 635 continue 636 } 637 638 // Deprecated fields should never prompt 639 if v.Deprecated != "" { 640 continue 641 } 642 643 // Skip things that have a value of some sort already 644 if _, ok := c.Raw[k]; ok { 645 continue 646 } 647 648 // Skip if it has a default value 649 defaultValue, err := v.DefaultValue() 650 if err != nil { 651 return nil, fmt.Errorf("%s: error loading default: %s", k, err) 652 } 653 if defaultValue != nil { 654 continue 655 } 656 657 var value interface{} 658 switch v.Type { 659 case TypeBool, TypeInt, TypeFloat, TypeSet, TypeList: 660 continue 661 case TypeString: 662 value, err = m.inputString(input, k, v) 663 default: 664 panic(fmt.Sprintf("Unknown type for input: %#v", v.Type)) 665 } 666 667 if err != nil { 668 return nil, fmt.Errorf( 669 "%s: %s", k, err) 670 } 671 672 c.Config[k] = value 673 } 674 675 return c, nil 676 } 677 678 // Validate validates the configuration against this schema mapping. 679 func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { 680 return m.validateObject("", m, c) 681 } 682 683 // InternalValidate validates the format of this schema. This should be called 684 // from a unit test (and not in user-path code) to verify that a schema 685 // is properly built. 686 func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { 687 return m.internalValidate(topSchemaMap, false) 688 } 689 690 func (m schemaMap) internalValidate(topSchemaMap schemaMap, attrsOnly bool) error { 691 if topSchemaMap == nil { 692 topSchemaMap = m 693 } 694 for k, v := range m { 695 if v.Type == TypeInvalid { 696 return fmt.Errorf("%s: Type must be specified", k) 697 } 698 699 if v.Optional && v.Required { 700 return fmt.Errorf("%s: Optional or Required must be set, not both", k) 701 } 702 703 if v.Required && v.Computed { 704 return fmt.Errorf("%s: Cannot be both Required and Computed", k) 705 } 706 707 if !v.Required && !v.Optional && !v.Computed { 708 return fmt.Errorf("%s: One of optional, required, or computed must be set", k) 709 } 710 711 computedOnly := v.Computed && !v.Optional 712 713 switch v.ConfigMode { 714 case SchemaConfigModeBlock: 715 if _, ok := v.Elem.(*Resource); !ok { 716 return fmt.Errorf("%s: ConfigMode of block is allowed only when Elem is *schema.Resource", k) 717 } 718 if attrsOnly { 719 return fmt.Errorf("%s: ConfigMode of block cannot be used in child of schema with ConfigMode of attribute", k) 720 } 721 if computedOnly { 722 return fmt.Errorf("%s: ConfigMode of block cannot be used for computed schema", k) 723 } 724 case SchemaConfigModeAttr: 725 // anything goes 726 case SchemaConfigModeAuto: 727 // Since "Auto" for Elem: *Resource would create a nested block, 728 // and that's impossible inside an attribute, we require it to be 729 // explicitly overridden as mode "Attr" for clarity. 730 if _, ok := v.Elem.(*Resource); ok { 731 if attrsOnly { 732 return fmt.Errorf("%s: in *schema.Resource with ConfigMode of attribute, so must also have ConfigMode of attribute", k) 733 } 734 } 735 default: 736 return fmt.Errorf("%s: invalid ConfigMode value", k) 737 } 738 739 if v.Computed && v.Default != nil { 740 return fmt.Errorf("%s: Default must be nil if computed", k) 741 } 742 743 if v.Required && v.Default != nil { 744 return fmt.Errorf("%s: Default cannot be set with Required", k) 745 } 746 747 if len(v.ComputedWhen) > 0 && !v.Computed { 748 return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k) 749 } 750 751 if len(v.ConflictsWith) > 0 && v.Required { 752 return fmt.Errorf("%s: ConflictsWith cannot be set with Required", k) 753 } 754 755 if len(v.ConflictsWith) > 0 { 756 for _, key := range v.ConflictsWith { 757 parts := strings.Split(key, ".") 758 sm := topSchemaMap 759 var target *Schema 760 for _, part := range parts { 761 // Skip index fields 762 if _, err := strconv.Atoi(part); err == nil { 763 continue 764 } 765 766 var ok bool 767 if target, ok = sm[part]; !ok { 768 return fmt.Errorf("%s: ConflictsWith references unknown attribute (%s) at part (%s)", k, key, part) 769 } 770 771 if subResource, ok := target.Elem.(*Resource); ok { 772 sm = schemaMap(subResource.Schema) 773 } 774 } 775 if target == nil { 776 return fmt.Errorf("%s: ConflictsWith cannot find target attribute (%s), sm: %#v", k, key, sm) 777 } 778 if target.Required { 779 return fmt.Errorf("%s: ConflictsWith cannot contain Required attribute (%s)", k, key) 780 } 781 782 if len(target.ComputedWhen) > 0 { 783 return fmt.Errorf("%s: ConflictsWith cannot contain Computed(When) attribute (%s)", k, key) 784 } 785 } 786 } 787 788 if v.Type == TypeList || v.Type == TypeSet { 789 if v.Elem == nil { 790 return fmt.Errorf("%s: Elem must be set for lists", k) 791 } 792 793 if v.Default != nil { 794 return fmt.Errorf("%s: Default is not valid for lists or sets", k) 795 } 796 797 if v.Type != TypeSet && v.Set != nil { 798 return fmt.Errorf("%s: Set can only be set for TypeSet", k) 799 } 800 801 switch t := v.Elem.(type) { 802 case *Resource: 803 attrsOnly := attrsOnly || v.ConfigMode == SchemaConfigModeAttr 804 805 if err := schemaMap(t.Schema).internalValidate(topSchemaMap, attrsOnly); err != nil { 806 return err 807 } 808 case *Schema: 809 bad := t.Computed || t.Optional || t.Required 810 if bad { 811 return fmt.Errorf( 812 "%s: Elem must have only Type set", k) 813 } 814 } 815 } else { 816 if v.MaxItems > 0 || v.MinItems > 0 { 817 return fmt.Errorf("%s: MaxItems and MinItems are only supported on lists or sets", k) 818 } 819 } 820 821 // Computed-only field 822 if v.Computed && !v.Optional { 823 if v.ValidateFunc != nil { 824 return fmt.Errorf("%s: ValidateFunc is for validating user input, "+ 825 "there's nothing to validate on computed-only field", k) 826 } 827 if v.DiffSuppressFunc != nil { 828 return fmt.Errorf("%s: DiffSuppressFunc is for suppressing differences"+ 829 " between config and state representation. "+ 830 "There is no config for computed-only field, nothing to compare.", k) 831 } 832 } 833 834 if v.ValidateFunc != nil { 835 switch v.Type { 836 case TypeList, TypeSet: 837 return fmt.Errorf("%s: ValidateFunc is not yet supported on lists or sets.", k) 838 } 839 } 840 841 if v.Deprecated == "" && v.Removed == "" { 842 if !isValidFieldName(k) { 843 return fmt.Errorf("%s: Field name may only contain lowercase alphanumeric characters & underscores.", k) 844 } 845 } 846 } 847 848 return nil 849 } 850 851 func isValidFieldName(name string) bool { 852 re := regexp.MustCompile("^[a-z0-9_]+$") 853 return re.MatchString(name) 854 } 855 856 // resourceDiffer is an interface that is used by the private diff functions. 857 // This helps facilitate diff logic for both ResourceData and ResoureDiff with 858 // minimal divergence in code. 859 type resourceDiffer interface { 860 diffChange(string) (interface{}, interface{}, bool, bool, bool) 861 Get(string) interface{} 862 GetChange(string) (interface{}, interface{}) 863 GetOk(string) (interface{}, bool) 864 HasChange(string) bool 865 Id() string 866 } 867 868 func (m schemaMap) diff( 869 k string, 870 schema *Schema, 871 diff *terraform.InstanceDiff, 872 d resourceDiffer, 873 all bool) error { 874 875 unsupressedDiff := new(terraform.InstanceDiff) 876 unsupressedDiff.Attributes = make(map[string]*terraform.ResourceAttrDiff) 877 878 var err error 879 switch schema.Type { 880 case TypeBool, TypeInt, TypeFloat, TypeString: 881 err = m.diffString(k, schema, unsupressedDiff, d, all) 882 case TypeList: 883 err = m.diffList(k, schema, unsupressedDiff, d, all) 884 case TypeMap: 885 err = m.diffMap(k, schema, unsupressedDiff, d, all) 886 case TypeSet: 887 err = m.diffSet(k, schema, unsupressedDiff, d, all) 888 default: 889 err = fmt.Errorf("%s: unknown type %#v", k, schema.Type) 890 } 891 892 for attrK, attrV := range unsupressedDiff.Attributes { 893 switch rd := d.(type) { 894 case *ResourceData: 895 if schema.DiffSuppressFunc != nil && attrV != nil && 896 schema.DiffSuppressFunc(attrK, attrV.Old, attrV.New, rd) { 897 // If this attr diff is suppressed, we may still need it in the 898 // overall diff if it's contained within a set. Rather than 899 // dropping the diff, make it a NOOP. 900 if !all { 901 continue 902 } 903 904 attrV = &terraform.ResourceAttrDiff{ 905 Old: attrV.Old, 906 New: attrV.Old, 907 } 908 } 909 } 910 diff.Attributes[attrK] = attrV 911 } 912 913 return err 914 } 915 916 func (m schemaMap) diffList( 917 k string, 918 schema *Schema, 919 diff *terraform.InstanceDiff, 920 d resourceDiffer, 921 all bool) error { 922 o, n, _, computedList, customized := d.diffChange(k) 923 if computedList { 924 n = nil 925 } 926 nSet := n != nil 927 928 // If we have an old value and no new value is set or will be 929 // computed once all variables can be interpolated and we're 930 // computed, then nothing has changed. 931 if o != nil && n == nil && !computedList && schema.Computed { 932 return nil 933 } 934 935 if o == nil { 936 o = []interface{}{} 937 } 938 if n == nil { 939 n = []interface{}{} 940 } 941 if s, ok := o.(*Set); ok { 942 o = s.List() 943 } 944 if s, ok := n.(*Set); ok { 945 n = s.List() 946 } 947 os := o.([]interface{}) 948 vs := n.([]interface{}) 949 950 // If the new value was set, and the two are equal, then we're done. 951 // We have to do this check here because sets might be NOT 952 // reflect.DeepEqual so we need to wait until we get the []interface{} 953 if !all && nSet && reflect.DeepEqual(os, vs) { 954 return nil 955 } 956 957 // Get the counts 958 oldLen := len(os) 959 newLen := len(vs) 960 oldStr := strconv.FormatInt(int64(oldLen), 10) 961 962 // If the whole list is computed, then say that the # is computed 963 if computedList { 964 diff.Attributes[k+".#"] = &terraform.ResourceAttrDiff{ 965 Old: oldStr, 966 NewComputed: true, 967 RequiresNew: schema.ForceNew, 968 } 969 return nil 970 } 971 972 // If the counts are not the same, then record that diff 973 changed := oldLen != newLen 974 computed := oldLen == 0 && newLen == 0 && schema.Computed 975 if changed || computed || all { 976 countSchema := &Schema{ 977 Type: TypeInt, 978 Computed: schema.Computed, 979 ForceNew: schema.ForceNew, 980 } 981 982 newStr := "" 983 if !computed { 984 newStr = strconv.FormatInt(int64(newLen), 10) 985 } else { 986 oldStr = "" 987 } 988 989 diff.Attributes[k+".#"] = countSchema.finalizeDiff( 990 &terraform.ResourceAttrDiff{ 991 Old: oldStr, 992 New: newStr, 993 }, 994 customized, 995 ) 996 } 997 998 // Figure out the maximum 999 maxLen := oldLen 1000 if newLen > maxLen { 1001 maxLen = newLen 1002 } 1003 1004 switch t := schema.Elem.(type) { 1005 case *Resource: 1006 // This is a complex resource 1007 for i := 0; i < maxLen; i++ { 1008 for k2, schema := range t.Schema { 1009 subK := fmt.Sprintf("%s.%d.%s", k, i, k2) 1010 err := m.diff(subK, schema, diff, d, all) 1011 if err != nil { 1012 return err 1013 } 1014 } 1015 } 1016 case *Schema: 1017 // Copy the schema so that we can set Computed/ForceNew from 1018 // the parent schema (the TypeList). 1019 t2 := *t 1020 t2.ForceNew = schema.ForceNew 1021 1022 // This is just a primitive element, so go through each and 1023 // just diff each. 1024 for i := 0; i < maxLen; i++ { 1025 subK := fmt.Sprintf("%s.%d", k, i) 1026 err := m.diff(subK, &t2, diff, d, all) 1027 if err != nil { 1028 return err 1029 } 1030 } 1031 default: 1032 return fmt.Errorf("%s: unknown element type (internal)", k) 1033 } 1034 1035 return nil 1036 } 1037 1038 func (m schemaMap) diffMap( 1039 k string, 1040 schema *Schema, 1041 diff *terraform.InstanceDiff, 1042 d resourceDiffer, 1043 all bool) error { 1044 prefix := k + "." 1045 1046 // First get all the values from the state 1047 var stateMap, configMap map[string]string 1048 o, n, _, nComputed, customized := d.diffChange(k) 1049 if err := mapstructure.WeakDecode(o, &stateMap); err != nil { 1050 return fmt.Errorf("%s: %s", k, err) 1051 } 1052 if err := mapstructure.WeakDecode(n, &configMap); err != nil { 1053 return fmt.Errorf("%s: %s", k, err) 1054 } 1055 1056 // Keep track of whether the state _exists_ at all prior to clearing it 1057 stateExists := o != nil 1058 1059 // Delete any count values, since we don't use those 1060 delete(configMap, "%") 1061 delete(stateMap, "%") 1062 1063 // Check if the number of elements has changed. 1064 oldLen, newLen := len(stateMap), len(configMap) 1065 changed := oldLen != newLen 1066 if oldLen != 0 && newLen == 0 && schema.Computed { 1067 changed = false 1068 } 1069 1070 // It is computed if we have no old value, no new value, the schema 1071 // says it is computed, and it didn't exist in the state before. The 1072 // last point means: if it existed in the state, even empty, then it 1073 // has already been computed. 1074 computed := oldLen == 0 && newLen == 0 && schema.Computed && !stateExists 1075 1076 // If the count has changed or we're computed, then add a diff for the 1077 // count. "nComputed" means that the new value _contains_ a value that 1078 // is computed. We don't do granular diffs for this yet, so we mark the 1079 // whole map as computed. 1080 if changed || computed || nComputed { 1081 countSchema := &Schema{ 1082 Type: TypeInt, 1083 Computed: schema.Computed || nComputed, 1084 ForceNew: schema.ForceNew, 1085 } 1086 1087 oldStr := strconv.FormatInt(int64(oldLen), 10) 1088 newStr := "" 1089 if !computed && !nComputed { 1090 newStr = strconv.FormatInt(int64(newLen), 10) 1091 } else { 1092 oldStr = "" 1093 } 1094 1095 diff.Attributes[k+".%"] = countSchema.finalizeDiff( 1096 &terraform.ResourceAttrDiff{ 1097 Old: oldStr, 1098 New: newStr, 1099 }, 1100 customized, 1101 ) 1102 } 1103 1104 // If the new map is nil and we're computed, then ignore it. 1105 if n == nil && schema.Computed { 1106 return nil 1107 } 1108 1109 // Now we compare, preferring values from the config map 1110 for k, v := range configMap { 1111 old, ok := stateMap[k] 1112 delete(stateMap, k) 1113 1114 if old == v && ok && !all { 1115 continue 1116 } 1117 1118 diff.Attributes[prefix+k] = schema.finalizeDiff( 1119 &terraform.ResourceAttrDiff{ 1120 Old: old, 1121 New: v, 1122 }, 1123 customized, 1124 ) 1125 } 1126 for k, v := range stateMap { 1127 diff.Attributes[prefix+k] = schema.finalizeDiff( 1128 &terraform.ResourceAttrDiff{ 1129 Old: v, 1130 NewRemoved: true, 1131 }, 1132 customized, 1133 ) 1134 } 1135 1136 return nil 1137 } 1138 1139 func (m schemaMap) diffSet( 1140 k string, 1141 schema *Schema, 1142 diff *terraform.InstanceDiff, 1143 d resourceDiffer, 1144 all bool) error { 1145 1146 o, n, _, computedSet, customized := d.diffChange(k) 1147 if computedSet { 1148 n = nil 1149 } 1150 nSet := n != nil 1151 1152 // If we have an old value and no new value is set or will be 1153 // computed once all variables can be interpolated and we're 1154 // computed, then nothing has changed. 1155 if o != nil && n == nil && !computedSet && schema.Computed { 1156 return nil 1157 } 1158 1159 if o == nil { 1160 o = schema.ZeroValue().(*Set) 1161 } 1162 if n == nil { 1163 n = schema.ZeroValue().(*Set) 1164 } 1165 os := o.(*Set) 1166 ns := n.(*Set) 1167 1168 // If the new value was set, compare the listCode's to determine if 1169 // the two are equal. Comparing listCode's instead of the actual values 1170 // is needed because there could be computed values in the set which 1171 // would result in false positives while comparing. 1172 if !all && nSet && reflect.DeepEqual(os.listCode(), ns.listCode()) { 1173 return nil 1174 } 1175 1176 // Get the counts 1177 oldLen := os.Len() 1178 newLen := ns.Len() 1179 oldStr := strconv.Itoa(oldLen) 1180 newStr := strconv.Itoa(newLen) 1181 1182 // Build a schema for our count 1183 countSchema := &Schema{ 1184 Type: TypeInt, 1185 Computed: schema.Computed, 1186 ForceNew: schema.ForceNew, 1187 } 1188 1189 // If the set computed then say that the # is computed 1190 if computedSet || schema.Computed && !nSet { 1191 // If # already exists, equals 0 and no new set is supplied, there 1192 // is nothing to record in the diff 1193 count, ok := d.GetOk(k + ".#") 1194 if ok && count.(int) == 0 && !nSet && !computedSet { 1195 return nil 1196 } 1197 1198 // Set the count but make sure that if # does not exist, we don't 1199 // use the zeroed value 1200 countStr := strconv.Itoa(count.(int)) 1201 if !ok { 1202 countStr = "" 1203 } 1204 1205 diff.Attributes[k+".#"] = countSchema.finalizeDiff( 1206 &terraform.ResourceAttrDiff{ 1207 Old: countStr, 1208 NewComputed: true, 1209 }, 1210 customized, 1211 ) 1212 return nil 1213 } 1214 1215 // If the counts are not the same, then record that diff 1216 changed := oldLen != newLen 1217 if changed || all { 1218 diff.Attributes[k+".#"] = countSchema.finalizeDiff( 1219 &terraform.ResourceAttrDiff{ 1220 Old: oldStr, 1221 New: newStr, 1222 }, 1223 customized, 1224 ) 1225 } 1226 1227 // Build the list of codes that will make up our set. This is the 1228 // removed codes as well as all the codes in the new codes. 1229 codes := make([][]string, 2) 1230 codes[0] = os.Difference(ns).listCode() 1231 codes[1] = ns.listCode() 1232 for _, list := range codes { 1233 for _, code := range list { 1234 switch t := schema.Elem.(type) { 1235 case *Resource: 1236 // This is a complex resource 1237 for k2, schema := range t.Schema { 1238 subK := fmt.Sprintf("%s.%s.%s", k, code, k2) 1239 err := m.diff(subK, schema, diff, d, true) 1240 if err != nil { 1241 return err 1242 } 1243 } 1244 case *Schema: 1245 // Copy the schema so that we can set Computed/ForceNew from 1246 // the parent schema (the TypeSet). 1247 t2 := *t 1248 t2.ForceNew = schema.ForceNew 1249 1250 // This is just a primitive element, so go through each and 1251 // just diff each. 1252 subK := fmt.Sprintf("%s.%s", k, code) 1253 err := m.diff(subK, &t2, diff, d, true) 1254 if err != nil { 1255 return err 1256 } 1257 default: 1258 return fmt.Errorf("%s: unknown element type (internal)", k) 1259 } 1260 } 1261 } 1262 1263 return nil 1264 } 1265 1266 func (m schemaMap) diffString( 1267 k string, 1268 schema *Schema, 1269 diff *terraform.InstanceDiff, 1270 d resourceDiffer, 1271 all bool) error { 1272 var originalN interface{} 1273 var os, ns string 1274 o, n, _, computed, customized := d.diffChange(k) 1275 if schema.StateFunc != nil && n != nil { 1276 originalN = n 1277 n = schema.StateFunc(n) 1278 } 1279 nraw := n 1280 if nraw == nil && o != nil { 1281 nraw = schema.Type.Zero() 1282 } 1283 if err := mapstructure.WeakDecode(o, &os); err != nil { 1284 return fmt.Errorf("%s: %s", k, err) 1285 } 1286 if err := mapstructure.WeakDecode(nraw, &ns); err != nil { 1287 return fmt.Errorf("%s: %s", k, err) 1288 } 1289 1290 if os == ns && !all && !computed { 1291 // They're the same value. If there old value is not blank or we 1292 // have an ID, then return right away since we're already set up. 1293 if os != "" || d.Id() != "" { 1294 return nil 1295 } 1296 1297 // Otherwise, only continue if we're computed 1298 if !schema.Computed { 1299 return nil 1300 } 1301 } 1302 1303 removed := false 1304 if o != nil && n == nil && !computed { 1305 removed = true 1306 } 1307 if removed && schema.Computed { 1308 return nil 1309 } 1310 1311 diff.Attributes[k] = schema.finalizeDiff( 1312 &terraform.ResourceAttrDiff{ 1313 Old: os, 1314 New: ns, 1315 NewExtra: originalN, 1316 NewRemoved: removed, 1317 NewComputed: computed, 1318 }, 1319 customized, 1320 ) 1321 1322 return nil 1323 } 1324 1325 func (m schemaMap) inputString( 1326 input terraform.UIInput, 1327 k string, 1328 schema *Schema) (interface{}, error) { 1329 result, err := input.Input(context.Background(), &terraform.InputOpts{ 1330 Id: k, 1331 Query: k, 1332 Description: schema.Description, 1333 Default: schema.InputDefault, 1334 }) 1335 1336 return result, err 1337 } 1338 1339 func (m schemaMap) validate( 1340 k string, 1341 schema *Schema, 1342 c *terraform.ResourceConfig) ([]string, []error) { 1343 raw, ok := c.Get(k) 1344 if !ok && schema.DefaultFunc != nil { 1345 // We have a dynamic default. Check if we have a value. 1346 var err error 1347 raw, err = schema.DefaultFunc() 1348 if err != nil { 1349 return nil, []error{fmt.Errorf( 1350 "%q, error loading default: %s", k, err)} 1351 } 1352 1353 // We're okay as long as we had a value set 1354 ok = raw != nil 1355 } 1356 if !ok { 1357 if schema.Required { 1358 return nil, []error{fmt.Errorf( 1359 "%q: required field is not set", k)} 1360 } 1361 1362 return nil, nil 1363 } 1364 1365 if !schema.Required && !schema.Optional { 1366 // This is a computed-only field 1367 return nil, []error{fmt.Errorf( 1368 "%q: this field cannot be set", k)} 1369 } 1370 1371 // If the value is unknown then we can't validate it yet. 1372 // In particular, this avoids spurious type errors where downstream 1373 // validation code sees UnknownVariableValue as being just a string. 1374 // The SDK has to allow the unknown value through initially, so that 1375 // Required fields set via an interpolated value are accepted. 1376 if !isWhollyKnown(raw) { 1377 if schema.Deprecated != "" { 1378 return []string{fmt.Sprintf("%q: [DEPRECATED] %s", k, schema.Deprecated)}, nil 1379 } 1380 return nil, nil 1381 } 1382 1383 err := m.validateConflictingAttributes(k, schema, c) 1384 if err != nil { 1385 return nil, []error{err} 1386 } 1387 1388 return m.validateType(k, raw, schema, c) 1389 } 1390 1391 // isWhollyKnown returns false if the argument contains an UnknownVariableValue 1392 func isWhollyKnown(raw interface{}) bool { 1393 switch raw := raw.(type) { 1394 case string: 1395 if raw == hcl2shim.UnknownVariableValue { 1396 return false 1397 } 1398 case []interface{}: 1399 for _, v := range raw { 1400 if !isWhollyKnown(v) { 1401 return false 1402 } 1403 } 1404 case map[string]interface{}: 1405 for _, v := range raw { 1406 if !isWhollyKnown(v) { 1407 return false 1408 } 1409 } 1410 } 1411 return true 1412 } 1413 func (m schemaMap) validateConflictingAttributes( 1414 k string, 1415 schema *Schema, 1416 c *terraform.ResourceConfig) error { 1417 1418 if len(schema.ConflictsWith) == 0 { 1419 return nil 1420 } 1421 1422 for _, conflictingKey := range schema.ConflictsWith { 1423 if raw, ok := c.Get(conflictingKey); ok { 1424 if raw == hcl2shim.UnknownVariableValue { 1425 // An unknown value might become unset (null) once known, so 1426 // we must defer validation until it's known. 1427 continue 1428 } 1429 return fmt.Errorf( 1430 "%q: conflicts with %s", k, conflictingKey) 1431 } 1432 } 1433 1434 return nil 1435 } 1436 1437 func (m schemaMap) validateList( 1438 k string, 1439 raw interface{}, 1440 schema *Schema, 1441 c *terraform.ResourceConfig) ([]string, []error) { 1442 // first check if the list is wholly unknown 1443 if s, ok := raw.(string); ok { 1444 if s == hcl2shim.UnknownVariableValue { 1445 return nil, nil 1446 } 1447 } 1448 1449 // schemaMap can't validate nil 1450 if raw == nil { 1451 return nil, nil 1452 } 1453 1454 // We use reflection to verify the slice because you can't 1455 // case to []interface{} unless the slice is exactly that type. 1456 rawV := reflect.ValueOf(raw) 1457 1458 // If we support promotion and the raw value isn't a slice, wrap 1459 // it in []interface{} and check again. 1460 if schema.PromoteSingle && rawV.Kind() != reflect.Slice { 1461 raw = []interface{}{raw} 1462 rawV = reflect.ValueOf(raw) 1463 } 1464 1465 if rawV.Kind() != reflect.Slice { 1466 return nil, []error{fmt.Errorf( 1467 "%s: should be a list", k)} 1468 } 1469 1470 // We can't validate list length if this came from a dynamic block. 1471 // Since there's no way to determine if something was from a dynamic block 1472 // at this point, we're going to skip validation in the new protocol if 1473 // there are any unknowns. Validate will eventually be called again once 1474 // all values are known. 1475 if isProto5() && !isWhollyKnown(raw) { 1476 return nil, nil 1477 } 1478 1479 // Validate length 1480 if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems { 1481 return nil, []error{fmt.Errorf( 1482 "%s: attribute supports %d item maximum, config has %d declared", k, schema.MaxItems, rawV.Len())} 1483 } 1484 1485 if schema.MinItems > 0 && rawV.Len() < schema.MinItems { 1486 return nil, []error{fmt.Errorf( 1487 "%s: attribute supports %d item as a minimum, config has %d declared", k, schema.MinItems, rawV.Len())} 1488 } 1489 1490 // Now build the []interface{} 1491 raws := make([]interface{}, rawV.Len()) 1492 for i, _ := range raws { 1493 raws[i] = rawV.Index(i).Interface() 1494 } 1495 1496 var ws []string 1497 var es []error 1498 for i, raw := range raws { 1499 key := fmt.Sprintf("%s.%d", k, i) 1500 1501 // Reify the key value from the ResourceConfig. 1502 // If the list was computed we have all raw values, but some of these 1503 // may be known in the config, and aren't individually marked as Computed. 1504 if r, ok := c.Get(key); ok { 1505 raw = r 1506 } 1507 1508 var ws2 []string 1509 var es2 []error 1510 switch t := schema.Elem.(type) { 1511 case *Resource: 1512 // This is a sub-resource 1513 ws2, es2 = m.validateObject(key, t.Schema, c) 1514 case *Schema: 1515 ws2, es2 = m.validateType(key, raw, t, c) 1516 } 1517 1518 if len(ws2) > 0 { 1519 ws = append(ws, ws2...) 1520 } 1521 if len(es2) > 0 { 1522 es = append(es, es2...) 1523 } 1524 } 1525 1526 return ws, es 1527 } 1528 1529 func (m schemaMap) validateMap( 1530 k string, 1531 raw interface{}, 1532 schema *Schema, 1533 c *terraform.ResourceConfig) ([]string, []error) { 1534 // first check if the list is wholly unknown 1535 if s, ok := raw.(string); ok { 1536 if s == hcl2shim.UnknownVariableValue { 1537 return nil, nil 1538 } 1539 } 1540 1541 // schemaMap can't validate nil 1542 if raw == nil { 1543 return nil, nil 1544 } 1545 // We use reflection to verify the slice because you can't 1546 // case to []interface{} unless the slice is exactly that type. 1547 rawV := reflect.ValueOf(raw) 1548 switch rawV.Kind() { 1549 case reflect.String: 1550 // If raw and reified are equal, this is a string and should 1551 // be rejected. 1552 reified, reifiedOk := c.Get(k) 1553 if reifiedOk && raw == reified && !c.IsComputed(k) { 1554 return nil, []error{fmt.Errorf("%s: should be a map", k)} 1555 } 1556 // Otherwise it's likely raw is an interpolation. 1557 return nil, nil 1558 case reflect.Map: 1559 case reflect.Slice: 1560 default: 1561 return nil, []error{fmt.Errorf("%s: should be a map", k)} 1562 } 1563 1564 // If it is not a slice, validate directly 1565 if rawV.Kind() != reflect.Slice { 1566 mapIface := rawV.Interface() 1567 if _, errs := validateMapValues(k, mapIface.(map[string]interface{}), schema); len(errs) > 0 { 1568 return nil, errs 1569 } 1570 if schema.ValidateFunc != nil { 1571 return schema.ValidateFunc(mapIface, k) 1572 } 1573 return nil, nil 1574 } 1575 1576 // It is a slice, verify that all the elements are maps 1577 raws := make([]interface{}, rawV.Len()) 1578 for i, _ := range raws { 1579 raws[i] = rawV.Index(i).Interface() 1580 } 1581 1582 for _, raw := range raws { 1583 v := reflect.ValueOf(raw) 1584 if v.Kind() != reflect.Map { 1585 return nil, []error{fmt.Errorf( 1586 "%s: should be a map", k)} 1587 } 1588 mapIface := v.Interface() 1589 if _, errs := validateMapValues(k, mapIface.(map[string]interface{}), schema); len(errs) > 0 { 1590 return nil, errs 1591 } 1592 } 1593 1594 if schema.ValidateFunc != nil { 1595 validatableMap := make(map[string]interface{}) 1596 for _, raw := range raws { 1597 for k, v := range raw.(map[string]interface{}) { 1598 validatableMap[k] = v 1599 } 1600 } 1601 1602 return schema.ValidateFunc(validatableMap, k) 1603 } 1604 1605 return nil, nil 1606 } 1607 1608 func validateMapValues(k string, m map[string]interface{}, schema *Schema) ([]string, []error) { 1609 for key, raw := range m { 1610 valueType, err := getValueType(k, schema) 1611 if err != nil { 1612 return nil, []error{err} 1613 } 1614 1615 switch valueType { 1616 case TypeBool: 1617 var n bool 1618 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1619 return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} 1620 } 1621 case TypeInt: 1622 var n int 1623 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1624 return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} 1625 } 1626 case TypeFloat: 1627 var n float64 1628 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1629 return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} 1630 } 1631 case TypeString: 1632 var n string 1633 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1634 return nil, []error{fmt.Errorf("%s (%s): %s", k, key, err)} 1635 } 1636 default: 1637 panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type)) 1638 } 1639 } 1640 return nil, nil 1641 } 1642 1643 func getValueType(k string, schema *Schema) (ValueType, error) { 1644 if schema.Elem == nil { 1645 return TypeString, nil 1646 } 1647 if vt, ok := schema.Elem.(ValueType); ok { 1648 return vt, nil 1649 } 1650 1651 // If a Schema is provided to a Map, we use the Type of that schema 1652 // as the type for each element in the Map. 1653 if s, ok := schema.Elem.(*Schema); ok { 1654 return s.Type, nil 1655 } 1656 1657 if _, ok := schema.Elem.(*Resource); ok { 1658 // TODO: We don't actually support this (yet) 1659 // but silently pass the validation, until we decide 1660 // how to handle nested structures in maps 1661 return TypeString, nil 1662 } 1663 return 0, fmt.Errorf("%s: unexpected map value type: %#v", k, schema.Elem) 1664 } 1665 1666 func (m schemaMap) validateObject( 1667 k string, 1668 schema map[string]*Schema, 1669 c *terraform.ResourceConfig) ([]string, []error) { 1670 raw, _ := c.Get(k) 1671 1672 // schemaMap can't validate nil 1673 if raw == nil { 1674 return nil, nil 1675 } 1676 1677 if _, ok := raw.(map[string]interface{}); !ok && !c.IsComputed(k) { 1678 return nil, []error{fmt.Errorf( 1679 "%s: expected object, got %s", 1680 k, reflect.ValueOf(raw).Kind())} 1681 } 1682 1683 var ws []string 1684 var es []error 1685 for subK, s := range schema { 1686 key := subK 1687 if k != "" { 1688 key = fmt.Sprintf("%s.%s", k, subK) 1689 } 1690 1691 ws2, es2 := m.validate(key, s, c) 1692 if len(ws2) > 0 { 1693 ws = append(ws, ws2...) 1694 } 1695 if len(es2) > 0 { 1696 es = append(es, es2...) 1697 } 1698 } 1699 1700 // Detect any extra/unknown keys and report those as errors. 1701 if m, ok := raw.(map[string]interface{}); ok { 1702 for subk, _ := range m { 1703 if _, ok := schema[subk]; !ok { 1704 if subk == TimeoutsConfigKey { 1705 continue 1706 } 1707 es = append(es, fmt.Errorf( 1708 "%s: invalid or unknown key: %s", k, subk)) 1709 } 1710 } 1711 } 1712 1713 return ws, es 1714 } 1715 1716 func (m schemaMap) validatePrimitive( 1717 k string, 1718 raw interface{}, 1719 schema *Schema, 1720 c *terraform.ResourceConfig) ([]string, []error) { 1721 1722 // a nil value shouldn't happen in the old protocol, and in the new 1723 // protocol the types have already been validated. Either way, we can't 1724 // reflect on nil, so don't panic. 1725 if raw == nil { 1726 return nil, nil 1727 } 1728 1729 // Catch if the user gave a complex type where a primitive was 1730 // expected, so we can return a friendly error message that 1731 // doesn't contain Go type system terminology. 1732 switch reflect.ValueOf(raw).Type().Kind() { 1733 case reflect.Slice: 1734 return nil, []error{ 1735 fmt.Errorf("%s must be a single value, not a list", k), 1736 } 1737 case reflect.Map: 1738 return nil, []error{ 1739 fmt.Errorf("%s must be a single value, not a map", k), 1740 } 1741 default: // ok 1742 } 1743 1744 if c.IsComputed(k) { 1745 // If the key is being computed, then it is not an error as 1746 // long as it's not a slice or map. 1747 return nil, nil 1748 } 1749 1750 var decoded interface{} 1751 switch schema.Type { 1752 case TypeBool: 1753 // Verify that we can parse this as the correct type 1754 var n bool 1755 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1756 return nil, []error{fmt.Errorf("%s: %s", k, err)} 1757 } 1758 decoded = n 1759 case TypeInt: 1760 switch { 1761 case isProto5(): 1762 // We need to verify the type precisely, because WeakDecode will 1763 // decode a float as an integer. 1764 1765 // the config shims only use int for integral number values 1766 if v, ok := raw.(int); ok { 1767 decoded = v 1768 } else { 1769 return nil, []error{fmt.Errorf("%s: must be a whole number, got %v", k, raw)} 1770 } 1771 default: 1772 // Verify that we can parse this as an int 1773 var n int 1774 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1775 return nil, []error{fmt.Errorf("%s: %s", k, err)} 1776 } 1777 decoded = n 1778 } 1779 case TypeFloat: 1780 // Verify that we can parse this as an int 1781 var n float64 1782 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1783 return nil, []error{fmt.Errorf("%s: %s", k, err)} 1784 } 1785 decoded = n 1786 case TypeString: 1787 // Verify that we can parse this as a string 1788 var n string 1789 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1790 return nil, []error{fmt.Errorf("%s: %s", k, err)} 1791 } 1792 decoded = n 1793 default: 1794 panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type)) 1795 } 1796 1797 if schema.ValidateFunc != nil { 1798 return schema.ValidateFunc(decoded, k) 1799 } 1800 1801 return nil, nil 1802 } 1803 1804 func (m schemaMap) validateType( 1805 k string, 1806 raw interface{}, 1807 schema *Schema, 1808 c *terraform.ResourceConfig) ([]string, []error) { 1809 var ws []string 1810 var es []error 1811 switch schema.Type { 1812 case TypeSet, TypeList: 1813 ws, es = m.validateList(k, raw, schema, c) 1814 case TypeMap: 1815 ws, es = m.validateMap(k, raw, schema, c) 1816 default: 1817 ws, es = m.validatePrimitive(k, raw, schema, c) 1818 } 1819 1820 if schema.Deprecated != "" { 1821 ws = append(ws, fmt.Sprintf( 1822 "%q: [DEPRECATED] %s", k, schema.Deprecated)) 1823 } 1824 1825 if schema.Removed != "" { 1826 es = append(es, fmt.Errorf( 1827 "%q: [REMOVED] %s", k, schema.Removed)) 1828 } 1829 1830 return ws, es 1831 } 1832 1833 // Zero returns the zero value for a type. 1834 func (t ValueType) Zero() interface{} { 1835 switch t { 1836 case TypeInvalid: 1837 return nil 1838 case TypeBool: 1839 return false 1840 case TypeInt: 1841 return 0 1842 case TypeFloat: 1843 return 0.0 1844 case TypeString: 1845 return "" 1846 case TypeList: 1847 return []interface{}{} 1848 case TypeMap: 1849 return map[string]interface{}{} 1850 case TypeSet: 1851 return new(Set) 1852 case typeObject: 1853 return map[string]interface{}{} 1854 default: 1855 panic(fmt.Sprintf("unknown type %s", t)) 1856 } 1857 }