-
Notifications
You must be signed in to change notification settings - Fork 96
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
Cannot write state to interface in generic func #1035
Comments
Hi @JacobPotter 👋🏾, Sorry that you're running into trouble here. The As a workaround, you can try implementing the tftypes.ValueConverter interface on |
@SBGoods Can you show an example of how ValueConverter would be implemented? EDIT: I am trying to implement it, but I am unsure how the ValueConverter method would interact with the model and what it would return |
@SBGoods I have updated my comment above, bumping for visibility. |
Hey there @JacobPotter, unfortunately there aren't a ton of examples of implementing that interface since most provider implementations just use the reflection as-is. There is a dynamic resource that Terraform core uses for it's own testing that implements it: https://github.com/hashicorp/terraform-provider-tfcoremock/blob/b8029786d035fdc5d5003511b3299da3a17ef74e/internal/data/resource.go#L67 It also implements the |
So I attempted to implement both interfaces, and I am running into an issue with reflect. Here is my implementation of func (g *GroupResourceModel) FromTerraform5Value(value tftypes.Value) error {
// It has to be an object we are converting from.
if !value.Type().Is(tftypes.Object{}) {
return errors.New("can only convert between object types")
}
values, err := fromTerraform5Value(value)
if err != nil {
return err
}
// We know these kinds of conversions are safe now, as we checked the type
// at the beginning.
if v, ok := values.(map[string]any); ok {
switch v["id"].(type) {
case big.Float, *big.Float:
i, _ := v["id"].(*big.Float).Int64()
g.ID = types.Int64Value(i)
case int, int64:
g.ID = types.Int64Value(v["id"].(int64))
default:
return fmt.Errorf("bad id value type: %v", reflect.TypeOf(v))
}
g.URL = types.StringValue(v["url"].(string))
g.Name = types.StringValue(v["name"].(string))
g.Default = types.BoolValue(v["default"].(bool))
g.Deleted = types.BoolValue(v["deleted"].(bool))
g.IsPublic = types.BoolValue(v["is_public"].(bool))
g.Description = types.StringValue(v["description"].(string))
g.CreatedAt = types.StringValue(v["created_at"].(string))
g.UpdatedAt = types.StringValue(v["updated_at"].(string))
} else {
return errors.New("can only convert between object types")
}
return nil
} When trying to run, am getting a panic:
Looking at a debugger when this happens, it seems to be failing when trying to create a So my implementation of |
Hmm, very interesting! That looks like there's a potential missing case in our reflection utility before trying to retrieve that method. A lot of the reflection logic was written pre-generics, so it wouldn't be surprising 🤔. I'll try to recreate that just in the framework logic and report back. |
Yeah, it would be nice if there was cleaner support for generics in general. Needing to implement |
Just spent some time looking at your examples and I think I may have skipped by a detail in there: func CreateResource[M any](ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse, resourceModel ResourceTransformWithID[M], createFunc func(ctx context.Context, newResource M) (M, error)) {
response.Diagnostics.Append(request.Plan.Get(ctx, &resourceModel)...) In your provider, for parameter If that's the case I think you'd want to pass that in directly, rather than passing a pointer of func CreateResource[M any](ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse, resourceModel ResourceTransformWithID[M], createFunc func(ctx context.Context, newResource M) (M, error)) {
// resourceModel is already a pointer to a struct
response.Diagnostics.Append(request.Plan.Get(ctx, resourceModel)...) If you're able to do that, I don't believe you'll need to implement Side note: I notice your provider is private, but are you able to share some of the code that instantiates/calls your |
While attempting to recreate the behavior, I think I stumbled across a different bug, top-level structs that implement Fields on a struct that implement With that aside, I'm still thinking we should be able to resolve your problem by passing in the variable that contains the pointer struct ( |
@austinvalle Sorry for the late response, but this exercise's whole point was to create some generic functions to handle different resource models. If we pass in the actual struct instead of the interface, doesn't that leave me where I was in the first place, where I have repeated logic for different models? |
I might just be doing a poor job of explaining what I mean 🤔 My comment above was suggesting to just modify your generic I wrote a quick compilable playground example to try and illustrate what I'm talking about: https://go.dev/play/p/ehnEgBpxEWk You may run into a build timeout running this in the Go playground, but you can always just copy it locally into a go file and run it. Output should show all the data flowing through properly: New State will be: tftypes.Object["id":tftypes.Number, "name":tftypes.String]<"id":tftypes.Number<"1234">, "name":tftypes.String<"test-name">> Notice in that example, if you uncomment line 87, you'll get the same error you're referencing: panic: [{{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:
don't know how to reflect tftypes.Object["id":tftypes.Number, "name":tftypes.String] into main.ResourceTransformWithID[main.ZenDeskDynamicContentItem] Value Conversion Error} {[]}}]
goroutine 1 [running]:
main.main()
/tmp/sandbox2824779807/prog.go:75 +0x874 Here is the entire example in-case that link ever diespackage main
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)
func main() {
// -----------------
// All of this logic is just here to simulate a request/response that comes from terraform-plugin-framework.
// This is just simulating creating a plan with an unknown id and a configured "test-name".
// -----------------
rawValue := tftypes.NewValue(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.Number,
"name": tftypes.String,
},
},
map[string]tftypes.Value{
"id": tftypes.NewValue(tftypes.Number, tftypes.UnknownValue),
"name": tftypes.NewValue(tftypes.String, "test-name"),
},
)
fwSchema := schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Computed: true,
},
"name": schema.StringAttribute{
Required: true,
},
},
}
fwReq := resource.CreateRequest{
Config: tfsdk.Config{
Raw: rawValue,
Schema: fwSchema,
},
Plan: tfsdk.Plan{
Raw: rawValue,
Schema: fwSchema,
},
}
fwResp := resource.CreateResponse{
State: tfsdk.State{
Raw: rawValue,
Schema: fwSchema,
},
}
// -----------------
// Creating a pointer to a struct, this struct satisfies the ResourceTransformWithID[M] interface and can be reflected on by Plan.Get
dynamicContentItem := &DynamicContentItemResourceModel{}
CreateResource(context.Background(), fwReq, &fwResp, dynamicContentItem, func(ctx context.Context, newResource ZenDeskDynamicContentItem) (ZenDeskDynamicContentItem, error) {
// Simulating an API call
return ZenDeskDynamicContentItem{
ID: 1234,
Name: "test-name",
}, nil
})
// If we get an error just print it and exit
if fwResp.Diagnostics.HasError() {
panic(fmt.Sprintf("%s", fwResp.Diagnostics))
}
// If no error, print what the new state will be (populated by the createFunc)
fmt.Printf("New State will be: %s", fwResp.State.Raw)
}
func CreateResource[M any](ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse, resourceModel ResourceTransformWithID[M], createFunc func(ctx context.Context, newResource M) (M, error)) {
// This was the original line from: https://github.com/hashicorp/terraform-plugin-framework/issues/1035
// Plan.Get will eventually grab the type of => ResourceTransformWithID[M], which is an interface and cannot be created.
// ---- Uncomment the line below to see the original error message -----
//
// response.Diagnostics.Append(request.Plan.Get(ctx, &resourceModel)...)
// This is the suggested line from: https://github.com/hashicorp/terraform-plugin-framework/issues/1035#issuecomment-2403459626
// Plan.Get will eventually grab the type of => DynamicContentItem, which is a struct that can be created.
//
response.Diagnostics.Append(request.Plan.Get(ctx, resourceModel)...)
if response.Diagnostics.HasError() {
return
}
newResource, diags := resourceModel.GetApiModelFromTfModel(ctx)
response.Diagnostics.Append(diags...)
if response.Diagnostics.HasError() {
return
}
resp, err := createFunc(ctx, newResource)
if err != nil {
response.Diagnostics.AddError("Error creating resource", fmt.Sprintf("Error: %s", err))
return
}
response.Diagnostics.Append(resourceModel.GetTfModelFromApiModel(ctx, resp)...)
if response.Diagnostics.HasError() {
return
}
response.Diagnostics.Append(response.State.Set(ctx, resourceModel)...)
}
type ResourceTransformWithID[M any] interface {
GetID() int64
ResourceTransform[M]
}
type ResourceTransform[M any] interface {
GetApiModelFromTfModel(context.Context) (M, diag.Diagnostics)
GetTfModelFromApiModel(context.Context, M) diag.Diagnostics
}
type DynamicContentItemResourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}
// => zendesk.DynamicContentItem
type ZenDeskDynamicContentItem struct {
ID int64
Name string
}
func (d *DynamicContentItemResourceModel) GetID() int64 {
return d.ID.ValueInt64()
}
func (d *DynamicContentItemResourceModel) GetApiModelFromTfModel(_ context.Context) (dci ZenDeskDynamicContentItem, diags diag.Diagnostics) {
return ZenDeskDynamicContentItem{
ID: d.ID.ValueInt64(),
Name: d.Name.ValueString(),
}, nil
}
func (d *DynamicContentItemResourceModel) GetTfModelFromApiModel(_ context.Context, dci ZenDeskDynamicContentItem) (diags diag.Diagnostics) {
d.ID = types.Int64Value(dci.ID)
d.Name = types.StringValue(dci.Name)
return nil
} |
Hi @JacobPotter have you solved somehow generics vs framework issue? It looks like we have a similar case in our dev work :/ |
@austinvalle, I have not had time to test this out, but I'll get back to you soon with my findings. @DariuszPorowski, unfortunately, I have not resolved this, but I'll let you know if I do. |
@austinvalle your solution worked; thanks for your help here |
Awesome! So for anyone else that may run into a similar error here, you want to ensure that you're passing an actual struct pointer value to any reflection logic ( Feel free to open a separate issue with more details if you run into a similar problem and need help investigating 🔍 👀 ! |
Module version
Relevant provider source code
SEE ACTUAL BEHAVIOR FOR EXAMPLES
Terraform Configuration Files
...
Debug Output
Expected Behavior
Actual Behavior
We are using an interface for our resource models that stores the state of a provider resource. This interface defines methods for converting data from API to TF and vice versa. For example:
We have an interface here:
Which is implemented by the model below
If I try to use a generic function like this:
I get the following error when trying to apply:
It looks like
request.Plan.Get
is having an issue writing the state data into the model when using an interface.Steps to Reproduce
References
The text was updated successfully, but these errors were encountered: