diff --git a/CHANGELOG.md b/CHANGELOG.md index 8309f0872e..fcd976c6cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ BUG FIXES: * Fix inmem backend crash due to missing struct field ([#1619](https://github.com/opentofu/opentofu/pull/1619)) * Added a check in the `tofu test` to validate that the names of test run blocks do not contain spaces. ([#1489](https://github.com/opentofu/opentofu/pull/1489)) * `tofu test` now supports accessing module outputs when the module has no resources. ([#1409](https://github.com/opentofu/opentofu/pull/1409)) +* Fixed support for provider functions in tests ([#1603](https://github.com/opentofu/opentofu/pull/1603)) ## Previous Releases diff --git a/internal/tofu/context_functions.go b/internal/tofu/context_functions.go index 78ca6f54c4..b5b03ca7c9 100644 --- a/internal/tofu/context_functions.go +++ b/internal/tofu/context_functions.go @@ -15,7 +15,7 @@ import ( // This builds a provider function using an EvalContext and some additional information // This is split out of BuiltinEvalContext for testing -func evalContextProviderFunction(ctx EvalContext, mc *configs.Config, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { +func evalContextProviderFunction(providers func(addrs.AbsProviderConfig) providers.Interface, mc *configs.Config, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics pr, ok := mc.Module.ProviderRequirements.RequiredProviders[pf.ProviderName] @@ -35,7 +35,7 @@ func evalContextProviderFunction(ctx EvalContext, mc *configs.Config, op walkOpe Alias: pf.ProviderAlias, } - provider := ctx.Provider(absPc) + provider := providers(absPc) if provider == nil { // Configured provider (NodeApplyableProvider) not required via transform_provider.go. Instead we should use the unconfigured instance (NodeEvalableProvider) in the root. @@ -59,7 +59,7 @@ func evalContextProviderFunction(ctx EvalContext, mc *configs.Config, op walkOpe } } - provider = ctx.Provider(addrs.AbsProviderConfig{Provider: pr.Type}) + provider = providers(addrs.AbsProviderConfig{Provider: pr.Type}) if provider == nil { // This should not be possible return nil, diags.Append(&hcl.Diagnostic{ diff --git a/internal/tofu/context_functions_test.go b/internal/tofu/context_functions_test.go index 113d97c250..fc1b735247 100644 --- a/internal/tofu/context_functions_test.go +++ b/internal/tofu/context_functions_test.go @@ -134,7 +134,7 @@ func TestFunctions(t *testing.T) { } // Provider missing - _, diags := evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::invalid::unknown"), rng) + _, diags := evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::invalid::unknown"), rng) if !diags.HasErrors() { t.Fatal("expected unknown function provider") } @@ -143,7 +143,7 @@ func TestFunctions(t *testing.T) { } // Provider not initialized - _, diags = evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) + _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) if !diags.HasErrors() { t.Fatal("expected unknown function provider") } @@ -156,7 +156,7 @@ func TestFunctions(t *testing.T) { // Function missing (validate) mockProvider.GetFunctionsCalled = false - _, diags = evalContextProviderFunction(mockCtx, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) + _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -166,7 +166,7 @@ func TestFunctions(t *testing.T) { // Function missing (Non-validate) mockProvider.GetFunctionsCalled = false - _, diags = evalContextProviderFunction(mockCtx, cfg, walkPlan, providerFunc("provider::mockname::missing"), rng) + _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkPlan, providerFunc("provider::mockname::missing"), rng) if !diags.HasErrors() { t.Fatal("expected unknown function") } @@ -188,7 +188,7 @@ func TestFunctions(t *testing.T) { // Load functions into ctx for _, fn := range []string{"echo", "concat", "coalesce", "unknown_param", "error_param"} { pf := providerFunc("provider::mockname::" + fn) - impl, diags := evalContextProviderFunction(mockCtx, cfg, walkPlan, pf, rng) + impl, diags := evalContextProviderFunction(mockCtx.Provider, cfg, walkPlan, pf, rng) if diags.HasErrors() { t.Fatal(diags.Err()) } diff --git a/internal/tofu/eval_context_builtin.go b/internal/tofu/eval_context_builtin.go index 35cb36b5c4..53db8bf7fb 100644 --- a/internal/tofu/eval_context_builtin.go +++ b/internal/tofu/eval_context_builtin.go @@ -526,7 +526,7 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source } scope := ctx.Evaluator.Scope(data, self, source, func(pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { - return evalContextProviderFunction(ctx, mc, ctx.Evaluator.Operation, pf, rng) + return evalContextProviderFunction(ctx.Provider, mc, ctx.Evaluator.Operation, pf, rng) }) scope.SetActiveExperiments(mc.Module.ActiveExperiments) diff --git a/internal/tofu/test_context.go b/internal/tofu/test_context.go index 758b9932b9..d5ee37cf30 100644 --- a/internal/tofu/test_context.go +++ b/internal/tofu/test_context.go @@ -7,17 +7,20 @@ package tofu import ( "fmt" + "log" "sync" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/lang" "github.com/opentofu/opentofu/internal/moduletest" "github.com/opentofu/opentofu/internal/plans" + "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/states" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -99,11 +102,50 @@ func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.Changes Operation: operation, } + var providerInstanceLock sync.Mutex + providerInstances := make(map[addrs.Provider]providers.Interface) + defer func() { + for addr, inst := range providerInstances { + log.Printf("[INFO] Shutting down test provider %s", addr) + inst.Close() + } + }() + + providerSupplier := func(addr addrs.AbsProviderConfig) providers.Interface { + providerInstanceLock.Lock() + defer providerInstanceLock.Unlock() + + if inst, ok := providerInstances[addr.Provider]; ok { + return inst + } + + factory, ok := ctx.plugins.providerFactories[addr.Provider] + if !ok { + log.Printf("[WARN] Unable to find provider %s in test context", addr) + providerInstances[addr.Provider] = nil + return nil + } + log.Printf("[INFO] Starting test provider %s", addr) + inst, err := factory() + if err != nil { + log.Printf("[WARN] Unable to start provider %s in test context", addr) + providerInstances[addr.Provider] = nil + return nil + } else { + log.Printf("[INFO] Shutting down test provider %s", addr) + providerInstances[addr.Provider] = inst + return inst + } + } + scope := &lang.Scope{ Data: data, BaseDir: ".", PureOnly: operation != walkApply, PlanTimestamp: ctx.Plan.Timestamp, + ProviderFunctions: func(pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { + return evalContextProviderFunction(providerSupplier, ctx.Config, walkPlan, pf, rng) + }, } // We're going to assume the run has passed, and then if anything fails this