mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #12660 from hashicorp/f-state-env-var
core: expose terraform.env interpolation var
This commit is contained in:
commit
38fc08306c
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
1
command/test-fixtures/apply-terraform-env/main.tf
Normal file
1
command/test-fixtures/apply-terraform-env/main.tf
Normal file
@ -0,0 +1 @@
|
||||
output "output" { value = "${terraform.env}" }
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 := ""
|
||||
|
@ -63,6 +63,14 @@ func TestNewInterpolatedVariable(t *testing.T) {
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"terraform.env",
|
||||
&TerraformVariable{
|
||||
Field: "env",
|
||||
key: "terraform.env",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range cases {
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
3
terraform/test-fixtures/apply-terraform-env/main.tf
Normal file
3
terraform/test-fixtures/apply-terraform-env/main.tf
Normal file
@ -0,0 +1,3 @@
|
||||
output "output" {
|
||||
value = "${terraform.env}"
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user