mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-02 12:17:39 -06:00
bd1a215580
Due to the lossiness of our legacy models for diff and state, shimming a diff and then creating a state from it produces a different result than shimming a state directly. That means that ImportStateVerify no longer works as expected if there are any Computed attributes in the schema where d.Set isn't called during Read. Fixing that for every case would require some risky changes to the shim behavior, so we're instead going to ask provider developers to address it by adding `d.Set` calls where needed, since that is the contract for "Computed" anyway -- a default value should be produced during Create, and thus by extension during Import. However, since a common situation where this occurs is attributes marked as "Removed", since all of the code that deals with them has generally been deleted, we'll avoid problems in that case here by treating Removed attributes as ignored for the purposes of ImportStateVerify. This required exporting some functionality that was formerly unexported in helper/schema, but it's a relatively harmless schema introspection function so shouldn't be a big deal to export it.
344 lines
8.2 KiB
Go
344 lines
8.2 KiB
Go
package schema
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// FieldReaders are responsible for decoding fields out of data into
|
|
// the proper typed representation. ResourceData uses this to query data
|
|
// out of multiple sources: config, state, diffs, etc.
|
|
type FieldReader interface {
|
|
ReadField([]string) (FieldReadResult, error)
|
|
}
|
|
|
|
// FieldReadResult encapsulates all the resulting data from reading
|
|
// a field.
|
|
type FieldReadResult struct {
|
|
// Value is the actual read value. NegValue is the _negative_ value
|
|
// or the items that should be removed (if they existed). NegValue
|
|
// doesn't make sense for primitives but is important for any
|
|
// container types such as maps, sets, lists.
|
|
Value interface{}
|
|
ValueProcessed interface{}
|
|
|
|
// Exists is true if the field was found in the data. False means
|
|
// it wasn't found if there was no error.
|
|
Exists bool
|
|
|
|
// Computed is true if the field was found but the value
|
|
// is computed.
|
|
Computed bool
|
|
}
|
|
|
|
// ValueOrZero returns the value of this result or the zero value of the
|
|
// schema type, ensuring a consistent non-nil return value.
|
|
func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
|
|
if r.Value != nil {
|
|
return r.Value
|
|
}
|
|
|
|
return s.ZeroValue()
|
|
}
|
|
|
|
// SchemasForFlatmapPath tries its best to find a sequence of schemas that
|
|
// the given dot-delimited attribute path traverses through.
|
|
func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema {
|
|
parts := strings.Split(path, ".")
|
|
return addrToSchema(parts, schemaMap)
|
|
}
|
|
|
|
// addrToSchema finds the final element schema for the given address
|
|
// and the given schema. It returns all the schemas that led to the final
|
|
// schema. These are in order of the address (out to in).
|
|
func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
|
|
current := &Schema{
|
|
Type: typeObject,
|
|
Elem: schemaMap,
|
|
}
|
|
|
|
// If we aren't given an address, then the user is requesting the
|
|
// full object, so we return the special value which is the full object.
|
|
if len(addr) == 0 {
|
|
return []*Schema{current}
|
|
}
|
|
|
|
result := make([]*Schema, 0, len(addr))
|
|
for len(addr) > 0 {
|
|
k := addr[0]
|
|
addr = addr[1:]
|
|
|
|
REPEAT:
|
|
// We want to trim off the first "typeObject" since its not a
|
|
// real lookup that people do. i.e. []string{"foo"} in a structure
|
|
// isn't {typeObject, typeString}, its just a {typeString}.
|
|
if len(result) > 0 || current.Type != typeObject {
|
|
result = append(result, current)
|
|
}
|
|
|
|
switch t := current.Type; t {
|
|
case TypeBool, TypeInt, TypeFloat, TypeString:
|
|
if len(addr) > 0 {
|
|
return nil
|
|
}
|
|
case TypeList, TypeSet:
|
|
isIndex := len(addr) > 0 && addr[0] == "#"
|
|
|
|
switch v := current.Elem.(type) {
|
|
case *Resource:
|
|
current = &Schema{
|
|
Type: typeObject,
|
|
Elem: v.Schema,
|
|
}
|
|
case *Schema:
|
|
current = v
|
|
case ValueType:
|
|
current = &Schema{Type: v}
|
|
default:
|
|
// we may not know the Elem type and are just looking for the
|
|
// index
|
|
if isIndex {
|
|
break
|
|
}
|
|
|
|
if len(addr) == 0 {
|
|
// we've processed the address, so return what we've
|
|
// collected
|
|
return result
|
|
}
|
|
|
|
if len(addr) == 1 {
|
|
if _, err := strconv.Atoi(addr[0]); err == nil {
|
|
// we're indexing a value without a schema. This can
|
|
// happen if the list is nested in another schema type.
|
|
// Default to a TypeString like we do with a map
|
|
current = &Schema{Type: TypeString}
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// If we only have one more thing and the next thing
|
|
// is a #, then we're accessing the index which is always
|
|
// an int.
|
|
if isIndex {
|
|
current = &Schema{Type: TypeInt}
|
|
break
|
|
}
|
|
|
|
case TypeMap:
|
|
if len(addr) > 0 {
|
|
switch v := current.Elem.(type) {
|
|
case ValueType:
|
|
current = &Schema{Type: v}
|
|
case *Schema:
|
|
current, _ = current.Elem.(*Schema)
|
|
default:
|
|
// maps default to string values. This is all we can have
|
|
// if this is nested in another list or map.
|
|
current = &Schema{Type: TypeString}
|
|
}
|
|
}
|
|
case typeObject:
|
|
// If we're already in the object, then we want to handle Sets
|
|
// and Lists specially. Basically, their next key is the lookup
|
|
// key (the set value or the list element). For these scenarios,
|
|
// we just want to skip it and move to the next element if there
|
|
// is one.
|
|
if len(result) > 0 {
|
|
lastType := result[len(result)-2].Type
|
|
if lastType == TypeSet || lastType == TypeList {
|
|
if len(addr) == 0 {
|
|
break
|
|
}
|
|
|
|
k = addr[0]
|
|
addr = addr[1:]
|
|
}
|
|
}
|
|
|
|
m := current.Elem.(map[string]*Schema)
|
|
val, ok := m[k]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
current = val
|
|
goto REPEAT
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// readListField is a generic method for reading a list field out of a
|
|
// a FieldReader. It does this based on the assumption that there is a key
|
|
// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
|
|
// after that point.
|
|
func readListField(
|
|
r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) {
|
|
addrPadded := make([]string, len(addr)+1)
|
|
copy(addrPadded, addr)
|
|
addrPadded[len(addrPadded)-1] = "#"
|
|
|
|
// Get the number of elements in the list
|
|
countResult, err := r.ReadField(addrPadded)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if !countResult.Exists {
|
|
// No count, means we have no list
|
|
countResult.Value = 0
|
|
}
|
|
|
|
// If we have an empty list, then return an empty list
|
|
if countResult.Computed || countResult.Value.(int) == 0 {
|
|
return FieldReadResult{
|
|
Value: []interface{}{},
|
|
Exists: countResult.Exists,
|
|
Computed: countResult.Computed,
|
|
}, nil
|
|
}
|
|
|
|
// Go through each count, and get the item value out of it
|
|
result := make([]interface{}, countResult.Value.(int))
|
|
for i, _ := range result {
|
|
is := strconv.FormatInt(int64(i), 10)
|
|
addrPadded[len(addrPadded)-1] = is
|
|
rawResult, err := r.ReadField(addrPadded)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if !rawResult.Exists {
|
|
// This should never happen, because by the time the data
|
|
// gets to the FieldReaders, all the defaults should be set by
|
|
// Schema.
|
|
rawResult.Value = nil
|
|
}
|
|
|
|
result[i] = rawResult.Value
|
|
}
|
|
|
|
return FieldReadResult{
|
|
Value: result,
|
|
Exists: true,
|
|
}, nil
|
|
}
|
|
|
|
// readObjectField is a generic method for reading objects out of FieldReaders
|
|
// based on the assumption that building an address of []string{k, FIELD}
|
|
// will result in the proper field data.
|
|
func readObjectField(
|
|
r FieldReader,
|
|
addr []string,
|
|
schema map[string]*Schema) (FieldReadResult, error) {
|
|
result := make(map[string]interface{})
|
|
exists := false
|
|
for field, s := range schema {
|
|
addrRead := make([]string, len(addr), len(addr)+1)
|
|
copy(addrRead, addr)
|
|
addrRead = append(addrRead, field)
|
|
rawResult, err := r.ReadField(addrRead)
|
|
if err != nil {
|
|
return FieldReadResult{}, err
|
|
}
|
|
if rawResult.Exists {
|
|
exists = true
|
|
}
|
|
|
|
result[field] = rawResult.ValueOrZero(s)
|
|
}
|
|
|
|
return FieldReadResult{
|
|
Value: result,
|
|
Exists: exists,
|
|
}, nil
|
|
}
|
|
|
|
// convert map values to the proper primitive type based on schema.Elem
|
|
func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
|
|
elemType, err := getValueType(k, schema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch elemType {
|
|
case TypeInt, TypeFloat, TypeBool:
|
|
for k, v := range m {
|
|
vs, ok := v.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
v, err := stringToPrimitive(vs, false, &Schema{Type: elemType})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m[k] = v
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringToPrimitive(
|
|
value string, computed bool, schema *Schema) (interface{}, error) {
|
|
var returnVal interface{}
|
|
switch schema.Type {
|
|
case TypeBool:
|
|
if value == "" {
|
|
returnVal = false
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = v
|
|
case TypeFloat:
|
|
if value == "" {
|
|
returnVal = 0.0
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseFloat(value, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = v
|
|
case TypeInt:
|
|
if value == "" {
|
|
returnVal = 0
|
|
break
|
|
}
|
|
if computed {
|
|
break
|
|
}
|
|
|
|
v, err := strconv.ParseInt(value, 0, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
returnVal = int(v)
|
|
case TypeString:
|
|
returnVal = value
|
|
default:
|
|
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
|
|
}
|
|
|
|
return returnVal, nil
|
|
}
|