github.com/bengesoff/terraform@v0.3.1-0.20141018223233-b25a53629922/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 "reflect" 17 "sort" 18 "strconv" 19 "strings" 20 21 "github.com/hashicorp/terraform/terraform" 22 "github.com/mitchellh/mapstructure" 23 ) 24 25 // ValueType is an enum of the type that can be represented by a schema. 26 type ValueType int 27 28 const ( 29 TypeInvalid ValueType = iota 30 TypeBool 31 TypeInt 32 TypeString 33 TypeList 34 TypeMap 35 TypeSet 36 ) 37 38 // Schema is used to describe the structure of a value. 39 // 40 // Read the documentation of the struct elements for important details. 41 type Schema struct { 42 // Type is the type of the value and must be one of the ValueType values. 43 // 44 // This type not only determines what type is expected/valid in configuring 45 // this value, but also what type is returned when ResourceData.Get is 46 // called. The types returned by Get are: 47 // 48 // TypeBool - bool 49 // TypeInt - int 50 // TypeString - string 51 // TypeList - []interface{} 52 // TypeMap - map[string]interface{} 53 // TypeSet - *schema.Set 54 // 55 Type ValueType 56 57 // If one of these is set, then this item can come from the configuration. 58 // Both cannot be set. If Optional is set, the value is optional. If 59 // Required is set, the value is required. 60 // 61 // One of these must be set if the value is not computed. That is: 62 // value either comes from the config, is computed, or is both. 63 Optional bool 64 Required bool 65 66 // If this is non-nil, then this will be a default value that is used 67 // when this item is not set in the configuration/state. 68 // 69 // DefaultFunc can be specified if you want a dynamic default value. 70 // Only one of Default or DefaultFunc can be set. 71 // 72 // If Required is true above, then Default cannot be set. DefaultFunc 73 // can be set with Required. If the DefaultFunc returns nil, then there 74 // will no default and the user will be asked to fill it in. 75 // 76 // If either of these is set, then the user won't be asked for input 77 // for this key if the default is not nil. 78 Default interface{} 79 DefaultFunc SchemaDefaultFunc 80 81 // Description is used as the description for docs or asking for user 82 // input. It should be relatively short (a few sentences max) and should 83 // be formatted to fit a CLI. 84 Description string 85 86 // InputDefault is the default value to use for when inputs are requested. 87 // This differs from Default in that if Default is set, no input is 88 // asked for. If Input is asked, this will be the default value offered. 89 InputDefault string 90 91 // The fields below relate to diffs. 92 // 93 // If Computed is true, then the result of this value is computed 94 // (unless specified by config) on creation. 95 // 96 // If ForceNew is true, then a change in this resource necessitates 97 // the creation of a new resource. 98 // 99 // StateFunc is a function called to change the value of this before 100 // storing it in the state (and likewise before comparing for diffs). 101 // The use for this is for example with large strings, you may want 102 // to simply store the hash of it. 103 Computed bool 104 ForceNew bool 105 StateFunc SchemaStateFunc 106 107 // The following fields are only set for a TypeList or TypeSet Type. 108 // 109 // Elem must be either a *Schema or a *Resource only if the Type is 110 // TypeList, and represents what the element type is. If it is *Schema, 111 // the element type is just a simple value. If it is *Resource, the 112 // element type is a complex structure, potentially with its own lifecycle. 113 Elem interface{} 114 115 // The follow fields are only valid for a TypeSet type. 116 // 117 // Set defines a function to determine the unique ID of an item so that 118 // a proper set can be built. 119 Set SchemaSetFunc 120 121 // ComputedWhen is a set of queries on the configuration. Whenever any 122 // of these things is changed, it will require a recompute (this requires 123 // that Computed is set to true). 124 // 125 // NOTE: This currently does not work. 126 ComputedWhen []string 127 } 128 129 // SchemaDefaultFunc is a function called to return a default value for 130 // a field. 131 type SchemaDefaultFunc func() (interface{}, error) 132 133 // SchemaSetFunc is a function that must return a unique ID for the given 134 // element. This unique ID is used to store the element in a hash. 135 type SchemaSetFunc func(interface{}) int 136 137 // SchemaStateFunc is a function used to convert some type to a string 138 // to be stored in the state. 139 type SchemaStateFunc func(interface{}) string 140 141 func (s *Schema) GoString() string { 142 return fmt.Sprintf("*%#v", *s) 143 } 144 145 func (s *Schema) finalizeDiff( 146 d *terraform.ResourceAttrDiff) *terraform.ResourceAttrDiff { 147 if d == nil { 148 return d 149 } 150 151 if d.NewRemoved { 152 return d 153 } 154 155 if s.Computed { 156 if d.Old != "" && d.New == "" { 157 // This is a computed value with an old value set already, 158 // just let it go. 159 return nil 160 } 161 162 if d.New == "" { 163 // Computed attribute without a new value set 164 d.NewComputed = true 165 } 166 } 167 168 if s.ForceNew { 169 // Force new, set it to true in the diff 170 d.RequiresNew = true 171 } 172 173 return d 174 } 175 176 // schemaMap is a wrapper that adds nice functions on top of schemas. 177 type schemaMap map[string]*Schema 178 179 // Data returns a ResourceData for the given schema, state, and diff. 180 // 181 // The diff is optional. 182 func (m schemaMap) Data( 183 s *terraform.InstanceState, 184 d *terraform.InstanceDiff) (*ResourceData, error) { 185 return &ResourceData{ 186 schema: m, 187 state: s, 188 diff: d, 189 }, nil 190 } 191 192 // Diff returns the diff for a resource given the schema map, 193 // state, and configuration. 194 func (m schemaMap) Diff( 195 s *terraform.InstanceState, 196 c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { 197 result := new(terraform.InstanceDiff) 198 result.Attributes = make(map[string]*terraform.ResourceAttrDiff) 199 200 d := &ResourceData{ 201 schema: m, 202 state: s, 203 config: c, 204 diffing: true, 205 } 206 207 for k, schema := range m { 208 err := m.diff(k, schema, result, d) 209 if err != nil { 210 return nil, err 211 } 212 } 213 214 // If the diff requires a new resource, then we recompute the diff 215 // so we have the complete new resource diff, and preserve the 216 // RequiresNew fields where necessary so the user knows exactly what 217 // caused that. 218 if result.RequiresNew() { 219 // Create the new diff 220 result2 := new(terraform.InstanceDiff) 221 result2.Attributes = make(map[string]*terraform.ResourceAttrDiff) 222 223 // Reset the data to not contain state 224 d.state = nil 225 226 // Perform the diff again 227 for k, schema := range m { 228 err := m.diff(k, schema, result2, d) 229 if err != nil { 230 return nil, err 231 } 232 } 233 234 // Force all the fields to not force a new since we know what we 235 // want to force new. 236 for k, attr := range result2.Attributes { 237 if attr == nil { 238 continue 239 } 240 241 if attr.RequiresNew { 242 attr.RequiresNew = false 243 } 244 245 if s != nil { 246 attr.Old = s.Attributes[k] 247 } 248 } 249 250 // Now copy in all the requires new diffs... 251 for k, attr := range result.Attributes { 252 if attr == nil { 253 continue 254 } 255 256 newAttr, ok := result2.Attributes[k] 257 if !ok { 258 newAttr = attr 259 } 260 261 if attr.RequiresNew { 262 newAttr.RequiresNew = true 263 } 264 265 result2.Attributes[k] = newAttr 266 } 267 268 // And set the diff! 269 result = result2 270 } 271 272 // Remove any nil diffs just to keep things clean 273 for k, v := range result.Attributes { 274 if v == nil { 275 delete(result.Attributes, k) 276 } 277 } 278 279 // Go through and detect all of the ComputedWhens now that we've 280 // finished the diff. 281 // TODO 282 283 if result.Empty() { 284 // If we don't have any diff elements, just return nil 285 return nil, nil 286 } 287 288 return result, nil 289 } 290 291 // Input implements the terraform.ResourceProvider method by asking 292 // for input for required configuration keys that don't have a value. 293 func (m schemaMap) Input( 294 input terraform.UIInput, 295 c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { 296 keys := make([]string, 0, len(m)) 297 for k, _ := range m { 298 keys = append(keys, k) 299 } 300 sort.Strings(keys) 301 302 for _, k := range keys { 303 v := m[k] 304 305 // Skip things that don't require config, if that is even valid 306 // for a provider schema. 307 if !v.Required && !v.Optional { 308 continue 309 } 310 311 // Skip things that have a value of some sort already 312 if _, ok := c.Raw[k]; ok { 313 continue 314 } 315 316 // Skip if it has a default 317 if v.Default != nil { 318 continue 319 } 320 if f := v.DefaultFunc; f != nil { 321 value, err := f() 322 if err != nil { 323 return nil, fmt.Errorf( 324 "%s: error loading default: %s", k, err) 325 } 326 if value != nil { 327 continue 328 } 329 } 330 331 var value interface{} 332 var err error 333 switch v.Type { 334 case TypeBool: 335 fallthrough 336 case TypeInt: 337 fallthrough 338 case TypeString: 339 value, err = m.inputString(input, k, v) 340 default: 341 panic(fmt.Sprintf("Unknown type for input: %s", v.Type)) 342 } 343 344 if err != nil { 345 return nil, fmt.Errorf( 346 "%s: %s", k, err) 347 } 348 349 c.Config[k] = value 350 } 351 352 return c, nil 353 } 354 355 // Validate validates the configuration against this schema mapping. 356 func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { 357 return m.validateObject("", m, c) 358 } 359 360 // InternalValidate validates the format of this schema. This should be called 361 // from a unit test (and not in user-path code) to verify that a schema 362 // is properly built. 363 func (m schemaMap) InternalValidate() error { 364 for k, v := range m { 365 if v.Type == TypeInvalid { 366 return fmt.Errorf("%s: Type must be specified", k) 367 } 368 369 if v.Optional && v.Required { 370 return fmt.Errorf("%s: Optional or Required must be set, not both", k) 371 } 372 373 if v.Required && v.Computed { 374 return fmt.Errorf("%s: Cannot be both Required and Computed", k) 375 } 376 377 if !v.Required && !v.Optional && !v.Computed { 378 return fmt.Errorf("%s: One of optional, required, or computed must be set", k) 379 } 380 381 if v.Computed && v.Default != nil { 382 return fmt.Errorf("%s: Default must be nil if computed", k) 383 } 384 385 if v.Required && v.Default != nil { 386 return fmt.Errorf("%s: Default cannot be set with Required", k) 387 } 388 389 if len(v.ComputedWhen) > 0 && !v.Computed { 390 return fmt.Errorf("%s: ComputedWhen can only be set with Computed", k) 391 } 392 393 if v.Type == TypeList || v.Type == TypeSet { 394 if v.Elem == nil { 395 return fmt.Errorf("%s: Elem must be set for lists", k) 396 } 397 398 if v.Default != nil { 399 return fmt.Errorf("%s: Default is not valid for lists or sets", k) 400 } 401 402 if v.Type == TypeList && v.Set != nil { 403 return fmt.Errorf("%s: Set can only be set for TypeSet", k) 404 } else if v.Type == TypeSet && v.Set == nil { 405 return fmt.Errorf("%s: Set must be set", k) 406 } 407 408 switch t := v.Elem.(type) { 409 case *Resource: 410 if err := t.InternalValidate(); err != nil { 411 return err 412 } 413 case *Schema: 414 bad := t.Computed || t.Optional || t.Required 415 if bad { 416 return fmt.Errorf( 417 "%s: Elem must have only Type set", k) 418 } 419 } 420 } 421 } 422 423 return nil 424 } 425 426 func (m schemaMap) diff( 427 k string, 428 schema *Schema, 429 diff *terraform.InstanceDiff, 430 d *ResourceData) error { 431 var err error 432 switch schema.Type { 433 case TypeBool: 434 fallthrough 435 case TypeInt: 436 fallthrough 437 case TypeString: 438 err = m.diffString(k, schema, diff, d) 439 case TypeList: 440 err = m.diffList(k, schema, diff, d) 441 case TypeMap: 442 err = m.diffMap(k, schema, diff, d) 443 case TypeSet: 444 err = m.diffSet(k, schema, diff, d) 445 default: 446 err = fmt.Errorf("%s: unknown type %#v", k, schema.Type) 447 } 448 449 return err 450 } 451 452 func (m schemaMap) diffList( 453 k string, 454 schema *Schema, 455 diff *terraform.InstanceDiff, 456 d *ResourceData) error { 457 o, n, _, computedList := d.diffChange(k) 458 nSet := n != nil 459 460 // If we have an old value, but no new value set but we're computed, 461 // then nothing has changed. 462 if o != nil && n == nil && schema.Computed { 463 return nil 464 } 465 466 if o == nil { 467 o = []interface{}{} 468 } 469 if n == nil { 470 n = []interface{}{} 471 } 472 if s, ok := o.(*Set); ok { 473 o = s.List() 474 } 475 if s, ok := n.(*Set); ok { 476 n = s.List() 477 } 478 os := o.([]interface{}) 479 vs := n.([]interface{}) 480 481 // If the new value was set, and the two are equal, then we're done. 482 // We have to do this check here because sets might be NOT 483 // reflect.DeepEqual so we need to wait until we get the []interface{} 484 if nSet && reflect.DeepEqual(os, vs) { 485 return nil 486 } 487 488 // Get the counts 489 oldLen := len(os) 490 newLen := len(vs) 491 oldStr := strconv.FormatInt(int64(oldLen), 10) 492 493 // If the whole list is computed, then say that the # is computed 494 if computedList { 495 diff.Attributes[k+".#"] = &terraform.ResourceAttrDiff{ 496 Old: oldStr, 497 NewComputed: true, 498 } 499 return nil 500 } 501 502 // If the counts are not the same, then record that diff 503 changed := oldLen != newLen 504 computed := oldLen == 0 && newLen == 0 && schema.Computed 505 if changed || computed { 506 countSchema := &Schema{ 507 Type: TypeInt, 508 Computed: schema.Computed, 509 ForceNew: schema.ForceNew, 510 } 511 512 newStr := "" 513 if !computed { 514 newStr = strconv.FormatInt(int64(newLen), 10) 515 } else { 516 oldStr = "" 517 } 518 519 diff.Attributes[k+".#"] = countSchema.finalizeDiff(&terraform.ResourceAttrDiff{ 520 Old: oldStr, 521 New: newStr, 522 }) 523 } 524 525 // Figure out the maximum 526 maxLen := oldLen 527 if newLen > maxLen { 528 maxLen = newLen 529 } 530 531 switch t := schema.Elem.(type) { 532 case *Schema: 533 // Copy the schema so that we can set Computed/ForceNew from 534 // the parent schema (the TypeList). 535 t2 := *t 536 t2.ForceNew = schema.ForceNew 537 538 // This is just a primitive element, so go through each and 539 // just diff each. 540 for i := 0; i < maxLen; i++ { 541 subK := fmt.Sprintf("%s.%d", k, i) 542 err := m.diff(subK, &t2, diff, d) 543 if err != nil { 544 return err 545 } 546 } 547 case *Resource: 548 // This is a complex resource 549 for i := 0; i < maxLen; i++ { 550 for k2, schema := range t.Schema { 551 subK := fmt.Sprintf("%s.%d.%s", k, i, k2) 552 err := m.diff(subK, schema, diff, d) 553 if err != nil { 554 return err 555 } 556 } 557 } 558 default: 559 return fmt.Errorf("%s: unknown element type (internal)", k) 560 } 561 562 return nil 563 } 564 565 func (m schemaMap) diffMap( 566 k string, 567 schema *Schema, 568 diff *terraform.InstanceDiff, 569 d *ResourceData) error { 570 //elemSchema := &Schema{Type: TypeString} 571 prefix := k + "." 572 573 // First get all the values from the state 574 var stateMap, configMap map[string]string 575 o, n, _, _ := d.diffChange(k) 576 if err := mapstructure.WeakDecode(o, &stateMap); err != nil { 577 return fmt.Errorf("%s: %s", k, err) 578 } 579 if err := mapstructure.WeakDecode(n, &configMap); err != nil { 580 return fmt.Errorf("%s: %s", k, err) 581 } 582 583 // Now we compare, preferring values from the config map 584 for k, v := range configMap { 585 old := stateMap[k] 586 delete(stateMap, k) 587 588 if old == v { 589 continue 590 } 591 592 diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 593 Old: old, 594 New: v, 595 }) 596 } 597 for k, v := range stateMap { 598 diff.Attributes[prefix+k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 599 Old: v, 600 NewRemoved: true, 601 }) 602 } 603 604 return nil 605 } 606 607 func (m schemaMap) diffSet( 608 k string, 609 schema *Schema, 610 diff *terraform.InstanceDiff, 611 d *ResourceData) error { 612 return m.diffList(k, schema, diff, d) 613 } 614 615 func (m schemaMap) diffString( 616 k string, 617 schema *Schema, 618 diff *terraform.InstanceDiff, 619 d *ResourceData) error { 620 var originalN interface{} 621 var os, ns string 622 o, n, _, _ := d.diffChange(k) 623 if n == nil { 624 n = schema.Default 625 if schema.DefaultFunc != nil { 626 var err error 627 n, err = schema.DefaultFunc() 628 if err != nil { 629 return fmt.Errorf("%s, error loading default: %s", err) 630 } 631 } 632 } 633 if schema.StateFunc != nil { 634 originalN = n 635 n = schema.StateFunc(n) 636 } 637 if err := mapstructure.WeakDecode(o, &os); err != nil { 638 return fmt.Errorf("%s: %s", k, err) 639 } 640 if err := mapstructure.WeakDecode(n, &ns); err != nil { 641 return fmt.Errorf("%s: %s", k, err) 642 } 643 644 if os == ns { 645 // They're the same value. If there old value is not blank or we 646 // have an ID, then return right away since we're already setup. 647 if os != "" || d.Id() != "" { 648 return nil 649 } 650 651 // Otherwise, only continue if we're computed 652 if !schema.Computed { 653 return nil 654 } 655 } 656 657 removed := false 658 if o != nil && n == nil { 659 removed = true 660 } 661 if removed && schema.Computed { 662 return nil 663 } 664 665 diff.Attributes[k] = schema.finalizeDiff(&terraform.ResourceAttrDiff{ 666 Old: os, 667 New: ns, 668 NewExtra: originalN, 669 NewRemoved: removed, 670 }) 671 672 return nil 673 } 674 675 func (m schemaMap) inputString( 676 input terraform.UIInput, 677 k string, 678 schema *Schema) (interface{}, error) { 679 result, err := input.Input(&terraform.InputOpts{ 680 Id: k, 681 Query: k, 682 Description: schema.Description, 683 Default: schema.InputDefault, 684 }) 685 686 return result, err 687 } 688 689 func (m schemaMap) validate( 690 k string, 691 schema *Schema, 692 c *terraform.ResourceConfig) ([]string, []error) { 693 raw, ok := c.Get(k) 694 if !ok && schema.DefaultFunc != nil { 695 // We have a dynamic default. Check if we have a value. 696 var err error 697 raw, err = schema.DefaultFunc() 698 if err != nil { 699 return nil, []error{fmt.Errorf( 700 "%s, error loading default: %s", k, err)} 701 } 702 703 // We're okay as long as we had a value set 704 ok = raw != nil 705 } 706 if !ok { 707 if schema.Required { 708 return nil, []error{fmt.Errorf( 709 "%s: required field is not set", k)} 710 } 711 712 return nil, nil 713 } 714 715 if !schema.Required && !schema.Optional { 716 // This is a computed-only field 717 return nil, []error{fmt.Errorf( 718 "%s: this field cannot be set", k)} 719 } 720 721 return m.validatePrimitive(k, raw, schema, c) 722 } 723 724 func (m schemaMap) validateList( 725 k string, 726 raw interface{}, 727 schema *Schema, 728 c *terraform.ResourceConfig) ([]string, []error) { 729 // We use reflection to verify the slice because you can't 730 // case to []interface{} unless the slice is exactly that type. 731 rawV := reflect.ValueOf(raw) 732 if rawV.Kind() != reflect.Slice { 733 return nil, []error{fmt.Errorf( 734 "%s: should be a list", k)} 735 } 736 737 // Now build the []interface{} 738 raws := make([]interface{}, rawV.Len()) 739 for i, _ := range raws { 740 raws[i] = rawV.Index(i).Interface() 741 } 742 743 var ws []string 744 var es []error 745 for i, raw := range raws { 746 key := fmt.Sprintf("%s.%d", k, i) 747 748 var ws2 []string 749 var es2 []error 750 switch t := schema.Elem.(type) { 751 case *Resource: 752 // This is a sub-resource 753 ws2, es2 = m.validateObject(key, t.Schema, c) 754 case *Schema: 755 // This is some sort of primitive 756 ws2, es2 = m.validatePrimitive(key, raw, t, c) 757 } 758 759 if len(ws2) > 0 { 760 ws = append(ws, ws2...) 761 } 762 if len(es2) > 0 { 763 es = append(es, es2...) 764 } 765 } 766 767 return ws, es 768 } 769 770 func (m schemaMap) validateObject( 771 k string, 772 schema map[string]*Schema, 773 c *terraform.ResourceConfig) ([]string, []error) { 774 var ws []string 775 var es []error 776 for subK, s := range schema { 777 key := subK 778 if k != "" { 779 key = fmt.Sprintf("%s.%s", k, subK) 780 } 781 782 ws2, es2 := m.validate(key, s, c) 783 if len(ws2) > 0 { 784 ws = append(ws, ws2...) 785 } 786 if len(es2) > 0 { 787 es = append(es, es2...) 788 } 789 } 790 791 // Detect any extra/unknown keys and report those as errors. 792 prefix := k + "." 793 for configK, _ := range c.Raw { 794 if k != "" { 795 if !strings.HasPrefix(configK, prefix) { 796 continue 797 } 798 799 configK = configK[len(prefix):] 800 } 801 802 if _, ok := schema[configK]; !ok { 803 es = append(es, fmt.Errorf( 804 "%s: invalid or unknown key: %s", k, configK)) 805 } 806 } 807 808 return ws, es 809 } 810 811 func (m schemaMap) validatePrimitive( 812 k string, 813 raw interface{}, 814 schema *Schema, 815 c *terraform.ResourceConfig) ([]string, []error) { 816 if c.IsComputed(k) { 817 // If the key is being computed, then it is not an error 818 return nil, nil 819 } 820 821 switch schema.Type { 822 case TypeSet: 823 fallthrough 824 case TypeList: 825 return m.validateList(k, raw, schema, c) 826 case TypeInt: 827 // Verify that we can parse this as an int 828 var n int 829 if err := mapstructure.WeakDecode(raw, &n); err != nil { 830 return nil, []error{err} 831 } 832 } 833 834 return nil, nil 835 }