github.com/richardbowden/terraform@v0.6.12-0.20160901200758-30ea22c25211/helper/schema/schema.go (about) 1 // schema is a high-level framework for easily writing new providers 2 // for Terraform. Usage of schema is recommended over attempting to write 3 // to the low-level plugin interfaces manually. 4 // 5 // schema breaks down provider creation into simple CRUD operations for 6 // resources. The logic of diffing, destroying before creating, updating 7 // or creating, etc. is all handled by the framework. The plugin author 8 // only needs to implement a configuration schema and the CRUD operations and 9 // everything else is meant to just work. 10 // 11 // A good starting point is to view the Provider structure. 12 package schema 13 14 import ( 15 "fmt" 16 "os" 17 "reflect" 18 "sort" 19 "strconv" 20 "strings" 21 22 "github.com/hashicorp/terraform/terraform" 23 "github.com/mitchellh/mapstructure" 24 ) 25 26 // Schema is used to describe the structure of a value. 27 // 28 // Read the documentation of the struct elements for important details. 29 type Schema struct { 30 // Type is the type of the value and must be one of the ValueType values. 31 // 32 // This type not only determines what type is expected/valid in configuring 33 // this value, but also what type is returned when ResourceData.Get is 34 // called. The types returned by Get are: 35 // 36 // TypeBool - bool 37 // TypeInt - int 38 // TypeFloat - float64 39 // TypeString - string 40 // TypeList - []interface{} 41 // TypeMap - map[string]interface{} 42 // TypeSet - *schema.Set 43 // 44 Type ValueType 45 46 // If one of these is set, then this item can come from the configuration. 47 // Both cannot be set. If Optional is set, the value is optional. If 48 // Required is set, the value is required. 49 // 50 // One of these must be set if the value is not computed. That is: 51 // value either comes from the config, is computed, or is both. 52 Optional bool 53 Required bool 54 55 // If this is non-nil, the provided function will be used during diff 56 // of this field. If this is nil, a default diff for the type of the 57 // schema will be used. 58 // 59 // This allows comparison based on something other than primitive, list 60 // or map equality - for example SSH public keys may be considered 61 // equivalent regardless of trailing whitespace. 62 DiffSuppressFunc SchemaDiffSuppressFunc 63 64 // If this is non-nil, then this will be a default value that is used 65 // when this item is not set in the configuration/state. 66 // 67 // DefaultFunc can be specified if you want a dynamic default value. 68 // Only one of Default or DefaultFunc can be set. 69 // 70 // If Required is true above, then Default cannot be set. DefaultFunc 71 // can be set with Required. If the DefaultFunc returns nil, then there 72 // will be no default and the user will be asked to fill it in. 73 // 74 // If either of these is set, then the user won't be asked for input 75 // for this key if the default is not nil. 76 Default interface{} 77 DefaultFunc SchemaDefaultFunc 78 79 // Description is used as the description for docs or asking for user 80 // input. It should be relatively short (a few sentences max) and should 81 // be formatted to fit a CLI. 82 Description string 83 84 // InputDefault is the default value to use for when inputs are requested. 85 // This differs from Default in that if Default is set, no input is 86 // asked for. If Input is asked, this will be the default value offered. 87 InputDefault string 88 89 // The fields below relate to diffs. 90 // 91 // If Computed is true, then the result of this value is computed 92 // (unless specified by config) on creation. 93 // 94 // If ForceNew is true, then a change in this resource necessitates 95 // the creation of a new resource. 96 // 97 // StateFunc is a function called to change the value of this before 98 // storing it in the state (and likewise before comparing for diffs). 99 // The use for this is for example with large strings, you may want 100 // to simply store the hash of it. 101 Computed bool 102 ForceNew bool 103 StateFunc SchemaStateFunc 104 105 // The following fields are only set for a TypeList or TypeSet Type. 106 // 107 // Elem must be either a *Schema or a *Resource only if the Type is 108 // TypeList, and represents what the element type is. If it is *Schema, 109 // the element type is just a simple value. If it is *Resource, the 110 // element type is a complex structure, potentially with its own lifecycle. 111 // 112 // MaxItems defines a maximum amount of items that can exist within a 113 // TypeSet or TypeList. Specific use cases would be if a TypeSet is being 114 // used to wrap a complex structure, however more than one instance would 115 // cause instability. 116 Elem interface{} 117 MaxItems int 118 119 // The following fields are only valid for a TypeSet type. 120 // 121 // Set defines a function to determine the unique ID of an item so that 122 // a proper set can be built. 123 Set SchemaSetFunc 124 125 // ComputedWhen is a set of queries on the configuration. Whenever any 126 // of these things is changed, it will require a recompute (this requires 127 // that Computed is set to true). 128 // 129 // NOTE: This currently does not work. 130 ComputedWhen []string 131 132 // ConflictsWith is a set of schema keys that conflict with this schema 133 ConflictsWith []string 134 135 // When Deprecated is set, this attribute is deprecated. 136 // 137 // A deprecated field still works, but will probably stop working in near 138 // future. This string is the message shown to the user with instructions on 139 // how to address the deprecation. 140 Deprecated string 141 142 // When Removed is set, this attribute has been removed from the schema 143 // 144 // Removed attributes can be left in the Schema to generate informative error 145 // messages for the user when they show up in resource configurations. 146 // This string is the message shown to the user with instructions on 147 // what do to about the removed attribute. 148 Removed string 149 150 // ValidateFunc allows individual fields to define arbitrary validation 151 // logic. It is yielded the provided config value as an interface{} that is 152 // guaranteed to be of the proper Schema type, and it can yield warnings or 153 // errors based on inspection of that value. 154 // 155 // ValidateFunc currently only works for primitive types. 156 ValidateFunc SchemaValidateFunc 157 158 // Sensitive ensures that the attribute's value does not get displayed in 159 // logs or regular output. It should be used for passwords or other 160 // secret fields. Futrure versions of Terraform may encrypt these 161 // values. 162 Sensitive bool 163 } 164 165 // SchemaDiffSuppresFunc is a function which can be used to determine 166 // whether a detected diff on a schema element is "valid" or not, and 167 // suppress it from the plan if necessary. 168 // 169 // Return true if the diff should be suppressed, false to retain it. 170 type SchemaDiffSuppressFunc func(k, old, new string, d *ResourceData) bool 171 172 // SchemaDefaultFunc is a function called to return a default value for 173 // a field. 174 type SchemaDefaultFunc func() (interface{}, error) 175 176 // EnvDefaultFunc is a helper function that returns the value of the 177 // given environment variable, if one exists, or the default value 178 // otherwise. 179 func EnvDefaultFunc(k string, dv interface{}) SchemaDefaultFunc { 180 return func() (interface{}, error) { 181 if v := os.Getenv(k); v != "" { 182 return v, nil 183 } 184 185 return dv, nil 186 } 187 } 188 189 // MultiEnvDefaultFunc is a helper function that returns the value of the first 190 // environment variable in the given list that returns a non-empty value. If 191 // none of the environment variables return a value, the default value is 192 // returned. 193 func MultiEnvDefaultFunc(ks []string, dv interface{}) SchemaDefaultFunc { 194 return func() (interface{}, error) { 195 for _, k := range ks { 196 if v := os.Getenv(k); v != "" { 197 return v, nil 198 } 199 } 200 return dv, nil 201 } 202 } 203 204 // SchemaSetFunc is a function that must return a unique ID for the given 205 // element. This unique ID is used to store the element in a hash. 206 type SchemaSetFunc func(interface{}) int 207 208 // SchemaStateFunc is a function used to convert some type to a string 209 // to be stored in the state. 210 type SchemaStateFunc func(interface{}) string 211 212 // SchemaValidateFunc is a function used to validate a single field in the 213 // schema. 214 type SchemaValidateFunc func(interface{}, string) ([]string, []error) 215 216 func (s *Schema) GoString() string { 217 return fmt.Sprintf("*%#v", *s) 218 } 219 220 // Returns a default value for this schema by either reading Default or 221 // evaluating DefaultFunc. If neither of these are defined, returns nil. 222 func (s *Schema) DefaultValue() (interface{}, error) { 223 if s.Default != nil { 224 return s.Default, nil 225 } 226 227 if s.DefaultFunc != nil { 228 defaultValue, err := s.DefaultFunc() 229 if err != nil { 230 return nil, fmt.Errorf("error loading default: %s", err) 231 } 232 return defaultValue, nil 233 } 234 235 return nil, nil 236 } 237 238 // Returns a zero value for the schema. 239 func (s *Schema) ZeroValue() interface{} { 240 // If it's a set then we'll do a bit of extra work to provide the 241 // right hashing function in our empty value. 242 if s.Type == TypeSet { 243 setFunc := s.Set 244 if setFunc == nil { 245 // Default set function uses the schema to hash the whole value 246 elem := s.Elem 247 switch t := elem.(type) { 248 case *Schema: 249 setFunc = HashSchema(t) 250 case *Resource: 251 setFunc = HashResource(t) 252 default: 253 panic("invalid set element type") 254 } 255 } 256 return &Set{F: setFunc} 257 } else { 258 return s.Type.Zero() 259 } 260 } 261 262 func (s *Schema) finalizeDiff( 263 d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff { 264 if d == nil { 265 return d 266 } 267 268 if s.Type == TypeBool { 269 normalizeBoolString := func(s string) string { 270 switch s { 271 case "0": 272 return "false" 273 case "1": 274 return "true" 275 } 276 return s 277 } 278 d.Old = normalizeBoolString(d.Old) 279 d.New = normalizeBoolString(d.New) 280 } 281 282 if d.NewRemoved { 283 return d 284 } 285 286 if s.Computed { 287 if d.Old != "" && d.New == "" { 288 // This is a computed value with an old value set already, 289 // just let it go. 290 return nil 291 } 292 293 if d.New == "" { 294 // Computed attribute without a new value set 295 d.NewComputed = true 296 } 297 } 298 299 if s.ForceNew { 300 // Force new, set it to true in the diff 301 d.RequiresNew = true 302 } 303 304 if s.Sensitive { 305 // Set the Sensitive flag so output is hidden in the UI 306 d.Sensitive = true 307 } 308 309 return d 310 } 311 312 // schemaMap is a wrapper that adds nice functions on top of schemas. 313 type schemaMap map[string]*Schema 314 315 // Data returns a ResourceData for the given schema, state, and diff. 316 // 317 // The diff is optional. 318 func (m schemaMap) Data( 319 s *terraform.InstanceState, 320 d *terraform.InstanceDiff) (*ResourceData, error) { 321 return &ResourceData{ 322 schema: m, 323 state: s, 324 diff: d, 325 }, nil 326 } 327 328 // Diff returns the diff for a resource given the schema map, 329 // state, and configuration. 330 func (m schemaMap) Diff( 331 s *terraform.InstanceState, 332 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 333 result := new(terraform.InstanceDiff) 334 result.Attributes = make(map[string]*terraform.ResourceAttrDiff) 335 336 // Make sure to mark if the resource is tainted 337 if s != nil { 338 result.DestroyTainted = s.Tainted 339 } 340 341 d := &ResourceData{ 342 schema: m, 343 state: s, 344 config: c, 345 } 346 347 for k, schema := range m { 348 err := m.diff(k, schema, result, d, false) 349 if err != nil { 350 return nil, err 351 } 352 } 353 354 // If the diff requires a new resource, then we recompute the diff 355 // so we have the complete new resource diff, and preserve the 356 // RequiresNew fields where necessary so the user knows exactly what 357 // caused that. 358 if result.RequiresNew() { 359 // Create the new diff 360 result2 := new(terraform.InstanceDiff) 361 result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) 362 363 // Preserve the DestroyTainted flag 364 result2.DestroyTainted = result.DestroyTainted 365 366 // Reset the data to not contain state. We have to call init() 367 // again in order to reset the FieldReaders. 368 d.state = nil 369 d.init() 370 371 // Perform the diff again 372 for k, schema := range m { 373 err := m.diff(k, schema, result2, d, false) 374 if err != nil { 375 return nil, err 376 } 377 } 378 379 // Force all the fields to not force a new since we know what we 380 // want to force new. 381 for k, attr := range result2.Attributes { 382 if attr == nil { 383 continue 384 } 385 386 if attr.RequiresNew { 387 attr.RequiresNew = false 388 } 389 390 if s != nil { 391 attr.Old = s.Attributes[k] 392 } 393 } 394 395 // Now copy in all the requires new diffs... 396 for k, attr := range result.Attributes { 397 if attr == nil { 398 continue 399 } 400 401 newAttr, ok := result2.Attributes[k] 402 if !ok { 403 newAttr = attr 404 } 405 406 if attr.RequiresNew { 407 newAttr.RequiresNew = true 408 } 409 410 result2.Attributes[k] = newAttr 411 } 412 413 // And set the diff! 414 result = result2 415 } 416 417 // Remove any nil diffs just to keep things clean 418 for k, v := range result.Attributes { 419 if v == nil { 420 delete(result.Attributes, k) 421 } 422 } 423 424 // Go through and detect all of the ComputedWhens now that we've 425 // finished the diff. 426 // TODO 427 428 if result.Empty() { 429 // If we don't have any diff elements, just return nil 430 return nil, nil 431 } 432 433 return result, nil 434 } 435 436 // Input implements the terraform.ResourceProvider method by asking 437 // for input for required configuration keys that don't have a value. 438 func (m schemaMap) Input( 439 input terraform.UIInput, 440 c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { 441 keys := make([]string, 0, len(m)) 442 for k, _ := range m { 443 keys = append(keys, k) 444 } 445 sort.Strings(keys) 446 447 for _, k := range keys { 448 v := m[k] 449 450 // Skip things that don't require config, if that is even valid 451 // for a provider schema. 452 if !v.Required && !v.Optional { 453 continue 454 } 455 456 // Deprecated fields should never prompt 457 if v.Deprecated != "" { 458 continue 459 } 460 461 // Skip things that have a value of some sort already 462 if _, ok := c.Raw[k]; ok { 463 continue 464 } 465 466 // Skip if it has a default value 467 defaultValue, err := v.DefaultValue() 468 if err != nil { 469 return nil, fmt.Errorf("%s: error loading default: %s", k, err) 470 } 471 if defaultValue != nil { 472 continue 473 } 474 475 var value interface{} 476 switch v.Type { 477 case TypeBool, TypeInt, TypeFloat, TypeSet: 478 continue 479 case TypeString: 480 value, err = m.inputString(input, k, v) 481 default: 482 panic(fmt.Sprintf("Unknown type for input: %#v", v.Type)) 483 } 484 485 if err != nil { 486 return nil, fmt.Errorf( 487 "%s: %s", k, err) 488 } 489 490 c.Config[k] = value 491 } 492 493 return c, nil 494 } 495 496 // Validate validates the configuration against this schema mapping. 497 func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { 498 return m.validateObject("", m, c) 499 } 500 501 // InternalValidate validates the format of this schema. This should be called 502 // from a unit test (and not in user-path code) to verify that a schema 503 // is properly built. 504 func (m schemaMap) InternalValidate(topSchemaMap schemaMap) error { 505 if topSchemaMap == nil { 506 topSchemaMap = m 507 } 508 for k, v := range m { 509 if v.Type == TypeInvalid { 510 return fmt.Errorf("%s: Type must be specified", k) 511 } 512 513 if v.Optional && v.Required { 514 return fmt.Errorf("%s: Optional or Required must be set, not both", k) 515 } 516 517 if v.Required && v.Computed { 518 return fmt.Errorf("%s: Cannot be both Required and Computed", k) 519 } 520 521 if !v.Required && !v.Optional && !v.Computed { 522 return fmt.Errorf("%s: One of optional, required, or computed must be set", k) 523 } 524 525 if v.Computed && v.Default != nil { 526 return fmt.Errorf("%s: Default must be nil if computed", k) 527 } 528 529 if v.Required && v.Default != nil { 530 return fmt.Errorf("%s: Default cannot be set with Required", k) 531 } 532 533 if len(v.ComputedWhen) > 0 && !v.Computed { 534 return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k) 535 } 536 537 if len(v.ConflictsWith) > 0 && v.Required { 538 return fmt.Errorf("%s: ConflictsWith cannot be set with Required", k) 539 } 540 541 if len(v.ConflictsWith) > 0 { 542 for _, key := range v.ConflictsWith { 543 parts := strings.Split(key, ".") 544 sm := topSchemaMap 545 var target *Schema 546 for _, part := range parts { 547 // Skip index fields 548 if _, err := strconv.Atoi(part); err == nil { 549 continue 550 } 551 552 var ok bool 553 if target, ok = sm[part]; !ok { 554 return fmt.Errorf("%s: ConflictsWith references unknown attribute (%s)", k, key) 555 } 556 557 if subResource, ok := target.Elem.(*Resource); ok { 558 sm = schemaMap(subResource.Schema) 559 } 560 } 561 if target == nil { 562 return fmt.Errorf("%s: ConflictsWith cannot find target attribute (%s), sm: %#v", k, key, sm) 563 } 564 if target.Required { 565 return fmt.Errorf("%s: ConflictsWith cannot contain Required attribute (%s)", k, key) 566 } 567 568 if target.Computed || len(target.ComputedWhen) > 0 { 569 return fmt.Errorf("%s: ConflictsWith cannot contain Computed(When) attribute (%s)", k, key) 570 } 571 } 572 } 573 574 if v.Type == TypeList || v.Type == TypeSet { 575 if v.Elem == nil { 576 return fmt.Errorf("%s: Elem must be set for lists", k) 577 } 578 579 if v.Default != nil { 580 return fmt.Errorf("%s: Default is not valid for lists or sets", k) 581 } 582 583 if v.Type != TypeSet && v.Set != nil { 584 return fmt.Errorf("%s: Set can only be set for TypeSet", k) 585 } 586 587 switch t := v.Elem.(type) { 588 case *Resource: 589 if err := t.InternalValidate(topSchemaMap, true); err != nil { 590 return err 591 } 592 case *Schema: 593 bad := t.Computed || t.Optional || t.Required 594 if bad { 595 return fmt.Errorf( 596 "%s: Elem must have only Type set", k) 597 } 598 } 599 } else { 600 if v.MaxItems > 0 { 601 return fmt.Errorf("%s: MaxItems is only supported on lists or sets", k) 602 } 603 } 604 605 if v.ValidateFunc != nil { 606 switch v.Type { 607 case TypeList, TypeSet: 608 return fmt.Errorf("ValidateFunc is not yet supported on lists or sets.") 609 } 610 } 611 } 612 613 return nil 614 } 615 616 func (m schemaMap) diff( 617 k string, 618 schema *Schema, 619 diff *terraform.InstanceDiff, 620 d *ResourceData, 621 all bool) error { 622 623 unsupressedDiff := new(terraform.InstanceDiff) 624 unsupressedDiff.Attributes = make(map[string]*terraform.ResourceAttrDiff) 625 626 var err error 627 switch schema.Type { 628 case TypeBool, TypeInt, TypeFloat, TypeString: 629 err = m.diffString(k, schema, unsupressedDiff, d, all) 630 case TypeList: 631 err = m.diffList(k, schema, unsupressedDiff, d, all) 632 case TypeMap: 633 err = m.diffMap(k, schema, unsupressedDiff, d, all) 634 case TypeSet: 635 err = m.diffSet(k, schema, unsupressedDiff, d, all) 636 default: 637 err = fmt.Errorf("%s: unknown type %#v", k, schema.Type) 638 } 639 640 for attrK, attrV := range unsupressedDiff.Attributes { 641 if schema.DiffSuppressFunc != nil && schema.DiffSuppressFunc(attrK, attrV.Old, attrV.New, d) { 642 continue 643 } 644 645 diff.Attributes[attrK] = attrV 646 } 647 648 return err 649 } 650 651 func (m schemaMap) diffList( 652 k string, 653 schema *Schema, 654 diff *terraform.InstanceDiff, 655 d *ResourceData, 656 all bool) error { 657 o, n, _, computedList := d.diffChange(k) 658 if computedList { 659 n = nil 660 } 661 nSet := n != nil 662 663 // If we have an old value and no new value is set or will be 664 // computed once all variables can be interpolated and we're 665 // computed, then nothing has changed. 666 if o != nil && n == nil && !computedList && schema.Computed { 667 return nil 668 } 669 670 if o == nil { 671 o = []interface{}{} 672 } 673 if n == nil { 674 n = []interface{}{} 675 } 676 if s, ok := o.(*Set); ok { 677 o = s.List() 678 } 679 if s, ok := n.(*Set); ok { 680 n = s.List() 681 } 682 os := o.([]interface{}) 683 vs := n.([]interface{}) 684 685 // If the new value was set, and the two are equal, then we're done. 686 // We have to do this check here because sets might be NOT 687 // reflect.DeepEqual so we need to wait until we get the []interface{} 688 if !all && nSet && reflect.DeepEqual(os, vs) { 689 return nil 690 } 691 692 // Get the counts 693 oldLen := len(os) 694 newLen := len(vs) 695 oldStr := strconv.FormatInt(int64(oldLen), 10) 696 697 // If the whole list is computed, then say that the # is computed 698 if computedList { 699 diff.Attributes[k+".#"] = &terraform.ResourceAttrDiff{ 700 Old: oldStr, 701 NewComputed: true, 702 } 703 return nil 704 } 705 706 // If the counts are not the same, then record that diff 707 changed := oldLen != newLen 708 computed := oldLen == 0 && newLen == 0 && schema.Computed 709 if changed || computed || all { 710 countSchema := &Schema{ 711 Type: TypeInt, 712 Computed: schema.Computed, 713 ForceNew: schema.ForceNew, 714 } 715 716 newStr := "" 717 if !computed { 718 newStr = strconv.FormatInt(int64(newLen), 10) 719 } else { 720 oldStr = "" 721 } 722 723 diff.Attributes[k+".#"] = countSchema.finalizeDiff(&terraform.ResourceAttrDiff{ 724 Old: oldStr, 725 New: newStr, 726 }) 727 } 728 729 // Figure out the maximum 730 maxLen := oldLen 731 if newLen > maxLen { 732 maxLen = newLen 733 } 734 735 switch t := schema.Elem.(type) { 736 case *Resource: 737 // This is a complex resource 738 for i := 0; i < maxLen; i++ { 739 for k2, schema := range t.Schema { 740 subK := fmt.Sprintf("%s.%d.%s", k, i, k2) 741 err := m.diff(subK, schema, diff, d, all) 742 if err != nil { 743 return err 744 } 745 } 746 } 747 case *Schema: 748 // Copy the schema so that we can set Computed/ForceNew from 749 // the parent schema (the TypeList). 750 t2 := *t 751 t2.ForceNew = schema.ForceNew 752 753 // This is just a primitive element, so go through each and 754 // just diff each. 755 for i := 0; i < maxLen; i++ { 756 subK := fmt.Sprintf("%s.%d", k, i) 757 err := m.diff(subK, &t2, diff, d, all) 758 if err != nil { 759 return err 760 } 761 } 762 default: 763 return fmt.Errorf("%s: unknown element type (internal)", k) 764 } 765 766 return nil 767 } 768 769 func (m schemaMap) diffMap( 770 k string, 771 schema *Schema, 772 diff *terraform.InstanceDiff, 773 d *ResourceData, 774 all bool) error { 775 prefix := k + "." 776 777 // First get all the values from the state 778 var stateMap, configMap map[string]string 779 o, n, _, nComputed := d.diffChange(k) 780 if err := mapstructure.WeakDecode(o, &stateMap); err != nil { 781 return fmt.Errorf("%s: %s", k, err) 782 } 783 if err := mapstructure.WeakDecode(n, &configMap); err != nil { 784 return fmt.Errorf("%s: %s", k, err) 785 } 786 787 // Keep track of whether the state _exists_ at all prior to clearing it 788 stateExists := o != nil 789 790 // Delete any count values, since we don't use those 791 delete(configMap, "%") 792 delete(stateMap, "%") 793 794 // Check if the number of elements has changed. 795 oldLen, newLen := len(stateMap), len(configMap) 796 changed := oldLen != newLen 797 if oldLen != 0 && newLen == 0 && schema.Computed { 798 changed = false 799 } 800 801 // It is computed if we have no old value, no new value, the schema 802 // says it is computed, and it didn't exist in the state before. The 803 // last point means: if it existed in the state, even empty, then it 804 // has already been computed. 805 computed := oldLen == 0 && newLen == 0 && schema.Computed && !stateExists 806 807 // If the count has changed or we're computed, then add a diff for the 808 // count. "nComputed" means that the new value _contains_ a value that 809 // is computed. We don't do granular diffs for this yet, so we mark the 810 // whole map as computed. 811 if changed || computed || nComputed { 812 countSchema := &Schema{ 813 Type: TypeInt, 814 Computed: schema.Computed || nComputed, 815 ForceNew: schema.ForceNew, 816 } 817 818 oldStr := strconv.FormatInt(int64(oldLen), 10) 819 newStr := "" 820 if !computed && !nComputed { 821 newStr = strconv.FormatInt(int64(newLen), 10) 822 } else { 823 oldStr = "" 824 } 825 826 diff.Attributes[k+".%"] = countSchema.finalizeDiff( 827 &terraform.ResourceAttrDiff{ 828 Old: oldStr, 829 New: newStr, 830 }, 831 ) 832 } 833 834 // If the new map is nil and we're computed, then ignore it. 835 if n == nil && schema.Computed { 836 return nil 837 } 838 839 // Now we compare, preferring values from the config map 840 for k, v := range configMap { 841 old, ok := stateMap[k] 842 delete(stateMap, k) 843 844 if old == v && ok && !all { 845 continue 846 } 847 848 diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 849 Old: old, 850 New: v, 851 }) 852 } 853 for k, v := range stateMap { 854 diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 855 Old: v, 856 NewRemoved: true, 857 }) 858 } 859 860 return nil 861 } 862 863 func (m schemaMap) diffSet( 864 k string, 865 schema *Schema, 866 diff *terraform.InstanceDiff, 867 d *ResourceData, 868 all bool) error { 869 o, n, _, computedSet := d.diffChange(k) 870 if computedSet { 871 n = nil 872 } 873 nSet := n != nil 874 875 // If we have an old value and no new value is set or will be 876 // computed once all variables can be interpolated and we're 877 // computed, then nothing has changed. 878 if o != nil && n == nil && !computedSet && schema.Computed { 879 return nil 880 } 881 882 if o == nil { 883 o = schema.ZeroValue().(*Set) 884 } 885 if n == nil { 886 n = schema.ZeroValue().(*Set) 887 } 888 os := o.(*Set) 889 ns := n.(*Set) 890 891 // If the new value was set, compare the listCode's to determine if 892 // the two are equal. Comparing listCode's instead of the actual values 893 // is needed because there could be computed values in the set which 894 // would result in false positives while comparing. 895 if !all && nSet && reflect.DeepEqual(os.listCode(), ns.listCode()) { 896 return nil 897 } 898 899 // Get the counts 900 oldLen := os.Len() 901 newLen := ns.Len() 902 oldStr := strconv.Itoa(oldLen) 903 newStr := strconv.Itoa(newLen) 904 905 // If the set computed then say that the # is computed 906 if computedSet || schema.Computed && !nSet { 907 // If # already exists, equals 0 and no new set is supplied, there 908 // is nothing to record in the diff 909 count, ok := d.GetOk(k + ".#") 910 if ok && count.(int) == 0 && !nSet && !computedSet { 911 return nil 912 } 913 914 // Set the count but make sure that if # does not exist, we don't 915 // use the zeroed value 916 countStr := strconv.Itoa(count.(int)) 917 if !ok { 918 countStr = "" 919 } 920 921 diff.Attributes[k+".#"] = &terraform.ResourceAttrDiff{ 922 Old: countStr, 923 NewComputed: true, 924 } 925 return nil 926 } 927 928 // If the counts are not the same, then record that diff 929 changed := oldLen != newLen 930 if changed || all { 931 countSchema := &Schema{ 932 Type: TypeInt, 933 Computed: schema.Computed, 934 ForceNew: schema.ForceNew, 935 } 936 937 diff.Attributes[k+".#"] = countSchema.finalizeDiff(&terraform.ResourceAttrDiff{ 938 Old: oldStr, 939 New: newStr, 940 }) 941 } 942 943 // Build the list of codes that will make up our set. This is the 944 // removed codes as well as all the codes in the new codes. 945 codes := make([][]string, 2) 946 codes[0] = os.Difference(ns).listCode() 947 codes[1] = ns.listCode() 948 for _, list := range codes { 949 for _, code := range list { 950 switch t := schema.Elem.(type) { 951 case *Resource: 952 // This is a complex resource 953 for k2, schema := range t.Schema { 954 subK := fmt.Sprintf("%s.%s.%s", k, code, k2) 955 err := m.diff(subK, schema, diff, d, true) 956 if err != nil { 957 return err 958 } 959 } 960 case *Schema: 961 // Copy the schema so that we can set Computed/ForceNew from 962 // the parent schema (the TypeSet). 963 t2 := *t 964 t2.ForceNew = schema.ForceNew 965 966 // This is just a primitive element, so go through each and 967 // just diff each. 968 subK := fmt.Sprintf("%s.%s", k, code) 969 err := m.diff(subK, &t2, diff, d, true) 970 if err != nil { 971 return err 972 } 973 default: 974 return fmt.Errorf("%s: unknown element type (internal)", k) 975 } 976 } 977 } 978 979 return nil 980 } 981 982 func (m schemaMap) diffString( 983 k string, 984 schema *Schema, 985 diff *terraform.InstanceDiff, 986 d *ResourceData, 987 all bool) error { 988 var originalN interface{} 989 var os, ns string 990 o, n, _, _ := d.diffChange(k) 991 if schema.StateFunc != nil && n != nil { 992 originalN = n 993 n = schema.StateFunc(n) 994 } 995 nraw := n 996 if nraw == nil && o != nil { 997 nraw = schema.Type.Zero() 998 } 999 if err := mapstructure.WeakDecode(o, &os); err != nil { 1000 return fmt.Errorf("%s: %s", k, err) 1001 } 1002 if err := mapstructure.WeakDecode(nraw, &ns); err != nil { 1003 return fmt.Errorf("%s: %s", k, err) 1004 } 1005 1006 if os == ns && !all { 1007 // They're the same value. If there old value is not blank or we 1008 // have an ID, then return right away since we're already setup. 1009 if os != "" || d.Id() != "" { 1010 return nil 1011 } 1012 1013 // Otherwise, only continue if we're computed 1014 if !schema.Computed { 1015 return nil 1016 } 1017 } 1018 1019 removed := false 1020 if o != nil && n == nil { 1021 removed = true 1022 } 1023 if removed && schema.Computed { 1024 return nil 1025 } 1026 1027 diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 1028 Old: os, 1029 New: ns, 1030 NewExtra: originalN, 1031 NewRemoved: removed, 1032 }) 1033 1034 return nil 1035 } 1036 1037 func (m schemaMap) inputString( 1038 input terraform.UIInput, 1039 k string, 1040 schema *Schema) (interface{}, error) { 1041 result, err := input.Input(&terraform.InputOpts{ 1042 Id: k, 1043 Query: k, 1044 Description: schema.Description, 1045 Default: schema.InputDefault, 1046 }) 1047 1048 return result, err 1049 } 1050 1051 func (m schemaMap) validate( 1052 k string, 1053 schema *Schema, 1054 c *terraform.ResourceConfig) ([]string, []error) { 1055 raw, ok := c.Get(k) 1056 if !ok && schema.DefaultFunc != nil { 1057 // We have a dynamic default. Check if we have a value. 1058 var err error 1059 raw, err = schema.DefaultFunc() 1060 if err != nil { 1061 return nil, []error{fmt.Errorf( 1062 "%q, error loading default: %s", k, err)} 1063 } 1064 1065 // We're okay as long as we had a value set 1066 ok = raw != nil 1067 } 1068 if !ok { 1069 if schema.Required { 1070 return nil, []error{fmt.Errorf( 1071 "%q: required field is not set", k)} 1072 } 1073 1074 return nil, nil 1075 } 1076 1077 if !schema.Required && !schema.Optional { 1078 // This is a computed-only field 1079 return nil, []error{fmt.Errorf( 1080 "%q: this field cannot be set", k)} 1081 } 1082 1083 err := m.validateConflictingAttributes(k, schema, c) 1084 if err != nil { 1085 return nil, []error{err} 1086 } 1087 1088 return m.validateType(k, raw, schema, c) 1089 } 1090 1091 func (m schemaMap) validateConflictingAttributes( 1092 k string, 1093 schema *Schema, 1094 c *terraform.ResourceConfig) error { 1095 1096 if len(schema.ConflictsWith) == 0 { 1097 return nil 1098 } 1099 1100 for _, conflicting_key := range schema.ConflictsWith { 1101 if value, ok := c.Get(conflicting_key); ok { 1102 return fmt.Errorf( 1103 "%q: conflicts with %s (%#v)", k, conflicting_key, value) 1104 } 1105 } 1106 1107 return nil 1108 } 1109 1110 func (m schemaMap) validateList( 1111 k string, 1112 raw interface{}, 1113 schema *Schema, 1114 c *terraform.ResourceConfig) ([]string, []error) { 1115 // We use reflection to verify the slice because you can't 1116 // case to []interface{} unless the slice is exactly that type. 1117 rawV := reflect.ValueOf(raw) 1118 if rawV.Kind() != reflect.Slice { 1119 return nil, []error{fmt.Errorf( 1120 "%s: should be a list", k)} 1121 } 1122 1123 // Validate length 1124 if schema.MaxItems > 0 && rawV.Len() > schema.MaxItems { 1125 return nil, []error{fmt.Errorf( 1126 "%s: attribute supports %d item maximum, config has %d declared", k, schema.MaxItems, rawV.Len())} 1127 } 1128 1129 // Now build the []interface{} 1130 raws := make([]interface{}, rawV.Len()) 1131 for i, _ := range raws { 1132 raws[i] = rawV.Index(i).Interface() 1133 } 1134 1135 var ws []string 1136 var es []error 1137 for i, raw := range raws { 1138 key := fmt.Sprintf("%s.%d", k, i) 1139 1140 var ws2 []string 1141 var es2 []error 1142 switch t := schema.Elem.(type) { 1143 case *Resource: 1144 // This is a sub-resource 1145 ws2, es2 = m.validateObject(key, t.Schema, c) 1146 case *Schema: 1147 ws2, es2 = m.validateType(key, raw, t, c) 1148 } 1149 1150 if len(ws2) > 0 { 1151 ws = append(ws, ws2...) 1152 } 1153 if len(es2) > 0 { 1154 es = append(es, es2...) 1155 } 1156 } 1157 1158 return ws, es 1159 } 1160 1161 func (m schemaMap) validateMap( 1162 k string, 1163 raw interface{}, 1164 schema *Schema, 1165 c *terraform.ResourceConfig) ([]string, []error) { 1166 // We use reflection to verify the slice because you can't 1167 // case to []interface{} unless the slice is exactly that type. 1168 rawV := reflect.ValueOf(raw) 1169 switch rawV.Kind() { 1170 case reflect.String: 1171 // If raw and reified are equal, this is a string and should 1172 // be rejected. 1173 reified, reifiedOk := c.Get(k) 1174 if reifiedOk && raw == reified && !c.IsComputed(k) { 1175 return nil, []error{fmt.Errorf("%s: should be a map", k)} 1176 } 1177 // Otherwise it's likely raw is an interpolation. 1178 return nil, nil 1179 case reflect.Map: 1180 case reflect.Slice: 1181 default: 1182 return nil, []error{fmt.Errorf("%s: should be a map", k)} 1183 } 1184 1185 // If it is not a slice, it is valid 1186 if rawV.Kind() != reflect.Slice { 1187 return nil, nil 1188 } 1189 1190 // It is a slice, verify that all the elements are maps 1191 raws := make([]interface{}, rawV.Len()) 1192 for i, _ := range raws { 1193 raws[i] = rawV.Index(i).Interface() 1194 } 1195 1196 for _, raw := range raws { 1197 v := reflect.ValueOf(raw) 1198 if v.Kind() != reflect.Map { 1199 return nil, []error{fmt.Errorf( 1200 "%s: should be a map", k)} 1201 } 1202 } 1203 1204 if schema.ValidateFunc != nil { 1205 validatableMap := make(map[string]interface{}) 1206 for _, raw := range raws { 1207 for k, v := range raw.(map[string]interface{}) { 1208 validatableMap[k] = v 1209 } 1210 } 1211 1212 return schema.ValidateFunc(validatableMap, k) 1213 } 1214 1215 return nil, nil 1216 } 1217 1218 func (m schemaMap) validateObject( 1219 k string, 1220 schema map[string]*Schema, 1221 c *terraform.ResourceConfig) ([]string, []error) { 1222 raw, _ := c.GetRaw(k) 1223 if _, ok := raw.(map[string]interface{}); !ok { 1224 return nil, []error{fmt.Errorf( 1225 "%s: expected object, got %s", 1226 k, reflect.ValueOf(raw).Kind())} 1227 } 1228 1229 var ws []string 1230 var es []error 1231 for subK, s := range schema { 1232 key := subK 1233 if k != "" { 1234 key = fmt.Sprintf("%s.%s", k, subK) 1235 } 1236 1237 ws2, es2 := m.validate(key, s, c) 1238 if len(ws2) > 0 { 1239 ws = append(ws, ws2...) 1240 } 1241 if len(es2) > 0 { 1242 es = append(es, es2...) 1243 } 1244 } 1245 1246 // Detect any extra/unknown keys and report those as errors. 1247 if m, ok := raw.(map[string]interface{}); ok { 1248 for subk, _ := range m { 1249 if _, ok := schema[subk]; !ok { 1250 es = append(es, fmt.Errorf( 1251 "%s: invalid or unknown key: %s", k, subk)) 1252 } 1253 } 1254 } 1255 1256 return ws, es 1257 } 1258 1259 func (m schemaMap) validatePrimitive( 1260 k string, 1261 raw interface{}, 1262 schema *Schema, 1263 c *terraform.ResourceConfig) ([]string, []error) { 1264 1265 // Catch if the user gave a complex type where a primitive was 1266 // expected, so we can return a friendly error message that 1267 // doesn't contain Go type system terminology. 1268 switch reflect.ValueOf(raw).Type().Kind() { 1269 case reflect.Slice: 1270 return nil, []error{ 1271 fmt.Errorf("%s must be a single value, not a list", k), 1272 } 1273 case reflect.Map: 1274 return nil, []error{ 1275 fmt.Errorf("%s must be a single value, not a map", k), 1276 } 1277 default: // ok 1278 } 1279 1280 if c.IsComputed(k) { 1281 // If the key is being computed, then it is not an error as 1282 // long as it's not a slice or map. 1283 return nil, nil 1284 } 1285 1286 var decoded interface{} 1287 switch schema.Type { 1288 case TypeBool: 1289 // Verify that we can parse this as the correct type 1290 var n bool 1291 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1292 return nil, []error{err} 1293 } 1294 decoded = n 1295 case TypeInt: 1296 // Verify that we can parse this as an int 1297 var n int 1298 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1299 return nil, []error{err} 1300 } 1301 decoded = n 1302 case TypeFloat: 1303 // Verify that we can parse this as an int 1304 var n float64 1305 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1306 return nil, []error{err} 1307 } 1308 decoded = n 1309 case TypeString: 1310 // Verify that we can parse this as a string 1311 var n string 1312 if err := mapstructure.WeakDecode(raw, &n); err != nil { 1313 return nil, []error{err} 1314 } 1315 decoded = n 1316 default: 1317 panic(fmt.Sprintf("Unknown validation type: %#v", schema.Type)) 1318 } 1319 1320 if schema.ValidateFunc != nil { 1321 return schema.ValidateFunc(decoded, k) 1322 } 1323 1324 return nil, nil 1325 } 1326 1327 func (m schemaMap) validateType( 1328 k string, 1329 raw interface{}, 1330 schema *Schema, 1331 c *terraform.ResourceConfig) ([]string, []error) { 1332 var ws []string 1333 var es []error 1334 switch schema.Type { 1335 case TypeSet, TypeList: 1336 ws, es = m.validateList(k, raw, schema, c) 1337 case TypeMap: 1338 ws, es = m.validateMap(k, raw, schema, c) 1339 default: 1340 ws, es = m.validatePrimitive(k, raw, schema, c) 1341 } 1342 1343 if schema.Deprecated != "" { 1344 ws = append(ws, fmt.Sprintf( 1345 "%q: [DEPRECATED] %s", k, schema.Deprecated)) 1346 } 1347 1348 if schema.Removed != "" { 1349 es = append(es, fmt.Errorf( 1350 "%q: [REMOVED] %s", k, schema.Removed)) 1351 } 1352 1353 return ws, es 1354 } 1355 1356 // Zero returns the zero value for a type. 1357 func (t ValueType) Zero() interface{} { 1358 switch t { 1359 case TypeInvalid: 1360 return nil 1361 case TypeBool: 1362 return false 1363 case TypeInt: 1364 return 0 1365 case TypeFloat: 1366 return 0.0 1367 case TypeString: 1368 return "" 1369 case TypeList: 1370 return []interface{}{} 1371 case TypeMap: 1372 return map[string]interface{}{} 1373 case TypeSet: 1374 return new(Set) 1375 case typeObject: 1376 return map[string]interface{}{} 1377 default: 1378 panic(fmt.Sprintf("unknown type %s", t)) 1379 } 1380 }