diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b78fd2ddc..c762982b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ BUG FIXES: * Fix crash in tofu test when using deprecated outputs ([#3249](https://github.com/opentofu/opentofu/pull/3249)) * Fix missing provider functions when parentheses are used ([#3402](https://github.com/opentofu/opentofu/pull/3402)) +* `for_each` inside `dynamic` blocks can now call provider-defined functions. ([#3429](https://github.com/opentofu/opentofu/issues/3429)) ## 1.10.6 diff --git a/internal/lang/eval.go b/internal/lang/eval.go index 80572f14c7..29ef5bcc67 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -36,6 +36,9 @@ func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body spec := schema.DecoderSpec() traversals := dynblock.ExpandVariablesHCLDec(body, spec) + // using ExpandFunctionsHCLDec to extract strictly the functions that are referenced inside the `dynamic` + // block, since that is what is needed to be injected into the expansion evalCtx for the expansion to work + traversals = append(traversals, filterProviderFunctions(dynblock.ExpandFunctionsHCLDec(body, spec))...) refs, diags := References(s.ParseRef, traversals) ctx, ctxDiags := s.EvalContext(refs) diff --git a/internal/tofu/context_apply2_test.go b/internal/tofu/context_apply2_test.go index e9770aa08b..6e1c8b4fb6 100644 --- a/internal/tofu/context_apply2_test.go +++ b/internal/tofu/context_apply2_test.go @@ -10,6 +10,8 @@ import ( "context" "errors" "fmt" + "math/big" + "strconv" "strings" "sync" "testing" @@ -5625,3 +5627,110 @@ check "http_check" { t.Fatal(diags.Err()) } } + +// TestContext2Apply_callingProviderFunctionFromDynamicBlock checks that a +// provider function can be used by referencing it in a dynamic block inside +// a resource. +func TestContext2Apply_callingProviderFunctionFromDynamicBlock(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "example.com/foo/test" + } + } +} + +locals { + urls = ["foo:80", "bar:81"] +} +resource "test_resource" "res" { + dynamic "allow" { + iterator = item + for_each = { + for z in local.urls : + z => provider::test::extract_port(z) if contains([80, 81], provider::test::extract_port(z)) + } + content { + port = item.value + } + } +} +`, + }) + + p := MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Block: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "allow": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "port": { + Type: cty.Number, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }, + Functions: map[string]providers.FunctionSpec{ + "extract_port": { + Parameters: []providers.FunctionParameterSpec{ + { + Name: "in", + Type: cty.String, + AllowNullValue: false, + AllowUnknownValues: false, + }, + }, + Return: cty.Number, + }, + }, + } + p.CallFunctionFn = func(request providers.CallFunctionRequest) providers.CallFunctionResponse { + // Since there is only a single function defined, we don't want to make this implementation more complex than + // needed, so we have only the implementation for that. + v := request.Arguments[0].AsString() + idx := strings.LastIndex(v, ":") + if idx >= 0 { + v = v[idx+1:] + } + port, err := strconv.ParseFloat(v, 64) + if err != nil { + return providers.CallFunctionResponse{ + Error: err, + } + } + return providers.CallFunctionResponse{ + Result: cty.NumberVal(big.NewFloat(port)), + } + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.MustParseProviderSourceString("example.com/foo/test"): testProviderFuncFixed(&p), + }, + }) + + assertState := func(t *testing.T, s *states.State) { + res := s.Resource(mustAbsResourceAddr("test_resource.res")) + diff := cmp.Diff(`{"allow":[{"port":81},{"port":80}]}`, string(res.Instances[addrs.NoKey].Current.AttrsJSON)) + if diff != "" { + t.Fatalf("wrong expected resource change found (-wanted, +got):\n%s", diff) + } + } + plan, diags := ctx.Plan(context.Background(), m, states.NewState(), SimplePlanOpts(plans.NormalMode, nil)) + assertNoErrors(t, diags) + assertState(t, plan.PlannedState) + + state, diags := ctx.Apply(context.Background(), plan, m) + assertNoErrors(t, diags) + assertState(t, state) +} \ No newline at end of file