[Backport][v1.10] for_each inside dynamic blocks can now call provider-defined functions (#3442)

Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
Andrei Ciobanu
2025-10-29 14:13:44 +02:00
committed by GitHub
parent 988f7c5822
commit ce351b0ddd
3 changed files with 113 additions and 0 deletions

View File

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

View File

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

View File

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