opentofu/config/interpolate_walk.go
Mitchell Hashimoto b3f80b9469
config: maintain slice index accounting for computed keys
Fixes #10075
Fixes #10013

When interpolating, we were only maintaining the last known slice index.
If you had sibling slices then you could lose your slice index when
exiting the slice. The resulting behavior was that no some runs the
computed key would be: "slice.0.attr" and on others would be
"slice.attr", the latter being incorrect.

We now maintain a list of slice indexes so that as we unnest, we
properly restore the old value.

Surprisingly unrelated to the graph but the shadow graph caught this
which is great. :)
2016-11-14 18:33:29 -08:00

278 lines
6.5 KiB
Go

package config
import (
"fmt"
"reflect"
"strings"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/mitchellh/reflectwalk"
)
// interpolationWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically
// execute a callback for an interpolation.
type interpolationWalker struct {
// F is the function to call for every interpolation. It can be nil.
//
// If Replace is true, then the return value of F will be used to
// replace the interpolation.
F interpolationWalkerFunc
Replace bool
// ContextF is an advanced version of F that also receives the
// location of where it is in the structure. This lets you do
// context-aware validation.
ContextF interpolationWalkerContextFunc
key []string
lastValue reflect.Value
loc reflectwalk.Location
cs []reflect.Value
csKey []reflect.Value
csData interface{}
sliceIndex []int
unknownKeys []string
}
// interpolationWalkerFunc is the callback called by interpolationWalk.
// It is called with any interpolation found. It should return a value
// to replace the interpolation with, along with any errors.
//
// If Replace is set to false in interpolationWalker, then the replace
// value can be anything as it will have no effect.
type interpolationWalkerFunc func(ast.Node) (interface{}, error)
// interpolationWalkerContextFunc is called by interpolationWalk if
// ContextF is set. This receives both the interpolation and the location
// where the interpolation is.
//
// This callback can be used to validate the location of the interpolation
// within the configuration.
type interpolationWalkerContextFunc func(reflectwalk.Location, ast.Node)
func (w *interpolationWalker) Enter(loc reflectwalk.Location) error {
w.loc = loc
return nil
}
func (w *interpolationWalker) Exit(loc reflectwalk.Location) error {
w.loc = reflectwalk.None
switch loc {
case reflectwalk.Map:
w.cs = w.cs[:len(w.cs)-1]
case reflectwalk.MapValue:
w.key = w.key[:len(w.key)-1]
w.csKey = w.csKey[:len(w.csKey)-1]
case reflectwalk.Slice:
// Split any values that need to be split
w.splitSlice()
w.cs = w.cs[:len(w.cs)-1]
case reflectwalk.SliceElem:
w.csKey = w.csKey[:len(w.csKey)-1]
w.sliceIndex = w.sliceIndex[:len(w.sliceIndex)-1]
}
return nil
}
func (w *interpolationWalker) Map(m reflect.Value) error {
w.cs = append(w.cs, m)
return nil
}
func (w *interpolationWalker) MapElem(m, k, v reflect.Value) error {
w.csData = k
w.csKey = append(w.csKey, k)
if l := len(w.sliceIndex); l > 0 {
w.key = append(w.key, fmt.Sprintf("%d.%s", w.sliceIndex[l-1], k.String()))
} else {
w.key = append(w.key, k.String())
}
w.lastValue = v
return nil
}
func (w *interpolationWalker) Slice(s reflect.Value) error {
w.cs = append(w.cs, s)
return nil
}
func (w *interpolationWalker) SliceElem(i int, elem reflect.Value) error {
w.csKey = append(w.csKey, reflect.ValueOf(i))
w.sliceIndex = append(w.sliceIndex, i)
return nil
}
func (w *interpolationWalker) Primitive(v reflect.Value) error {
setV := v
// We only care about strings
if v.Kind() == reflect.Interface {
setV = v
v = v.Elem()
}
if v.Kind() != reflect.String {
return nil
}
astRoot, err := hil.Parse(v.String())
if err != nil {
return err
}
// If the AST we got is just a literal string value with the same
// value then we ignore it. We have to check if its the same value
// because it is possible to input a string, get out a string, and
// have it be different. For example: "foo-$${bar}" turns into
// "foo-${bar}"
if n, ok := astRoot.(*ast.LiteralNode); ok {
if s, ok := n.Value.(string); ok && s == v.String() {
return nil
}
}
if w.ContextF != nil {
w.ContextF(w.loc, astRoot)
}
if w.F == nil {
return nil
}
replaceVal, err := w.F(astRoot)
if err != nil {
return fmt.Errorf(
"%s in:\n\n%s",
err, v.String())
}
if w.Replace {
// We need to determine if we need to remove this element
// if the result contains any "UnknownVariableValue" which is
// set if it is computed. This behavior is different if we're
// splitting (in a SliceElem) or not.
remove := false
if w.loc == reflectwalk.SliceElem {
switch typedReplaceVal := replaceVal.(type) {
case string:
if typedReplaceVal == UnknownVariableValue {
remove = true
}
case []interface{}:
if hasUnknownValue(typedReplaceVal) {
remove = true
}
}
} else if replaceVal == UnknownVariableValue {
remove = true
}
if remove {
w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, "."))
}
resultVal := reflect.ValueOf(replaceVal)
switch w.loc {
case reflectwalk.MapKey:
m := w.cs[len(w.cs)-1]
// Delete the old value
var zero reflect.Value
m.SetMapIndex(w.csData.(reflect.Value), zero)
// Set the new key with the existing value
m.SetMapIndex(resultVal, w.lastValue)
// Set the key to be the new key
w.csData = resultVal
case reflectwalk.MapValue:
// If we're in a map, then the only way to set a map value is
// to set it directly.
m := w.cs[len(w.cs)-1]
mk := w.csData.(reflect.Value)
m.SetMapIndex(mk, resultVal)
default:
// Otherwise, we should be addressable
setV.Set(resultVal)
}
}
return nil
}
func (w *interpolationWalker) replaceCurrent(v reflect.Value) {
c := w.cs[len(w.cs)-2]
switch c.Kind() {
case reflect.Map:
// Get the key and delete it
k := w.csKey[len(w.csKey)-1]
c.SetMapIndex(k, v)
}
}
func hasUnknownValue(variable []interface{}) bool {
for _, value := range variable {
if strVal, ok := value.(string); ok {
if strVal == UnknownVariableValue {
return true
}
}
}
return false
}
func (w *interpolationWalker) splitSlice() {
raw := w.cs[len(w.cs)-1]
var s []interface{}
switch v := raw.Interface().(type) {
case []interface{}:
s = v
case []map[string]interface{}:
return
}
split := false
for _, val := range s {
if varVal, ok := val.(ast.Variable); ok && varVal.Type == ast.TypeList {
split = true
}
if _, ok := val.([]interface{}); ok {
split = true
}
}
if !split {
return
}
result := make([]interface{}, 0)
for _, v := range s {
switch val := v.(type) {
case ast.Variable:
switch val.Type {
case ast.TypeList:
elements := val.Value.([]ast.Variable)
for _, element := range elements {
result = append(result, element.Value)
}
default:
result = append(result, val.Value)
}
case []interface{}:
for _, element := range val {
result = append(result, element)
}
default:
result = append(result, v)
}
}
w.replaceCurrent(reflect.ValueOf(result))
}