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).
This commit is contained in:
James Bardin 2025-10-31 11:01:52 -04:00
parent f8ae45cfc8
commit eaf225a871
4 changed files with 47 additions and 1 deletions

View file

@ -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)
@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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 {