From e3a6bcab9658f1b89312b8d22f07b2cbf9f07992 Mon Sep 17 00:00:00 2001 From: Christian Mesh Date: Wed, 23 Oct 2024 10:42:38 -0400 Subject: [PATCH] Fix provider functions in child modules (#2082) Signed-off-by: Christian Mesh --- internal/providers/schema_cache.go | 6 + internal/tofu/context_apply.go | 31 +- internal/tofu/context_eval.go | 16 +- internal/tofu/context_functions.go | 58 +--- internal/tofu/context_functions_test.go | 416 +++++++++++++++++++++--- internal/tofu/context_import.go | 20 +- internal/tofu/context_plan.go | 81 ++--- internal/tofu/context_validate.go | 16 +- internal/tofu/context_validate_test.go | 81 ----- internal/tofu/context_walk.go | 31 +- internal/tofu/eval_context_builtin.go | 44 ++- internal/tofu/eval_variable.go | 2 +- internal/tofu/graph_builder_apply.go | 4 +- internal/tofu/graph_builder_eval.go | 4 +- internal/tofu/graph_builder_plan.go | 4 +- internal/tofu/graph_walk_context.go | 74 +++-- internal/tofu/test_context.go | 28 +- internal/tofu/transform_provider.go | 89 +++-- 18 files changed, 664 insertions(+), 341 deletions(-) diff --git a/internal/providers/schema_cache.go b/internal/providers/schema_cache.go index 7540037e7a..16429f4694 100644 --- a/internal/providers/schema_cache.go +++ b/internal/providers/schema_cache.go @@ -41,3 +41,9 @@ func (c *schemaCache) Get(p addrs.Provider) (ProviderSchema, bool) { s, ok := c.m[p] return s, ok } + +func (c *schemaCache) Remove(p addrs.Provider) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.m, p) +} diff --git a/internal/tofu/context_apply.go b/internal/tofu/context_apply.go index 4b3a3d481d..883fa7a4eb 100644 --- a/internal/tofu/context_apply.go +++ b/internal/tofu/context_apply.go @@ -54,7 +54,9 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State } } - graph, operation, diags := c.applyGraph(plan, config, true) + providerFunctionTracker := make(ProviderFunctionMapping) + + graph, operation, diags := c.applyGraph(plan, config, true, providerFunctionTracker) if diags.HasErrors() { return nil, diags } @@ -71,7 +73,8 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State PlanTimeCheckResults: plan.Checks, // We also want to propagate the timestamp from the plan file. - PlanTimeTimestamp: plan.Timestamp, + PlanTimeTimestamp: plan.Timestamp, + ProviderFunctionTracker: providerFunctionTracker, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -119,7 +122,8 @@ Note that the -target option is not suitable for routine use, and is provided on return newState, diags } -func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { +//nolint:revive,unparam // TODO remove validate bool as it's not used +func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool, providerFunctionTracker ProviderFunctionMapping) (*Graph, walkOperation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics variables := InputValues{} @@ -171,15 +175,16 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate } graph, moreDiags := (&ApplyGraphBuilder{ - Config: config, - Changes: plan.Changes, - State: plan.PriorState, - RootVariableValues: variables, - Plugins: c.plugins, - Targets: plan.TargetAddrs, - ForceReplace: plan.ForceReplaceAddrs, - Operation: operation, - ExternalReferences: plan.ExternalReferences, + Config: config, + Changes: plan.Changes, + State: plan.PriorState, + RootVariableValues: variables, + Plugins: c.plugins, + Targets: plan.TargetAddrs, + ForceReplace: plan.ForceReplaceAddrs, + Operation: operation, + ExternalReferences: plan.ExternalReferences, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -204,7 +209,7 @@ func (c *Context) ApplyGraphForUI(plan *plans.Plan, config *configs.Config) (*Gr var diags tfdiags.Diagnostics - graph, _, moreDiags := c.applyGraph(plan, config, false) + graph, _, moreDiags := c.applyGraph(plan, config, false, make(ProviderFunctionMapping)) diags = diags.Append(moreDiags) return graph, diags } diff --git a/internal/tofu/context_eval.go b/internal/tofu/context_eval.go index 03b1ce131d..d58abacd4f 100644 --- a/internal/tofu/context_eval.go +++ b/internal/tofu/context_eval.go @@ -64,11 +64,14 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a log.Printf("[DEBUG] Building and walking 'eval' graph") + providerFunctionTracker := make(ProviderFunctionMapping) + graph, moreDiags := (&EvalGraphBuilder{ - Config: config, - State: state, - RootVariableValues: variables, - Plugins: c.plugins, + Config: config, + State: state, + RootVariableValues: variables, + Plugins: c.plugins, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -76,8 +79,9 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a } walkOpts := &graphWalkOpts{ - InputState: state, - Config: config, + InputState: state, + Config: config, + ProviderFunctionTracker: providerFunctionTracker, } walker, moreDiags = c.walk(graph, walkEval, walkOpts) diff --git a/internal/tofu/context_functions.go b/internal/tofu/context_functions.go index 7957cea8fa..e8e3ffc650 100644 --- a/internal/tofu/context_functions.go +++ b/internal/tofu/context_functions.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/opentofu/opentofu/internal/addrs" - "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/tfdiags" "github.com/zclconf/go-cty/cty" @@ -20,62 +19,9 @@ import ( // This builds a provider function using an EvalContext and some additional information // This is split out of BuiltinEvalContext for testing -func evalContextProviderFunction(providers func(addrs.AbsProviderConfig) providers.Interface, mc *configs.Config, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { +func evalContextProviderFunction(provider providers.Interface, op walkOperation, pf addrs.ProviderFunction, rng tfdiags.SourceRange) (*function.Function, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - pr, ok := mc.Module.ProviderRequirements.RequiredProviders[pf.ProviderName] - if !ok { - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unknown function provider", - Detail: fmt.Sprintf("Provider %q does not exist within the required_providers of this module", pf.ProviderName), - Subject: rng.ToHCL().Ptr(), - }) - } - - // Very similar to transform_provider.go - absPc := addrs.AbsProviderConfig{ - Provider: pr.Type, - Module: mc.Path, - Alias: pf.ProviderAlias, - } - - 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. - - // Make sure the alias is valid - validAlias := pf.ProviderAlias == "" - if !validAlias { - for _, alias := range pr.Aliases { - if alias.Alias == pf.ProviderAlias { - validAlias = true - break - } - } - if !validAlias { - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unknown function provider", - Detail: fmt.Sprintf("No provider instance %q with alias %q", pf.ProviderName, pf.ProviderAlias), - Subject: rng.ToHCL().Ptr(), - }) - } - } - - provider = providers(addrs.AbsProviderConfig{Provider: pr.Type}) - if provider == nil { - // This should not be possible - return nil, diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "BUG: Uninitialized function provider", - Detail: fmt.Sprintf("Provider %q has not yet been initialized", absPc.String()), - Subject: rng.ToHCL().Ptr(), - }) - } - } - // First try to look up the function from provider schema schema := provider.GetProviderSchema() if schema.Diagnostics.HasErrors() { @@ -117,7 +63,7 @@ func evalContextProviderFunction(providers func(addrs.AbsProviderConfig) provide return nil, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Function not found in provider", - Detail: fmt.Sprintf("Function %q was not registered by provider %q", pf.Function, absPc.String()), + Detail: fmt.Sprintf("Function %q was not registered by provider", pf), Subject: rng.ToHCL().Ptr(), }) } diff --git a/internal/tofu/context_functions_test.go b/internal/tofu/context_functions_test.go index 3a92340c42..b2d534b403 100644 --- a/internal/tofu/context_functions_test.go +++ b/internal/tofu/context_functions_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/opentofu/opentofu/internal/addrs" - "github.com/opentofu/opentofu/internal/configs" + "github.com/opentofu/opentofu/internal/configs/configschema" "github.com/opentofu/opentofu/internal/lang/marks" "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/tfdiags" @@ -117,51 +117,15 @@ func TestFunctions(t *testing.T) { return resp } - addr := addrs.NewDefaultProvider("mock") rng := tfdiags.SourceRange{} providerFunc := func(fn string) addrs.ProviderFunction { pf, _ := addrs.ParseFunction(fn).AsProviderFunction() return pf } - mockCtx := new(MockEvalContext) - cfg := &configs.Config{ - Module: &configs.Module{ - ProviderRequirements: &configs.RequiredProviders{ - RequiredProviders: map[string]*configs.RequiredProvider{ - "mockname": &configs.RequiredProvider{ - Name: "mock", - Type: addr, - }, - }, - }, - }, - } - - // Provider missing - _, diags := evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::invalid::unknown"), rng) - if !diags.HasErrors() { - t.Fatal("expected unknown function provider") - } - if diags.Err().Error() != `Unknown function provider: Provider "invalid" does not exist within the required_providers of this module` { - t.Fatal(diags.Err()) - } - - // Provider not initialized - _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) - if !diags.HasErrors() { - t.Fatal("expected unknown function provider") - } - if diags.Err().Error() != `BUG: Uninitialized function provider: Provider "provider[\"registry.opentofu.org/hashicorp/mock\"]" has not yet been initialized` { - t.Fatal(diags.Err()) - } - - // "initialize" provider - mockCtx.ProviderProvider = mockProvider - // Function missing (validate) mockProvider.GetFunctionsCalled = false - _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng) + _, diags := evalContextProviderFunction(mockProvider, walkValidate, providerFunc("provider::mockname::missing"), rng) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -171,11 +135,11 @@ func TestFunctions(t *testing.T) { // Function missing (Non-validate) mockProvider.GetFunctionsCalled = false - _, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkPlan, providerFunc("provider::mockname::missing"), rng) + _, diags = evalContextProviderFunction(mockProvider, walkPlan, providerFunc("provider::mockname::missing"), rng) if !diags.HasErrors() { t.Fatal("expected unknown function") } - if diags.Err().Error() != `Function not found in provider: Function "missing" was not registered by provider "provider[\"registry.opentofu.org/hashicorp/mock\"]"` { + if diags.Err().Error() != `Function not found in provider: Function "provider::mockname::missing" was not registered by provider` { t.Fatal(diags.Err()) } if !mockProvider.GetFunctionsCalled { @@ -193,7 +157,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.Provider, cfg, walkPlan, pf, rng) + impl, diags := evalContextProviderFunction(mockProvider, walkPlan, pf, rng) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -316,3 +280,373 @@ func TestFunctions(t *testing.T) { } }) } + +// Standard scenario using root provider explicitly passed +func TestContext2Functions_providerFunctions(t *testing.T) { + p := testProvider("aws") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": &configschema.Attribute{ + Type: cty.String, + }, + }, + }, + }, + Functions: map[string]providers.FunctionSpec{ + "arn_parse": providers.FunctionSpec{ + Parameters: []providers.FunctionParameterSpec{{ + Name: "arn", + Type: cty.String, + }}, + Return: cty.Bool, + }, + }, + } + p.CallFunctionResponse = &providers.CallFunctionResponse{ + Result: cty.True, + } + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +provider "aws" { + region="us-east-1" +} + +module "mod" { + source = "./mod" + providers = { + aws = aws + } +} + `, + "mod/mod.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +variable "obfmod" { + type = object({ + arns = optional(list(string)) + }) + description = "Configuration for xxx." + + validation { + condition = alltrue([ + for arn in var.obfmod.arns: can(provider::aws::arn_parse(arn)) + ]) + error_message = "All arns MUST BE a valid AWS ARN format." + } + + default = { + arns = [ + "arn:partition:service:region:account-id:resource-id", + ] + } +} +`, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + if !p.CallFunctionCalled { + t.Fatalf("Expected function call") + } +} + +// Explicitly passed provider with custom function +func TestContext2Functions_providerFunctionsCustom(t *testing.T) { + p := testProvider("aws") + p.GetFunctionsResponse = &providers.GetFunctionsResponse{ + Functions: map[string]providers.FunctionSpec{ + "arn_parse_custom": providers.FunctionSpec{ + Parameters: []providers.FunctionParameterSpec{{ + Name: "arn", + Type: cty.String, + }}, + Return: cty.Bool, + }, + }, + } + p.CallFunctionResponse = &providers.CallFunctionResponse{ + Result: cty.True, + } + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +provider "aws" { + region="us-east-1" + alias = "primary" +} + +module "mod" { + source = "./mod" + providers = { + aws = aws.primary + } +} + `, + "mod/mod.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +variable "obfmod" { + type = object({ + arns = optional(list(string)) + }) + description = "Configuration for xxx." + + validation { + condition = alltrue([ + for arn in var.obfmod.arns: can(provider::aws::arn_parse_custom(arn)) + ]) + error_message = "All arns MUST BE a valid AWS ARN format." + } + + default = { + arns = [ + "arn:partition:service:region:account-id:resource-id", + ] + } +} +`, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + if p.GetFunctionsCalled { + t.Fatalf("Unexpected function call") + } + if p.CallFunctionCalled { + t.Fatalf("Unexpected function call") + } + + p.GetFunctionsCalled = false + p.CallFunctionCalled = false + _, diags = ctx.Plan(m, nil, nil) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + if !p.GetFunctionsCalled { + t.Fatalf("Expected function call") + } + if !p.CallFunctionCalled { + t.Fatalf("Expected function call") + } +} + +// Defaulted stub provider with non-custom function +func TestContext2Functions_providerFunctionsStub(t *testing.T) { + p := testProvider("aws") + addr := addrs.ImpliedProviderForUnqualifiedType("aws") + + // Explicitly non-parallel + t.Setenv("foo", "bar") + defer providers.SchemaCache.Remove(addr) + + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionSpec{ + "arn_parse": providers.FunctionSpec{ + Parameters: []providers.FunctionParameterSpec{{ + Name: "arn", + Type: cty.String, + }}, + Return: cty.Bool, + }, + }, + } + p.CallFunctionResponse = &providers.CallFunctionResponse{ + Result: cty.True, + } + + // SchemaCache is initialzed earlier on in the command package + providers.SchemaCache.Set(addr, *p.GetProviderSchemaResponse) + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + `, + "mod/mod.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +variable "obfmod" { + type = object({ + arns = optional(list(string)) + }) + description = "Configuration for xxx." + + validation { + condition = alltrue([ + for arn in var.obfmod.arns: can(provider::aws::arn_parse(arn)) + ]) + error_message = "All arns MUST BE a valid AWS ARN format." + } + + default = { + arns = [ + "arn:partition:service:region:account-id:resource-id", + ] + } +} +`, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + if !p.GetProviderSchemaCalled { + t.Fatalf("Unexpected function call") + } + if p.GetFunctionsCalled { + t.Fatalf("Unexpected function call") + } + if !p.CallFunctionCalled { + t.Fatalf("Unexpected function call") + } + + p.GetProviderSchemaCalled = false + p.GetFunctionsCalled = false + p.CallFunctionCalled = false + _, diags = ctx.Plan(m, nil, nil) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + if !p.GetProviderSchemaCalled { + t.Fatalf("Unexpected function call") + } + if p.GetFunctionsCalled { + t.Fatalf("Expected function call") + } + if !p.CallFunctionCalled { + t.Fatalf("Expected function call") + } +} + +// Defaulted stub provider with custom function (no allowed) +func TestContext2Functions_providerFunctionsStubCustom(t *testing.T) { + p := testProvider("aws") + addr := addrs.ImpliedProviderForUnqualifiedType("aws") + + // Explicitly non-parallel + t.Setenv("foo", "bar") + defer providers.SchemaCache.Remove(addr) + + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionSpec{ + "arn_parse": providers.FunctionSpec{ + Parameters: []providers.FunctionParameterSpec{{ + Name: "arn", + Type: cty.String, + }}, + Return: cty.Bool, + }, + }, + } + p.CallFunctionResponse = &providers.CallFunctionResponse{ + Result: cty.True, + } + + // SchemaCache is initialzed earlier on in the command package + providers.SchemaCache.Set(addr, *p.GetProviderSchemaResponse) + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + `, + "mod/mod.tf": ` +terraform { + required_providers { + aws = ">=5.70.0" + } +} + +variable "obfmod" { + type = object({ + arns = optional(list(string)) + }) + description = "Configuration for xxx." + + validation { + condition = alltrue([ + for arn in var.obfmod.arns: can(provider::aws::arn_parse_custom(arn)) + ]) + error_message = "All arns MUST BE a valid AWS ARN format." + } + + default = { + arns = [ + "arn:partition:service:region:account-id:resource-id", + ] + } +} +`, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if !diags.HasErrors() { + t.Fatal("Expected error!") + } + expected := `Unknown provider function: Provider "module.mod.provider[\"registry.opentofu.org/hashicorp/aws\"]" does not have a function "arn_parse_custom" or has not been configured` + if expected != diags.Err().Error() { + t.Fatalf("Expected error %q, got %q", expected, diags.Err().Error()) + } + if p.GetFunctionsCalled { + t.Fatalf("Unexpected function call") + } + if p.CallFunctionCalled { + t.Fatalf("Unexpected function call") + } +} diff --git a/internal/tofu/context_import.go b/internal/tofu/context_import.go index 5dd0c3dbc4..a434878ee8 100644 --- a/internal/tofu/context_import.go +++ b/internal/tofu/context_import.go @@ -251,14 +251,17 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt variables := opts.SetVariables + providerFunctionTracker := make(ProviderFunctionMapping) + // Initialize our graph builder builder := &PlanGraphBuilder{ - ImportTargets: opts.Targets, - Config: config, - State: state, - RootVariableValues: variables, - Plugins: c.plugins, - Operation: walkImport, + ImportTargets: opts.Targets, + Config: config, + State: state, + RootVariableValues: variables, + Plugins: c.plugins, + Operation: walkImport, + ProviderFunctionTracker: providerFunctionTracker, } // Build the graph @@ -270,8 +273,9 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt // Walk it walker, walkDiags := c.walk(graph, walkImport, &graphWalkOpts{ - Config: config, - InputState: state, + Config: config, + InputState: state, + ProviderFunctionTracker: providerFunctionTracker, }) diags = diags.Append(walkDiags) if walkDiags.HasErrors() { diff --git a/internal/tofu/context_plan.go b/internal/tofu/context_plan.go index 788a57b5bc..b13bf1d86b 100644 --- a/internal/tofu/context_plan.go +++ b/internal/tofu/context_plan.go @@ -277,7 +277,7 @@ func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdi return nil } log.Println("[DEBUG] building apply graph to check for errors") - _, _, diags := c.applyGraph(plan, config, true) + _, _, diags := c.applyGraph(plan, config, true, make(ProviderFunctionMapping)) return diags } @@ -673,8 +673,9 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o // strange problems that may lead to confusing error messages. return nil, diags } + providerFunctionTracker := make(ProviderFunctionMapping) - graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts) + graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts, providerFunctionTracker) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags @@ -686,11 +687,12 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o // we can now walk. changes := plans.NewChanges() walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ - Config: config, - InputState: prevRunState, - Changes: changes, - MoveResults: moveResults, - PlanTimeTimestamp: timestamp, + Config: config, + InputState: prevRunState, + Changes: changes, + MoveResults: moveResults, + PlanTimeTimestamp: timestamp, + ProviderFunctionTracker: providerFunctionTracker, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -754,47 +756,50 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o return plan, diags } -func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*Graph, walkOperation, tfdiags.Diagnostics) { +func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts, providerFunctionTracker ProviderFunctionMapping) (*Graph, walkOperation, tfdiags.Diagnostics) { switch mode := opts.Mode; mode { case plans.NormalMode: graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - ForceReplace: opts.ForceReplace, - skipRefresh: opts.SkipRefresh, - preDestroyRefresh: opts.PreDestroyRefresh, - Operation: walkPlan, - ExternalReferences: opts.ExternalReferences, - ImportTargets: opts.ImportTargets, - GenerateConfigPath: opts.GenerateConfigPath, - EndpointsToRemove: opts.EndpointsToRemove, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + Plugins: c.plugins, + Targets: opts.Targets, + ForceReplace: opts.ForceReplace, + skipRefresh: opts.SkipRefresh, + preDestroyRefresh: opts.PreDestroyRefresh, + Operation: walkPlan, + ExternalReferences: opts.ExternalReferences, + ImportTargets: opts.ImportTargets, + GenerateConfigPath: opts.GenerateConfigPath, + EndpointsToRemove: opts.EndpointsToRemove, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.RefreshOnlyMode: graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - skipRefresh: opts.SkipRefresh, - skipPlanChanges: true, // this activates "refresh only" mode. - Operation: walkPlan, - ExternalReferences: opts.ExternalReferences, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + Plugins: c.plugins, + Targets: opts.Targets, + skipRefresh: opts.SkipRefresh, + skipPlanChanges: true, // this activates "refresh only" mode. + Operation: walkPlan, + ExternalReferences: opts.ExternalReferences, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.DestroyMode: graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - skipRefresh: opts.SkipRefresh, - Operation: walkPlanDestroy, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + Plugins: c.plugins, + Targets: opts.Targets, + skipRefresh: opts.SkipRefresh, + Operation: walkPlanDestroy, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) return graph, walkPlanDestroy, diags default: @@ -962,7 +967,7 @@ func (c *Context) PlanGraphForUI(config *configs.Config, prevRunState *states.St opts := &PlanOpts{Mode: mode} - graph, _, moreDiags := c.planGraph(config, prevRunState, opts) + graph, _, moreDiags := c.planGraph(config, prevRunState, opts, make(ProviderFunctionMapping)) diags = diags.Append(moreDiags) return graph, diags } diff --git a/internal/tofu/context_validate.go b/internal/tofu/context_validate.go index 590b9eb250..df060bd61c 100644 --- a/internal/tofu/context_validate.go +++ b/internal/tofu/context_validate.go @@ -60,12 +60,15 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } } + providerFunctionTracker := make(ProviderFunctionMapping) + graph, moreDiags := (&PlanGraphBuilder{ - Config: config, - Plugins: c.plugins, - State: states.NewState(), - RootVariableValues: varValues, - Operation: walkValidate, + Config: config, + Plugins: c.plugins, + State: states.NewState(), + RootVariableValues: varValues, + Operation: walkValidate, + ProviderFunctionTracker: providerFunctionTracker, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -73,7 +76,8 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{ - Config: config, + Config: config, + ProviderFunctionTracker: providerFunctionTracker, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) diff --git a/internal/tofu/context_validate_test.go b/internal/tofu/context_validate_test.go index d882988d07..0bdaf02e32 100644 --- a/internal/tofu/context_validate_test.go +++ b/internal/tofu/context_validate_test.go @@ -2487,84 +2487,3 @@ locals { t.Fatalf("expected deprecated warning, got: %q\n", warn) } } - -func TextContext2Validate_providerFunctions(t *testing.T) { - p := testProvider("aws") - p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Functions: map[string]providers.FunctionSpec{ - "arn_parse": providers.FunctionSpec{ - Parameters: []providers.FunctionParameterSpec{{ - Name: "arn", - Type: cty.String, - }}, - Return: cty.Bool, - }, - }, - } - p.CallFunctionResponse = &providers.CallFunctionResponse{ - Result: cty.True, - } - m := testModuleInline(t, map[string]string{ - "main.tf": ` -terraform { - required_providers { - aws = ">=5.70.0" - } -} - -provider "aws" { - region="us-east-1" -} - -module "mod" { - source = "./mod" - providers = { - aws = aws - } -} - `, - "mod/mod.tf": ` - terraform { - required_providers { - aws = ">=5.70.0" - } -} - -variable "obfmod" { - type = object({ - arns = optional(list(string)) - }) - description = "Configuration for xxx." - - validation { - condition = alltrue([ - for arn in var.obfmod.arns: can(provider::aws::arn_parse(arn)) - ]) - error_message = "All arns MUST BE a valid AWS ARN format." - } - - default = { - arns = [ - "arn:partition:service:region:account-id:resource-id", - ] - } -} -`, - }) - - ctx := testContext2(t, &ContextOpts{ - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), - }, - }) - - diags := ctx.Validate(m) - warn := diags.ErrWithWarnings().Error() - if !strings.Contains(warn, `The attribute "foo" is deprecated`) { - t.Fatalf("expected deprecated warning, got: %q\n", warn) - } - - if !p.CallFunctionCalled { - t.Fatalf("Expected function call") - } -} diff --git a/internal/tofu/context_walk.go b/internal/tofu/context_walk.go index a1744cefdf..ca16b66ef2 100644 --- a/internal/tofu/context_walk.go +++ b/internal/tofu/context_walk.go @@ -44,6 +44,8 @@ type graphWalkOpts struct { PlanTimeTimestamp time.Time MoveResults refactoring.MoveResults + + ProviderFunctionTracker ProviderFunctionMapping } func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpts) (*ContextGraphWalker, tfdiags.Diagnostics) { @@ -140,19 +142,20 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con } return &ContextGraphWalker{ - Context: c, - State: state, - Config: opts.Config, - RefreshState: refreshState, - PrevRunState: prevRunState, - Changes: changes.SyncWrapper(), - Checks: checkState, - InstanceExpander: instances.NewExpander(), - MoveResults: opts.MoveResults, - ImportResolver: NewImportResolver(), - Operation: operation, - StopContext: c.runContext, - PlanTimestamp: opts.PlanTimeTimestamp, - Encryption: c.encryption, + Context: c, + State: state, + Config: opts.Config, + RefreshState: refreshState, + PrevRunState: prevRunState, + Changes: changes.SyncWrapper(), + Checks: checkState, + InstanceExpander: instances.NewExpander(), + MoveResults: opts.MoveResults, + ImportResolver: NewImportResolver(), + Operation: operation, + StopContext: c.runContext, + PlanTimestamp: opts.PlanTimeTimestamp, + Encryption: c.encryption, + ProviderFunctionTracker: opts.ProviderFunctionTracker, } } diff --git a/internal/tofu/eval_context_builtin.go b/internal/tofu/eval_context_builtin.go index 86cc8a5567..fb9ff7791a 100644 --- a/internal/tofu/eval_context_builtin.go +++ b/internal/tofu/eval_context_builtin.go @@ -73,15 +73,16 @@ type BuiltinEvalContext struct { ProvisionerLock *sync.Mutex ProvisionerCache map[string]provisioners.Interface - ChangesValue *plans.ChangesSync - StateValue *states.SyncState - ChecksValue *checks.State - RefreshStateValue *states.SyncState - PrevRunStateValue *states.SyncState - InstanceExpanderValue *instances.Expander - MoveResultsValue refactoring.MoveResults - ImportResolverValue *ImportResolver - Encryption encryption.Encryption + ChangesValue *plans.ChangesSync + StateValue *states.SyncState + ChecksValue *checks.State + RefreshStateValue *states.SyncState + PrevRunStateValue *states.SyncState + InstanceExpanderValue *instances.Expander + MoveResultsValue refactoring.MoveResults + ImportResolverValue *ImportResolver + Encryption encryption.Encryption + ProviderFunctionTracker ProviderFunctionMapping } // BuiltinEvalContext implements EvalContext @@ -432,7 +433,30 @@ 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.Provider, mc, ctx.Evaluator.Operation, pf, rng) + absPc, ok := ctx.ProviderFunctionTracker.Lookup(ctx.PathValue.Module(), pf) + if !ok { + // This should not be possible if references are tracked correctly + return nil, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "BUG: Uninitialized function provider", + Detail: fmt.Sprintf("Provider function %q has not been tracked properly", pf), + Subject: rng.ToHCL().Ptr(), + }) + } + + provider := ctx.Provider(absPc) + + if provider == nil { + // This should not be possible if references are tracked correctly + return nil, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "BUG: Uninitialized function provider", + Detail: fmt.Sprintf("Provider %q has not yet been initialized", absPc.String()), + Subject: rng.ToHCL().Ptr(), + }) + } + + return evalContextProviderFunction(provider, ctx.Evaluator.Operation, pf, rng) }) scope.SetActiveExperiments(mc.Module.ActiveExperiments) diff --git a/internal/tofu/eval_variable.go b/internal/tofu/eval_variable.go index 1033033db2..f1456eb6b8 100644 --- a/internal/tofu/eval_variable.go +++ b/internal/tofu/eval_variable.go @@ -245,7 +245,7 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config continue } - hclCtx, ctxDiags := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey).EvalContext(append(condFuncs, errFuncs...)) + hclCtx, ctxDiags := ctx.WithPath(addr.Module).EvaluationScope(nil, nil, EvalDataForNoInstanceKey).EvalContext(append(condFuncs, errFuncs...)) diags = diags.Append(ctxDiags) if diags.HasErrors() { continue diff --git a/internal/tofu/graph_builder_apply.go b/internal/tofu/graph_builder_apply.go index afeffe8788..d3b12997f2 100644 --- a/internal/tofu/graph_builder_apply.go +++ b/internal/tofu/graph_builder_apply.go @@ -59,6 +59,8 @@ type ApplyGraphBuilder struct { // nodes that should not be pruned even if they are not referenced within // the actual graph. ExternalReferences []*addrs.Reference + + ProviderFunctionTracker ProviderFunctionMapping } // See GraphBuilder @@ -147,7 +149,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config}, // After schema transformer, we can add function references - &ProviderFunctionTransformer{Config: b.Config}, + &ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker}, // Remove unused providers and proxies &PruneProviderTransformer{}, diff --git a/internal/tofu/graph_builder_eval.go b/internal/tofu/graph_builder_eval.go index 3610cdb5c2..0ea210306c 100644 --- a/internal/tofu/graph_builder_eval.go +++ b/internal/tofu/graph_builder_eval.go @@ -43,6 +43,8 @@ type EvalGraphBuilder struct { // Plugins is a library of plug-in components (providers and // provisioners) available for use. Plugins *contextPlugins + + ProviderFunctionTracker ProviderFunctionMapping } // See GraphBuilder @@ -90,7 +92,7 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer { &AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config}, // After schema transformer, we can add function references - &ProviderFunctionTransformer{Config: b.Config}, + &ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker}, // Remove unused providers and proxies &PruneProviderTransformer{}, diff --git a/internal/tofu/graph_builder_plan.go b/internal/tofu/graph_builder_plan.go index 0ab7e8c873..7eb3460a7f 100644 --- a/internal/tofu/graph_builder_plan.go +++ b/internal/tofu/graph_builder_plan.go @@ -92,6 +92,8 @@ type PlanGraphBuilder struct { // // If empty, then config will not be generated. GenerateConfigPath string + + ProviderFunctionTracker ProviderFunctionMapping } // See GraphBuilder @@ -200,7 +202,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &AttachSchemaTransformer{Plugins: b.Plugins, Config: b.Config}, // After schema transformer, we can add function references - &ProviderFunctionTransformer{Config: b.Config}, + &ProviderFunctionTransformer{Config: b.Config, ProviderFunctionTracker: b.ProviderFunctionTracker}, // Remove unused providers and proxies &PruneProviderTransformer{}, diff --git a/internal/tofu/graph_walk_context.go b/internal/tofu/graph_walk_context.go index 750274fc72..a2ad471503 100644 --- a/internal/tofu/graph_walk_context.go +++ b/internal/tofu/graph_walk_context.go @@ -31,21 +31,22 @@ type ContextGraphWalker struct { NullGraphWalker // Configurable values - Context *Context - State *states.SyncState // Used for safe concurrent access to state - RefreshState *states.SyncState // Used for safe concurrent access to state - PrevRunState *states.SyncState // Used for safe concurrent access to state - Changes *plans.ChangesSync // Used for safe concurrent writes to changes - Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results - InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances - ImportResolver *ImportResolver // Tracks import targets as they are being resolved - MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements - Operation walkOperation - StopContext context.Context - RootVariableValues InputValues - Config *configs.Config - PlanTimestamp time.Time - Encryption encryption.Encryption + Context *Context + State *states.SyncState // Used for safe concurrent access to state + RefreshState *states.SyncState // Used for safe concurrent access to state + PrevRunState *states.SyncState // Used for safe concurrent access to state + Changes *plans.ChangesSync // Used for safe concurrent writes to changes + Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results + InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances + ImportResolver *ImportResolver // Tracks import targets as they are being resolved + MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements + Operation walkOperation + StopContext context.Context + RootVariableValues InputValues + Config *configs.Config + PlanTimestamp time.Time + Encryption encryption.Encryption + ProviderFunctionTracker ProviderFunctionMapping // This is an output. Do not set this, nor read it while a graph walk // is in progress. @@ -99,27 +100,28 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { } ctx := &BuiltinEvalContext{ - StopContext: w.StopContext, - Hooks: w.Context.hooks, - InputValue: w.Context.uiInput, - InstanceExpanderValue: w.InstanceExpander, - Plugins: w.Context.plugins, - MoveResultsValue: w.MoveResults, - ImportResolverValue: w.ImportResolver, - ProviderCache: w.providerCache, - ProviderInputConfig: w.Context.providerInputConfig, - ProviderLock: &w.providerLock, - ProvisionerCache: w.provisionerCache, - ProvisionerLock: &w.provisionerLock, - ChangesValue: w.Changes, - ChecksValue: w.Checks, - StateValue: w.State, - RefreshStateValue: w.RefreshState, - PrevRunStateValue: w.PrevRunState, - Evaluator: evaluator, - VariableValues: w.variableValues, - VariableValuesLock: &w.variableValuesLock, - Encryption: w.Encryption, + StopContext: w.StopContext, + Hooks: w.Context.hooks, + InputValue: w.Context.uiInput, + InstanceExpanderValue: w.InstanceExpander, + Plugins: w.Context.plugins, + MoveResultsValue: w.MoveResults, + ImportResolverValue: w.ImportResolver, + ProviderCache: w.providerCache, + ProviderInputConfig: w.Context.providerInputConfig, + ProviderLock: &w.providerLock, + ProvisionerCache: w.provisionerCache, + ProvisionerLock: &w.provisionerLock, + ChangesValue: w.Changes, + ChecksValue: w.Checks, + StateValue: w.State, + RefreshStateValue: w.RefreshState, + PrevRunStateValue: w.PrevRunState, + Evaluator: evaluator, + VariableValues: w.variableValues, + VariableValuesLock: &w.variableValuesLock, + Encryption: w.Encryption, + ProviderFunctionTracker: w.ProviderFunctionTracker, } return ctx diff --git a/internal/tofu/test_context.go b/internal/tofu/test_context.go index d5ee37cf30..b931834b7b 100644 --- a/internal/tofu/test_context.go +++ b/internal/tofu/test_context.go @@ -111,29 +111,29 @@ func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.Changes } }() - providerSupplier := func(addr addrs.AbsProviderConfig) providers.Interface { + providerSupplier := func(addr addrs.Provider) providers.Interface { providerInstanceLock.Lock() defer providerInstanceLock.Unlock() - if inst, ok := providerInstances[addr.Provider]; ok { + if inst, ok := providerInstances[addr]; ok { return inst } - factory, ok := ctx.plugins.providerFactories[addr.Provider] + factory, ok := ctx.plugins.providerFactories[addr] if !ok { log.Printf("[WARN] Unable to find provider %s in test context", addr) - providerInstances[addr.Provider] = nil + providerInstances[addr] = 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 + providerInstances[addr] = nil return nil } else { log.Printf("[INFO] Shutting down test provider %s", addr) - providerInstances[addr.Provider] = inst + providerInstances[addr] = inst return inst } } @@ -144,7 +144,21 @@ func (ctx *TestContext) evaluate(state *states.SyncState, changes *plans.Changes 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) + // This is a simpler flow than what is allowed during normal exection. + // We only support non-configured functions here. + pr, ok := ctx.Config.Module.ProviderRequirements.RequiredProviders[pf.ProviderName] + if !ok { + return nil, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unknown function provider", + Detail: fmt.Sprintf("Provider %q does not exist within the required_providers of this module", pf.ProviderName), + Subject: rng.ToHCL().Ptr(), + }) + } + + provider := providerSupplier(pr.Type) + + return evalContextProviderFunction(provider, walkPlan, pf, rng) }, } diff --git a/internal/tofu/transform_provider.go b/internal/tofu/transform_provider.go index e13d52a33d..2cda067fea 100644 --- a/internal/tofu/transform_provider.go +++ b/internal/tofu/transform_provider.go @@ -13,6 +13,7 @@ import ( "github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/dag" + "github.com/opentofu/opentofu/internal/providers" "github.com/opentofu/opentofu/internal/tfdiags" ) @@ -212,10 +213,36 @@ func (t *ProviderTransformer) Transform(g *Graph) error { return diags.Err() } +// ProviderFunctionReference is all the information needed to identify +// the provider required in a given module path. Alternatively, this +// could be seen as a Module path + addrs.LocalProviderConfig. +type ProviderFunctionReference struct { + ModulePath string + ProviderName string + ProviderAlias string +} + +// ProviderFunctionMapping maps a provider used by functions at a given location in the graph to the actual AbsProviderConfig +// that's required. This is due to the provider inheritence logic and proxy logic in the below +// transformer needing to be known in other parts of the application. +// Ideally, this would not be needed and be built like the ProviderTransformer. Unfortunately, it's +// a significant refactor to get to that point which adds a lot of complexity. +type ProviderFunctionMapping map[ProviderFunctionReference]addrs.AbsProviderConfig + +func (m ProviderFunctionMapping) Lookup(module addrs.Module, pf addrs.ProviderFunction) (addrs.AbsProviderConfig, bool) { + addr, ok := m[ProviderFunctionReference{ + ModulePath: module.String(), + ProviderName: pf.ProviderName, + ProviderAlias: pf.ProviderAlias, + }] + return addr, ok +} + // ProviderFunctionTransformer is a GraphTransformer that maps nodes which reference functions to providers // within the graph. This will error if there are any provider functions that don't map to known providers. type ProviderFunctionTransformer struct { - Config *configs.Config + Config *configs.Config + ProviderFunctionTracker ProviderFunctionMapping } func (t *ProviderFunctionTransformer) Transform(g *Graph) error { @@ -227,26 +254,20 @@ func (t *ProviderFunctionTransformer) Transform(g *Graph) error { return nil } - // Locate all providers in the graph - providers := providerVertexMap(g) - - type providerReference struct { - path string - name string - alias string - } + // Locate all providerVerts in the graph + providerVerts := providerVertexMap(g) // LuT of provider reference -> provider vertex - providerReferences := make(map[providerReference]dag.Vertex) + providerReferences := make(map[ProviderFunctionReference]dag.Vertex) for _, v := range g.Vertices() { // Provider function references if nr, ok := v.(GraphNodeReferencer); ok && t.Config != nil { for _, ref := range nr.References() { if pf, ok := ref.Subject.(addrs.ProviderFunction); ok { - key := providerReference{ - path: nr.ModulePath().String(), - name: pf.ProviderName, - alias: pf.ProviderAlias, + key := ProviderFunctionReference{ + ModulePath: nr.ModulePath().String(), + ProviderName: pf.ProviderName, + ProviderAlias: pf.ProviderAlias, } // We already know about this provider and can link directly @@ -291,12 +312,35 @@ func (t *ProviderFunctionTransformer) Transform(g *Graph) error { log.Printf("[TRACE] ProviderFunctionTransformer: %s in %s is provided by %s", pf, dag.VertexName(v), absPc) // Lookup provider via full address - provider := providers[absPc.String()] + provider := providerVerts[absPc.String()] if provider != nil { // Providers with configuration will already exist within the graph and can be directly referenced log.Printf("[TRACE] ProviderFunctionTransformer: exact match for %s serving %s", absPc, dag.VertexName(v)) } else { + // At this point, all provider schemas should be loaded. We + // can now check to see if configuration is optional for this function. + providerSchema, ok := providers.SchemaCache.Get(absPc.Provider) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unknown provider for function", + Detail: fmt.Sprintf("Provider %q does not have it's schema initialized", absPc.Provider), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + _, functionOk := providerSchema.Functions[pf.Function] + if !functionOk { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unknown provider function", + Detail: fmt.Sprintf("Provider %q does not have a function %q or has not been configured", absPc, pf.Function), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + // If this provider doesn't need to be configured then we can just // stub it out with an init-only provider node, which will just // start up the provider and fetch its schema. @@ -304,22 +348,24 @@ func (t *ProviderFunctionTransformer) Transform(g *Graph) error { Module: addrs.RootModule, Provider: absPc.Provider, } - if provider, ok = providers[stubAddr.String()]; !ok { - stub := &NodeEvalableProvider{ + // Try to look up an existing stub + provider, ok = providerVerts[stubAddr.String()] + // If it does not exist, create it + if !ok { + log.Printf("[TRACE] ProviderFunctionTransformer: creating init-only node for %s", stubAddr) + + provider = &NodeEvalableProvider{ &NodeAbstractProvider{ Addr: stubAddr, }, } - providers[stubAddr.String()] = stub - log.Printf("[TRACE] ProviderFunctionTransformer: creating init-only node for %s", stubAddr) - provider = stub + providerVerts[stubAddr.String()] = provider g.Add(provider) } } // see if this is a proxy provider pointing to another concrete config if p, ok := provider.(*graphNodeProxyProvider); ok { - g.Remove(p) provider = p.Target() } @@ -328,6 +374,7 @@ func (t *ProviderFunctionTransformer) Transform(g *Graph) error { // Save for future lookups providerReferences[key] = provider + t.ProviderFunctionTracker[key] = provider.ProviderAddr() } } }