diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 4ee704e45..48d8a9f49 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -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(` + +Outputs: + +result_1 = hello +result_3 = hello world + +module.child: + + Outputs: + + result = hello +`) + if got != want { + t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s", got, want) + } +} diff --git a/terraform/eval_local.go b/terraform/eval_local.go new file mode 100644 index 000000000..1b63bf4f4 --- /dev/null +++ b/terraform/eval_local.go @@ -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 +} diff --git a/terraform/eval_local_test.go b/terraform/eval_local_test.go new file mode 100644 index 000000000..47803be5d --- /dev/null +++ b/terraform/eval_local_test.go @@ -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), + ) + } + }) + } + +} diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index 38a90f277..4d7832772 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -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}, diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 22ddce6c8..52ce1e886 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -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, diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index 46c0cbf8c..c497b43b8 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -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{} diff --git a/terraform/node_local.go b/terraform/node_local.go index e6ae140a6..da1564e39 100644 --- a/terraform/node_local.go +++ b/terraform/node_local.go @@ -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, + }, }, }, } diff --git a/terraform/state.go b/terraform/state.go index 0c46194d6..66097cf4c 100644 --- a/terraform/state.go +++ b/terraform/state.go @@ -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. diff --git a/terraform/test-fixtures/apply-local-val/child/child.tf b/terraform/test-fixtures/apply-local-val/child/child.tf new file mode 100644 index 000000000..f7febc42f --- /dev/null +++ b/terraform/test-fixtures/apply-local-val/child/child.tf @@ -0,0 +1,4 @@ + +output "result" { + value = "hello" +} diff --git a/terraform/test-fixtures/apply-local-val/main.tf b/terraform/test-fixtures/apply-local-val/main.tf new file mode 100644 index 000000000..67e6053fe --- /dev/null +++ b/terraform/test-fixtures/apply-local-val/main.tf @@ -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}" +} diff --git a/terraform/test-fixtures/interpolate-local/main.tf b/terraform/test-fixtures/interpolate-local/main.tf new file mode 100644 index 000000000..699667a14 --- /dev/null +++ b/terraform/test-fixtures/interpolate-local/main.tf @@ -0,0 +1,3 @@ +locals { + foo = "..." +}