diff --git a/config/config.go b/config/config.go index e6cd2a8c3..9d629f578 100644 --- a/config/config.go +++ b/config/config.go @@ -201,17 +201,22 @@ func (c *Config) Validate() error { // Check that all count variables are valid. for source, vs := range vars { - for _, v := range vs { - cv, ok := v.(*CountVariable) - if !ok { - continue - } - - if cv.Type == CountValueInvalid { - errs = append(errs, fmt.Errorf( - "%s: invalid count variable: %s", - source, - cv.FullKey())) + for _, rawV := range vs { + switch v := rawV.(type) { + case *CountVariable: + if v.Type == CountValueInvalid { + errs = append(errs, fmt.Errorf( + "%s: invalid count variable: %s", + source, + v.FullKey())) + } + case *PathVariable: + if v.Type == PathValueInvalid { + errs = append(errs, fmt.Errorf( + "%s: invalid path variable: %s", + source, + v.FullKey())) + } } } } diff --git a/config/config_test.go b/config/config_test.go index f5208b032..0ea8c2892 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -144,6 +144,20 @@ func TestConfigValidate_outputBadField(t *testing.T) { } } +func TestConfigValidate_pathVar(t *testing.T) { + c := testConfig(t, "validate-path-var") + if err := c.Validate(); err != nil { + t.Fatal("err: %s", err) + } +} + +func TestConfigValidate_pathVarInvalid(t *testing.T) { + c := testConfig(t, "validate-path-var-invalid") + if err := c.Validate(); err == nil { + t.Fatal("should not be valid") + } +} + func TestConfigValidate_unknownThing(t *testing.T) { c := testConfig(t, "validate-unknownthing") if err := c.Validate(); err == nil { diff --git a/config/interpolate.go b/config/interpolate.go index 4b0b30341..e96b6b76c 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -75,6 +75,22 @@ type ModuleVariable struct { key string } +// A PathVariable is a variable that references path information about the +// module. +type PathVariable struct { + Type PathValueType + key string +} + +type PathValueType byte + +const ( + PathValueInvalid PathValueType = iota + PathValueCwd + PathValueModule + PathValueRoot +) + // A ResourceVariable is a variable that is referencing the field // of a resource, such as "${aws_instance.foo.ami}" type ResourceVariable struct { @@ -101,6 +117,8 @@ type UserVariable struct { func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { if strings.HasPrefix(v, "count.") { return NewCountVariable(v) + } else if strings.HasPrefix(v, "path.") { + return NewPathVariable(v) } else if strings.HasPrefix(v, "var.") { return NewUserVariable(v) } else if strings.HasPrefix(v, "module.") { @@ -206,6 +224,28 @@ func (v *ModuleVariable) FullKey() string { return v.key } +func NewPathVariable(key string) (*PathVariable, error) { + var fieldType PathValueType + parts := strings.SplitN(key, ".", 2) + switch parts[1] { + case "cwd": + fieldType = PathValueCwd + case "module": + fieldType = PathValueModule + case "root": + fieldType = PathValueRoot + } + + return &PathVariable{ + Type: fieldType, + key: key, + }, nil +} + +func (v *PathVariable) FullKey() string { + return v.key +} + func NewResourceVariable(key string) (*ResourceVariable, error) { parts := strings.SplitN(key, ".", 3) if len(parts) < 3 { diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 3c4bf7f9a..9ecbb9a71 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -45,6 +45,14 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "path.module", + &PathVariable{ + Type: PathValueModule, + key: "path.module", + }, + false, + }, } for i, tc := range cases { diff --git a/config/module/test-fixtures/child/foo/bar/main.tf b/config/module/test-fixtures/child/foo/bar/main.tf new file mode 100644 index 000000000..df5927501 --- /dev/null +++ b/config/module/test-fixtures/child/foo/bar/main.tf @@ -0,0 +1,2 @@ +# Hello + diff --git a/config/module/test-fixtures/child/foo/main.tf b/config/module/test-fixtures/child/foo/main.tf new file mode 100644 index 000000000..548d21b99 --- /dev/null +++ b/config/module/test-fixtures/child/foo/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "bar" { + source = "./bar" +} diff --git a/config/module/test-fixtures/child/main.tf b/config/module/test-fixtures/child/main.tf new file mode 100644 index 000000000..383063715 --- /dev/null +++ b/config/module/test-fixtures/child/main.tf @@ -0,0 +1,5 @@ +# Hello + +module "foo" { + source = "./foo" +} diff --git a/config/module/tree.go b/config/module/tree.go index bb2afc16e..fbc467317 100644 --- a/config/module/tree.go +++ b/config/module/tree.go @@ -67,6 +67,20 @@ func (t *Tree) Config() *config.Config { return t.config } +// Child returns the child with the given path (by name). +func (t *Tree) Child(path []string) *Tree { + if len(path) == 0 { + return t + } + + c := t.Children()[path[0]] + if c == nil { + return nil + } + + return c.Child(path[1:]) +} + // Children returns the children of this tree (the modules that are // imported by this root). // diff --git a/config/module/tree_test.go b/config/module/tree_test.go index 837519d27..d8a753522 100644 --- a/config/module/tree_test.go +++ b/config/module/tree_test.go @@ -6,6 +6,42 @@ import ( "testing" ) +func TestTreeChild(t *testing.T) { + storage := testStorage(t) + tree := NewTree("", testConfig(t, "child")) + if err := tree.Load(storage, GetModeGet); err != nil { + t.Fatalf("err: %s", err) + } + + // Should be able to get the root child + if c := tree.Child([]string{}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "root" { + t.Fatalf("bad: %#v", c.Name()) + } + + // Should be able to get the root child + if c := tree.Child(nil); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "root" { + t.Fatalf("bad: %#v", c.Name()) + } + + // Should be able to get the foo child + if c := tree.Child([]string{"foo"}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "foo" { + t.Fatalf("bad: %#v", c.Name()) + } + + // Should be able to get the nested child + if c := tree.Child([]string{"foo", "bar"}); c == nil { + t.Fatal("should not be nil") + } else if c.Name() != "bar" { + t.Fatalf("bad: %#v", c.Name()) + } +} + func TestTreeLoad(t *testing.T) { storage := testStorage(t) tree := NewTree("", testConfig(t, "basic")) diff --git a/config/test-fixtures/validate-path-var-invalid/main.tf b/config/test-fixtures/validate-path-var-invalid/main.tf new file mode 100644 index 000000000..63c352d98 --- /dev/null +++ b/config/test-fixtures/validate-path-var-invalid/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${path.nope}" +} diff --git a/config/test-fixtures/validate-path-var/main.tf b/config/test-fixtures/validate-path-var/main.tf new file mode 100644 index 000000000..1c723ca18 --- /dev/null +++ b/config/test-fixtures/validate-path-var/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${path.module}" +} diff --git a/terraform/context.go b/terraform/context.go index fa3ff1065..c70634a9a 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -3,6 +3,7 @@ package terraform import ( "fmt" "log" + "os" "sort" "strconv" "strings" @@ -54,7 +55,7 @@ type ContextOpts struct { Provisioners map[string]ResourceProvisionerFactory Variables map[string]string - UIInput UIInput + UIInput UIInput } // NewContext creates a new context. @@ -1437,6 +1438,24 @@ func (c *walkContext) computeVars( } vs[n] = value + case *config.PathVariable: + switch v.Type { + case config.PathValueCwd: + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf( + "Couldn't get cwd for var %s: %s", + v.FullKey(), err) + } + + vs[n] = wd + case config.PathValueModule: + if t := c.Context.module.Child(c.Path[1:]); t != nil { + vs[n] = t.Config().Dir + } + case config.PathValueRoot: + vs[n] = c.Context.module.Config().Dir + } case *config.ResourceVariable: var attr string var err error diff --git a/terraform/context_test.go b/terraform/context_test.go index 3d2c1cf3a..8134dc674 100644 --- a/terraform/context_test.go +++ b/terraform/context_test.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "os" "reflect" "sort" "strings" @@ -2822,6 +2823,43 @@ func TestContextPlan_moduleDestroy(t *testing.T) { } } +func TestContextPlan_pathVar(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + + m := testModule(t, "plan-path-var") + p := testProvider("aws") + p.DiffFn = testDiffFn + ctx := testContext(t, &ContextOpts{ + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + plan, err := ctx.Plan(nil) + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(plan.String()) + expected := strings.TrimSpace(testTerraformPlanPathVarStr) + + // Warning: this ordering REALLY matters for this test. The + // order is: cwd, module, root. + expected = fmt.Sprintf( + expected, + cwd, + m.Config().Dir, + m.Config().Dir) + + if actual != expected { + t.Fatalf("bad:\n%s\n\nexpected:\n\n%s", actual, expected) + } +} + func TestContextPlan_diffVar(t *testing.T) { m := testModule(t, "plan-diffvar") p := testProvider("aws") diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index c0ac52537..6aedf6447 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -875,3 +875,18 @@ STATE: ` + +const testTerraformPlanPathVarStr = ` +DIFF: + +CREATE: aws_instance.foo + cwd: "" => "%s/barpath" + module: "" => "%s/foopath" + root: "" => "%s/barpath" + type: "" => "aws_instance" + +STATE: + + +` + diff --git a/terraform/test-fixtures/plan-path-var/main.tf b/terraform/test-fixtures/plan-path-var/main.tf new file mode 100644 index 000000000..130125698 --- /dev/null +++ b/terraform/test-fixtures/plan-path-var/main.tf @@ -0,0 +1,5 @@ +resource "aws_instance" "foo" { + cwd = "${path.cwd}/barpath" + module = "${path.module}/foopath" + root = "${path.root}/barpath" +} diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 5e43bbd2b..5679b4ff0 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -39,6 +39,12 @@ For example, `${count.index}` will interpolate the current index in a multi-count resource. For more information on count, see the resource configuration page. +**To reference path information**, the syntax is `path.TYPE`. +TYPE can be `cwd`, `module`, or `root`. `cwd` will interpolate the +cwd. `module` will interpolate the path to the current module. `root` +will interpolate the path of the root module. In general, you probably +want the `path.module` variable. + ## Built-in Functions Terraform ships with built-in functions. Functions are called with diff --git a/website/source/docs/modules/create.html.markdown b/website/source/docs/modules/create.html.markdown index ce0a7f1c4..bfe12395f 100644 --- a/website/source/docs/modules/create.html.markdown +++ b/website/source/docs/modules/create.html.markdown @@ -79,6 +79,32 @@ And that is all there is to it. Variables and outputs are used to configure modules and provide results. Resources within a module are isolated, and the whole thing is managed as a single unit. +## Paths and Embedded Files + +It is sometimes useful to embed files within the module that aren't +Terraform configuration files, such as a script to provision a resource +or a file to upload. + +In these cases, you can't use a relative path, since paths in Terraform +are generally relative to the working directory that Terraform was executed +from. Instead, you want to use a module-relative path. To do this, use +the [path interpolated variables](/docs/configuration/interpolation.html). + +An example is shown below: + +``` +resource "aws_instance" "server" { + ... + + provisioner "remote-exec" { + script = "${path.module}/script.sh" + } +} +``` + +In the above, we use `${path.module}` to get a module-relative path. This +is usually what you'll want in any case. + ## Nested Modules You can use a module within a module just like you would anywhere else.