Merge pull request #12660 from hashicorp/f-state-env-var

core: expose terraform.env interpolation var
This commit is contained in:
Mitchell Hashimoto 2017-03-14 09:01:31 -07:00 committed by GitHub
commit 38fc08306c
15 changed files with 261 additions and 3 deletions

View File

@ -603,7 +603,6 @@ func TestApply_plan_backup(t *testing.T) {
if err != nil {
t.Fatal(err)
}
args := []string{
"-state-out", statePath,
"-backup", backupPath,
@ -1531,6 +1530,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(`
<no state>
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(`
<no state>
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 {

View File

@ -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
}

View File

@ -0,0 +1 @@
output "output" { value = "${terraform.env}" }

View File

@ -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))
}
}

View File

@ -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 := ""

View File

@ -63,6 +63,14 @@ func TestNewInterpolatedVariable(t *testing.T) {
},
false,
},
{
"terraform.env",
&TerraformVariable{
Field: "env",
key: "terraform.env",
},
false,
},
}
for i, tc := range cases {

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -0,0 +1,3 @@
output "output" {
value = "${terraform.env}"
}

View File

@ -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).
<a id="conditionals"></a>
## 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

View File

@ -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