diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 8f107fcebe20..647d315d16f4 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -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": ` diff --git a/internal/terraform/evaluate_data.go b/internal/terraform/evaluate_data.go index 8fd58d8dd97e..1840b0018943 100644 --- a/internal/terraform/evaluate_data.go +++ b/internal/terraform/evaluate_data.go @@ -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" ) @@ -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{