Skip to content

Commit b7b7fdc

Browse files
github-actions[bot]jbardinDanielMSchmidt
authored
Backport of Allow inconsistent filesystem function results for provider configuration into v1.14 (#37870)
* allow inconsistent function results for providers Due to the inherently ephemeral nature of provider configuration, inconsistent function results were tolerated while evaluating provider config. This loophole was found to be used by a number of configurations, which took advantage of it to create the equivalent of ephemeral values before they formally existed in the language. In order to work around this, we create a special evaluation scope just for providers, allowing us to override the results check for filesystem functions. I've opted to not further clutter the EvalContext interface since this is intended to be a temporary workaround, and does not contribute to the testing of that interface (the interface is solely for internal unit tests anyway). * provider eval scope test * CHANGELOG --------- Co-authored-by: James Bardin <[email protected]> Co-authored-by: Daniel Schmidt <[email protected]>
1 parent 6fd7563 commit b7b7fdc

File tree

10 files changed

+135
-1
lines changed

10 files changed

+135
-1
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: Allow filesystem functions to return inconsistent results when evaluated within provider configuration
3+
time: 2025-11-03T11:20:34.913068-05:00
4+
custom:
5+
Issue: "37854"

internal/command/apply_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,3 +2901,72 @@ func mustNewDynamicValue(val string, ty cty.Type) plans.DynamicValue {
29012901
}
29022902
return ret
29032903
}
2904+
2905+
func TestProviderInconsistentFileFunc(t *testing.T) {
2906+
// Verify that providers can still accept inconsistent results from
2907+
// filesystem functions. We allow this for backwards compatibility, but
2908+
// ephemeral values should be used in the long-term to allow for controlled
2909+
// changes in values between plan and apply.
2910+
td := t.TempDir()
2911+
planDir := filepath.Join(td, "plan")
2912+
applyDir := filepath.Join(td, "apply")
2913+
testCopyDir(t, testFixturePath("changed-file-func-plan"), planDir)
2914+
testCopyDir(t, testFixturePath("changed-file-func-apply"), applyDir)
2915+
t.Chdir(planDir)
2916+
2917+
p := planVarsFixtureProvider()
2918+
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
2919+
Provider: providers.Schema{
2920+
Body: &configschema.Block{
2921+
Attributes: map[string]*configschema.Attribute{
2922+
"foo": {Type: cty.String, Optional: true},
2923+
},
2924+
},
2925+
},
2926+
ResourceTypes: map[string]providers.Schema{
2927+
"test_instance": {
2928+
Body: &configschema.Block{
2929+
Attributes: map[string]*configschema.Attribute{
2930+
"id": {Type: cty.String, Optional: true, Computed: true},
2931+
},
2932+
},
2933+
},
2934+
},
2935+
}
2936+
2937+
view, done := testView(t)
2938+
c := &PlanCommand{
2939+
Meta: Meta{
2940+
testingOverrides: metaOverridesForProvider(p),
2941+
View: view,
2942+
},
2943+
}
2944+
2945+
args := []string{
2946+
"-out", filepath.Join(applyDir, "planfile"),
2947+
}
2948+
code := c.Run(args)
2949+
output := done(t)
2950+
if code != 0 {
2951+
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
2952+
}
2953+
2954+
t.Chdir(applyDir)
2955+
2956+
view, done = testView(t)
2957+
apply := &ApplyCommand{
2958+
Meta: Meta{
2959+
testingOverrides: metaOverridesForProvider(p),
2960+
Ui: new(cli.MockUi),
2961+
View: view,
2962+
},
2963+
}
2964+
args = []string{
2965+
"planfile",
2966+
}
2967+
code = apply.Run(args)
2968+
output = done(t)
2969+
if code != 0 {
2970+
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
2971+
}
2972+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
apply
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
provider "test" {
2+
foo = file("./data")
3+
}
4+
5+
resource "test_instance" "foo" {
6+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
plan
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
provider "test" {
2+
foo = file("./data")
3+
}
4+
5+
resource "test_instance" "foo" {
6+
}

internal/lang/functions.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ var templateFunctions = collections.NewSetCmp[string](
5252
// Functions returns the set of functions that should be used to when evaluating
5353
// expressions in the receiving scope.
5454
func (s *Scope) Functions() map[string]function.Function {
55+
// For backwards compatibility, filesystem functions are allowed to return
56+
// inconsistent results when called from within a provider configuration, so
57+
// here we override the checks with a noop wrapper. This misbehavior was
58+
// found to be used by a number of configurations, which took advantage of
59+
// it to create the equivalent of ephemeral values before they formally
60+
// existed in the language.
61+
immutableResults := immutableResults
62+
if s.ForProvider {
63+
immutableResults = filesystemNoopWrapper
64+
}
65+
5566
s.funcsLock.Lock()
5667
if s.funcs == nil {
5768
s.funcs = baseFunctions(s.BaseDir)
@@ -468,6 +479,10 @@ func immutableResults(name string, priorResults *FunctionResults) func(fn functi
468479
}
469480
}
470481

482+
func filesystemNoopWrapper(name string, priorResults *FunctionResults) func(fn function.ImplFunc) function.ImplFunc {
483+
return noopWrapper
484+
}
485+
471486
func noopWrapper(fn function.ImplFunc) function.ImplFunc {
472487
return fn
473488
}

internal/lang/scope.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ type Scope struct {
7878
// PlanTimestamp is a timestamp representing when the plan was made. It will
7979
// either have been generated during this operation or read from the plan.
8080
PlanTimestamp time.Time
81+
82+
// ForProvider indicates a special case where a provider configuration is
83+
// being evaluated and can tolerate inconsistent results which are not
84+
// marked as ephemeral.
85+
// FIXME: plan to officially deprecate this workaround.
86+
ForProvider bool
8187
}
8288

8389
// SetActiveExperiments allows a caller to declare that a set of experiments

internal/terraform/eval_context_builtin.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,23 @@ func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema
329329
return val, body, diags
330330
}
331331

332+
// EvaluateBlockForProvider is a workaround to allow providers to access a more
333+
// ephemeral context, where filesystem functions can return inconsistent
334+
// results. Prior to ephemeral values, some configurations were using this
335+
// loophole to inject different credentials between plan and apply. This
336+
// exception is not added to the EvalContext interface, so in order to access
337+
// this workaround the context type must be asserted as BuiltinEvalContext.
338+
func (ctx *BuiltinEvalContext) EvaluateBlockForProvider(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
339+
var diags tfdiags.Diagnostics
340+
scope := ctx.EvaluationScope(self, nil, keyData)
341+
scope.ForProvider = true
342+
body, evalDiags := scope.ExpandBlock(body, schema)
343+
diags = diags.Append(evalDiags)
344+
val, evalDiags := scope.EvalBlock(body, schema)
345+
diags = diags.Append(evalDiags)
346+
return val, body, diags
347+
}
348+
332349
func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) {
333350
scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey)
334351
return scope.EvalExpr(expr, wantType)

internal/terraform/node_provider.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,16 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov
111111
return diags
112112
}
113113

114+
// BuiltinEvalContext contains a workaround for providers to allow
115+
// inconsistent filesystem function results, which can be accepted due to
116+
// the ephemeral nature of a provider configuration.
117+
eval := ctx.EvaluateBlock
118+
if ctx, ok := ctx.(*BuiltinEvalContext); ok {
119+
eval = ctx.EvaluateBlockForProvider
120+
}
121+
114122
configSchema := resp.Provider.Body
115-
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey)
123+
configVal, configBody, evalDiags := eval(configBody, configSchema, nil, EvalDataForNoInstanceKey)
116124
diags = diags.Append(evalDiags)
117125
if evalDiags.HasErrors() {
118126
if config == nil {

0 commit comments

Comments
 (0)