mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
f49583d25a
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.
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
package terraform
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/config"
|
|
"github.com/hashicorp/terraform/config/module"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
// 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).
|
|
//
|
|
// Currently since the type system is simple, we currently do not make
|
|
// use of the values since it is only valid to pass string values. The
|
|
// structure is in place for extension of the type system, however.
|
|
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 {
|
|
// This is only necessary when we _actually_ check. It is left as a reminder
|
|
// that at the current time we are dealing with a type system consisting only
|
|
// of strings and maps - where the only valid inter-module variable type is
|
|
// string.
|
|
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:
|
|
// This will need actual verification once we aren't dealing with
|
|
// a map[string]string but this is sufficient for now.
|
|
switch proposedValue.(type) {
|
|
case string:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
|
|
name, modulePathDescription, declaredType.Printable(), proposedValue)
|
|
}
|
|
continue
|
|
case config.VariableTypeMap:
|
|
switch proposedValue.(type) {
|
|
case map[string]interface{}:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
|
|
name, modulePathDescription, declaredType.Printable(), proposedValue)
|
|
}
|
|
case config.VariableTypeList:
|
|
switch proposedValue.(type) {
|
|
case []interface{}:
|
|
continue
|
|
default:
|
|
return nil, fmt.Errorf("variable %s%s should be type %s, got %T",
|
|
name, modulePathDescription, declaredType.Printable(), proposedValue)
|
|
}
|
|
default:
|
|
// This will need the actual type substituting when we have more than
|
|
// just strings and maps.
|
|
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{}
|
|
}
|
|
|
|
// TODO: test
|
|
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 {
|
|
var vString string
|
|
if err := mapstructure.WeakDecode(v, &vString); err == nil {
|
|
n.VariableValues[k] = vString
|
|
continue
|
|
}
|
|
|
|
var vMap map[string]interface{}
|
|
if err := mapstructure.WeakDecode(v, &vMap); err == nil {
|
|
n.VariableValues[k] = vMap
|
|
continue
|
|
}
|
|
|
|
var vSlice []interface{}
|
|
if err := mapstructure.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
|
|
}
|