Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/v1.13/BUG FIXES-20251103-112034.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: Allow filesystem functions to return inconsistent results when evaluated within provider configuration
time: 2025-11-03T11:20:34.913068-05:00
custom:
Issue: "37854"
5 changes: 5 additions & 0 deletions .changes/v1.14/BUG FIXES-20251103-112034.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: Allow filesystem functions to return inconsistent results when evaluated within provider configuration
time: 2025-11-03T11:20:34.913068-05:00
custom:
Issue: "37854"
69 changes: 69 additions & 0 deletions internal/command/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2901,3 +2901,72 @@ func mustNewDynamicValue(val string, ty cty.Type) plans.DynamicValue {
}
return ret
}

func TestProviderInconsistentFileFunc(t *testing.T) {
// Verify that providers can still accept inconsistent results from
// filesystem functions. We allow this for backwards compatibility, but
// ephemeral values should be used in the long-term to allow for controlled
// changes in values between plan and apply.
td := t.TempDir()
planDir := filepath.Join(td, "plan")
applyDir := filepath.Join(td, "apply")
testCopyDir(t, testFixturePath("changed-file-func-plan"), planDir)
testCopyDir(t, testFixturePath("changed-file-func-apply"), applyDir)
t.Chdir(planDir)

p := planVarsFixtureProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
Provider: providers.Schema{
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"foo": {Type: cty.String, Optional: true},
},
},
},
ResourceTypes: map[string]providers.Schema{
"test_instance": {
Body: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {Type: cty.String, Optional: true, Computed: true},
},
},
},
},
}

view, done := testView(t)
c := &PlanCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
View: view,
},
}

args := []string{
"-out", filepath.Join(applyDir, "planfile"),
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
}

t.Chdir(applyDir)

view, done = testView(t)
apply := &ApplyCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(p),
Ui: new(cli.MockUi),
View: view,
},
}
args = []string{
"planfile",
}
code = apply.Run(args)
output = done(t)
if code != 0 {
t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr())
}
}
1 change: 1 addition & 0 deletions internal/command/testdata/changed-file-func-apply/data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
apply
6 changes: 6 additions & 0 deletions internal/command/testdata/changed-file-func-apply/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
provider "test" {
foo = file("./data")
}

resource "test_instance" "foo" {
}
1 change: 1 addition & 0 deletions internal/command/testdata/changed-file-func-plan/data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plan
6 changes: 6 additions & 0 deletions internal/command/testdata/changed-file-func-plan/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
provider "test" {
foo = file("./data")
}

resource "test_instance" "foo" {
}
15 changes: 15 additions & 0 deletions internal/lang/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ var templateFunctions = collections.NewSetCmp[string](
// Functions returns the set of functions that should be used to when evaluating
// expressions in the receiving scope.
func (s *Scope) Functions() map[string]function.Function {
// For backwards compatibility, filesystem functions are allowed to return
// inconsistent results when called from within a provider configuration, so
// here we override the checks with a noop wrapper. This misbehavior 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.
immutableResults := immutableResults
if s.ForProvider {
immutableResults = filesystemNoopWrapper
}

s.funcsLock.Lock()
if s.funcs == nil {
s.funcs = baseFunctions(s.BaseDir)
Expand Down Expand Up @@ -468,6 +479,10 @@ func immutableResults(name string, priorResults *FunctionResults) func(fn functi
}
}

func filesystemNoopWrapper(name string, priorResults *FunctionResults) func(fn function.ImplFunc) function.ImplFunc {
return noopWrapper
}

func noopWrapper(fn function.ImplFunc) function.ImplFunc {
return fn
}
6 changes: 6 additions & 0 deletions internal/lang/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type Scope struct {
// PlanTimestamp is a timestamp representing when the plan was made. It will
// either have been generated during this operation or read from the plan.
PlanTimestamp time.Time

// ForProvider indicates a special case where a provider configuration is
// being evaluated and can tolerate inconsistent results which are not
// marked as ephemeral.
// FIXME: plan to officially deprecate this workaround.
ForProvider bool
}

// SetActiveExperiments allows a caller to declare that a set of experiments
Expand Down
17 changes: 17 additions & 0 deletions internal/terraform/eval_context_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,23 @@ func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema
return val, body, diags
}

// EvaluateBlockForProvider is a workaround to allow providers to access a more
// ephemeral context, where filesystem functions can return inconsistent
// results. Prior to ephemeral values, some configurations were using this
// loophole to inject different credentials between plan and apply. This
// exception is not added to the EvalContext interface, so in order to access
// this workaround the context type must be asserted as BuiltinEvalContext.
func (ctx *BuiltinEvalContext) EvaluateBlockForProvider(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
scope := ctx.EvaluationScope(self, nil, keyData)
scope.ForProvider = true
body, evalDiags := scope.ExpandBlock(body, schema)
diags = diags.Append(evalDiags)
val, evalDiags := scope.EvalBlock(body, schema)
diags = diags.Append(evalDiags)
return val, body, diags
}

func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) {
scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey)
return scope.EvalExpr(expr, wantType)
Expand Down
10 changes: 9 additions & 1 deletion internal/terraform/node_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,16 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov
return diags
}

// BuiltinEvalContext contains a workaround for providers to allow
// inconsistent filesystem function results, which can be accepted due to
// the ephemeral nature of a provider configuration.
eval := ctx.EvaluateBlock
if ctx, ok := ctx.(*BuiltinEvalContext); ok {
eval = ctx.EvaluateBlockForProvider
}

configSchema := resp.Provider.Body
configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey)
configVal, configBody, evalDiags := eval(configBody, configSchema, nil, EvalDataForNoInstanceKey)
diags = diags.Append(evalDiags)
if evalDiags.HasErrors() {
if config == nil {
Expand Down
Loading