Extract the provider functions references from the dynamic blocks (#3429)

Signed-off-by: Andrei Ciobanu <andrei.ciobanu@opentofu.org>
This commit is contained in:
Andrei Ciobanu
2025-10-28 17:10:07 +02:00
committed by GitHub
parent 934be4ad5e
commit af43817e57
2 changed files with 112 additions and 0 deletions

View File

@@ -38,6 +38,9 @@ func (s *Scope) ExpandBlock(ctx context.Context, body hcl.Body, schema *configsc
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)
hclCtx, ctxDiags := s.EvalContext(ctx, refs)

View File

@@ -10,7 +10,9 @@ import (
"context"
"errors"
"fmt"
"math/big"
"slices"
"strconv"
"strings"
"sync"
"testing"
@@ -6569,3 +6571,110 @@ func TestContext2Apply_enabledForModule(t *testing.T) {
}
}
}
// 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, nil)
assertNoErrors(t, diags)
assertState(t, state)
}