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