280 lines
8.1 KiB
Go
280 lines
8.1 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
"github.com/hashicorp/terraform/helper/hilmapstructure"
|
|
)
|
|
|
|
// EvalTypeCheckVariable is an EvalNode which ensures that the variable
|
|
// values which are assigned as inputs to a module (including the root)
|
|
// match the types which are either declared for the variables explicitly
|
|
// or inferred from the default values.
|
|
//
|
|
// In order to achieve this three things are required:
|
|
// - a map of the proposed variable values
|
|
// - the configuration tree of the module in which the variable is
|
|
// declared
|
|
// - the path to the module (so we know which part of the tree to
|
|
// compare the values against).
|
|
type EvalTypeCheckVariable struct {
|
|
Variables map[string]interface{}
|
|
ModulePath []string
|
|
ModuleTree *module.Tree
|
|
}
|
|
|
|
func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) {
|
|
currentTree := n.ModuleTree
|
|
for _, pathComponent := range n.ModulePath[1:] {
|
|
currentTree = currentTree.Children()[pathComponent]
|
|
}
|
|
targetConfig := currentTree.Config()
|
|
|
|
prototypes := make(map[string]config.VariableType)
|
|
for _, variable := range targetConfig.Variables {
|
|
prototypes[variable.Name] = variable.Type()
|
|
}
|
|
|
|
// Only display a module in an error message if we are not in the root module
|
|
modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], "."))
|
|
if len(n.ModulePath) == 1 {
|
|
modulePathDescription = ""
|
|
}
|
|
|
|
for name, declaredType := range prototypes {
|
|
proposedValue, ok := n.Variables[name]
|
|
if !ok {
|
|
// This means the default value should be used as no overriding value
|
|
// has been set. Therefore we should continue as no check is necessary.
|
|
continue
|
|
}
|
|
|
|
if proposedValue == config.UnknownVariableValue {
|
|
continue
|
|
}
|
|
|
|
switch declaredType {
|
|
case config.VariableTypeString:
|
|
switch proposedValue.(type) {
|
|
case string:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
|
|
name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
|
|
}
|
|
case config.VariableTypeMap:
|
|
switch proposedValue.(type) {
|
|
case map[string]interface{}:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
|
|
name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
|
|
}
|
|
case config.VariableTypeList:
|
|
switch proposedValue.(type) {
|
|
case []interface{}:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %s",
|
|
name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue))
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got type string",
|
|
name, modulePathDescription, declaredType.Printable())
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalSetVariables is an EvalNode implementation that sets the variables
|
|
// explicitly for interpolation later.
|
|
type EvalSetVariables struct {
|
|
Module *string
|
|
Variables map[string]interface{}
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalSetVariables) Eval(ctx EvalContext) (interface{}, error) {
|
|
ctx.SetVariables(*n.Module, n.Variables)
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalVariableBlock is an EvalNode implementation that evaluates the
|
|
// given configuration, and uses the final values as a way to set the
|
|
// mapping.
|
|
type EvalVariableBlock struct {
|
|
Config **ResourceConfig
|
|
VariableValues map[string]interface{}
|
|
}
|
|
|
|
func (n *EvalVariableBlock) Eval(ctx EvalContext) (interface{}, error) {
|
|
// Clear out the existing mapping
|
|
for k, _ := range n.VariableValues {
|
|
delete(n.VariableValues, k)
|
|
}
|
|
|
|
// Get our configuration
|
|
rc := *n.Config
|
|
for k, v := range rc.Config {
|
|
vKind := reflect.ValueOf(v).Type().Kind()
|
|
|
|
switch vKind {
|
|
case reflect.Slice:
|
|
var vSlice []interface{}
|
|
if err := hilmapstructure.WeakDecode(v, &vSlice); err == nil {
|
|
n.VariableValues[k] = vSlice
|
|
continue
|
|
}
|
|
case reflect.Map:
|
|
var vMap map[string]interface{}
|
|
if err := hilmapstructure.WeakDecode(v, &vMap); err == nil {
|
|
n.VariableValues[k] = vMap
|
|
continue
|
|
}
|
|
default:
|
|
var vString string
|
|
if err := hilmapstructure.WeakDecode(v, &vString); err == nil {
|
|
n.VariableValues[k] = vString
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("Variable value for %s is not a string, list or map type", k)
|
|
}
|
|
|
|
for _, path := range rc.ComputedKeys {
|
|
log.Printf("[DEBUG] Setting Unknown Variable Value for computed key: %s", path)
|
|
err := n.setUnknownVariableValueForPath(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *EvalVariableBlock) setUnknownVariableValueForPath(path string) error {
|
|
pathComponents := strings.Split(path, ".")
|
|
|
|
if len(pathComponents) < 1 {
|
|
return fmt.Errorf("No path comoponents in %s", path)
|
|
}
|
|
|
|
if len(pathComponents) == 1 {
|
|
// Special case the "top level" since we know the type
|
|
if _, ok := n.VariableValues[pathComponents[0]]; !ok {
|
|
n.VariableValues[pathComponents[0]] = config.UnknownVariableValue
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Otherwise find the correct point in the tree and then set to unknown
|
|
var current interface{} = n.VariableValues[pathComponents[0]]
|
|
for i := 1; i < len(pathComponents); i++ {
|
|
switch tCurrent := current.(type) {
|
|
case []interface{}:
|
|
index, err := strconv.Atoi(pathComponents[i])
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot convert %s to slice index in path %s",
|
|
pathComponents[i], path)
|
|
}
|
|
current = tCurrent[index]
|
|
case []map[string]interface{}:
|
|
index, err := strconv.Atoi(pathComponents[i])
|
|
if err != nil {
|
|
return fmt.Errorf("Cannot convert %s to slice index in path %s",
|
|
pathComponents[i], path)
|
|
}
|
|
current = tCurrent[index]
|
|
case map[string]interface{}:
|
|
if val, hasVal := tCurrent[pathComponents[i]]; hasVal {
|
|
current = val
|
|
continue
|
|
}
|
|
|
|
tCurrent[pathComponents[i]] = config.UnknownVariableValue
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EvalCoerceMapVariable is an EvalNode implementation that recognizes a
|
|
// specific ambiguous HCL parsing situation and resolves it. In HCL parsing, a
|
|
// bare map literal is indistinguishable from a list of maps w/ one element.
|
|
//
|
|
// We take all the same inputs as EvalTypeCheckVariable above, since we need
|
|
// both the target type and the proposed value in order to properly coerce.
|
|
type EvalCoerceMapVariable struct {
|
|
Variables map[string]interface{}
|
|
ModulePath []string
|
|
ModuleTree *module.Tree
|
|
}
|
|
|
|
// Eval implements the EvalNode interface. See EvalCoerceMapVariable for
|
|
// details.
|
|
func (n *EvalCoerceMapVariable) Eval(ctx EvalContext) (interface{}, error) {
|
|
currentTree := n.ModuleTree
|
|
for _, pathComponent := range n.ModulePath[1:] {
|
|
currentTree = currentTree.Children()[pathComponent]
|
|
}
|
|
targetConfig := currentTree.Config()
|
|
|
|
prototypes := make(map[string]config.VariableType)
|
|
for _, variable := range targetConfig.Variables {
|
|
prototypes[variable.Name] = variable.Type()
|
|
}
|
|
|
|
for name, declaredType := range prototypes {
|
|
if declaredType != config.VariableTypeMap {
|
|
continue
|
|
}
|
|
|
|
proposedValue, ok := n.Variables[name]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
if list, ok := proposedValue.([]interface{}); ok && len(list) == 1 {
|
|
if m, ok := list[0].(map[string]interface{}); ok {
|
|
log.Printf("[DEBUG] EvalCoerceMapVariable: "+
|
|
"Coercing single element list into map: %#v", m)
|
|
n.Variables[name] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// hclTypeName returns the name of the type that would represent this value in
|
|
// a config file, or falls back to the Go type name if there's no corresponding
|
|
// HCL type. This is used for formatted output, not for comparing types.
|
|
func hclTypeName(i interface{}) string {
|
|
switch k := reflect.Indirect(reflect.ValueOf(i)).Kind(); k {
|
|
case reflect.Bool:
|
|
return "boolean"
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
|
|
reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
|
|
return "number"
|
|
case reflect.Array, reflect.Slice:
|
|
return "list"
|
|
case reflect.Map:
|
|
return "map"
|
|
case reflect.String:
|
|
return "string"
|
|
default:
|
|
// fall back to the Go type if there's no match
|
|
return k.String()
|
|
}
|
|
}
|