core: evaluate locals and return them for interpolation
We stash the locals in the module state in a map that is ignored for JSON serialization. We don't include locals in the persisted state because they can be trivially recomputed and this allows us to assume that they will pass through verbatim, without any normalization or other transforms caused by the JSON serialization. From a user standpoint a local is just a named alias for an expression, so it's desirable that the result passes through here in as raw a form as possible, so it behaves as closely as possible to simply using the given expression directly.
This commit is contained in:
parent
5b66953d1d
commit
3a30bfe845
|
@ -8761,3 +8761,40 @@ module.child.subchild:
|
|||
type = aws_instance
|
||||
`)
|
||||
}
|
||||
|
||||
func TestContext2Apply_localVal(t *testing.T) {
|
||||
m := testModule(t, "apply-local-val")
|
||||
ctx := testContext2(t, &ContextOpts{
|
||||
Module: m,
|
||||
ProviderResolver: ResourceProviderResolverFixed(
|
||||
map[string]ResourceProviderFactory{},
|
||||
),
|
||||
})
|
||||
|
||||
if _, err := ctx.Plan(); err != nil {
|
||||
t.Fatalf("error during plan: %s", err)
|
||||
}
|
||||
|
||||
state, err := ctx.Apply()
|
||||
if err != nil {
|
||||
t.Fatalf("error during apply: %s", err)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(state.String())
|
||||
want := strings.TrimSpace(`
|
||||
<no state>
|
||||
Outputs:
|
||||
|
||||
result_1 = hello
|
||||
result_3 = hello world
|
||||
|
||||
module.child:
|
||||
<no state>
|
||||
Outputs:
|
||||
|
||||
result = hello
|
||||
`)
|
||||
if got != want {
|
||||
t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
)
|
||||
|
||||
// EvalLocal is an EvalNode implementation that evaluates the
|
||||
// expression for a local value and writes it into a transient part of
|
||||
// the state.
|
||||
type EvalLocal struct {
|
||||
Name string
|
||||
Value *config.RawConfig
|
||||
}
|
||||
|
||||
func (n *EvalLocal) Eval(ctx EvalContext) (interface{}, error) {
|
||||
cfg, err := ctx.Interpolate(n.Value, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("local.%s: %s", n.Name, err)
|
||||
}
|
||||
|
||||
state, lock := ctx.State()
|
||||
if state == nil {
|
||||
return nil, fmt.Errorf("cannot write local value to nil state")
|
||||
}
|
||||
|
||||
// Get a write lock so we can access the state
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
// Look for the module state. If we don't have one, create it.
|
||||
mod := state.ModuleByPath(ctx.Path())
|
||||
if mod == nil {
|
||||
mod = state.AddModule(ctx.Path())
|
||||
}
|
||||
|
||||
// Get the value from the config
|
||||
var valueRaw interface{} = config.UnknownVariableValue
|
||||
if cfg != nil {
|
||||
var ok bool
|
||||
valueRaw, ok = cfg.Get("value")
|
||||
if !ok {
|
||||
valueRaw = ""
|
||||
}
|
||||
if cfg.IsComputed("value") {
|
||||
valueRaw = config.UnknownVariableValue
|
||||
}
|
||||
}
|
||||
|
||||
if mod.Locals == nil {
|
||||
// initialize
|
||||
mod.Locals = map[string]interface{}{}
|
||||
}
|
||||
mod.Locals[n.Name] = valueRaw
|
||||
|
||||
return nil, nil
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/terraform/config"
|
||||
)
|
||||
|
||||
func TestEvalLocal_impl(t *testing.T) {
|
||||
var _ EvalNode = new(EvalLocal)
|
||||
}
|
||||
|
||||
func TestEvalLocal(t *testing.T) {
|
||||
tests := []struct {
|
||||
Value string
|
||||
Want interface{}
|
||||
Err bool
|
||||
}{
|
||||
{
|
||||
"hello!",
|
||||
"hello!",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Value, func(t *testing.T) {
|
||||
rawConfig, err := config.NewRawConfig(map[string]interface{}{
|
||||
"value": test.Value,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
n := &EvalLocal{
|
||||
Name: "foo",
|
||||
Value: rawConfig,
|
||||
}
|
||||
ctx := &MockEvalContext{
|
||||
StateState: &State{},
|
||||
StateLock: &sync.RWMutex{},
|
||||
|
||||
InterpolateConfigResult: testResourceConfig(t, map[string]interface{}{
|
||||
"value": test.Want,
|
||||
}),
|
||||
}
|
||||
|
||||
_, err = n.Eval(ctx)
|
||||
if (err != nil) != test.Err {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %s", err)
|
||||
} else {
|
||||
t.Errorf("successful Eval; want error")
|
||||
}
|
||||
}
|
||||
|
||||
ms := ctx.StateState.ModuleByPath([]string{})
|
||||
gotLocals := ms.Locals
|
||||
wantLocals := map[string]interface{}{
|
||||
"foo": test.Want,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotLocals, wantLocals) {
|
||||
t.Errorf(
|
||||
"wrong locals after Eval\ngot: %swant: %s",
|
||||
spew.Sdump(gotLocals), spew.Sdump(wantLocals),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
|
@ -108,6 +108,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
|
|||
// Add root variables
|
||||
&RootVariableTransformer{Module: b.Module},
|
||||
|
||||
// Add the local values
|
||||
&LocalTransformer{Module: b.Module},
|
||||
|
||||
// Add the outputs
|
||||
&OutputTransformer{Module: b.Module},
|
||||
|
||||
|
|
|
@ -90,6 +90,8 @@ func (i *Interpolater) Values(
|
|||
err = i.valueSimpleVar(scope, n, v, result)
|
||||
case *config.TerraformVariable:
|
||||
err = i.valueTerraformVar(scope, n, v, result)
|
||||
case *config.LocalVariable:
|
||||
err = i.valueLocalVar(scope, n, v, result)
|
||||
case *config.UserVariable:
|
||||
err = i.valueUserVar(scope, n, v, result)
|
||||
default:
|
||||
|
@ -335,6 +337,59 @@ func (i *Interpolater) valueTerraformVar(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (i *Interpolater) valueLocalVar(
|
||||
scope *InterpolationScope,
|
||||
n string,
|
||||
v *config.LocalVariable,
|
||||
result map[string]ast.Variable,
|
||||
) error {
|
||||
i.StateLock.RLock()
|
||||
defer i.StateLock.RUnlock()
|
||||
|
||||
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 cl *config.Local
|
||||
for _, l := range modTree.Config().Locals {
|
||||
if l.Name == v.Name {
|
||||
cl = l
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cl == nil {
|
||||
return fmt.Errorf("%s: no local value of this name has been declared", n)
|
||||
}
|
||||
|
||||
// Get the relevant module
|
||||
module := i.State.ModuleByPath(scope.Path)
|
||||
if module == nil {
|
||||
result[n] = unknownVariable()
|
||||
return nil
|
||||
}
|
||||
|
||||
rawV, exists := module.Locals[v.Name]
|
||||
if !exists {
|
||||
result[n] = unknownVariable()
|
||||
return nil
|
||||
}
|
||||
|
||||
varV, err := hil.InterfaceToVariable(rawV)
|
||||
if err != nil {
|
||||
// Should never happen, since interpolation should always produce
|
||||
// something we can feed back in to interpolation.
|
||||
return fmt.Errorf("%s: %s", n, err)
|
||||
}
|
||||
|
||||
result[n] = varV
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Interpolater) valueUserVar(
|
||||
scope *InterpolationScope,
|
||||
n string,
|
||||
|
|
|
@ -100,6 +100,35 @@ func TestInterpolater_moduleVariable(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestInterpolater_localVal(t *testing.T) {
|
||||
lock := new(sync.RWMutex)
|
||||
state := &State{
|
||||
Modules: []*ModuleState{
|
||||
&ModuleState{
|
||||
Path: rootModulePath,
|
||||
Locals: map[string]interface{}{
|
||||
"foo": "hello!",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
i := &Interpolater{
|
||||
Module: testModule(t, "interpolate-local"),
|
||||
State: state,
|
||||
StateLock: lock,
|
||||
}
|
||||
|
||||
scope := &InterpolationScope{
|
||||
Path: rootModulePath,
|
||||
}
|
||||
|
||||
testInterpolate(t, i, scope, "local.foo", ast.Variable{
|
||||
Value: "hello!",
|
||||
Type: ast.TypeString,
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterpolater_pathCwd(t *testing.T) {
|
||||
i := &Interpolater{}
|
||||
scope := &InterpolationScope{}
|
||||
|
|
|
@ -70,10 +70,10 @@ func (n *NodeLocal) EvalTree() EvalNode {
|
|||
},
|
||||
Node: &EvalSequence{
|
||||
Nodes: []EvalNode{
|
||||
/*&EvalWriteLocal{
|
||||
Name: n.Config.Name,
|
||||
Value: n.Config.RawConfig,
|
||||
},*/
|
||||
&EvalLocal{
|
||||
Name: n.Config.Name,
|
||||
Value: n.Config.RawConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -977,6 +977,10 @@ type ModuleState struct {
|
|||
// always disjoint, so the path represents amodule tree
|
||||
Path []string `json:"path"`
|
||||
|
||||
// Locals are kept only transiently in-memory, because we can always
|
||||
// re-compute them.
|
||||
Locals map[string]interface{} `json:"-"`
|
||||
|
||||
// Outputs declared by the module and maintained for each module
|
||||
// even though only the root module technically needs to be kept.
|
||||
// This allows operators to inspect values at the boundaries.
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
output "result" {
|
||||
value = "hello"
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
|
||||
module "child" {
|
||||
source = "./child"
|
||||
}
|
||||
|
||||
locals {
|
||||
result_1 = "${module.child.result}"
|
||||
result_2 = "${local.result_1}"
|
||||
result_3 = "${local.result_2} world"
|
||||
}
|
||||
|
||||
output "result_1" {
|
||||
value = "${local.result_1}"
|
||||
}
|
||||
|
||||
output "result_3" {
|
||||
value = "${local.result_3}"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
locals {
|
||||
foo = "..."
|
||||
}
|
Loading…
Reference in New Issue