diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 6a93ad2852..6c8712815f 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -2515,3 +2515,363 @@ resource "aws_instance" "follow" { t.Fatal(diags.ErrWithWarnings()) } } + +func TestContext2Validate_providerContributedFunctions(t *testing.T) { + mockProvider := func() *MockProvider { + p := testProvider("test") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionDecl{ + "count_e": { + ReturnType: cty.Number, + Parameters: []providers.FunctionParam{ + { + Name: "string", + Type: cty.String, + }, + }, + }, + }, + } + p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + if req.FunctionName != "count_e" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("incorrect function name %q", req.FunctionName)) + return resp + } + if len(req.Arguments) != 1 { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong number of arguments %d", len(req.Arguments))) + return resp + } + if req.Arguments[0].Type() != cty.String { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("wrong argument type %#v", req.Arguments[0].Type())) + return resp + } + if !req.Arguments[0].IsKnown() { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("argument is unknown")) + return resp + } + if req.Arguments[0].IsNull() { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("argument is null")) + return resp + } + + str := req.Arguments[0].AsString() + count := strings.Count(str, "e") + resp.Result = cty.NumberIntVal(int64(count)) + return resp + } + return p + } + + t.Run("valid", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +locals { + result = provider::test::count_e("cheese") +} +output "result" { + value = local.result + precondition { + condition = (local.result == 3) + error_message = "Wrong number of Es in my cheese." + } +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + if !p.CallFunctionCalled { + t.Fatal("CallFunction was not called") + } + }) + t.Run("wrong name", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::cout_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Call to unknown function: There is no function named \"cout_e\" in namespace provider::test::."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + + }) + t.Run("wrong namespace", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::toast::count_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Call to unknown function: There are no functions in namespace \"provider::toast::\"."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("wrong argument type", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e([]) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Invalid function argument: Invalid value for \"string\" parameter: string required."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("insufficient arguments", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e() +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Not enough function arguments: Function \"provider::test::count_e\" expects 1 argument(s). Missing value for \"string\"."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("too many arguments", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e("cheese", "louise") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Too many function arguments: Function \"provider::test::count_e\" expects only 1 argument(s)."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("unexpected null argument", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e(null) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Invalid function argument: Invalid value for \"string\" parameter: argument must not be null."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("unhandled unknown argument", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e(timestamp()) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // For this case, validation should succeed without calling the + // function yet, because the function doesn't declare that it handles + // unknown values and so we must defer validation until a later phase. + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + }) + t.Run("provider not declared", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + # Intentionally no declaration of local name "test" here + } +} +output "result" { + value = provider::test::count_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + // Module author must declare a provider requirement in order to + // import a provider's functions. + if got, want := diags.Err().Error(), "Call to unknown function: There are no functions in namespace \"provider::test::\"."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) +} diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index 87481f60b9..4ace412ad8 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -443,7 +443,7 @@ func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source InstanceKeyData: keyData, Operation: ctx.Evaluator.Operation, } - scope := ctx.Evaluator.Scope(data, self, source, lang.ExternalFuncs{}) + scope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) // ctx.PathValue is the path of the module that contains whatever // expression the caller will be trying to evaluate, so this will diff --git a/internal/terraform/provider_mock.go b/internal/terraform/provider_mock.go index e46e37bc06..d3913b23d1 100644 --- a/internal/terraform/provider_mock.go +++ b/internal/terraform/provider_mock.go @@ -529,7 +529,7 @@ func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) providers.C p.CallFunctionCalled = true p.CallFunctionRequest = r - if p.ReadDataSourceFn != nil { + if p.CallFunctionFn != nil { return p.CallFunctionFn(r) }