eval Data needs to operate on whole modules
In order to be able to use module values, and handle operations like possibly invalid module indexes in conditional statements, whole modules must always be returned during evaluation.
This commit is contained in:
parent
aeadb8ca90
commit
600d4c3e1f
|
@ -26,7 +26,7 @@ type Data interface {
|
|||
GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetLocalValue(addrs.LocalValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetModuleInstance(addrs.ModuleCallInstance, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetModule(addrs.ModuleCall, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetModuleInstanceOutput(addrs.AbsModuleCallOutput, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics)
|
||||
|
|
|
@ -43,7 +43,7 @@ func (d *dataForTests) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRa
|
|||
return d.LocalValues[addr.Name], nil
|
||||
}
|
||||
|
||||
func (d *dataForTests) GetModuleInstance(addr addrs.ModuleCallInstance, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
func (d *dataForTests) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
|
||||
return d.Modules[addr.String()], nil
|
||||
}
|
||||
|
||||
|
|
121
lang/eval.go
121
lang/eval.go
|
@ -2,8 +2,6 @@ package lang
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/ext/dynblock"
|
||||
|
@ -196,7 +194,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
|||
// that's redundant in the process of populating our values map.
|
||||
dataResources := map[string]map[string]cty.Value{}
|
||||
managedResources := map[string]map[string]cty.Value{}
|
||||
wholeModules := map[string]map[addrs.InstanceKey]cty.Value{}
|
||||
wholeModules := map[string]cty.Value{}
|
||||
moduleOutputs := map[string]map[addrs.InstanceKey]map[string]cty.Value{}
|
||||
inputVariables := map[string]cty.Value{}
|
||||
localValues := map[string]cty.Value{}
|
||||
|
@ -258,9 +256,14 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
|||
|
||||
// This type switch must cover all of the "Referenceable" implementations
|
||||
// in package addrs, however we are removing the possibility of
|
||||
// ResourceInstance beforehand.
|
||||
if addr, ok := rawSubj.(addrs.ResourceInstance); ok {
|
||||
// Instances beforehand.
|
||||
switch addr := rawSubj.(type) {
|
||||
case addrs.ResourceInstance:
|
||||
rawSubj = addr.ContainingResource()
|
||||
case addrs.ModuleCallInstance:
|
||||
rawSubj = addr.Call
|
||||
case addrs.AbsModuleCallOutput:
|
||||
rawSubj = addr.Call.Call
|
||||
}
|
||||
|
||||
switch subj := rawSubj.(type) {
|
||||
|
@ -284,14 +287,10 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
|||
}
|
||||
into[r.Type][r.Name] = val
|
||||
|
||||
case addrs.ModuleCallInstance:
|
||||
val, valDiags := normalizeRefValue(s.Data.GetModuleInstance(subj, rng))
|
||||
case addrs.ModuleCall:
|
||||
val, valDiags := normalizeRefValue(s.Data.GetModule(subj, rng))
|
||||
diags = diags.Append(valDiags)
|
||||
|
||||
if wholeModules[subj.Call.Name] == nil {
|
||||
wholeModules[subj.Call.Name] = make(map[addrs.InstanceKey]cty.Value)
|
||||
}
|
||||
wholeModules[subj.Call.Name][subj.Key] = val
|
||||
wholeModules[subj.Name] = val
|
||||
|
||||
case addrs.AbsModuleCallOutput:
|
||||
val, valDiags := normalizeRefValue(s.Data.GetModuleInstanceOutput(subj, rng))
|
||||
|
@ -347,7 +346,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl
|
|||
vals[k] = v
|
||||
}
|
||||
vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources))
|
||||
vals["module"] = cty.ObjectVal(buildModuleObjects(wholeModules, moduleOutputs))
|
||||
vals["module"] = cty.ObjectVal(wholeModules)
|
||||
vals["var"] = cty.ObjectVal(inputVariables)
|
||||
vals["local"] = cty.ObjectVal(localValues)
|
||||
vals["path"] = cty.ObjectVal(pathAttrs)
|
||||
|
@ -369,102 +368,6 @@ func buildResourceObjects(resources map[string]map[string]cty.Value) map[string]
|
|||
return vals
|
||||
}
|
||||
|
||||
func buildModuleObjects(wholeModules map[string]map[addrs.InstanceKey]cty.Value, moduleOutputs map[string]map[addrs.InstanceKey]map[string]cty.Value) map[string]cty.Value {
|
||||
vals := make(map[string]cty.Value)
|
||||
|
||||
for name, keys := range wholeModules {
|
||||
vals[name] = buildInstanceObjects(keys)
|
||||
}
|
||||
|
||||
for name, keys := range moduleOutputs {
|
||||
if _, exists := wholeModules[name]; exists {
|
||||
// If we also have a whole module value for this name then we'll
|
||||
// skip this since the individual outputs are embedded in that result.
|
||||
continue
|
||||
}
|
||||
|
||||
// The shape of this collection isn't compatible with buildInstanceObjects,
|
||||
// but rather than replicating most of the buildInstanceObjects logic
|
||||
// here we'll instead first transform the structure to be what that
|
||||
// function expects and then use it. This is a little wasteful, but
|
||||
// we do not expect this these maps to be large and so the extra work
|
||||
// here should not hurt too much.
|
||||
flattened := make(map[addrs.InstanceKey]cty.Value, len(keys))
|
||||
for k, vals := range keys {
|
||||
flattened[k] = cty.ObjectVal(vals)
|
||||
}
|
||||
vals[name] = buildInstanceObjects(flattened)
|
||||
}
|
||||
|
||||
return vals
|
||||
}
|
||||
|
||||
func buildInstanceObjects(keys map[addrs.InstanceKey]cty.Value) cty.Value {
|
||||
if val, exists := keys[addrs.NoKey]; exists {
|
||||
// If present, a "no key" value supersedes all other values,
|
||||
// since they should be embedded inside it.
|
||||
return val
|
||||
}
|
||||
|
||||
// If we only have individual values then we need to construct
|
||||
// either a list or a map, depending on what sort of keys we
|
||||
// have.
|
||||
haveInt := false
|
||||
haveString := false
|
||||
maxInt := 0
|
||||
|
||||
for k := range keys {
|
||||
switch tk := k.(type) {
|
||||
case addrs.IntKey:
|
||||
haveInt = true
|
||||
if int(tk) > maxInt {
|
||||
maxInt = int(tk)
|
||||
}
|
||||
case addrs.StringKey:
|
||||
haveString = true
|
||||
}
|
||||
}
|
||||
|
||||
// We should either have ints or strings and not both, but
|
||||
// if we have both then we'll prefer strings and let the
|
||||
// language interpreter try to convert the int keys into
|
||||
// strings in a map.
|
||||
switch {
|
||||
case haveString:
|
||||
vals := make(map[string]cty.Value)
|
||||
for k, v := range keys {
|
||||
switch tk := k.(type) {
|
||||
case addrs.StringKey:
|
||||
vals[string(tk)] = v
|
||||
case addrs.IntKey:
|
||||
sk := strconv.Itoa(int(tk))
|
||||
vals[sk] = v
|
||||
}
|
||||
}
|
||||
return cty.ObjectVal(vals)
|
||||
case haveInt:
|
||||
// We'll make a tuple that is long enough for our maximum
|
||||
// index value. It doesn't matter if we end up shorter than
|
||||
// the number of instances because if length(...) were
|
||||
// being evaluated we would've got a NoKey reference and
|
||||
// thus not ended up in this codepath at all.
|
||||
vals := make([]cty.Value, maxInt+1)
|
||||
for i := range vals {
|
||||
if v, exists := keys[addrs.IntKey(i)]; exists {
|
||||
vals[i] = v
|
||||
} else {
|
||||
// Just a placeholder, since nothing will access this anyway
|
||||
vals[i] = cty.DynamicVal
|
||||
}
|
||||
}
|
||||
return cty.TupleVal(vals)
|
||||
default:
|
||||
// Should never happen because there are no other key types.
|
||||
log.Printf("[ERROR] strange makeInstanceObjects call with no supported key types")
|
||||
return cty.EmptyObjectVal
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) {
|
||||
if diags.HasErrors() {
|
||||
// If there are errors then we will force an unknown result so that
|
||||
|
|
|
@ -178,6 +178,22 @@ func TestScopeEvalContext(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
// at this level, all instance references return the entire resource
|
||||
`null_resource.each["each1"].attr`,
|
||||
map[string]cty.Value{
|
||||
"null_resource": cty.ObjectVal(map[string]cty.Value{
|
||||
"each": cty.ObjectVal(map[string]cty.Value{
|
||||
"each0": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("each0"),
|
||||
}),
|
||||
"each1": cty.ObjectVal(map[string]cty.Value{
|
||||
"attr": cty.StringVal("each1"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
`foo(null_resource.multi, null_resource.multi[1])`,
|
||||
map[string]cty.Value{
|
||||
|
@ -216,11 +232,13 @@ func TestScopeEvalContext(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
},
|
||||
// any module reference returns the entire module
|
||||
{
|
||||
`module.foo.output1`,
|
||||
map[string]cty.Value{
|
||||
"module": cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.ObjectVal(map[string]cty.Value{
|
||||
"output0": cty.StringVal("bar0"),
|
||||
"output1": cty.StringVal("bar1"),
|
||||
}),
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue