mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
c6e03cba96
Part of the interpolation walk is to detect keys which involve computed values and therefore cannot be resolved at this time. The interplation walker keeps sufficient state to be able to populate the ResourceConfig with a slice of such keys. Previously they didn't take slice indexes into account, so in the following case: ``` "services": []interface{}{ map[string]interface{}{ "elb": "___something computed___", }, map[string]interface{}{ "elb": "___something else computed___", }, map[string]interface{}{ "elb": "not computed", }, } ``` Unknown keys would be populated as follows: ``` services.elb services.elb ``` This is not sufficient information to be useful, as it is impossible to distinguish which of the `services.elb`s are unknown vs not. This commit therefore retains the slice indexes as part of the key for unknown keys - producing for the example above: ``` services.0.elb services.1.elb ```
303 lines
7.0 KiB
Go
303 lines
7.0 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
|
|
if loc == reflectwalk.WalkLoc {
|
|
w.sliceIndex = -1
|
|
}
|
|
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 = -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 w.sliceIndex != -1 {
|
|
w.key = append(w.key, fmt.Sprintf("%d.%s", w.sliceIndex, 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 = 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.removeCurrent()
|
|
return nil
|
|
}
|
|
|
|
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) removeCurrent() {
|
|
// Append the key to the unknown keys
|
|
w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, "."))
|
|
|
|
for i := 1; i <= len(w.cs); i++ {
|
|
c := w.cs[len(w.cs)-i]
|
|
switch c.Kind() {
|
|
case reflect.Map:
|
|
// Zero value so that we delete the map key
|
|
var val reflect.Value
|
|
|
|
// Get the key and delete it
|
|
k := w.csData.(reflect.Value)
|
|
c.SetMapIndex(k, val)
|
|
return
|
|
}
|
|
}
|
|
|
|
panic("No container found for removeCurrent")
|
|
}
|
|
|
|
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))
|
|
}
|