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