Skip to content

Improve diagnostics when types that cannot handle unknown or null are used #495

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion internal/fwschemadata/data_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import (

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// Get populates the struct passed as `target` with the entire state.
func (d Data) Get(ctx context.Context, target any) diag.Diagnostics {
return reflect.Into(ctx, d.Schema.Type(), d.TerraformValue, target, reflect.Options{})
return reflect.Into(ctx, d.Schema.Type(), d.TerraformValue, target, reflect.Options{}, path.Empty())
}
7 changes: 1 addition & 6 deletions internal/fwschemadata/data_get_at_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,7 @@ func (d Data) GetAtPath(ctx context.Context, schemaPath path.Path, target any) d
return diags
}

reflectDiags := reflect.Into(ctx, attrValue.Type(ctx), raw, target, reflect.Options{})

// reflect.Into does not have path information for its Diagnostics.
for idx, valueAsDiag := range reflectDiags {
reflectDiags[idx] = diag.WithPath(schemaPath, valueAsDiag)
}
reflectDiags := reflect.Into(ctx, attrValue.Type(ctx), raw, target, reflect.Options{}, schemaPath)

diags.Append(reflectDiags...)

Expand Down
114 changes: 76 additions & 38 deletions internal/fwschemadata/data_get_at_path_test.go

Large diffs are not rendered by default.

114 changes: 76 additions & 38 deletions internal/fwschemadata/data_get_test.go

Large diffs are not rendered by default.

17 changes: 11 additions & 6 deletions internal/reflect/build_value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework/diag"
refl "github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

func TestBuildValue(t *testing.T) {
Expand All @@ -24,19 +25,23 @@ func TestBuildValue(t *testing.T) {
tfValue: tftypes.NewValue(tftypes.String, nil),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty(),
path.Root("id"),
"Value Conversion Error",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\nunhandled null value",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"Received null value, however the target type cannot handle null values. Use the corresponding `types` package type, a pointer type or a custom type that handles null values.\n\n"+
"Path: id\nTarget Type: string\nSuggested `types` Type: types.StringType\nSuggested Pointer Type: *string",
),
},
},
"unhandled-unknown": {
tfValue: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
expectedDiags: diag.Diagnostics{
diag.NewAttributeErrorDiagnostic(
path.Empty(),
path.Root("id"),
"Value Conversion Error",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\nunhandled unknown value",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+
"Path: id\nTarget Type: string\nSuggested Type: types.StringType",
),
},
},
Expand All @@ -48,7 +53,7 @@ func TestBuildValue(t *testing.T) {
t.Parallel()

var s string
_, diags := refl.BuildValue(context.Background(), types.StringType, tc.tfValue, reflect.ValueOf(s), refl.Options{}, path.Empty())
_, diags := refl.BuildValue(context.Background(), types.StringType, tc.tfValue, reflect.ValueOf(s), refl.Options{}, path.Root("id"))

if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" {
t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff)
Expand Down
23 changes: 14 additions & 9 deletions internal/reflect/into.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (
"math/big"
"reflect"

"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

// Into uses the data in `val` to populate `target`, using the reflection
Expand All @@ -22,19 +23,20 @@ import (
// in the tftypes.Value must have a corresponding property in the struct. Into
// will be called for each struct field. Slices will have Into called for each
// element.
func Into(ctx context.Context, typ attr.Type, val tftypes.Value, target interface{}, opts Options) diag.Diagnostics {
func Into(ctx context.Context, typ attr.Type, val tftypes.Value, target interface{}, opts Options, path path.Path) diag.Diagnostics {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that the path.Path information is available in this function, we should replace the diags.AddError() call below with diags.AddAttributeError() and consider adding Path: path.String()\nError: err.Error() to the details.

var diags diag.Diagnostics

v := reflect.ValueOf(target)
if v.Kind() != reflect.Ptr {
err := fmt.Errorf("target must be a pointer, got %T, which is a %s", target, v.Kind())
diags.AddError(
diags.AddAttributeError(
path,
"Value Conversion Error",
"An unexpected error was encountered trying to convert the value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(),
fmt.Sprintf("An unexpected error was encountered trying to convert the value. This is always an error in the provider. Please report the following to the provider developer:\n\nPath: %s\nError: %s", path.String(), err.Error()),
)
return diags
}
result, diags := BuildValue(ctx, typ, val, v.Elem(), opts, path.Empty())
result, diags := BuildValue(ctx, typ, val, v.Elem(), opts, path)
if diags.HasError() {
return diags
}
Expand Down Expand Up @@ -109,11 +111,12 @@ func BuildValue(ctx context.Context, typ attr.Type, val tftypes.Value, target re
// all that's left to us now is to set it as an empty value or
// throw an error, depending on what's in opts
if !opts.UnhandledUnknownAsEmpty {
Copy link

@apparentlymart apparentlymart Sep 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little concerned that this particular design may behave as a confusing trap for provider developers, for the reason I shared over in #191 (comment) .

Specifically: whether a value is known or not during planning is under the control of the module author, not the provider developer. Therefore I think it's technically invalid to decode any plan-time value into a type that cannot support unknowns, regardless of whether the value happens to actually be unknown in a particular request.

Do you think it would be reasonable to add (in another PR of course, since I see this one is already closed!) an additional flag to opts like RequireHandlingUnknown bool, and then if that's set then we'd always return an error if the target type can't handle unknowns, regardless of whether the given val happens to be unknown or not?

I would expect RequireUnknownHandling to be true for any call that happens while the framework is handling a PlanResourceChange request, one of the several "validate" RPCs, and ConfigureProvider, because those are all situations where the module author can compel the provider to deal with unknown values. This restriction would not be required for ApplyResourceChange, ReadResource, or ReadDataSource, because Terraform Core guarantees that everything will be known by the time those are called. Therefore this additional restriction would not affect the most common case where a provider is only implementing apply-time logic and letting the framework's own logic handle the validation and planning steps.

(A more accurate way to say it is: "config" and "proposed new value" can always potentially contain unknowns, but prior state should never.)

My motivation here is to let provider developers know during their initial development that they need to handle this case, rather than them "getting away with it" because their test cases happen to all have hard-coded known values and then only later getting bug reports from their users when they pass in an unknown value and get this error. I assume provider developers would rather know about this requirement early, rather than needing to rework their logic later only once an end-user discovers it.

Copy link
Contributor

@bflad bflad Sep 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd personally be worried about introducing an option, which if treated as opt-in, that developers won't opt into or know to opt into. Most of our current data handling documentation glosses over some of the available options and for most developers they won't encounter them until they are using the framework-defined types collection types, which usually support them in methods like As(), ElementsAs(), etc.

Admittedly, this is probably the most confusing aspect of provider development with the framework. Very regularly, we see developers struggle with these concepts in Discuss, etc. The associated website documentation is quite confusing where it tries to describe all the various rules. Our best advice is typically to use the framework-defined types types, but its so easy to reach for standard Go types, slices of provider-defined types in the case of lists/sets, and structs (generally without pointers) of provider-defined types in the case of objects. As you can imagine, these all run into the trouble you mention.

I wonder if it may be better if we took a more wholesale step back and re-evaluated the type system to ensure correctness, while still supporting some of the "nice-ities" of the current type system. We have been circling the idea of requiring function calls for interacting with values, rather than accessing/setting struct fields to cover #447. Combining that with an always-enabled rule like you mention in the internal reflection code, might do the trick?

I only mention combined these right now because we're running very low on time to make substantial changes to the framework before a first major version is released and its easier to consider the whole subsystem at once.

A quick sketch of these combined ideas might be:

  • Requiring all framework-defined types or custom provider-defined types that interact with schema-based data must always support null and unknown values.
  • Requiring the creation of framework-defined value types through function calls so there is no method for creating invalid values.
  • Requiring the access of framework-defined value types through method calls which can default to zero-values if null/unknown (NOTE: the attr.Value interface already requires IsNull() bool and IsUnknown() bool methods, so there's no need to access any sort of weird pointer or sentinel value representations)
// Creating framework-defined value types

func NullBool() Bool
func NullFloat64() Float64
func NullInt64() Int64
func NullList() List
func NullMap() Map
func NullNumber() Number
func NullObject() Object
func NullSet() Set
func NullString() String

func UnknownBool() Bool
func UnknownFloat64() Float64
func UnknownInt64() Int64
func UnknownList() List
func UnknownMap() Map
func UnknownNumber() Number
func UnknownObject() Object
func UnknownSet() Set
func UnknownString() String

func BoolValue(bool) Bool
func Float64Value(float64) Float64
func Int64Value(int64) Int64
func ListValue([]attr.Value) List
func MapValue(map[string]attr.Value) Map
func NumberValue(*big.Float) Number
func ObjectValue(struct) Object
func SetValue([]attr.Value) Set
func StringValue(string) String

// Access framework-defined value type values

func (b Bool) BoolValue() b
func (f Float64) Float64Value() float64 // could consider Float32Value(), etc.
func (i Int64) Int64Value() int64 // could consider IntValue(), etc.
func (n Number) BigFloatValue() *big.Float // could consider Float64Value() with errors, etc.
func (s String) StringValue() string
// refer to existing collection type methods, such as As()

// provider-defined types

tfsdk.Schema{
  Attributes: map[string]tfsdk.Attribute{
    "example: {
      // One of Computed, Optional, Required: true, doesn't matter
      Type: types.StringType,
     },
  },
}

type A struct {
  Example string `tfsdk:"example"`
}

type B struct {
  Example *string `tfsdk:"example"`
}

type B struct {
  Example types.String `tfsdk:"example"`
}

var (
  a A
  b B
  c C
  d string
  e *string
  f types.String
)

(tfsdk.Config).Get(ctx, &a) // Always errors
(tfsdk.Config).Get(ctx, &b) // Always errors
(tfsdk.Config).Get(ctx, &c) // No error for this condition
(tfsdk.Config).GetAttribute(ctx, path.Root("example"), &d) // Always errors
(tfsdk.Config).GetAttribute(ctx, path.Root("example"), &e) // Always errors
(tfsdk.Config).GetAttribute(ctx, path.Root("example"), &f) // No error for this condition

(tfsdk.Plan).Get(ctx, &a) // Always errors
(tfsdk.Plan).Get(ctx, &b) // Always errors
(tfsdk.Plan).Get(ctx, &c) // No error for this condition
(tfsdk.Plan).GetAttribute(ctx, path.Root("example"), &d) // Always errors
(tfsdk.Plan).GetAttribute(ctx, path.Root("example"), &e) // Always errors
(tfsdk.Plan).GetAttribute(ctx, path.Root("example"), &f) // No error for this condition

(tfsdk.State).Get(ctx, &a) // Always errors
(tfsdk.State).Get(ctx, &b) // Always errors
(tfsdk.State).Get(ctx, &c) // No error for this condition
(tfsdk.State).GetAttribute(ctx, path.Root("example"), &d) // Always errors
(tfsdk.State).GetAttribute(ctx, path.Root("example"), &e) // Always errors
(tfsdk.State).GetAttribute(ctx, path.Root("example"), &f) // No error for this condition

I think we can still be lenient when setting values as long as they follow the current reflection implementation.

Does this align with what you would expect?

Copy link

@apparentlymart apparentlymart Sep 23, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be a little clearer, I was understanding this reflect.Into function as being an internal implementation detail of the higher level API for fetching values from the config, plan, and state, and so I was therefore assuming that the "options" here are always generated by the framework's own code rather than from provider code. If that isn't true then I would agree that the details of what I proposed don't make sense!

I defer to you on the API design details since I'm still very new to the framework design! I think the scenarios you wrote out here seem to make sense as far as I can mentally evaluate them, although I think it may be possible to make the tfsdk.State API be more permissive and fail only if the specific given value happens to be wrong, because:

  • tfsdk.State should never encounter unknown values, I think?
  • By the time we're dealing in state, the provider is able to control whether null values are valid or not and so can in principle use Required in the schema or validation and/or plan-time rules to ensure that they'll never need to deal with null in a particular argument.

In the above I'm making the assumption that tfsdk.State is only used to describe final state values, such as what would be passed in as the prior state to ReadResource and PlanResourceChange, and what would be returned from ApplyResourceChange. It would be trickier if we ever use tfsdk.State to represent a "proposed new state" or a "planned new state", because of course those can potentially contain unknown values if the configuration contains unknown values.

On the other hand, I think it could potentially be reasonable to argue that it's better to be consistent and have the same rules everywhere, so that there's the most opportunity for code reuse. Particularly with Go 1.18 type parameters it seems possible to write a generic type that can represent a possibly-null, possibly-unknown value of any Go type so that there doesn't need to be a custom type for every combination, which may lower the burden of using these enough that it's reasonable to just make them always required. 🤔

type ValueState int

const (
    ValueConcrete ValueState = iota
    ValueNull
    ValueUnknown
)

// (I don't know what would be a good name for this type,
// so this is just a placeholder.)
type SomethingValue[T any] {
    Value T
    State ValueState
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, I think it could potentially be reasonable to argue that it's better to be consistent and have the same rules everywhere, so that there's the most opportunity for code reuse.

This is the direction that feels right to me personally -- having inconsistent ways to interact with "schema-based data" between the various RPCs, attribute configurability, and type of data (config, plan, state) makes for harder comprehension, documentation, etc. for provider developers. We can certainly document Terraform's general implementation details there if developers want to potentially shortcut some logic (like checking for unknown values when there cannot be any), but I think there are diminishing returns by trying too hard to support Go standard library types or provider-defined slices/structs directly in the reflection when they can be incompatible outside the control of the provider developer.

I will say, it's been very nice to do things like this even in the current framework design:

type exampleResourceModel struct {
  ExampleList types.List `tfsdk:"example_list"`
  ID          types.String `tfsdk:"id"`
}

func (r exampleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
	var data exampleResourceModel

	// Read plan data into model
	resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

	if resp.Diagnostics.HasError() {
		return
	}

	// Potentially interact with infrastructure

	// Set any Computed/Unknown values
	data.ID = types.String{Value: "example"}

	// Write state data from model
	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

It's easy to document on our side and a fairly succinct implementation, code-wise, in my opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #498 to followup on this. 👍 Thank you for raising this here.

err := fmt.Errorf("unhandled unknown value")
diags.AddAttributeError(
path,
"Value Conversion Error",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(),
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.\n\n"+
fmt.Sprintf("Path: %s\nTarget Type: %s\nSuggested Type: %s", path.String(), target.Type(), typ.String()),
)
return target, diags
}
Expand All @@ -132,12 +135,14 @@ func BuildValue(ctx context.Context, typ attr.Type, val tftypes.Value, target re
return reflect.Zero(target.Type()), nil
}

err := fmt.Errorf("unhandled null value")
diags.AddAttributeError(
path,
"Value Conversion Error",
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+err.Error(),
"An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
"Received null value, however the target type cannot handle null values. Use the corresponding `types` package type, a pointer type or a custom type that handles null values.\n\n"+
fmt.Sprintf("Path: %s\nTarget Type: %s\nSuggested `types` Type: %s\nSuggested Pointer Type: *%s", path.String(), target.Type(), typ.String(), target.Type()),
)

return target, diags
}
// *big.Float and *big.Int are technically pointers, but we want them
Expand Down
3 changes: 2 additions & 1 deletion tfsdk/value_as.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
)

// ValueAs takes the attr.Value `val` and populates the Go value `target` with its content.
Expand All @@ -22,5 +23,5 @@ func ValueAs(ctx context.Context, val attr.Value, target interface{}) diag.Diagn
return diag.Diagnostics{diag.NewErrorDiagnostic("Error converting value",
fmt.Sprintf("An unexpected error was encountered converting a %T to its equivalent Terraform representation. This is always a bug in the provider.\n\nError: %s", val, err))}
}
return reflect.Into(ctx, val.Type(ctx), raw, target, reflect.Options{})
return reflect.Into(ctx, val.Type(ctx), raw, target, reflect.Options{}, path.Empty())
}
5 changes: 3 additions & 2 deletions types/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var (
Expand Down Expand Up @@ -203,7 +204,7 @@ func (l List) ElementsAs(ctx context.Context, target interface{}, allowUnhandled
return reflect.Into(ctx, ListType{ElemType: l.ElemType}, values, target, reflect.Options{
UnhandledNullAsEmpty: allowUnhandled,
UnhandledUnknownAsEmpty: allowUnhandled,
})
}, path.Empty())
}

// Type returns a ListType with the same element type as `l`.
Expand Down
6 changes: 4 additions & 2 deletions types/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import (
"sort"
"strings"

"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var (
Expand Down Expand Up @@ -205,10 +206,11 @@ func (m Map) ElementsAs(ctx context.Context, target interface{}, allowUnhandled
),
}
}

return reflect.Into(ctx, MapType{ElemType: m.ElemType}, val, target, reflect.Options{
UnhandledNullAsEmpty: allowUnhandled,
UnhandledUnknownAsEmpty: allowUnhandled,
})
}, path.Empty())
}

// Type returns a MapType with the same element type as `m`.
Expand Down
4 changes: 3 additions & 1 deletion types/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"

"github.com/hashicorp/terraform-plugin-go/tftypes"
)

Expand Down Expand Up @@ -197,7 +199,7 @@ func (o Object) As(ctx context.Context, target interface{}, opts ObjectAsOptions
return reflect.Into(ctx, obj, val, target, reflect.Options{
UnhandledNullAsEmpty: opts.UnhandledNullAsEmpty,
UnhandledUnknownAsEmpty: opts.UnhandledUnknownAsEmpty,
})
}, path.Empty())
}

// Type returns an ObjectType with the same attribute types as `o`.
Expand Down
5 changes: 3 additions & 2 deletions types/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-go/tftypes"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/attr/xattr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var (
Expand Down Expand Up @@ -235,7 +236,7 @@ func (s Set) ElementsAs(ctx context.Context, target interface{}, allowUnhandled
return reflect.Into(ctx, s.Type(ctx), val, target, reflect.Options{
UnhandledNullAsEmpty: allowUnhandled,
UnhandledUnknownAsEmpty: allowUnhandled,
})
}, path.Empty())
}

// Type returns a SetType with the same element type as `s`.
Expand Down