Skip to content

Commit 7c928fc

Browse files
terraform: terraform.applying returns true during the apply phase
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 )
1 parent 9dd16a7 commit 7c928fc

2 files changed

Lines changed: 126 additions & 5 deletions

File tree

internal/terraform/context_apply2_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2918,6 +2918,99 @@ resource "test_object" "obj" {
29182918
assertNoErrors(t, diags)
29192919
}
29202920

2921+
func TestContext2Apply_applyingFlag(t *testing.T) {
2922+
// This test is for references to the symbol "terraform.applying", which
2923+
// is an ephemeral value that's true during an apply phase but false in
2924+
// all other phases.
2925+
2926+
m := testModuleInline(t, map[string]string{
2927+
"main.tf": `
2928+
terraform {
2929+
required_providers {
2930+
test = {
2931+
source = "terraform.io/builtin/test"
2932+
}
2933+
}
2934+
2935+
# If this experimental feature becomes stablized and this test
2936+
# is still relevant, consider just removing this opt-in while
2937+
# retaining the rest.
2938+
experiments = [ephemeral_values]
2939+
}
2940+
2941+
provider "test" {
2942+
applying = terraform.applying
2943+
}
2944+
2945+
resource "test_thing" "placeholder" {
2946+
# This is here just to give Terraform a reason to configure
2947+
# the provider.
2948+
}
2949+
`,
2950+
})
2951+
2952+
p := new(testing_provider.MockProvider)
2953+
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
2954+
Provider: providers.Schema{
2955+
Block: &configschema.Block{
2956+
Attributes: map[string]*configschema.Attribute{
2957+
"applying": {
2958+
Type: cty.Bool,
2959+
Required: true,
2960+
},
2961+
},
2962+
},
2963+
},
2964+
ResourceTypes: map[string]providers.Schema{
2965+
"test_thing": {
2966+
Block: &configschema.Block{},
2967+
},
2968+
},
2969+
}
2970+
2971+
ctx := testContext2(t, &ContextOpts{
2972+
Providers: map[addrs.Provider]providers.Factory{
2973+
addrs.NewBuiltInProvider("test"): testProviderFuncFixed(p),
2974+
},
2975+
})
2976+
2977+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
2978+
assertNoErrors(t, diags)
2979+
2980+
if !p.ConfigureProviderCalled {
2981+
t.Fatalf("ConfigureProvider was not called during planning")
2982+
}
2983+
{
2984+
got := p.ConfigureProviderRequest.Config
2985+
want := cty.ObjectVal(map[string]cty.Value{
2986+
"applying": cty.False, // false during the planning phase
2987+
})
2988+
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
2989+
t.Errorf("wrong provider configuration during planning\n%s", diff)
2990+
}
2991+
}
2992+
2993+
// reset the mock provider so we can check it again after apply
2994+
p.ConfigureProviderCalled = false
2995+
p.ConfigureProviderRequest = providers.ConfigureProviderRequest{}
2996+
2997+
_, diags = ctx.Apply(plan, m, &ApplyOpts{})
2998+
assertNoErrors(t, diags)
2999+
3000+
if !p.ConfigureProviderCalled {
3001+
t.Fatalf("ConfigureProvider was not called while applying")
3002+
}
3003+
{
3004+
got := p.ConfigureProviderRequest.Config
3005+
want := cty.ObjectVal(map[string]cty.Value{
3006+
"applying": cty.True, // now true during the apply phase
3007+
})
3008+
if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
3009+
t.Errorf("wrong provider configuration while applying\n%s", diff)
3010+
}
3011+
}
3012+
}
3013+
29213014
func TestContext2Apply_applyTimeVariables(t *testing.T) {
29223015
m := testModuleInline(t, map[string]string{
29233016
"main.tf": `

internal/terraform/evaluate_data.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313

1414
"github.com/hashicorp/terraform/internal/addrs"
1515
"github.com/hashicorp/terraform/internal/didyoumean"
16+
"github.com/hashicorp/terraform/internal/experiments"
17+
"github.com/hashicorp/terraform/internal/lang/marks"
1618
"github.com/hashicorp/terraform/internal/tfdiags"
1719
)
1820

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

105+
// terraform.applying is an ephemeral boolean value that's set to true
106+
// during an apply walk or false in any other situation. This is
107+
// intended to allow, for example, using a more privileged auth role
108+
// in a provider configuration during the apply phase but a more
109+
// constrained role for other situations.
110+
//
111+
// Since this produces an ephemeral value, and ephemeral values are
112+
// currently experimental, this is available only in modules that
113+
// have opted in to the experiment. If this experiment gets stabilized,
114+
// it'll probably be best to incorporate this into the normal codepath
115+
// below, but it's currently handled separately up here so it'll be easier
116+
// to remove if the experiment is unsuccessful.
117+
if addr.Name == "applying" {
118+
modCfg := d.Evaluator.Config.Descendent(d.Module)
119+
if modCfg != nil && modCfg.Module.ActiveExperiments.Has(experiments.EphemeralValues) {
120+
return cty.BoolVal(d.Evaluator.Operation == walkApply).Mark(marks.Ephemeral), nil
121+
}
122+
// If the experiment isn't active then we just fall out to the other
123+
// code below, which will treat this situation just like any other
124+
// invalid attribute name.
125+
//
126+
// If you're here to stabilize the experiment, note also that some
127+
// of the error messages below assume that terraform.workspace is
128+
// the only currently-valid attribute and so will probably need revising
129+
// once terraform.applying is also valid.
130+
}
131+
103132
if d.Evaluator.Meta == nil || d.Evaluator.Meta.Env == "" {
104133
// The absense of an "env" (really: workspace) name suggests that
105134
// we're running in a non-workspace context, such as in a component
106-
// of a stack. terraform.workspace -- and the terraform symbol in
107-
// general -- is a legacy thing from workspaces mode that isn't
108-
// carried forward to stacks, because stack configurations can instead
109-
// vary their behavior based on input variables provided in the
110-
// deployment configuration.
135+
// of a stack. terraform.workspace is a legacy thing from workspaces
136+
// mode that isn't carried forward to stacks, because stack
137+
// configurations can instead vary their behavior based on input
138+
// variables provided in the deployment configuration.
111139
switch addr.Name {
112140
case "workspace":
113141
diags = diags.Append(&hcl.Diagnostic{

0 commit comments

Comments
 (0)