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