terraform/helper/schema/field_reader_config.go

345 lines
8.5 KiB
Go

package schema
import (
"fmt"
"strconv"
"strings"
"sync"
"github.com/hashicorp/terraform/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]
switch schema.Type {
case TypeBool, TypeFloat, TypeInt, TypeString:
return r.readPrimitive(k, schema)
case TypeList:
return r.readList(address, schema)
case TypeMap:
return r.readMap(k)
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) (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 {
return FieldReadResult{}, nil
}
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
}
default:
panic(fmt.Sprintf("unknown type: %#v", mraw))
}
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) readList(
address []string, schema *Schema) (FieldReadResult, error) {
addrPadded := make([]string, len(address)+1)
copy(addrPadded, address)
// Get the number of elements in the list
addrPadded[len(addrPadded)-1] = "#"
countResult, err := r.readPrimitive(
strings.Join(addrPadded, "."), &Schema{Type: TypeInt})
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 {
idx := strconv.Itoa(i)
addrPadded[len(addrPadded)-1] = idx
rawResult, err := r.readField(addrPadded, true)
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.
panic("missing field in list: " + strings.Join(addrPadded, "."))
}
result[i] = rawResult.Value
}
return FieldReadResult{
Value: result,
Exists: true,
}, 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 := r.readList(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)
}