| package schema |
| |
| import ( |
| "fmt" |
| "log" |
| "strconv" |
| "strings" |
| "sync" |
| |
| "github.com/hashicorp/terraform/internal/legacy/terraform" |
| "github.com/mitchellh/mapstructure" |
| ) |
| |
| // ConfigFieldReader reads fields out of an untyped map[string]string to the |
| // best of its ability. It also applies defaults from the Schema. (The other |
| // field readers do not need default handling because they source fully |
| // populated data structures.) |
| type ConfigFieldReader struct { |
| Config *terraform.ResourceConfig |
| Schema map[string]*Schema |
| |
| indexMaps map[string]map[string]int |
| once sync.Once |
| } |
| |
| func (r *ConfigFieldReader) ReadField(address []string) (FieldReadResult, error) { |
| r.once.Do(func() { r.indexMaps = make(map[string]map[string]int) }) |
| return r.readField(address, false) |
| } |
| |
| func (r *ConfigFieldReader) readField( |
| address []string, nested bool) (FieldReadResult, error) { |
| schemaList := addrToSchema(address, r.Schema) |
| if len(schemaList) == 0 { |
| return FieldReadResult{}, nil |
| } |
| |
| if !nested { |
| // If we have a set anywhere in the address, then we need to |
| // read that set out in order and actually replace that part of |
| // the address with the real list index. i.e. set.50 might actually |
| // map to set.12 in the config, since it is in list order in the |
| // config, not indexed by set value. |
| for i, v := range schemaList { |
| // Sets are the only thing that cause this issue. |
| if v.Type != TypeSet { |
| continue |
| } |
| |
| // If we're at the end of the list, then we don't have to worry |
| // about this because we're just requesting the whole set. |
| if i == len(schemaList)-1 { |
| continue |
| } |
| |
| // If we're looking for the count, then ignore... |
| if address[i+1] == "#" { |
| continue |
| } |
| |
| indexMap, ok := r.indexMaps[strings.Join(address[:i+1], ".")] |
| if !ok { |
| // Get the set so we can get the index map that tells us the |
| // mapping of the hash code to the list index |
| _, err := r.readSet(address[:i+1], v) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| indexMap = r.indexMaps[strings.Join(address[:i+1], ".")] |
| } |
| |
| index, ok := indexMap[address[i+1]] |
| if !ok { |
| return FieldReadResult{}, nil |
| } |
| |
| address[i+1] = strconv.FormatInt(int64(index), 10) |
| } |
| } |
| |
| k := strings.Join(address, ".") |
| schema := schemaList[len(schemaList)-1] |
| |
| // If we're getting the single element of a promoted list, then |
| // check to see if we have a single element we need to promote. |
| if address[len(address)-1] == "0" && len(schemaList) > 1 { |
| lastSchema := schemaList[len(schemaList)-2] |
| if lastSchema.Type == TypeList && lastSchema.PromoteSingle { |
| k := strings.Join(address[:len(address)-1], ".") |
| result, err := r.readPrimitive(k, schema) |
| if err == nil { |
| return result, nil |
| } |
| } |
| } |
| |
| if protoVersion5 { |
| switch schema.Type { |
| case TypeList, TypeSet, TypeMap, typeObject: |
| // Check if the value itself is unknown. |
| // The new protocol shims will add unknown values to this list of |
| // ComputedKeys. This is the only way we have to indicate that a |
| // collection is unknown in the config |
| for _, unknown := range r.Config.ComputedKeys { |
| if k == unknown { |
| log.Printf("[DEBUG] setting computed for %q from ComputedKeys", k) |
| return FieldReadResult{Computed: true, Exists: true}, nil |
| } |
| } |
| } |
| } |
| |
| switch schema.Type { |
| case TypeBool, TypeFloat, TypeInt, TypeString: |
| return r.readPrimitive(k, schema) |
| case TypeList: |
| // If we support promotion then we first check if we have a lone |
| // value that we must promote. |
| // a value that is alone. |
| if schema.PromoteSingle { |
| result, err := r.readPrimitive(k, schema.Elem.(*Schema)) |
| if err == nil && result.Exists { |
| result.Value = []interface{}{result.Value} |
| return result, nil |
| } |
| } |
| |
| return readListField(&nestedConfigFieldReader{r}, address, schema) |
| case TypeMap: |
| return r.readMap(k, schema) |
| case TypeSet: |
| return r.readSet(address, schema) |
| case typeObject: |
| return readObjectField( |
| &nestedConfigFieldReader{r}, |
| address, schema.Elem.(map[string]*Schema)) |
| default: |
| panic(fmt.Sprintf("Unknown type: %s", schema.Type)) |
| } |
| } |
| |
| func (r *ConfigFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) { |
| // We want both the raw value and the interpolated. We use the interpolated |
| // to store actual values and we use the raw one to check for |
| // computed keys. Actual values are obtained in the switch, depending on |
| // the type of the raw value. |
| mraw, ok := r.Config.GetRaw(k) |
| if !ok { |
| // check if this is from an interpolated field by seeing if it exists |
| // in the config |
| _, ok := r.Config.Get(k) |
| if !ok { |
| // this really doesn't exist |
| return FieldReadResult{}, nil |
| } |
| |
| // We couldn't fetch the value from a nested data structure, so treat the |
| // raw value as an interpolation string. The mraw value is only used |
| // for the type switch below. |
| mraw = "${INTERPOLATED}" |
| } |
| |
| result := make(map[string]interface{}) |
| computed := false |
| switch m := mraw.(type) { |
| case string: |
| // This is a map which has come out of an interpolated variable, so we |
| // can just get the value directly from config. Values cannot be computed |
| // currently. |
| v, _ := r.Config.Get(k) |
| |
| // If this isn't a map[string]interface, it must be computed. |
| mapV, ok := v.(map[string]interface{}) |
| if !ok { |
| return FieldReadResult{ |
| Exists: true, |
| Computed: true, |
| }, nil |
| } |
| |
| // Otherwise we can proceed as usual. |
| for i, iv := range mapV { |
| result[i] = iv |
| } |
| case []interface{}: |
| for i, innerRaw := range m { |
| for ik := range innerRaw.(map[string]interface{}) { |
| key := fmt.Sprintf("%s.%d.%s", k, i, ik) |
| if r.Config.IsComputed(key) { |
| computed = true |
| break |
| } |
| |
| v, _ := r.Config.Get(key) |
| result[ik] = v |
| } |
| } |
| case []map[string]interface{}: |
| for i, innerRaw := range m { |
| for ik := range innerRaw { |
| key := fmt.Sprintf("%s.%d.%s", k, i, ik) |
| if r.Config.IsComputed(key) { |
| computed = true |
| break |
| } |
| |
| v, _ := r.Config.Get(key) |
| result[ik] = v |
| } |
| } |
| case map[string]interface{}: |
| for ik := range m { |
| key := fmt.Sprintf("%s.%s", k, ik) |
| if r.Config.IsComputed(key) { |
| computed = true |
| break |
| } |
| |
| v, _ := r.Config.Get(key) |
| result[ik] = v |
| } |
| case nil: |
| // the map may have been empty on the configuration, so we leave the |
| // empty result |
| default: |
| panic(fmt.Sprintf("unknown type: %#v", mraw)) |
| } |
| |
| err := mapValuesToPrimitive(k, result, schema) |
| if err != nil { |
| return FieldReadResult{}, nil |
| } |
| |
| var value interface{} |
| if !computed { |
| value = result |
| } |
| |
| return FieldReadResult{ |
| Value: value, |
| Exists: true, |
| Computed: computed, |
| }, nil |
| } |
| |
| func (r *ConfigFieldReader) readPrimitive( |
| k string, schema *Schema) (FieldReadResult, error) { |
| raw, ok := r.Config.Get(k) |
| if !ok { |
| // Nothing in config, but we might still have a default from the schema |
| var err error |
| raw, err = schema.DefaultValue() |
| if err != nil { |
| return FieldReadResult{}, fmt.Errorf("%s, error loading default: %s", k, err) |
| } |
| |
| if raw == nil { |
| return FieldReadResult{}, nil |
| } |
| } |
| |
| var result string |
| if err := mapstructure.WeakDecode(raw, &result); err != nil { |
| return FieldReadResult{}, err |
| } |
| |
| computed := r.Config.IsComputed(k) |
| returnVal, err := stringToPrimitive(result, computed, schema) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| |
| return FieldReadResult{ |
| Value: returnVal, |
| Exists: true, |
| Computed: computed, |
| }, nil |
| } |
| |
| func (r *ConfigFieldReader) readSet( |
| address []string, schema *Schema) (FieldReadResult, error) { |
| indexMap := make(map[string]int) |
| // Create the set that will be our result |
| set := schema.ZeroValue().(*Set) |
| |
| raw, err := readListField(&nestedConfigFieldReader{r}, address, schema) |
| if err != nil { |
| return FieldReadResult{}, err |
| } |
| if !raw.Exists { |
| return FieldReadResult{Value: set}, nil |
| } |
| |
| // If the list is computed, the set is necessarilly computed |
| if raw.Computed { |
| return FieldReadResult{ |
| Value: set, |
| Exists: true, |
| Computed: raw.Computed, |
| }, nil |
| } |
| |
| // Build up the set from the list elements |
| for i, v := range raw.Value.([]interface{}) { |
| // Check if any of the keys in this item are computed |
| computed := r.hasComputedSubKeys( |
| fmt.Sprintf("%s.%d", strings.Join(address, "."), i), schema) |
| |
| code := set.add(v, computed) |
| indexMap[code] = i |
| } |
| |
| r.indexMaps[strings.Join(address, ".")] = indexMap |
| |
| return FieldReadResult{ |
| Value: set, |
| Exists: true, |
| }, nil |
| } |
| |
| // hasComputedSubKeys walks through a schema and returns whether or not the |
| // given key contains any subkeys that are computed. |
| func (r *ConfigFieldReader) hasComputedSubKeys(key string, schema *Schema) bool { |
| prefix := key + "." |
| |
| switch t := schema.Elem.(type) { |
| case *Resource: |
| for k, schema := range t.Schema { |
| if r.Config.IsComputed(prefix + k) { |
| return true |
| } |
| |
| if r.hasComputedSubKeys(prefix+k, schema) { |
| return true |
| } |
| } |
| } |
| |
| return false |
| } |
| |
| // nestedConfigFieldReader is a funny little thing that just wraps a |
| // ConfigFieldReader to call readField when ReadField is called so that |
| // we don't recalculate the set rewrites in the address, which leads to |
| // an infinite loop. |
| type nestedConfigFieldReader struct { |
| Reader *ConfigFieldReader |
| } |
| |
| func (r *nestedConfigFieldReader) ReadField( |
| address []string) (FieldReadResult, error) { |
| return r.Reader.readField(address, true) |
| } |