diff --git a/config/config.go b/config/config.go index 3f756dcf4d85..9549d0a50745 100644 --- a/config/config.go +++ b/config/config.go @@ -34,6 +34,7 @@ type Config struct { ProviderConfigs []*ProviderConfig Resources []*Resource Variables []*Variable + Locals []*Local Outputs []*Output // The fields below can be filled in by loaders for validation @@ -147,7 +148,7 @@ func (p *Provisioner) Copy() *Provisioner { } } -// Variable is a variable defined within the configuration. +// Variable is a module argument defined within the configuration. type Variable struct { Name string DeclaredType string `mapstructure:"type"` @@ -155,6 +156,12 @@ type Variable struct { Description string } +// Local is a local value defined within the configuration. +type Local struct { + Name string + RawConfig *RawConfig +} + // Output is an output defined within the configuration. An output is // resulting data that is highlighted by Terraform when finished. An // output marked Sensitive will be output in a masked form following @@ -680,6 +687,29 @@ func (c *Config) Validate() error { } } + // Check that all locals are valid + { + found := make(map[string]struct{}) + for _, l := range c.Locals { + if _, ok := found[l.Name]; ok { + errs = append(errs, fmt.Errorf( + "%s: duplicate local. local value names must be unique", + l.Name, + )) + continue + } + found[l.Name] = struct{}{} + + for _, v := range l.RawConfig.Variables { + if _, ok := v.(*CountVariable); ok { + errs = append(errs, fmt.Errorf( + "local %s: count variables are only valid within resources", l.Name, + )) + } + } + } + } + // Check that all outputs are valid { found := make(map[string]struct{}) diff --git a/config/config_string.go b/config/config_string.go index 0b3abbcd53a8..b57a09fb5834 100644 --- a/config/config_string.go +++ b/config/config_string.go @@ -148,6 +148,42 @@ func outputsStr(os []*Output) string { return strings.TrimSpace(result) } +func localsStr(ls []*Local) string { + ns := make([]string, 0, len(ls)) + m := make(map[string]*Local) + for _, l := range ls { + ns = append(ns, l.Name) + m[l.Name] = l + } + sort.Strings(ns) + + result := "" + for _, n := range ns { + l := m[n] + + result += fmt.Sprintf("%s\n", n) + + if len(l.RawConfig.Variables) > 0 { + result += fmt.Sprintf(" vars\n") + for _, rawV := range l.RawConfig.Variables { + kind := "unknown" + str := rawV.FullKey() + + switch rawV.(type) { + case *ResourceVariable: + kind = "resource" + case *UserVariable: + kind = "user" + } + + result += fmt.Sprintf(" %s: %s\n", kind, str) + } + } + } + + return strings.TrimSpace(result) +} + // This helper turns a provider configs field into a deterministic // string value for comparison in tests. func providerConfigsStr(pcs []*ProviderConfig) string { diff --git a/config/interpolate.go b/config/interpolate.go index bbb355541858..b18f2d3a110b 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -101,6 +101,12 @@ type UserVariable struct { key string } +// A LocalVariable is a variable that references a local value defined within +// the current module, via a "locals" block. This looks like "${local.foo}". +type LocalVariable struct { + Name string +} + func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { if strings.HasPrefix(v, "count.") { return NewCountVariable(v) @@ -112,6 +118,8 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { return NewTerraformVariable(v) } else if strings.HasPrefix(v, "var.") { return NewUserVariable(v) + } else if strings.HasPrefix(v, "local.") { + return NewLocalVariable(v) } else if strings.HasPrefix(v, "module.") { return NewModuleVariable(v) } else if !strings.ContainsRune(v, '.') { @@ -331,6 +339,25 @@ func (v *UserVariable) GoString() string { return fmt.Sprintf("*%#v", *v) } +func NewLocalVariable(key string) (*LocalVariable, error) { + name := key[len("local."):] + if idx := strings.Index(name, "."); idx > -1 { + return nil, fmt.Errorf("Can't use dot (.) attribute access in local.%s; use square bracket indexing", name) + } + + return &LocalVariable{ + Name: name, + }, nil +} + +func (v *LocalVariable) FullKey() string { + return fmt.Sprintf("local.%s", v.Name) +} + +func (v *LocalVariable) GoString() string { + return fmt.Sprintf("*%#v", *v) +} + // DetectVariables takes an AST root and returns all the interpolated // variables that are detected in the AST tree. func DetectVariables(root ast.Node) ([]InterpolatedVariable, error) { diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 0cdb18b69d8f..db2acf4d2be7 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -9,10 +9,10 @@ import ( ) func TestNewInterpolatedVariable(t *testing.T) { - cases := []struct { - Input string - Result InterpolatedVariable - Error bool + tests := []struct { + Input string + Want InterpolatedVariable + Error bool }{ { "var.foo", @@ -22,6 +22,18 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "local.foo", + &LocalVariable{ + Name: "foo", + }, + false, + }, + { + "local.foo.nope", + nil, + true, + }, { "module.foo.bar", &ModuleVariable{ @@ -73,14 +85,19 @@ func TestNewInterpolatedVariable(t *testing.T) { }, } - for i, tc := range cases { - actual, err := NewInterpolatedVariable(tc.Input) - if err != nil != tc.Error { - t.Fatalf("%d. Error: %s", i, err) - } - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d bad: %#v", i, actual) - } + for i, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, err := NewInterpolatedVariable(test.Input) + if err != nil != test.Error { + t.Errorf("%d. Error: %s", i, err) + } + if !test.Error && !reflect.DeepEqual(got, test.Want) { + t.Errorf( + "wrong result\ninput: %s\ngot: %#v\nwant: %#v", + test.Input, got, test.Want, + ) + } + }) } } diff --git a/config/loader_hcl.go b/config/loader_hcl.go index bcd4d43a4da6..311083744586 100644 --- a/config/loader_hcl.go +++ b/config/loader_hcl.go @@ -37,6 +37,7 @@ func (t *hclConfigurable) Config() (*Config, error) { validKeys := map[string]struct{}{ "atlas": struct{}{}, "data": struct{}{}, + "locals": struct{}{}, "module": struct{}{}, "output": struct{}{}, "provider": struct{}{}, @@ -72,6 +73,15 @@ func (t *hclConfigurable) Config() (*Config, error) { } } + // Build local values + if locals := list.Filter("locals"); len(locals.Items) > 0 { + var err error + config.Locals, err = loadLocalsHcl(locals) + if err != nil { + return nil, err + } + } + // Get Atlas configuration if atlas := list.Filter("atlas"); len(atlas.Items) > 0 { var err error @@ -408,6 +418,59 @@ func loadModulesHcl(list *ast.ObjectList) ([]*Module, error) { return result, nil } +// loadLocalsHcl recurses into the given HCL object turns it into +// a list of locals. +func loadLocalsHcl(list *ast.ObjectList) ([]*Local, error) { + + result := make([]*Local, 0, len(list.Items)) + + for _, block := range list.Items { + if len(block.Keys) > 0 { + return nil, fmt.Errorf( + "locals block at %s should not have label %q", + block.Pos(), block.Keys[0].Token.Value(), + ) + } + + blockObj, ok := block.Val.(*ast.ObjectType) + if !ok { + return nil, fmt.Errorf("locals value at %s should be a block", block.Val.Pos()) + } + + // blockObj now contains directly our local decls + for _, item := range blockObj.List.Items { + if len(item.Keys) != 1 { + return nil, fmt.Errorf("local declaration at %s may not be a block", item.Val.Pos()) + } + + // By the time we get here there can only be one item left, but + // we'll decode into a map anyway because it's a convenient way + // to extract both the key and the value robustly. + kv := map[string]interface{}{} + hcl.DecodeObject(&kv, item) + for k, v := range kv { + rawConfig, err := NewRawConfig(map[string]interface{}{ + "value": v, + }) + + if err != nil { + return nil, fmt.Errorf( + "error parsing local value %q at %s: %s", + k, item.Val.Pos(), err, + ) + } + + result = append(result, &Local{ + Name: k, + RawConfig: rawConfig, + }) + } + } + } + + return result, nil +} + // LoadOutputsHcl recurses into the given HCL object and turns // it into a mapping of outputs. func loadOutputsHcl(list *ast.ObjectList) ([]*Output, error) { diff --git a/config/loader_test.go b/config/loader_test.go index a3aeb7321ebd..30a3f24446be 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -180,17 +180,17 @@ func TestLoadFileBasic(t *testing.T) { } if c.Dir != "" { - t.Fatalf("bad: %#v", c.Dir) + t.Fatalf("wrong dir %#v; want %#v", c.Dir, "") } expectedTF := &Terraform{RequiredVersion: "foo"} if !reflect.DeepEqual(c.Terraform, expectedTF) { - t.Fatalf("bad: %#v", c.Terraform) + t.Fatalf("wrong terraform block %#v; want %#v", c.Terraform, expectedTF) } expectedAtlas := &AtlasConfig{Name: "mitchellh/foo"} if !reflect.DeepEqual(c.Atlas, expectedAtlas) { - t.Fatalf("bad: %#v", c.Atlas) + t.Fatalf("wrong atlas config %#v; want %#v", c.Atlas, expectedAtlas) } actual := variablesStr(c.Variables) @@ -208,6 +208,10 @@ func TestLoadFileBasic(t *testing.T) { t.Fatalf("bad:\n%s", actual) } + if actual, want := localsStr(c.Locals), strings.TrimSpace(basicLocalsStr); actual != want { + t.Fatalf("wrong locals:\n%s\nwant:\n%s", actual, want) + } + actual = outputsStr(c.Outputs) if actual != strings.TrimSpace(basicOutputsStr) { t.Fatalf("bad:\n%s", actual) @@ -288,6 +292,10 @@ func TestLoadFileBasic_json(t *testing.T) { t.Fatalf("bad:\n%s", actual) } + if actual, want := localsStr(c.Locals), strings.TrimSpace(basicLocalsStr); actual != want { + t.Fatalf("wrong locals:\n%s\nwant:\n%s", actual, want) + } + actual = outputsStr(c.Outputs) if actual != strings.TrimSpace(basicOutputsStr) { t.Fatalf("bad:\n%s", actual) @@ -1055,6 +1063,18 @@ web_ip resource: aws_instance.web.private_ip ` +const basicLocalsStr = ` +literal +literal_list +literal_map +security_group_ids + vars + resource: aws_security_group.firewall.*.id +web_ip + vars + resource: aws_instance.web.private_ip +` + const basicProvidersStr = ` aws access_key diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf index aa5a5c6edc77..f64d6a8d5182 100644 --- a/config/test-fixtures/basic.tf +++ b/config/test-fixtures/basic.tf @@ -58,6 +58,17 @@ resource aws_instance "web" { } } +locals { + security_group_ids = "${aws_security_group.firewall.*.id}" + web_ip = "${aws_instance.web.private_ip}" +} + +locals { + literal = 2 + literal_list = ["foo"] + literal_map = {"foo" = "bar"} +} + resource "aws_instance" "db" { security_groups = "${aws_security_group.firewall.*.id}" VPC = "foo" diff --git a/config/test-fixtures/basic.tf.json b/config/test-fixtures/basic.tf.json index be86d5de5a5d..6541beae6d86 100644 --- a/config/test-fixtures/basic.tf.json +++ b/config/test-fixtures/basic.tf.json @@ -79,6 +79,14 @@ } }, + "locals": { + "security_group_ids": "${aws_security_group.firewall.*.id}", + "web_ip": "${aws_instance.web.private_ip}", + "literal": 2, + "literal_list": ["foo"], + "literal_map": {"foo": "bar"} + }, + "output": { "web_ip": { "value": "${aws_instance.web.private_ip}" diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 4ee704e4512d..48d8a9f490b8 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 000000000000..1b63bf4f4f5e --- /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 000000000000..47803be5d9bb --- /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 38a90f2775f4..4d7832772e2a 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/graph_builder_plan.go b/terraform/graph_builder_plan.go index 9c7e4c1dbf22..9d05d4a43e20 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -74,6 +74,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { Module: b.Module, }, + // Add the local values + &LocalTransformer{Module: b.Module}, + // Add the outputs &OutputTransformer{Module: b.Module}, diff --git a/terraform/graph_builder_plan_test.go b/terraform/graph_builder_plan_test.go index 25578ebaf58a..321ddbe3fe13 100644 --- a/terraform/graph_builder_plan_test.go +++ b/terraform/graph_builder_plan_test.go @@ -61,16 +61,22 @@ aws_load_balancer.weblb provider.aws aws_security_group.firewall provider.aws +local.instance_id + aws_instance.web meta.count-boundary (count boundary fixup) aws_instance.web aws_load_balancer.weblb aws_security_group.firewall + local.instance_id openstack_floating_ip.random + output.instance_id provider.aws provider.openstack var.foo openstack_floating_ip.random provider.openstack +output.instance_id + local.instance_id provider.aws openstack_floating_ip.random provider.aws (close) diff --git a/terraform/graph_builder_refresh.go b/terraform/graph_builder_refresh.go index 3d3e968fae9e..beb5a4a4459d 100644 --- a/terraform/graph_builder_refresh.go +++ b/terraform/graph_builder_refresh.go @@ -133,6 +133,9 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer { &ParentProviderTransformer{}, &AttachProviderConfigTransformer{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 22ddce6c836b..52ce1e886aa0 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 46c0cbf8cd27..c497b43b8e01 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 new file mode 100644 index 000000000000..da1564e39045 --- /dev/null +++ b/terraform/node_local.go @@ -0,0 +1,80 @@ +package terraform + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/config" +) + +// NodeLocal represents a named local value in a particular module. +// +// Local value nodes only have one operation, common to all walk types: +// evaluate the result and place it in state. +type NodeLocal struct { + PathValue []string + Config *config.Local +} + +func (n *NodeLocal) Name() string { + result := fmt.Sprintf("local.%s", n.Config.Name) + if len(n.PathValue) > 1 { + result = fmt.Sprintf("%s.%s", modulePrefixStr(n.PathValue), result) + } + + return result +} + +// GraphNodeSubPath +func (n *NodeLocal) Path() []string { + return n.PathValue +} + +// RemovableIfNotTargeted +func (n *NodeLocal) RemoveIfNotTargeted() bool { + return true +} + +// GraphNodeReferenceable +func (n *NodeLocal) ReferenceableName() []string { + name := fmt.Sprintf("local.%s", n.Config.Name) + return []string{name} +} + +// GraphNodeReferencer +func (n *NodeLocal) References() []string { + var result []string + result = append(result, ReferencesFromConfig(n.Config.RawConfig)...) + for _, v := range result { + split := strings.Split(v, "/") + for i, s := range split { + split[i] = s + ".destroy" + } + + result = append(result, strings.Join(split, "/")) + } + + return result +} + +// GraphNodeEvalable +func (n *NodeLocal) EvalTree() EvalNode { + return &EvalOpFilter{ + Ops: []walkOperation{ + walkInput, + walkValidate, + walkRefresh, + walkPlan, + walkApply, + walkDestroy, + }, + Node: &EvalSequence{ + Nodes: []EvalNode{ + &EvalLocal{ + Name: n.Config.Name, + Value: n.Config.RawConfig, + }, + }, + }, + } +} diff --git a/terraform/state.go b/terraform/state.go index 0c46194d6fb5..66097cf4cbd7 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 000000000000..f7febc42f656 --- /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 000000000000..67e6053fefd8 --- /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/graph-builder-plan-basic/main.tf b/terraform/test-fixtures/graph-builder-plan-basic/main.tf index a40802cc98eb..47cf9590b41b 100644 --- a/terraform/test-fixtures/graph-builder-plan-basic/main.tf +++ b/terraform/test-fixtures/graph-builder-plan-basic/main.tf @@ -22,3 +22,11 @@ resource "aws_instance" "web" { resource "aws_load_balancer" "weblb" { members = "${aws_instance.web.id_list}" } + +locals { + instance_id = "${aws_instance.web.id}" +} + +output "instance_id" { + value = "${local.instance_id}" +} diff --git a/terraform/test-fixtures/interpolate-local/main.tf b/terraform/test-fixtures/interpolate-local/main.tf new file mode 100644 index 000000000000..699667a14328 --- /dev/null +++ b/terraform/test-fixtures/interpolate-local/main.tf @@ -0,0 +1,3 @@ +locals { + foo = "..." +} diff --git a/terraform/transform_local.go b/terraform/transform_local.go new file mode 100644 index 000000000000..95ecfc0a440a --- /dev/null +++ b/terraform/transform_local.go @@ -0,0 +1,40 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/config/module" +) + +// LocalTransformer is a GraphTransformer that adds all the local values +// from the configuration to the graph. +type LocalTransformer struct { + Module *module.Tree +} + +func (t *LocalTransformer) Transform(g *Graph) error { + return t.transformModule(g, t.Module) +} + +func (t *LocalTransformer) transformModule(g *Graph, m *module.Tree) error { + if m == nil { + // Can't have any locals if there's no config + return nil + } + + for _, local := range m.Config().Locals { + node := &NodeLocal{ + PathValue: normalizeModulePath(m.Path()), + Config: local, + } + + g.Add(node) + } + + // Also populate locals for child modules + for _, c := range m.Children() { + if err := t.transformModule(g, c); err != nil { + return err + } + } + + return nil +} diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go index c5452354d4b6..ce55e1d77564 100644 --- a/terraform/transform_reference.go +++ b/terraform/transform_reference.go @@ -296,6 +296,8 @@ func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) []string { return []string{fmt.Sprintf("%s.%d/%s.N", id, idx, id)} case *config.UserVariable: return []string{fmt.Sprintf("var.%s", v.Name)} + case *config.LocalVariable: + return []string{fmt.Sprintf("local.%s", v.Name)} default: return nil } diff --git a/website/docs/configuration/locals.html.md b/website/docs/configuration/locals.html.md new file mode 100644 index 000000000000..6067c5486b22 --- /dev/null +++ b/website/docs/configuration/locals.html.md @@ -0,0 +1,59 @@ +--- +layout: "docs" +page_title: "Configuring Local Values" +sidebar_current: "docs-config-locals" +description: |- + Local values assign a name to an expression that can then be used multiple times + within a module. +--- + +# Local Value Configuration + +Local values assign a name to an expression, that can then be used multiple +times within a module. + +Comparing modules to functions in a traditional programming language, +if [variables](./variables.html) are analogous to function arguments and +[outputs](./outputs.html) are analogous to function return values then +_local values_ are comparable to a function's local variables. + +This page assumes you're already familiar with +[the configuration syntax](/docs/configuration/syntax.html). + +## Example + +Local values are defined in `locals` blocks: + +```hcl +# Ids for multiple sets of EC2 instances, merged together +locals { + instance_ids = "${concat(aws_instance.blue.*.id, aws_instance.green.*.id)}" +} + +# A computed default name prefix +locals { + default_name_prefix = "${var.project_name}-web" + name_prefix = "${var.name_prefix != "" ? var.name_prefix : local.default_name_prefix}" +} +``` + +## Description + +The `locals` block defines one or more local variables within a module. +Each `locals` block can have as many locals as needed, and there can be any +number of `locals` blocks within a module. + +The names given for the items in the `locals` block must be unique throughout +a module. The given value can be any expression that is valid within +the current module. + +The expression of a local value can refer to other locals, but as usual +reference cycles are not allowed. That is, a local cannot refer to itself +or to a variable that refers (directly or indirectly) back to it. + +It's recommended to group together logically-related local values into +a single block, particulary if they depend on each other. This will help +the reader understand the relationships between variables. Conversely, +prefer to define _unrelated_ local values in _separate_ blocks, and consider +annotating each block with a comment describing any context common to all +of the enclosed locals. diff --git a/website/layouts/docs.erb b/website/layouts/docs.erb index 89a893545315..5457337f99b6 100644 --- a/website/layouts/docs.erb +++ b/website/layouts/docs.erb @@ -40,6 +40,10 @@ Outputs + > + Local Values + + > Modules