From 786334b6431c67cbd26144c06759eb89471bb1b7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:09:06 -0700 Subject: [PATCH 1/7] config: parse TerraformVariables --- config/interpolate.go | 25 +++++++++++++++++++++++++ config/interpolate_test.go | 8 ++++++++ 2 files changed, 33 insertions(+) diff --git a/config/interpolate.go b/config/interpolate.go index 5867c6333c..bbb3555418 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -84,6 +84,13 @@ type SimpleVariable struct { Key string } +// TerraformVariable is a "terraform."-prefixed variable used to access +// metadata about the Terraform run. +type TerraformVariable struct { + Field string + key string +} + // A UserVariable is a variable that is referencing a user variable // that is inputted from outside the configuration. This looks like // "${var.foo}" @@ -101,6 +108,8 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { return NewPathVariable(v) } else if strings.HasPrefix(v, "self.") { return NewSelfVariable(v) + } else if strings.HasPrefix(v, "terraform.") { + return NewTerraformVariable(v) } else if strings.HasPrefix(v, "var.") { return NewUserVariable(v) } else if strings.HasPrefix(v, "module.") { @@ -278,6 +287,22 @@ func (v *SimpleVariable) GoString() string { return fmt.Sprintf("*%#v", *v) } +func NewTerraformVariable(key string) (*TerraformVariable, error) { + field := key[len("terraform."):] + return &TerraformVariable{ + Field: field, + key: key, + }, nil +} + +func (v *TerraformVariable) FullKey() string { + return v.key +} + +func (v *TerraformVariable) GoString() string { + return fmt.Sprintf("*%#v", *v) +} + func NewUserVariable(key string) (*UserVariable, error) { name := key[len("var."):] elem := "" diff --git a/config/interpolate_test.go b/config/interpolate_test.go index e5224ee0d6..0cdb18b69d 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -63,6 +63,14 @@ func TestNewInterpolatedVariable(t *testing.T) { }, false, }, + { + "terraform.env", + &TerraformVariable{ + Field: "env", + key: "terraform.env", + }, + false, + }, } for i, tc := range cases { From 4e1511c77f1853032aba34d354de62e9b24b549d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:14:27 -0700 Subject: [PATCH 2/7] terraform: interpolate "terraform.env" --- terraform/context.go | 12 ++++++++++++ terraform/interpolate.go | 22 ++++++++++++++++++++++ terraform/interpolate_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/terraform/context.go b/terraform/context.go index beb0804479..15528beed8 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -49,6 +49,7 @@ var ( // ContextOpts are the user-configurable options to create a context with // NewContext. type ContextOpts struct { + Meta *ContextMeta Destroy bool Diff *Diff Hooks []Hook @@ -65,6 +66,14 @@ type ContextOpts struct { UIInput UIInput } +// ContextMeta is metadata about the running context. This is information +// that this package or structure cannot determine on its own but exposes +// into Terraform in various ways. This must be provided by the Context +// initializer. +type ContextMeta struct { + Env string // Env is the state environment +} + // Context represents all the context that Terraform needs in order to // perform operations on infrastructure. This structure is built using // NewContext. See the documentation for that. @@ -80,6 +89,7 @@ type Context struct { diff *Diff diffLock sync.RWMutex hooks []Hook + meta *ContextMeta module *module.Tree sh *stopHook shadow bool @@ -178,6 +188,7 @@ func NewContext(opts *ContextOpts) (*Context, error) { destroy: opts.Destroy, diff: diff, hooks: hooks, + meta: opts.Meta, module: opts.Module, shadow: opts.Shadow, state: state, @@ -313,6 +324,7 @@ func (c *Context) Interpolater() *Interpolater { var stateLock sync.RWMutex return &Interpolater{ Operation: walkApply, + Meta: c.meta, Module: c.module, State: c.state.DeepCopy(), StateLock: &stateLock, diff --git a/terraform/interpolate.go b/terraform/interpolate.go index 11d5a53dcf..993317e9e4 100644 --- a/terraform/interpolate.go +++ b/terraform/interpolate.go @@ -25,6 +25,7 @@ const ( // for interpolations such as `aws_instance.foo.bar`. type Interpolater struct { Operation walkOperation + Meta *ContextMeta Module *module.Tree State *State StateLock *sync.RWMutex @@ -87,6 +88,8 @@ func (i *Interpolater) Values( err = i.valueSelfVar(scope, n, v, result) case *config.SimpleVariable: err = i.valueSimpleVar(scope, n, v, result) + case *config.TerraformVariable: + err = i.valueTerraformVar(scope, n, v, result) case *config.UserVariable: err = i.valueUserVar(scope, n, v, result) default: @@ -309,6 +312,25 @@ func (i *Interpolater) valueSimpleVar( n) } +func (i *Interpolater) valueTerraformVar( + scope *InterpolationScope, + n string, + v *config.TerraformVariable, + result map[string]ast.Variable) error { + if v.Field != "env" { + return fmt.Errorf( + "%s: only supported key for 'terraform.X' interpolations is 'env'", n) + } + + if i.Meta == nil { + return fmt.Errorf( + "%s: internal error: nil Meta. Please report a bug.", n) + } + + result[n] = ast.Variable{Type: ast.TypeString, Value: i.Meta.Env} + return nil +} + func (i *Interpolater) valueUserVar( scope *InterpolationScope, n string, diff --git a/terraform/interpolate_test.go b/terraform/interpolate_test.go index bdadedc4fc..6f1d2c3448 100644 --- a/terraform/interpolate_test.go +++ b/terraform/interpolate_test.go @@ -893,6 +893,33 @@ func TestInterpolater_resourceUnknownVariableList(t *testing.T) { interfaceToVariableSwallowError([]interface{}{})) } +func TestInterpolater_terraformEnv(t *testing.T) { + i := &Interpolater{ + Meta: &ContextMeta{Env: "foo"}, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolate(t, i, scope, "terraform.env", ast.Variable{ + Value: "foo", + Type: ast.TypeString, + }) +} + +func TestInterpolater_terraformInvalid(t *testing.T) { + i := &Interpolater{ + Meta: &ContextMeta{Env: "foo"}, + } + + scope := &InterpolationScope{ + Path: rootModulePath, + } + + testInterpolateErr(t, i, scope, "terraform.nope") +} + func testInterpolate( t *testing.T, i *Interpolater, scope *InterpolationScope, From 9900bd752aaec635a4a5f4a99090519b201bc709 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:21:09 -0700 Subject: [PATCH 3/7] terraform: string through the context meta --- terraform/context_apply_test.go | 30 +++++++++++++++++++ terraform/graph_walk_context.go | 1 + terraform/shadow_context.go | 2 ++ .../test-fixtures/apply-terraform-env/main.tf | 3 ++ 4 files changed, 36 insertions(+) create mode 100644 terraform/test-fixtures/apply-terraform-env/main.tf diff --git a/terraform/context_apply_test.go b/terraform/context_apply_test.go index 04197b97dc..0ad2f3148d 100644 --- a/terraform/context_apply_test.go +++ b/terraform/context_apply_test.go @@ -8069,3 +8069,33 @@ func TestContext2Apply_dataDependsOn(t *testing.T) { t.Fatalf("bad:\n%s", strings.TrimSpace(state.String())) } } + +func TestContext2Apply_terraformEnv(t *testing.T) { + m := testModule(t, "apply-terraform-env") + p := testProvider("aws") + p.ApplyFn = testApplyFn + p.DiffFn = testDiffFn + + ctx := testContext2(t, &ContextOpts{ + Meta: &ContextMeta{Env: "foo"}, + Module: m, + Providers: map[string]ResourceProviderFactory{ + "aws": testProviderFuncFixed(p), + }, + }) + + if _, err := ctx.Plan(); err != nil { + t.Fatalf("err: %s", err) + } + + state, err := ctx.Apply() + if err != nil { + t.Fatalf("err: %s", err) + } + + actual := state.RootModule().Outputs["output"] + expected := "foo" + if actual == nil || actual.Value != expected { + t.Fatalf("bad: \n%s", actual) + } +} diff --git a/terraform/graph_walk_context.go b/terraform/graph_walk_context.go index 19fd47ceb6..e63b460356 100644 --- a/terraform/graph_walk_context.go +++ b/terraform/graph_walk_context.go @@ -84,6 +84,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext { StateLock: &w.Context.stateLock, Interpolater: &Interpolater{ Operation: w.Operation, + Meta: w.Context.meta, Module: w.Context.module, State: w.Context.state, StateLock: &w.Context.stateLock, diff --git a/terraform/shadow_context.go b/terraform/shadow_context.go index 5f7914328e..5588af252c 100644 --- a/terraform/shadow_context.go +++ b/terraform/shadow_context.go @@ -46,6 +46,7 @@ func newShadowContext(c *Context) (*Context, *Context, Shadow) { destroy: c.destroy, diff: c.diff.DeepCopy(), hooks: nil, + meta: c.meta, module: c.module, state: c.state.DeepCopy(), targets: targetRaw.([]string), @@ -77,6 +78,7 @@ func newShadowContext(c *Context) (*Context, *Context, Shadow) { diff: c.diff, // diffLock - no copy hooks: c.hooks, + meta: c.meta, module: c.module, sh: c.sh, state: c.state, diff --git a/terraform/test-fixtures/apply-terraform-env/main.tf b/terraform/test-fixtures/apply-terraform-env/main.tf new file mode 100644 index 0000000000..a5ab886177 --- /dev/null +++ b/terraform/test-fixtures/apply-terraform-env/main.tf @@ -0,0 +1,3 @@ +output "output" { + value = "${terraform.env}" +} From d475fc29a82368eafe43cae3a2f82b0c18c0595c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:25:27 -0700 Subject: [PATCH 4/7] command: test that terraform meta information is passed through --- command/apply_test.go | 89 ++++++++++++++++++- command/meta.go | 5 ++ .../test-fixtures/apply-terraform-env/main.tf | 1 + 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 command/test-fixtures/apply-terraform-env/main.tf diff --git a/command/apply_test.go b/command/apply_test.go index 7f21aa1c2f..65d341293e 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -603,9 +603,7 @@ func TestApply_plan_backup(t *testing.T) { if err != nil { t.Fatal(err) } - - args := []string{ - "-state-out", statePath, + args := []string{"-state-out", statePath, "-backup", backupPath, planPath, } @@ -1531,6 +1529,91 @@ func TestApply_disableBackup(t *testing.T) { } } +// Test that the Terraform env is passed through +func TestApply_terraformEnv(t *testing.T) { + statePath := testTempFile(t) + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + testFixturePath("apply-terraform-env"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + expected := strings.TrimSpace(` + +Outputs: + +output = default + `) + testStateOutput(t, statePath, expected) +} + +// Test that the Terraform env is passed through +func TestApply_terraformEnvNonDefault(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, 0755) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + // Create new env + { + ui := new(cli.MockUi) + newCmd := &EnvNewCommand{} + newCmd.Meta = Meta{Ui: ui} + if code := newCmd.Run([]string{"test"}); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + } + + // Switch to it + { + args := []string{"test"} + ui := new(cli.MockUi) + selCmd := &EnvSelectCommand{} + selCmd.Meta = Meta{Ui: ui} + if code := selCmd.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + } + + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + args := []string{ + testFixturePath("apply-terraform-env"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + statePath := filepath.Join("terraform.tfstate.d", "test", "terraform.tfstate") + expected := strings.TrimSpace(` + +Outputs: + +output = test + `) + testStateOutput(t, statePath, expected) +} + func testHttpServer(t *testing.T) net.Listener { ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { diff --git a/command/meta.go b/command/meta.go index c18c77fc74..0780c544f3 100644 --- a/command/meta.go +++ b/command/meta.go @@ -208,11 +208,16 @@ func (m *Meta) contextOpts() *terraform.ContextOpts { vs[k] = v } opts.Variables = vs + opts.Targets = m.targets opts.UIInput = m.UIInput() opts.Parallelism = m.parallelism opts.Shadow = m.shadow + opts.Meta = &terraform.ContextMeta{ + Env: m.Env(), + } + return &opts } diff --git a/command/test-fixtures/apply-terraform-env/main.tf b/command/test-fixtures/apply-terraform-env/main.tf new file mode 100644 index 0000000000..6fc63dbc55 --- /dev/null +++ b/command/test-fixtures/apply-terraform-env/main.tf @@ -0,0 +1 @@ +output "output" { value = "${terraform.env}" } From e2ca2c5911864fb090b21a36d35c2e294ec29ce0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:38:54 -0700 Subject: [PATCH 5/7] config: allow TerraformVars in count --- config/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/config.go b/config/config.go index bdae585242..bf064e57a8 100644 --- a/config/config.go +++ b/config/config.go @@ -501,10 +501,13 @@ func (c *Config) Validate() error { // Good case *ModuleVariable: case *ResourceVariable: + case *TerraformVariable: case *UserVariable: default: - panic(fmt.Sprintf("Unknown type in count var in %s: %T", n, v)) + errs = append(errs, fmt.Errorf( + "Internal error. Unknown type in count var in %s: %T", + n, v)) } } From 173e8562d49400135a084dcd5e185a78af833a5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:39:05 -0700 Subject: [PATCH 6/7] website: document terraform.env --- .../docs/configuration/interpolation.html.md | 8 ++++- .../source/docs/state/environments.html.md | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/website/source/docs/configuration/interpolation.html.md b/website/source/docs/configuration/interpolation.html.md index 2d1e3052bc..528dda6a5c 100644 --- a/website/source/docs/configuration/interpolation.html.md +++ b/website/source/docs/configuration/interpolation.html.md @@ -84,6 +84,12 @@ 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. +#### Terraform meta information + +The syntax is `terraform.FIELD`. This variable type contains metadata about +the currently executing Terraform run. FIELD can currently only be `env` to +reference the currently active [state environment](/docs/state/environments.html). + ## Conditionals @@ -273,7 +279,7 @@ The supported built-in functions are: * `pathexpand(string)` - Returns a filepath string with `~` expanded to the home directory. Note: This will create a plan diff between two different hosts, unless the filepaths are the same. - + * `replace(string, search, replace)` - Does a search and replace on the given string. All instances of `search` are replaced with the value of `replace`. If `search` is wrapped in forward slashes, it is treated diff --git a/website/source/docs/state/environments.html.md b/website/source/docs/state/environments.html.md index d2323d914d..f96b06d675 100644 --- a/website/source/docs/state/environments.html.md +++ b/website/source/docs/state/environments.html.md @@ -47,6 +47,35 @@ any existing resources that existed on the default (or any other) environment. **These resources still physically exist,** but are managed by another Terraform environment. +## Current Environment Interpolation + +Within your Terraform configuration, you may reference the current environment +using the `${terraform.env}` interpolation variable. This can be used anywhere +interpolations are allowed. + +Referencing the current environment is useful for changing behavior based +on the environment. For example, for non-default environments, it may be useful +to spin up smaller cluster sizes. You can do this: + +``` +resource "aws_instance" "example" { + count = "${terraform.env == "default" ? 5 : 1}" + + # ... other fields +} +``` + +Another popular use case is using the environment as part of naming or +tagging behavior: + +``` +resource "aws_instance" "example" { + tags { Name = "web - ${terraform.env}" } + + # ... other fields +} +``` + ## Best Practices An environment alone **should not** be used to manage the difference between From f7964194eb16c86a9a0792d90413dcb022980f6a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Mar 2017 16:41:33 -0700 Subject: [PATCH 7/7] command: fix odd formatting that snuck in --- command/apply_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/apply_test.go b/command/apply_test.go index 65d341293e..01c230326e 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -603,7 +603,8 @@ func TestApply_plan_backup(t *testing.T) { if err != nil { t.Fatal(err) } - args := []string{"-state-out", statePath, + args := []string{ + "-state-out", statePath, "-backup", backupPath, planPath, }