Skip to content

Commit

Permalink
terraform: terraform.applying returns true during the apply phase
Browse files Browse the repository at this point in the history
This new symbol returns an ephemeral boolean flag that is true only during
the apply phase and false otherwise. Since this involves an ephemeral
value and those are still experimental, this symbol is also available only
when opted in to the ephemeral_values experiment.

The intended use for this is to configure a provider with a different (and
probably more privileged) role or username during the apply phase when
Terraform is actually trying to change infrastructure, so that planning
can be done with a more limited level of access that might, for example,
only allow _reading_ from the remote system.

    role_arn = (
      terraform.applying ? local.write_role_arn : local.read_role_arn
    )
  • Loading branch information
apparentlymart committed Jun 5, 2024
1 parent 9dd16a7 commit 7c928fc
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 5 deletions.
93 changes: 93 additions & 0 deletions internal/terraform/context_apply2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2918,6 +2918,99 @@ resource "test_object" "obj" {
assertNoErrors(t, diags)
}

func TestContext2Apply_applyingFlag(t *testing.T) {
// This test is for references to the symbol "terraform.applying", which
// is an ephemeral value that's true during an apply phase but false in
// all other phases.

m := testModuleInline(t, map[string]string{
"main.tf": `
terraform {
required_providers {
test = {
source = "terraform.io/builtin/test"
}
}
# If this experimental feature becomes stablized and this test
# is still relevant, consider just removing this opt-in while
# retaining the rest.
experiments = [ephemeral_values]
}
provider "test" {
applying = terraform.applying
}
resource "test_thing" "placeholder" {
# This is here just to give Terraform a reason to configure
# the provider.
}
`,
})

p := new(testing_provider.MockProvider)
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"applying": {
Type: cty.Bool,
Required: true,
},
},
},
},
ResourceTypes: map[string]providers.Schema{
"test_thing": {
Block: &configschema.Block{},
},
},
}

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("test"): testProviderFuncFixed(p),
},
})

plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
assertNoErrors(t, diags)

if !p.ConfigureProviderCalled {
t.Fatalf("ConfigureProvider was not called during planning")
}
{
got := p.ConfigureProviderRequest.Config
want := cty.ObjectVal(map[string]cty.Value{
"applying": cty.False, // false during the planning phase
})
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong provider configuration during planning\n%s", diff)
}
}

// reset the mock provider so we can check it again after apply
p.ConfigureProviderCalled = false
p.ConfigureProviderRequest = providers.ConfigureProviderRequest{}

_, diags = ctx.Apply(plan, m, &ApplyOpts{})
assertNoErrors(t, diags)

if !p.ConfigureProviderCalled {
t.Fatalf("ConfigureProvider was not called while applying")
}
{
got := p.ConfigureProviderRequest.Config
want := cty.ObjectVal(map[string]cty.Value{
"applying": cty.True, // now true during the apply phase
})
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong provider configuration while applying\n%s", diff)
}
}
}

func TestContext2Apply_applyTimeVariables(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
Expand Down
38 changes: 33 additions & 5 deletions internal/terraform/evaluate_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (

"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/didyoumean"
"github.com/hashicorp/terraform/internal/experiments"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/tfdiags"
)

Expand Down Expand Up @@ -100,14 +102,40 @@ func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRang
func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics

// terraform.applying is an ephemeral boolean value that's set to true
// during an apply walk or false in any other situation. This is
// intended to allow, for example, using a more privileged auth role
// in a provider configuration during the apply phase but a more
// constrained role for other situations.
//
// Since this produces an ephemeral value, and ephemeral values are
// currently experimental, this is available only in modules that
// have opted in to the experiment. If this experiment gets stabilized,
// it'll probably be best to incorporate this into the normal codepath
// below, but it's currently handled separately up here so it'll be easier
// to remove if the experiment is unsuccessful.
if addr.Name == "applying" {
modCfg := d.Evaluator.Config.Descendent(d.Module)
if modCfg != nil && modCfg.Module.ActiveExperiments.Has(experiments.EphemeralValues) {
return cty.BoolVal(d.Evaluator.Operation == walkApply).Mark(marks.Ephemeral), nil
}
// If the experiment isn't active then we just fall out to the other
// code below, which will treat this situation just like any other
// invalid attribute name.
//
// If you're here to stabilize the experiment, note also that some
// of the error messages below assume that terraform.workspace is
// the only currently-valid attribute and so will probably need revising
// once terraform.applying is also valid.
}

if d.Evaluator.Meta == nil || d.Evaluator.Meta.Env == "" {
// The absense of an "env" (really: workspace) name suggests that
// we're running in a non-workspace context, such as in a component
// of a stack. terraform.workspace -- and the terraform symbol in
// general -- is a legacy thing from workspaces mode that isn't
// carried forward to stacks, because stack configurations can instead
// vary their behavior based on input variables provided in the
// deployment configuration.
// of a stack. terraform.workspace is a legacy thing from workspaces
// mode that isn't carried forward to stacks, because stack
// configurations can instead vary their behavior based on input
// variables provided in the deployment configuration.
switch addr.Name {
case "workspace":
diags = diags.Append(&hcl.Diagnostic{
Expand Down

0 comments on commit 7c928fc

Please sign in to comment.