From 679ab1d5153b2d428d90ea2faa13074760f2151a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Oct 2014 18:03:11 -0700 Subject: [PATCH 1/5] config: parse ${path.module} --- config/config.go | 27 +++++++++------ config/config_test.go | 14 ++++++++ config/interpolate.go | 34 +++++++++++++++++++ config/interpolate_test.go | 8 +++++ .../validate-path-var-invalid/main.tf | 3 ++ .../test-fixtures/validate-path-var/main.tf | 3 ++ 6 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 config/test-fixtures/validate-path-var-invalid/main.tf create mode 100644 config/test-fixtures/validate-path-var/main.tf diff --git a/config/config.go b/config/config.go index fae135dec0..67c4432f63 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 e4892a2770..e36a7c25dc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -137,6 +137,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 4b0b30341d..fc8ddea724 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -75,6 +75,20 @@ 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 + PathValueModule +) + // A ResourceVariable is a variable that is referencing the field // of a resource, such as "${aws_instance.foo.ami}" type ResourceVariable struct { @@ -101,6 +115,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 +222,24 @@ 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 "module": + fieldType = PathValueModule + } + + 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 3c4bf7f9a3..9ecbb9a718 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/test-fixtures/validate-path-var-invalid/main.tf b/config/test-fixtures/validate-path-var-invalid/main.tf new file mode 100644 index 0000000000..63c352d981 --- /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 0000000000..1c723ca18a --- /dev/null +++ b/config/test-fixtures/validate-path-var/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "foo" { + foo = "${path.module}" +} From 267d45df865d68628eb4eced1c96445ae2a375e0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Oct 2014 20:00:36 -0700 Subject: [PATCH 2/5] config/module: Can look up Child with Tree.Child --- .../test-fixtures/child/foo/bar/main.tf | 2 ++ config/module/test-fixtures/child/foo/main.tf | 5 +++++ config/module/test-fixtures/child/main.tf | 5 +++++ config/module/tree.go | 18 +++++++++++++++ config/module/tree_test.go | 22 +++++++++++++++++++ 5 files changed, 52 insertions(+) create mode 100644 config/module/test-fixtures/child/foo/bar/main.tf create mode 100644 config/module/test-fixtures/child/foo/main.tf create mode 100644 config/module/test-fixtures/child/main.tf 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 0000000000..df59275018 --- /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 0000000000..548d21b99d --- /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 0000000000..3830637158 --- /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 bb2afc16ec..33afbd2629 100644 --- a/config/module/tree.go +++ b/config/module/tree.go @@ -67,6 +67,24 @@ 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 nil + } + + c := t.Children()[path[0]] + if c == nil { + return nil + } + + if len(path) == 1 { + return c + } + + 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 837519d278..ae718a261e 100644 --- a/config/module/tree_test.go +++ b/config/module/tree_test.go @@ -6,6 +6,28 @@ 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 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")) From 1e00b4386cff39d7f539621c07a3e7e1b7900d68 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Oct 2014 20:02:18 -0700 Subject: [PATCH 3/5] config/module: Child(nil) or empty will return self --- config/module/tree.go | 6 +----- config/module/tree_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/config/module/tree.go b/config/module/tree.go index 33afbd2629..fbc4673176 100644 --- a/config/module/tree.go +++ b/config/module/tree.go @@ -70,7 +70,7 @@ func (t *Tree) Config() *config.Config { // Child returns the child with the given path (by name). func (t *Tree) Child(path []string) *Tree { if len(path) == 0 { - return nil + return t } c := t.Children()[path[0]] @@ -78,10 +78,6 @@ func (t *Tree) Child(path []string) *Tree { return nil } - if len(path) == 1 { - return c - } - return c.Child(path[1:]) } diff --git a/config/module/tree_test.go b/config/module/tree_test.go index ae718a261e..d8a7535225 100644 --- a/config/module/tree_test.go +++ b/config/module/tree_test.go @@ -13,6 +13,20 @@ func TestTreeChild(t *testing.T) { 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") From d714c6f2f1d520e13b170333e040f0582415fd38 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Oct 2014 20:09:30 -0700 Subject: [PATCH 4/5] terraform: test path variables --- config/interpolate.go | 6 +++ terraform/context.go | 21 +++++++++- terraform/context_test.go | 38 +++++++++++++++++++ terraform/terraform_test.go | 15 ++++++++ terraform/test-fixtures/plan-path-var/main.tf | 5 +++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 terraform/test-fixtures/plan-path-var/main.tf diff --git a/config/interpolate.go b/config/interpolate.go index fc8ddea724..e96b6b76cc 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -86,7 +86,9 @@ type PathValueType byte const ( PathValueInvalid PathValueType = iota + PathValueCwd PathValueModule + PathValueRoot ) // A ResourceVariable is a variable that is referencing the field @@ -226,8 +228,12 @@ 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{ diff --git a/terraform/context.go b/terraform/context.go index fa3ff10652..c70634a9a8 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 3d2c1cf3aa..8134dc6744 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 c0ac525370..6aedf64476 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 0000000000..1301256988 --- /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" +} From 770d62e588c306bf8ed48cfa186bf6e2464bd86e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 7 Oct 2014 20:15:08 -0700 Subject: [PATCH 5/5] website: document path variables --- .../docs/configuration/interpolation.html.md | 6 +++++ .../source/docs/modules/create.html.markdown | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 5e43bbd2bd..5679b4ff0a 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 ce0a7f1c4f..bfe12395f9 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.