mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-01 11:47:07 -06:00
b3f80b9469
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. :)
278 lines
6.5 KiB
Go
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))
|
|
}
|