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