Skip to content

Commit e39d577

Browse files
authored
list: framework changes to support list with sdkv2 resources (#1198)
* framework changes support list in sdkv2 * update Set and SetAtPath to accept values of tftypes.Value * move protov5 information into the resource metadataresponse * add tests for updated Set and SetAtPath functionality * remove redundant return statement * further clean up * add a schemas method for list * return expected and received types in diags and add tests for failure states * address review comments * updated expected error message * extend support for supplying raw schema types to proto6 * review comments
1 parent d2548a9 commit e39d577

11 files changed

+366
-42
lines changed

internal/fwschemadata/data_set.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,31 @@ import (
1010
"github.com/hashicorp/terraform-plugin-framework/diag"
1111
"github.com/hashicorp/terraform-plugin-framework/internal/reflect"
1212
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-go/tftypes"
1314
)
1415

15-
// Set replaces the entire value. The value should be a struct whose fields
16+
// Set replaces the entire value. The value can be a tftypes.Value or a struct whose fields
1617
// have one of the attr.Value types. Each field must have the tfsdk field tag.
1718
func (d *Data) Set(ctx context.Context, val any) diag.Diagnostics {
19+
var diags diag.Diagnostics
20+
21+
if v, ok := val.(tftypes.Value); ok {
22+
objType := d.Schema.Type().TerraformType(ctx)
23+
24+
if !objType.Equal(v.Type()) {
25+
diags.AddError(
26+
d.Description.Title()+" Write Error",
27+
"An unexpected error was encountered trying to write the "+d.Description.String()+". This is always an error in the provider. Please report the following to the provider developer:\n\n"+
28+
fmt.Sprintf("Error: Type mismatch between provided value and type of %s, expected %+v, got %+v", d.Description.String(), objType.String(), v.Type().String()),
29+
)
30+
return diags
31+
32+
}
33+
d.TerraformValue = v
34+
35+
return diags
36+
}
37+
1838
attrValue, diags := reflect.FromValue(ctx, d.Schema.Type(), val, path.Empty())
1939

2040
if diags.HasError() {

internal/fwschemadata/data_set_at_path.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,52 @@ import (
2828
// Lists can only have the next element added according to the current length.
2929
func (d *Data) SetAtPath(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics {
3030
var diags diag.Diagnostics
31-
3231
ctx = logging.FrameworkWithAttributePath(ctx, path.String())
3332

33+
if v, ok := val.(tftypes.Value); ok {
34+
atPath, atPathDiags := d.Schema.AttributeAtPath(ctx, path)
35+
36+
diags.Append(atPathDiags...)
37+
38+
if diags.HasError() {
39+
return diags
40+
}
41+
42+
attrType := atPath.GetType().TerraformType(ctx)
43+
44+
if !attrType.Equal(v.Type()) {
45+
diags.AddAttributeError(
46+
path,
47+
d.Description.Title()+" Write Error",
48+
"An unexpected error was encountered trying to write the "+d.Description.String()+". This is always an error in the provider. Please report the following to the provider developer:\n\n"+
49+
fmt.Sprintf("Error: Type of provided value does not match type of %q, expected %s, got %s", path.String(), attrType.String(), v.Type().String()),
50+
)
51+
return diags
52+
}
53+
54+
transformFunc, transformFuncDiags := d.SetAtPathTransformFunc(ctx, path, v, nil)
55+
diags.Append(transformFuncDiags...)
56+
57+
if diags.HasError() {
58+
return diags
59+
}
60+
61+
tfVal, err := tftypes.Transform(d.TerraformValue, transformFunc)
62+
if err != nil {
63+
diags.AddAttributeError(
64+
path,
65+
d.Description.Title()+" Write Error",
66+
"An unexpected error was encountered trying to write an attribute to the "+d.Description.String()+". This is always an error in the provider. Please report the following to the provider developer:\n\n"+
67+
"Error: Cannot transform data: "+err.Error(),
68+
)
69+
return diags
70+
}
71+
72+
d.TerraformValue = tfVal
73+
74+
return diags
75+
}
76+
3477
tftypesPath, tftypesPathDiags := totftypes.AttributePath(ctx, path)
3578

3679
diags.Append(tftypesPathDiags...)

internal/fwschemadata/data_set_at_path_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,6 +2924,63 @@ func TestDataSetAtPath(t *testing.T) {
29242924
"other": tftypes.NewValue(tftypes.DynamicPseudoType, nil),
29252925
}),
29262926
},
2927+
"write-tftypes-value": {
2928+
data: fwschemadata.Data{
2929+
TerraformValue: tftypes.NewValue(tftypes.Object{
2930+
AttributeTypes: map[string]tftypes.Type{
2931+
"test": tftypes.String,
2932+
"other": tftypes.String,
2933+
},
2934+
}, nil),
2935+
Schema: testschema.Schema{
2936+
Attributes: map[string]fwschema.Attribute{
2937+
"test": testschema.Attribute{
2938+
Type: types.StringType,
2939+
Required: true,
2940+
},
2941+
"other": testschema.Attribute{
2942+
Type: types.StringType,
2943+
Required: true,
2944+
},
2945+
},
2946+
},
2947+
},
2948+
path: path.Root("test"),
2949+
val: tftypes.NewValue(tftypes.String, "newvalue"),
2950+
expected: tftypes.NewValue(tftypes.Object{
2951+
AttributeTypes: map[string]tftypes.Type{
2952+
"test": tftypes.String,
2953+
"other": tftypes.String,
2954+
},
2955+
}, map[string]tftypes.Value{
2956+
"test": tftypes.NewValue(tftypes.String, "newvalue"),
2957+
"other": tftypes.NewValue(tftypes.String, nil),
2958+
}),
2959+
},
2960+
"write-tftypes-value-MismatchedTypeError": {
2961+
data: fwschemadata.Data{
2962+
TerraformValue: tftypes.Value{},
2963+
Schema: testschema.Schema{
2964+
Attributes: map[string]fwschema.Attribute{
2965+
"test": testschema.Attribute{
2966+
Type: types.StringType,
2967+
Required: true,
2968+
},
2969+
"other": testschema.Attribute{
2970+
Type: types.StringType,
2971+
Required: true,
2972+
},
2973+
},
2974+
},
2975+
},
2976+
path: path.Root("test"),
2977+
val: tftypes.NewValue(tftypes.Bool, false),
2978+
expected: tftypes.Value{},
2979+
expectedDiags: diag.Diagnostics{
2980+
diag.NewAttributeErrorDiagnostic(path.Root("test"), "Data Write Error", "An unexpected error was encountered trying to write the data. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
2981+
"Error: Type of provided value does not match type of \"test\", expected tftypes.String, got tftypes.Bool"),
2982+
},
2983+
},
29272984
"AttrTypeWithValidateError": {
29282985
data: fwschemadata.Data{
29292986
TerraformValue: tftypes.NewValue(tftypes.Object{

internal/fwschemadata/data_set_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,58 @@ func TestDataSet(t *testing.T) {
139139
),
140140
}),
141141
},
142+
"write-tftypes-values": {
143+
data: fwschemadata.Data{
144+
TerraformValue: tftypes.Value{},
145+
Schema: testschema.Schema{
146+
Attributes: map[string]fwschema.Attribute{
147+
"name": testschema.Attribute{
148+
Type: types.StringType,
149+
Required: true,
150+
},
151+
},
152+
},
153+
},
154+
val: tftypes.NewValue(tftypes.Object{
155+
AttributeTypes: map[string]tftypes.Type{
156+
"name": tftypes.String,
157+
},
158+
}, map[string]tftypes.Value{
159+
"name": tftypes.NewValue(tftypes.String, "newvalue"),
160+
}),
161+
expected: tftypes.NewValue(tftypes.Object{
162+
AttributeTypes: map[string]tftypes.Type{
163+
"name": tftypes.String,
164+
},
165+
}, map[string]tftypes.Value{
166+
"name": tftypes.NewValue(tftypes.String, "newvalue"),
167+
}),
168+
},
169+
"write-tftypes-values-MismatchedTypeError": {
170+
data: fwschemadata.Data{
171+
TerraformValue: tftypes.Value{},
172+
Schema: testschema.Schema{
173+
Attributes: map[string]fwschema.Attribute{
174+
"name": testschema.Attribute{
175+
Type: types.StringType,
176+
Required: true,
177+
},
178+
},
179+
},
180+
},
181+
val: tftypes.NewValue(tftypes.Object{
182+
AttributeTypes: map[string]tftypes.Type{
183+
"not_name": tftypes.String,
184+
},
185+
}, map[string]tftypes.Value{
186+
"not_name": tftypes.NewValue(tftypes.String, "newvalue"),
187+
}),
188+
expected: tftypes.Value{},
189+
expectedDiags: diag.Diagnostics{
190+
diag.NewErrorDiagnostic("Data Write Error", "An unexpected error was encountered trying to write the data. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
191+
"Error: Type mismatch between provided value and type of data, expected tftypes.Object[\"name\":tftypes.String], got tftypes.Object[\"not_name\":tftypes.String]"),
192+
},
193+
},
142194
"overwrite": {
143195
data: fwschemadata.Data{
144196
TerraformValue: tftypes.Value{},
@@ -163,6 +215,7 @@ func TestDataSet(t *testing.T) {
163215
}, map[string]tftypes.Value{
164216
"name": tftypes.NewValue(tftypes.String, "newvalue"),
165217
}),
218+
expectedDiags: diag.Diagnostics{},
166219
},
167220
"overwrite-dynamic": {
168221
data: fwschemadata.Data{

internal/fwserver/server_getmetadata_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,8 @@ func TestServerGetMetadata(t *testing.T) {
839839
diag.NewErrorDiagnostic(
840840
"ListResource Type Defined without a Matching Managed Resource Type",
841841
"The test_resource_1 ListResource type name was returned, but no matching managed Resource type was defined. "+
842+
"If the matching managed Resource type is not a framework resource either ProtoV5Schema and ProtoV5IdentitySchema must be specified in the RawV5Schemas method, "+
843+
"or ProtoV6Schema and ProtoV6IdentitySchema must be specified in the RawV6Schemas method. "+
842844
"This is always an issue with the provider and should be reported to the provider developers.",
843845
),
844846
},

internal/fwserver/server_listresource.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,14 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream
156156
logging.FrameworkTrace(ctx, "Called provider defined ListResource")
157157

158158
// If the provider returned a nil results stream, we return an empty stream.
159-
if stream.Results == nil {
160-
stream.Results = list.NoListResults
161-
}
162-
163159
if diagsStream.Results == nil {
164160
diagsStream.Results = list.NoListResults
165161
}
166162

163+
if stream.Results == nil {
164+
stream.Results = list.NoListResults
165+
}
166+
167167
fwStream.Results = processListResults(req, stream.Results, diagsStream.Results)
168168
}
169169

internal/fwserver/server_listresource_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,34 @@ func TestServerListResource(t *testing.T) {
184184
expectedStreamEvents: []fwserver.ListResult{},
185185
expectedError: "config cannot be nil",
186186
},
187+
"zero-results-with-warning-diagnostic": {
188+
server: &fwserver.Server{
189+
Provider: &testprovider.Provider{},
190+
},
191+
request: &fwserver.ListRequest{
192+
Config: &tfsdk.Config{},
193+
ListResource: &testprovider.ListResourceWithConfigure{
194+
ConfigureMethod: func(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
195+
resp.Diagnostics.AddWarning("Test Warning", "This is a test warning diagnostic")
196+
},
197+
ListResource: &testprovider.ListResource{
198+
ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) {
199+
resp.Results = list.NoListResults
200+
},
201+
},
202+
},
203+
},
204+
expectedStreamEvents: []fwserver.ListResult{
205+
{
206+
Identity: nil,
207+
Resource: nil,
208+
DisplayName: "",
209+
Diagnostics: diag.Diagnostics{
210+
diag.NewWarningDiagnostic("Test Warning", "This is a test warning diagnostic"),
211+
},
212+
},
213+
},
214+
},
187215
"listresource-configure-data": {
188216
server: &fwserver.Server{
189217
ListResourceConfigureData: "test-provider-configure-value",

internal/fwserver/server_listresources.go

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,28 @@ func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list.
8585
continue
8686
}
8787

88+
rawV5SchemasResp := list.RawV5SchemaResponse{}
89+
if listResourceWithSchemas, ok := listResource.(list.ListResourceWithRawV5Schemas); ok {
90+
listResourceWithSchemas.RawV5Schemas(ctx, list.RawV5SchemaRequest{}, &rawV5SchemasResp)
91+
}
92+
93+
rawV6SchemasResp := list.RawV6SchemaResponse{}
94+
if listResourceWithSchemas, ok := listResource.(list.ListResourceWithRawV6Schemas); ok {
95+
listResourceWithSchemas.RawV6Schemas(ctx, list.RawV6SchemaRequest{}, &rawV6SchemasResp)
96+
}
97+
8898
resourceFuncs, _ := s.ResourceFuncs(ctx)
8999
if _, ok := resourceFuncs[typeName]; !ok {
90-
s.listResourceFuncsDiags.AddError(
91-
"ListResource Type Defined without a Matching Managed Resource Type",
92-
fmt.Sprintf("The %s ListResource type name was returned, but no matching managed Resource type was defined. ", typeName)+
93-
"This is always an issue with the provider and should be reported to the provider developers.",
94-
)
95-
continue
100+
if (rawV5SchemasResp.ProtoV5Schema == nil || rawV5SchemasResp.ProtoV5IdentitySchema == nil) && (rawV6SchemasResp.ProtoV6Schema == nil || rawV6SchemasResp.ProtoV6IdentitySchema == nil) {
101+
s.listResourceFuncsDiags.AddError(
102+
"ListResource Type Defined without a Matching Managed Resource Type",
103+
fmt.Sprintf("The %s ListResource type name was returned, but no matching managed Resource type was defined. ", typeName)+
104+
"If the matching managed Resource type is not a framework resource either ProtoV5Schema and ProtoV5IdentitySchema must be specified in the RawV5Schemas method, "+
105+
"or ProtoV6Schema and ProtoV6IdentitySchema must be specified in the RawV6Schemas method. "+
106+
"This is always an issue with the provider and should be reported to the provider developers.",
107+
)
108+
continue
109+
}
96110
}
97111

98112
s.listResourceFuncs[typeName] = listResourceFunc

internal/proto5server/server_listresource.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/hashicorp/terraform-plugin-framework/internal/fromproto5"
1111
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1212
"github.com/hashicorp/terraform-plugin-framework/internal/toproto5"
13+
"github.com/hashicorp/terraform-plugin-framework/list"
1314
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
1415
)
1516

@@ -47,26 +48,49 @@ func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov5.ListResou
4748
return ListRequestErrorDiagnostics(ctx, allDiags...)
4849
}
4950

50-
resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, protoReq.TypeName)
51-
allDiags.Append(diags...)
52-
if diags.HasError() {
53-
return ListRequestErrorDiagnostics(ctx, allDiags...)
51+
req := &fwserver.ListRequest{
52+
Config: config,
53+
ListResource: listResource,
54+
IncludeResource: protoReq.IncludeResource,
55+
Limit: protoReq.Limit,
5456
}
5557

56-
identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, protoReq.TypeName)
57-
allDiags.Append(diags...)
58-
if diags.HasError() {
59-
return ListRequestErrorDiagnostics(ctx, allDiags...)
58+
schemaResp := list.RawV5SchemaResponse{}
59+
if listResourceWithProtoSchemas, ok := listResource.(list.ListResourceWithRawV5Schemas); ok {
60+
listResourceWithProtoSchemas.RawV5Schemas(ctx, list.RawV5SchemaRequest{}, &schemaResp)
6061
}
6162

62-
req := &fwserver.ListRequest{
63-
Config: config,
64-
ListResource: listResource,
65-
ResourceSchema: resourceSchema,
66-
ResourceIdentitySchema: identitySchema,
67-
IncludeResource: protoReq.IncludeResource,
68-
Limit: protoReq.Limit,
63+
// There's validation in ListResources that ensures both are set if either is provided so it should be sufficient to only nil check Identity
64+
if schemaResp.ProtoV5IdentitySchema != nil {
65+
var err error
66+
67+
req.ResourceSchema, err = fromproto5.ResourceSchema(ctx, schemaResp.ProtoV5Schema)
68+
if err != nil {
69+
diags.AddError("Converting Resource Schema", err.Error())
70+
allDiags.Append(diags...)
71+
return ListRequestErrorDiagnostics(ctx, allDiags...)
72+
}
73+
74+
req.ResourceIdentitySchema, err = fromproto5.IdentitySchema(ctx, schemaResp.ProtoV5IdentitySchema)
75+
if err != nil {
76+
diags.AddError("Converting Resource Identity Schema", err.Error())
77+
allDiags.Append(diags...)
78+
return ListRequestErrorDiagnostics(ctx, allDiags...)
79+
}
80+
} else {
81+
req.ResourceSchema, diags = s.FrameworkServer.ResourceSchema(ctx, protoReq.TypeName)
82+
allDiags.Append(diags...)
83+
if diags.HasError() {
84+
return ListRequestErrorDiagnostics(ctx, allDiags...)
85+
}
86+
87+
req.ResourceIdentitySchema, diags = s.FrameworkServer.ResourceIdentitySchema(ctx, protoReq.TypeName)
88+
allDiags.Append(diags...)
89+
if diags.HasError() {
90+
return ListRequestErrorDiagnostics(ctx, allDiags...)
91+
}
6992
}
93+
7094
stream := &fwserver.ListResultsStream{}
7195

7296
s.FrameworkServer.ListResource(ctx, req, stream)

0 commit comments

Comments
 (0)