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