Fix provider functions in child modules (#2082)

Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
Christian Mesh 2024-10-23 10:42:38 -04:00 committed by GitHub
parent c9541d81b6
commit e3a6bcab96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 664 additions and 341 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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{},

View File

@ -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{},

View File

@ -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{},

View File

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

View File

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

View File

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