opentofu/terraform/interpolate.go
James Nugent 7b6df27e4a helper/schema: Read native maps from configuration
This adds a test and the support necessary to read from native maps
passed as variables via interpolation - for example:

```
resource ...... {
     mapValue = "${var.map}"
}
```

We also add support for interpolating maps from the flat-mapped resource
config, which is necessary to support assignment of computed maps, which
is now valid.

Unfortunately there is no good way to distinguish between a list and a
map in the flatmap. In lieu of changing that representation (which is
risky), we assume that if all the keys are numeric, this is intended to
be a list, and if not it is intended to be a map. This does preclude
maps which have purely numeric keys, which should be noted as a
backwards compatibility concern.
2016-05-10 14:49:14 -04:00

642 lines
17 KiB
Go

package terraform
import (
"fmt"
"log"
"os"
"regexp"
"sort"
"strings"
"sync"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
)
const (
// VarEnvPrefix is the prefix of variables that are read from
// the environment to set variables here.
VarEnvPrefix = "TF_VAR_"
)
// Interpolater is the structure responsible for determining the values
// for interpolations such as `aws_instance.foo.bar`.
type Interpolater struct {
Operation walkOperation
Module *module.Tree
State *State
StateLock *sync.RWMutex
VariableValues map[string]interface{}
VariableValuesLock *sync.Mutex
}
// InterpolationScope is the current scope of execution. This is required
// since some variables which are interpolated are dependent on what we're
// operating on and where we are.
type InterpolationScope struct {
Path []string
Resource *Resource
}
// Values returns the values for all the variables in the given map.
func (i *Interpolater) Values(
scope *InterpolationScope,
vars map[string]config.InterpolatedVariable) (map[string]ast.Variable, error) {
result := make(map[string]ast.Variable, len(vars))
// Copy the default variables
if i.Module != nil && scope != nil {
mod := i.Module
if len(scope.Path) > 1 {
mod = i.Module.Child(scope.Path[1:])
}
for _, v := range mod.Config().Variables {
// Set default variables
if v.Default == nil {
continue
}
n := fmt.Sprintf("var.%s", v.Name)
variable, err := hil.InterfaceToVariable(v.Default)
if err != nil {
return nil, fmt.Errorf("invalid default map value for %s: %v", v.Name, v.Default)
}
result[n] = variable
}
}
for n, rawV := range vars {
var err error
switch v := rawV.(type) {
case *config.CountVariable:
err = i.valueCountVar(scope, n, v, result)
case *config.ModuleVariable:
err = i.valueModuleVar(scope, n, v, result)
case *config.PathVariable:
err = i.valuePathVar(scope, n, v, result)
case *config.ResourceVariable:
err = i.valueResourceVar(scope, n, v, result)
case *config.SelfVariable:
err = i.valueSelfVar(scope, n, v, result)
case *config.SimpleVariable:
err = i.valueSimpleVar(scope, n, v, result)
case *config.UserVariable:
err = i.valueUserVar(scope, n, v, result)
default:
err = fmt.Errorf("%s: unknown variable type: %T", n, rawV)
}
if err != nil {
return nil, err
}
}
return result, nil
}
func (i *Interpolater) valueCountVar(
scope *InterpolationScope,
n string,
v *config.CountVariable,
result map[string]ast.Variable) error {
switch v.Type {
case config.CountValueIndex:
if scope.Resource == nil {
return fmt.Errorf("%s: count.index is only valid within resources", n)
}
result[n] = ast.Variable{
Value: scope.Resource.CountIndex,
Type: ast.TypeInt,
}
return nil
default:
return fmt.Errorf("%s: unknown count type: %#v", n, v.Type)
}
}
func unknownVariable() ast.Variable {
return ast.Variable{
Type: ast.TypeString,
Value: config.UnknownVariableValue,
}
}
func (i *Interpolater) valueModuleVar(
scope *InterpolationScope,
n string,
v *config.ModuleVariable,
result map[string]ast.Variable) error {
// If we're computing all dynamic fields, then module vars count
// and we mark it as computed.
if i.Operation == walkValidate {
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
return nil
}
// Build the path to the child module we want
path := make([]string, len(scope.Path), len(scope.Path)+1)
copy(path, scope.Path)
path = append(path, v.Name)
// Grab the lock so that if other interpolations are running or
// state is being modified, we'll be safe.
i.StateLock.RLock()
defer i.StateLock.RUnlock()
// Get the module where we're looking for the value
mod := i.State.ModuleByPath(path)
if mod == nil {
// If the module doesn't exist, then we can return an empty string.
// This happens usually only in Refresh() when we haven't populated
// a state. During validation, we semantically verify that all
// modules reference other modules, and graph ordering should
// ensure that the module is in the state, so if we reach this
// point otherwise it really is a panic.
result[n] = unknownVariable()
} else {
// Get the value from the outputs
if value, ok := mod.Outputs[v.Field]; ok {
output, err := hil.InterfaceToVariable(value)
if err != nil {
return err
}
result[n] = output
} else {
// Same reasons as the comment above.
result[n] = unknownVariable()
}
}
return nil
}
func (i *Interpolater) valuePathVar(
scope *InterpolationScope,
n string,
v *config.PathVariable,
result map[string]ast.Variable) error {
switch v.Type {
case config.PathValueCwd:
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf(
"Couldn't get cwd for var %s: %s",
v.FullKey(), err)
}
result[n] = ast.Variable{
Value: wd,
Type: ast.TypeString,
}
case config.PathValueModule:
if t := i.Module.Child(scope.Path[1:]); t != nil {
result[n] = ast.Variable{
Value: t.Config().Dir,
Type: ast.TypeString,
}
}
case config.PathValueRoot:
result[n] = ast.Variable{
Value: i.Module.Config().Dir,
Type: ast.TypeString,
}
default:
return fmt.Errorf("%s: unknown path type: %#v", n, v.Type)
}
return nil
}
func (i *Interpolater) valueResourceVar(
scope *InterpolationScope,
n string,
v *config.ResourceVariable,
result map[string]ast.Variable) error {
// If we're computing all dynamic fields, then module vars count
// and we mark it as computed.
if i.Operation == walkValidate {
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
return nil
}
if v.Multi && v.Index == -1 {
variable, err := i.computeResourceMultiVariable(scope, v)
if err != nil {
return err
}
if variable == nil {
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
}
result[n] = *variable
} else {
variable, err := i.computeResourceVariable(scope, v)
if err != nil {
return err
}
if variable == nil {
return fmt.Errorf("no error reported by variable %q is nil", v.Name)
}
result[n] = *variable
}
return nil
}
func (i *Interpolater) valueSelfVar(
scope *InterpolationScope,
n string,
v *config.SelfVariable,
result map[string]ast.Variable) error {
if scope == nil || scope.Resource == nil {
return fmt.Errorf(
"%s: invalid scope, self variables are only valid on resources", n)
}
rv, err := config.NewResourceVariable(fmt.Sprintf(
"%s.%s.%d.%s",
scope.Resource.Type,
scope.Resource.Name,
scope.Resource.CountIndex,
v.Field))
if err != nil {
return err
}
return i.valueResourceVar(scope, n, rv, result)
}
func (i *Interpolater) valueSimpleVar(
scope *InterpolationScope,
n string,
v *config.SimpleVariable,
result map[string]ast.Variable) error {
// SimpleVars are never handled by Terraform's interpolator
result[n] = ast.Variable{
Value: config.UnknownVariableValue,
Type: ast.TypeString,
}
return nil
}
func (i *Interpolater) valueUserVar(
scope *InterpolationScope,
n string,
v *config.UserVariable,
result map[string]ast.Variable) error {
i.VariableValuesLock.Lock()
defer i.VariableValuesLock.Unlock()
val, ok := i.VariableValues[v.Name]
if ok {
varValue, err := hil.InterfaceToVariable(val)
if err != nil {
return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
}
result[n] = varValue
return nil
}
if _, ok := result[n]; !ok && i.Operation == walkValidate {
result[n] = unknownVariable()
return nil
}
// Look up if we have any variables with this prefix because
// those are map overrides. Include those.
for k, val := range i.VariableValues {
if strings.HasPrefix(k, v.Name+".") {
keyComponents := strings.Split(k, ".")
overrideKey := keyComponents[len(keyComponents)-1]
mapInterface, ok := result["var."+v.Name]
if !ok {
return fmt.Errorf("override for non-existent variable: %s", v.Name)
}
mapVariable := mapInterface.Value.(map[string]ast.Variable)
varValue, err := hil.InterfaceToVariable(val)
if err != nil {
return fmt.Errorf("cannot convert %s value %q to an ast.Variable for interpolation: %s",
v.Name, val, err)
}
mapVariable[overrideKey] = varValue
}
}
return nil
}
func (i *Interpolater) computeResourceVariable(
scope *InterpolationScope,
v *config.ResourceVariable) (*ast.Variable, error) {
id := v.ResourceId()
if v.Multi {
id = fmt.Sprintf("%s.%d", id, v.Index)
}
i.StateLock.RLock()
defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify
// that it exists and such.
module, _, err := i.resourceVariableInfo(scope, v)
if err != nil {
return nil, err
}
// If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 {
return nil, nil
}
// Get the resource out from the state. We know the state exists
// at this point and if there is a state, we expect there to be a
// resource with the given name.
r, ok := module.Resources[id]
if !ok && v.Multi && v.Index == 0 {
r, ok = module.Resources[v.ResourceId()]
}
if !ok {
r = nil
}
if r == nil {
goto MISSING
}
if r.Primary == nil {
goto MISSING
}
if attr, ok := r.Primary.Attributes[v.Field]; ok {
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
// computed list or map attribute
if _, ok := r.Primary.Attributes[v.Field+".#"]; ok {
variable, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes)
return &variable, err
}
// At apply time, we can't do the "maybe has it" check below
// that we need for plans since parent elements might be computed.
// Therefore, it is an error and we're missing the key.
//
// TODO: test by creating a state and configuration that is referencing
// a non-existent variable "foo.bar" where the state only has "foo"
// and verify plan works, but apply doesn't.
if i.Operation == walkApply || i.Operation == walkDestroy {
goto MISSING
}
// We didn't find the exact field, so lets separate the dots
// and see if anything along the way is a computed set. i.e. if
// we have "foo.0.bar" as the field, check to see if "foo" is
// a computed list. If so, then the whole thing is computed.
if parts := strings.Split(v.Field, "."); len(parts) > 1 {
for i := 1; i < len(parts); i++ {
// Lists and sets make this
key := fmt.Sprintf("%s.#", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok {
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
// Maps make this
key = fmt.Sprintf("%s", strings.Join(parts[:i], "."))
if attr, ok := r.Primary.Attributes[key]; ok {
return &ast.Variable{Type: ast.TypeString, Value: attr}, nil
}
}
}
MISSING:
// Validation for missing interpolations should happen at a higher
// semantic level. If we reached this point and don't have variables,
// just return the computed value.
if scope == nil && scope.Resource == nil {
return &unknownVariable, nil
}
// If the operation is refresh, it isn't an error for a value to
// be unknown. Instead, we return that the value is computed so
// that the graph can continue to refresh other nodes. It doesn't
// matter because the config isn't interpolated anyways.
//
// For a Destroy, we're also fine with computed values, since our goal is
// only to get destroy nodes for existing resources.
//
// For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return &unknownVariable, nil
}
return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
id,
v.Field,
v.FullKey())
}
func (i *Interpolater) computeResourceMultiVariable(
scope *InterpolationScope,
v *config.ResourceVariable) (*ast.Variable, error) {
i.StateLock.RLock()
defer i.StateLock.RUnlock()
unknownVariable := unknownVariable()
// Get the information about this resource variable, and verify
// that it exists and such.
module, cr, err := i.resourceVariableInfo(scope, v)
if err != nil {
return nil, err
}
// Get the count so we know how many to iterate over
count, err := cr.Count()
if err != nil {
return nil, fmt.Errorf(
"Error reading %s count: %s",
v.ResourceId(),
err)
}
// If we have no module in the state yet or count, return empty
if module == nil || len(module.Resources) == 0 || count == 0 {
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
}
var values []string
for j := 0; j < count; j++ {
id := fmt.Sprintf("%s.%d", v.ResourceId(), j)
// If we're dealing with only a single resource, then the
// ID doesn't have a trailing index.
if count == 1 {
id = v.ResourceId()
}
r, ok := module.Resources[id]
if !ok {
continue
}
if r.Primary == nil {
continue
}
if singleAttr, ok := r.Primary.Attributes[v.Field]; ok {
if singleAttr == config.UnknownVariableValue {
return &unknownVariable, nil
}
values = append(values, singleAttr)
continue
}
// computed list attribute
_, ok = r.Primary.Attributes[v.Field+".#"]
if !ok {
continue
}
multiAttr, err := i.interpolateComplexTypeAttribute(v.Field, r.Primary.Attributes)
if err != nil {
return nil, err
}
if multiAttr == unknownVariable {
return &ast.Variable{Type: ast.TypeString, Value: ""}, nil
}
for _, element := range multiAttr.Value.([]ast.Variable) {
strVal := element.Value.(string)
if strVal == config.UnknownVariableValue {
return &unknownVariable, nil
}
values = append(values, strVal)
}
}
if len(values) == 0 {
// If the operation is refresh, it isn't an error for a value to
// be unknown. Instead, we return that the value is computed so
// that the graph can continue to refresh other nodes. It doesn't
// matter because the config isn't interpolated anyways.
//
// For a Destroy, we're also fine with computed values, since our goal is
// only to get destroy nodes for existing resources.
//
// For an input walk, computed values are okay to return because we're only
// looking for missing variables to prompt the user for.
if i.Operation == walkRefresh || i.Operation == walkPlanDestroy || i.Operation == walkDestroy || i.Operation == walkInput {
return &unknownVariable, nil
}
return nil, fmt.Errorf(
"Resource '%s' does not have attribute '%s' "+
"for variable '%s'",
v.ResourceId(),
v.Field,
v.FullKey())
}
variable, err := hil.InterfaceToVariable(values)
return &variable, err
}
func (i *Interpolater) interpolateComplexTypeAttribute(
resourceID string,
attributes map[string]string) (ast.Variable, error) {
attr := attributes[resourceID+".#"]
log.Printf("[DEBUG] Interpolating computed complex type attribute %s (%s)",
resourceID, attr)
// In Terraform's internal dotted representation of list-like attributes, the
// ".#" count field is marked as unknown to indicate "this whole list is
// unknown". We must honor that meaning here so computed references can be
// treated properly during the plan phase.
if attr == config.UnknownVariableValue {
return unknownVariable(), nil
}
// At this stage we don't know whether the item is a list or a map, so we
// examine the keys to see whether they are all numeric.
var numericKeys []string
var allKeys []string
numberedListKey := regexp.MustCompile("^" + resourceID + "\\.[0-9]+$")
otherListKey := regexp.MustCompile("^" + resourceID + "\\.([^#]+)$")
for id, _ := range attributes {
if numberedListKey.MatchString(id) {
numericKeys = append(numericKeys, id)
}
if submatches := otherListKey.FindAllStringSubmatch(id, -1); len(submatches) > 0 {
allKeys = append(allKeys, submatches[0][1])
}
}
if len(numericKeys) == len(allKeys) {
// This is a list
var members []string
for _, key := range numericKeys {
members = append(members, attributes[key])
}
sort.Strings(members)
return hil.InterfaceToVariable(members)
} else {
// This is a map
members := make(map[string]interface{})
for _, key := range allKeys {
members[key] = attributes[resourceID+"."+key]
}
return hil.InterfaceToVariable(members)
}
}
func (i *Interpolater) resourceVariableInfo(
scope *InterpolationScope,
v *config.ResourceVariable) (*ModuleState, *config.Resource, error) {
// Get the module tree that contains our current path. This is
// either the current module (path is empty) or a child.
modTree := i.Module
if len(scope.Path) > 1 {
modTree = i.Module.Child(scope.Path[1:])
}
// Get the resource from the configuration so we can verify
// that the resource is in the configuration and so we can access
// the configuration if we need to.
var cr *config.Resource
for _, r := range modTree.Config().Resources {
if r.Id() == v.ResourceId() {
cr = r
break
}
}
if cr == nil {
return nil, nil, fmt.Errorf(
"Resource '%s' not found for variable '%s'",
v.ResourceId(),
v.FullKey())
}
// Get the relevant module
module := i.State.ModuleByPath(scope.Path)
return module, cr, nil
}