Skip to content

Commit

Permalink
core: evaluate locals and return them for interpolation
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed Aug 21, 2017
1 parent 6cdf9f7 commit 2ac7afd
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 4 deletions.
37 changes: 37 additions & 0 deletions terraform/context_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
58 changes: 58 additions & 0 deletions terraform/eval_local.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions terraform/eval_local_test.go
Original file line number Diff line number Diff line change
@@ -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),
)
}
})
}

}
3 changes: 3 additions & 0 deletions terraform/graph_builder_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},

Expand Down
55 changes: 55 additions & 0 deletions terraform/interpolate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions terraform/interpolate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
8 changes: 4 additions & 4 deletions terraform/node_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
}
Expand Down
4 changes: 4 additions & 0 deletions terraform/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions terraform/test-fixtures/apply-local-val/child/child.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

output "result" {
value = "hello"
}
18 changes: 18 additions & 0 deletions terraform/test-fixtures/apply-local-val/main.tf
Original file line number Diff line number Diff line change
@@ -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}"
}
3 changes: 3 additions & 0 deletions terraform/test-fixtures/interpolate-local/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
locals {
foo = "..."
}

0 comments on commit 2ac7afd

Please sign in to comment.