opentofu/terraform/eval_variable.go

225 lines
6.5 KiB
Go
Raw Normal View History

package terraform
import (
"fmt"
"log"
"reflect"
"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
}
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-21 19:03:24 -05:00
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))
}
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-21 19:03:24 -05:00
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))
core: support native list variables in config This commit adds support for native list variables and outputs, building up on the previous change to state. Interpolation functions now return native lists in preference to StringList. List variables are defined like this: variable "test" { # This can also be inferred type = "list" default = ["Hello", "World"] } output "test_out" { value = "${var.a_list}" } This results in the following state: ``` ... "outputs": { "test_out": [ "hello", "world" ] }, ... ``` And the result of terraform output is as follows: ``` $ terraform output test_out = [ hello world ] ``` Using the output name, an xargs-friendly representation is output: ``` $ terraform output test_out hello world ``` The output command also supports indexing into the list (with appropriate range checking and no wrapping): ``` $ terraform output test_out 1 world ``` Along with maps, list outputs from one module may be passed as variables into another, removing the need for the `join(",", var.list_as_string)` and `split(",", var.list_as_string)` which was previously necessary in Terraform configuration. This commit also updates the tests and implementations of built-in interpolation functions to take and return native lists where appropriate. A backwards compatibility note: previously the concat interpolation function was capable of concatenating either strings or lists. The strings use case was deprectated a long time ago but still remained. Because we cannot return `ast.TypeAny` from an interpolation function, this use case is no longer supported for strings - `concat` is only capable of concatenating lists. This should not be a huge issue - the type checker picks up incorrect parameters, and the native HIL string concatenation - or the `join` function - can be used to replicate the missing behaviour.
2016-04-21 19:03:24 -05:00
}
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
2015-02-14 00:58:41 -06:00
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{}
}
// TODO: test
2015-02-14 00:58:41 -06:00
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
2015-02-14 00:58:41 -06:00
rc := *n.Config
for k, v := range rc.Config {
var vString string
if err := hilmapstructure.WeakDecode(v, &vString); err == nil {
n.VariableValues[k] = vString
continue
}
var vMap map[string]interface{}
if err := hilmapstructure.WeakDecode(v, &vMap); err == nil {
n.VariableValues[k] = vMap
continue
}
var vSlice []interface{}
if err := hilmapstructure.WeakDecode(v, &vSlice); err == nil {
n.VariableValues[k] = vSlice
continue
}
return nil, fmt.Errorf("Variable value for %s is not a string, list or map type", k)
}
for k, _ := range rc.Raw {
if _, ok := n.VariableValues[k]; !ok {
n.VariableValues[k] = config.UnknownVariableValue
}
}
return nil, 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()
}
}