diff --git a/internal/lang/eval/config_validate.go b/internal/lang/eval/config_validate.go index 78ec6fcacd..5761d3b171 100644 --- a/internal/lang/eval/config_validate.go +++ b/internal/lang/eval/config_validate.go @@ -60,7 +60,7 @@ type validationGlue struct { } // ResourceInstanceValue implements evaluationGlue. -func (v *validationGlue) ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance) (cty.Value, tfdiags.Diagnostics) { +func (v *validationGlue) ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { schema, diags := v.providers.ResourceTypeSchema(ctx, ri.Provider, ri.Addr.Resource.Resource.Mode, @@ -72,18 +72,6 @@ func (v *validationGlue) ResourceInstanceValue(ctx context.Context, ri *configgr return cty.DynamicVal, diags } - // During the validation phase we always just use a placeholder value - // based on the config value, inserting unknown values in all of the - // locations where a provider could potentially choose a value during - // the plan or apply phases. - configVal, moreDiags := ri.ConfigValue(ctx) - diags = diags.Append(moreDiags) - if moreDiags.HasErrors() { - // In practice we shouldn't even get called when the config value - // isn't valid, so the following is just for robustness. - return cty.UnknownVal(schema.Block.ImpliedType()), diags - } - // We now have enough information to produce a placeholder "planned new // state" by placing unknown values in any location that the provider // would be allowed to choose a value. diff --git a/internal/lang/eval/evaluation_glue.go b/internal/lang/eval/evaluation_glue.go index 86fdaa5e95..819f7bad9d 100644 --- a/internal/lang/eval/evaluation_glue.go +++ b/internal/lang/eval/evaluation_glue.go @@ -29,5 +29,10 @@ type evaluationGlue interface { // // What "result value" means depends on the phase. For example, during // the planning phase it's the "planned new state". - ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance) (cty.Value, tfdiags.Diagnostics) + // + // The given configVal is the result of calling ConfigValue on the given + // resource instance object, but guaranteed to have already been validated. + // The implementation of this method should not call ConfigValue again + // and should instead just trust the value given as an argument. + ResourceInstanceValue(ctx context.Context, ri *configgraph.ResourceInstance, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) } diff --git a/internal/lang/eval/internal/configgraph/resource.go b/internal/lang/eval/internal/configgraph/resource.go index d5a2fa0ce9..88b1b168e7 100644 --- a/internal/lang/eval/internal/configgraph/resource.go +++ b/internal/lang/eval/internal/configgraph/resource.go @@ -45,7 +45,7 @@ type Resource struct { // GetInstanceResultValue is the callback that child instances of this // resource should use to obtain their result values. - GetInstanceResultValue func(ctx context.Context, inst *ResourceInstance) func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) + GetInstanceResultValue func(ctx context.Context, inst *ResourceInstance) func(ctx context.Context, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) // InstanceSelector represents a rule for deciding which instances of // this resource have been declared. diff --git a/internal/lang/eval/internal/configgraph/resource_instance.go b/internal/lang/eval/internal/configgraph/resource_instance.go index ad8f54607d..7bbca27b1e 100644 --- a/internal/lang/eval/internal/configgraph/resource_instance.go +++ b/internal/lang/eval/internal/configgraph/resource_instance.go @@ -59,7 +59,7 @@ type ResourceInstance struct { // MUST use the mechanisms from package grapheval in order to cooperate // with the self-dependency detection used by this package to prevent // deadlocks. - GetResultValue func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) + GetResultValue func(ctx context.Context, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) } var _ exprs.Valuer = (*ResourceInstance)(nil) @@ -86,6 +86,8 @@ func (ri *ResourceInstance) ConfigValue(ctx context.Context) (cty.Value, tfdiags // Value implements exprs.Valuer. func (ri *ResourceInstance) Value(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + // We use the configuration value here only for its marks, since that + // allows us to propagate any configVal, diags := ri.ConfigValue(ctx) if diags.HasErrors() { // If we don't have a valid config value then we'll stop early @@ -101,9 +103,10 @@ func (ri *ResourceInstance) Value(ctx context.Context) (cty.Value, tfdiags.Diagn // this evaluation in support of. Refer to the documentation of // the GetResultValue field for details on what we're expecting this // function to do. - resultVal, diags := ri.GetResultValue(ctx) - // The result must always be marked with ResourceInstanceMark{ri} so that - // we can detect when another value elsewhere is derived from this one. + resultVal, diags := ri.GetResultValue(ctx, configVal) + + // The result needs some additional preparation to make sure it's + // marked correctly for ongoing use in other expressions. return prepareResourceInstanceResult(resultVal, ri, configVal), diags } diff --git a/internal/lang/eval/module_instance.go b/internal/lang/eval/module_instance.go index b7a26d2fbe..58132ab8eb 100644 --- a/internal/lang/eval/module_instance.go +++ b/internal/lang/eval/module_instance.go @@ -236,7 +236,7 @@ func compileModuleInstanceResources( declScope exprs.Scope, moduleInstanceAddr addrs.ModuleInstance, providers Providers, - getResultValue func(context.Context, *configgraph.ResourceInstance) (cty.Value, tfdiags.Diagnostics), + getResultValue func(context.Context, *configgraph.ResourceInstance, cty.Value) (cty.Value, tfdiags.Diagnostics), ) map[addrs.Resource]*configgraph.Resource { ret := make(map[addrs.Resource]*configgraph.Resource, len(managedConfigs)+len(dataConfigs)+len(ephemeralConfigs)) for _, rc := range managedConfigs { @@ -260,7 +260,7 @@ func compileModuleInstanceResource( declScope exprs.Scope, moduleInstanceAddr addrs.ModuleInstance, providers Providers, - getResultValue func(context.Context, *configgraph.ResourceInstance) (cty.Value, tfdiags.Diagnostics), + getResultValue func(context.Context, *configgraph.ResourceInstance, cty.Value) (cty.Value, tfdiags.Diagnostics), ) (addrs.Resource, *configgraph.Resource) { resourceAddr := config.Addr() absAddr := moduleInstanceAddr.Resource(resourceAddr.Mode, resourceAddr.Type, resourceAddr.Name) @@ -294,9 +294,23 @@ func compileModuleInstanceResource( // Resource implementation calls this during resource instance // compilation to get a resource-instance-specific value fetcher // for each of its instances. - GetInstanceResultValue: func(ctx context.Context, inst *configgraph.ResourceInstance) func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { - return func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { - return getResultValue(ctx, inst) + GetInstanceResultValue: func(ctx context.Context, inst *configgraph.ResourceInstance) func(context.Context, cty.Value) (cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { + schema, moreDiags := providers.ResourceTypeSchema(ctx, config.Provider, resourceAddr.Mode, resourceAddr.Type) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.DynamicVal, diags + } + + moreDiags = providers.ValidateResourceConfig(ctx, config.Provider, resourceAddr.Mode, resourceAddr.Type, configVal) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return cty.UnknownVal(schema.Block.ImpliedType()), diags + } + + // The real getResultValue function can now assume that + // the given configuration is valid. + return getResultValue(ctx, inst, configVal) } }, diff --git a/internal/lang/eval/module_instance_test.go b/internal/lang/eval/module_instance_test.go index 54b8abf784..8a1ae10453 100644 --- a/internal/lang/eval/module_instance_test.go +++ b/internal/lang/eval/module_instance_test.go @@ -15,6 +15,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/lang/grapheval" + "github.com/opentofu/opentofu/internal/providers" ) func TestCompileModuleInstance_valuesOnly(t *testing.T) { @@ -43,6 +44,9 @@ func TestCompileModuleInstance_valuesOnly(t *testing.T) { "a": cty.True, }), evalContext: evalCtx, + evaluationGlue: &validationGlue{ + providers: ProvidersForTesting(map[addrs.Provider]*providers.GetProviderSchemaResponse{}), + }, } inst := compileModuleInstance(ctx, module, addrs.ModuleSourceLocal("."), call) diff --git a/internal/lang/exprs/marks.go b/internal/lang/exprs/marks.go index 2458c57c13..174835ff4e 100644 --- a/internal/lang/exprs/marks.go +++ b/internal/lang/exprs/marks.go @@ -7,6 +7,7 @@ package exprs import ( "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/ctymarks" ) // evaluationMark is the type used for a few cty marks we use to help @@ -78,14 +79,11 @@ func HasEvalErrors(v cty.Value) bool { // this extra complexity to callers that are merely consuming the finalized // results. func WithoutEvalErrorMarks(v cty.Value) cty.Value { - unmarked, pathMarks := v.UnmarkDeepWithPaths() - var filteredPathMarks []cty.PathValueMarks - // Locate EvalError marks and filter them out - for _, pm := range pathMarks { - delete(pm.Marks, EvalError) - if len(pm.Marks) > 0 { - filteredPathMarks = append(filteredPathMarks, pm) + v, _ = v.WrangleMarksDeep(func(mark any, path cty.Path) (ctymarks.WrangleAction, error) { + if mark == EvalError { + return ctymarks.WrangleDrop, nil } - } - return unmarked.MarkWithPaths(filteredPathMarks) + return nil, nil // Leave all other marks alone. + }) + return v }