more helpful provider function diagnostics

Use the new FunctionCallUnknownDiagExtra feature from hcl to help guide
users through problems with provider function calls.

Now that we can detect diagnostics from unknown function calls, we can
correlate the namespace and names with certain problems specific to
Terraform.
pull/34683/head
James Bardin 2 years ago
parent bc3c185f2e
commit bac2f892c6

@ -5,10 +5,13 @@ package lang
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/dynblock"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
@ -69,7 +72,7 @@ func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value,
body = blocktoattr.FixUpBlockAttrs(body, schema)
val, evalDiags := hcldec.Decode(body, spec, ctx)
diags = diags.Append(evalDiags)
diags = diags.Append(checkForUnknownFunctionDiags(evalDiags))
return val, diags
}
@ -147,7 +150,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem
}
val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), ctx)
diags = diags.Append(decDiags)
diags = diags.Append(checkForUnknownFunctionDiags(decDiags))
return val, diags
}
@ -173,7 +176,7 @@ func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfd
}
val, evalDiags := expr.Value(ctx)
diags = diags.Append(evalDiags)
diags = diags.Append(checkForUnknownFunctionDiags(evalDiags))
if wantType != cty.DynamicPseudoType {
var convErr error
@ -489,3 +492,87 @@ func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfd
}
return val, diags
}
// checkForUnknownFunctionDiags inspects the diagnostics for errors from unknown
// function calls, and tailors the messages to better suit Terraform. We now
// have multiple namespaces where functions may be declared, and it's up to the
// user to have properly configured the module to populate the provider
// namespace. The generic unknown function diagnostic from hcl does not direct
// the user on how to remedy the situation in Terraform, and we can give more
// useful information in a few Terraform specific cases here.
func checkForUnknownFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics {
for _, d := range diags {
extra, ok := hcl.DiagnosticExtra[hclsyntax.FunctionCallUnknownDiagExtra](d)
if !ok {
continue
}
name := extra.CalledFunctionName()
namespace := extra.CalledFunctionNamespace()
namespaceParts := strings.Split(namespace, "::")
if len(namespaceParts) < 2 {
// no namespace (namespace includes ::, so will have at least 2
// parts), but check if there is a matching name in a provider
// namspace.
if d.EvalContext == nil {
continue
}
for funcName := range d.EvalContext.Functions {
if strings.HasSuffix(funcName, "::"+name) {
d.Detail = fmt.Sprintf("%s Did you mean %q?", d.Detail, funcName)
break
}
}
continue
}
// the diagnostic isn't really shared with anything, and copying would
// still retain the internal pointers, so we're going to modify the
// diagnostic in-place if we want to change the output. Log the original
// diagnostic for debugging purposes in case we overwrite something
// potentially useful in the future from hcl.
log.Printf("[ERROR] UnknownFunctionCall: %s", d.Error())
d.Summary = "Unknown provider function"
if namespaceParts[0] != "provider" {
// help if the user is skipping the provider:: prefix before the
// provider name.
d.Detail = fmt.Sprintf(`The function namespace %q is not valid. Provider function calls must use the "provider::" namespace prefix.`, namespaceParts[0])
continue
}
if namespaceParts[1] == "" {
// missing provider name entirely
d.Detail = `The function call must include the provider name after the "provider::" prefix.`
continue
}
if d.EvalContext == nil {
// There's no eval context for some reason, so we can't inspect the
// available functions.
d.Detail = fmt.Sprintf(`There is no function named "%s%s".`, namespace, name)
continue
}
otherProviderFuncs := false
for funcName := range d.EvalContext.Functions {
// there are other functions in this provider namespace, so it must
// have been included in the configuration, and we can be clear that
// this a function which the provider does not support.
if strings.HasPrefix(funcName, namespace) {
otherProviderFuncs = true
break
}
}
if otherProviderFuncs {
d.Detail = fmt.Sprintf("The function %q is not available from the provider %q.", name, namespaceParts[1])
continue
}
// no other functions exist for this provider, so hint that the user may
// need to include it in the configuration.
d.Detail = fmt.Sprintf(`There is no function named "%s%s". The provider %q may need to be added to the required_providers block within the module configuration.`, namespace, name, namespaceParts[1])
}
return diags
}

@ -10,6 +10,7 @@ import (
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
@ -224,3 +225,98 @@ func TestContext2Plan_providerFunctionImpureApply(t *testing.T) {
t.Fatalf("expected error with %q, got %q", "provider function returned an inconsistent result", errs)
}
}
func TestContext2Validate_providerFunctionDiagnostics(t *testing.T) {
provider := &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{Block: simpleTestSchema()},
Functions: map[string]providers.FunctionDecl{
"echo": providers.FunctionDecl{
Parameters: []providers.FunctionParam{
{
Name: "arg",
Type: cty.String,
},
},
ReturnType: cty.String,
},
},
},
}
tests := []struct {
name string
cfg *configs.Config
expectedDiag string
}{
{
"missing provider",
testModuleInline(t, map[string]string{
"main.tf": `
output "first" {
value = provider::test::echo("input")
}`}),
`The provider "test" may need to be added to the required_providers block within the module configuration.`,
},
{
"invalid namespace",
testModuleInline(t, map[string]string{
"main.tf": `
output "first" {
value = test::echo("input")
}`}),
`The function namespace "test" is not valid. Provider function calls must use the "provider::" namespace prefix`,
},
{
"missing namespace",
testModuleInline(t, map[string]string{
"main.tf": `
terraform {
required_providers {
test = {
source = "registry.terraform.io/hashicorp/test"
}
}
}
output "first" {
value = echo("input")
}`}),
`There is no function named "echo". Did you mean "provider::test::echo"?`,
},
{
"no function from provider",
testModuleInline(t, map[string]string{
"main.tf": `
terraform {
required_providers {
test = {
source = "registry.terraform.io/hashicorp/test"
}
}
}
output "first" {
value = provider::test::missing("input")
}`}),
`Unknown provider function: The function "missing" is not available from the provider "test".`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(provider),
},
})
diags := ctx.Validate(test.cfg)
if !diags.HasErrors() {
t.Fatal("expected diagnsotics, got none")
}
got := diags.Err().Error()
if !strings.Contains(got, test.expectedDiag) {
t.Fatalf("expected %q, got %q", test.expectedDiag, got)
}
})
}
}

Loading…
Cancel
Save