Skip to content
Merged
22 changes: 21 additions & 1 deletion internal/fwschemadata/data_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,31 @@ import (
"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"
)

// Set replaces the entire value. The value should be a struct whose fields
// Set replaces the entire value. The value can be a tftypes.Value or a struct whose fields
// have one of the attr.Value types. Each field must have the tfsdk field tag.
func (d *Data) Set(ctx context.Context, val any) diag.Diagnostics {
var diags diag.Diagnostics

if v, ok := val.(tftypes.Value); ok {
objType := d.Schema.Type().TerraformType(ctx)

if !objType.Equal(v.Type()) {
diags.AddError(
d.Description.Title()+" Write Error",
"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"+
fmt.Sprintf("Error: Type mismatch between provided value and type of %s, expected %+v, got %+v", d.Description.String(), objType.String(), v.Type().String()),
)
return diags

}
d.TerraformValue = v

return diags
}

attrValue, diags := reflect.FromValue(ctx, d.Schema.Type(), val, path.Empty())

if diags.HasError() {
Expand Down
45 changes: 44 additions & 1 deletion internal/fwschemadata/data_set_at_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,52 @@ import (
// Lists can only have the next element added according to the current length.
func (d *Data) SetAtPath(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics {
var diags diag.Diagnostics

ctx = logging.FrameworkWithAttributePath(ctx, path.String())

if v, ok := val.(tftypes.Value); ok {
atPath, atPathDiags := d.Schema.AttributeAtPath(ctx, path)

diags.Append(atPathDiags...)

if diags.HasError() {
return diags
}

attrType := atPath.GetType().TerraformType(ctx)

if !attrType.Equal(v.Type()) {
diags.AddAttributeError(
path,
d.Description.Title()+" Write Error",
"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"+
fmt.Sprintf("Error: Type of provided value does not match type of %q, expected %s, got %s", path.String(), attrType.String(), v.Type().String()),
)
return diags
}

transformFunc, transformFuncDiags := d.SetAtPathTransformFunc(ctx, path, v, nil)
diags.Append(transformFuncDiags...)

if diags.HasError() {
return diags
}

tfVal, err := tftypes.Transform(d.TerraformValue, transformFunc)
if err != nil {
diags.AddAttributeError(
path,
d.Description.Title()+" Write Error",
"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"+
"Error: Cannot transform data: "+err.Error(),
)
return diags
}

d.TerraformValue = tfVal

return diags
}

tftypesPath, tftypesPathDiags := totftypes.AttributePath(ctx, path)

diags.Append(tftypesPathDiags...)
Expand Down
57 changes: 57 additions & 0 deletions internal/fwschemadata/data_set_at_path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2924,6 +2924,63 @@ func TestDataSetAtPath(t *testing.T) {
"other": tftypes.NewValue(tftypes.DynamicPseudoType, nil),
}),
},
"write-tftypes-value": {
data: fwschemadata.Data{
TerraformValue: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test": tftypes.String,
"other": tftypes.String,
},
}, nil),
Schema: testschema.Schema{
Attributes: map[string]fwschema.Attribute{
"test": testschema.Attribute{
Type: types.StringType,
Required: true,
},
"other": testschema.Attribute{
Type: types.StringType,
Required: true,
},
},
},
},
path: path.Root("test"),
val: tftypes.NewValue(tftypes.String, "newvalue"),
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"test": tftypes.String,
"other": tftypes.String,
},
}, map[string]tftypes.Value{
"test": tftypes.NewValue(tftypes.String, "newvalue"),
"other": tftypes.NewValue(tftypes.String, nil),
}),
},
"write-tftypes-value-MismatchedTypeError": {
data: fwschemadata.Data{
TerraformValue: tftypes.Value{},
Schema: testschema.Schema{
Attributes: map[string]fwschema.Attribute{
"test": testschema.Attribute{
Type: types.StringType,
Required: true,
},
"other": testschema.Attribute{
Type: types.StringType,
Required: true,
},
},
},
},
path: path.Root("test"),
val: tftypes.NewValue(tftypes.Bool, false),
expected: tftypes.Value{},
expectedDiags: diag.Diagnostics{
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"+
"Error: Type of provided value does not match type of \"test\", expected tftypes.String, got tftypes.Bool"),
},
},
"AttrTypeWithValidateError": {
data: fwschemadata.Data{
TerraformValue: tftypes.NewValue(tftypes.Object{
Expand Down
53 changes: 53 additions & 0 deletions internal/fwschemadata/data_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,58 @@ func TestDataSet(t *testing.T) {
),
}),
},
"write-tftypes-values": {
data: fwschemadata.Data{
TerraformValue: tftypes.Value{},
Schema: testschema.Schema{
Attributes: map[string]fwschema.Attribute{
"name": testschema.Attribute{
Type: types.StringType,
Required: true,
},
},
},
},
val: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
},
}, map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "newvalue"),
}),
expected: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
},
}, map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "newvalue"),
}),
},
"write-tftypes-values-MismatchedTypeError": {
data: fwschemadata.Data{
TerraformValue: tftypes.Value{},
Schema: testschema.Schema{
Attributes: map[string]fwschema.Attribute{
"name": testschema.Attribute{
Type: types.StringType,
Required: true,
},
},
},
},
val: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"not_name": tftypes.String,
},
}, map[string]tftypes.Value{
"not_name": tftypes.NewValue(tftypes.String, "newvalue"),
}),
expected: tftypes.Value{},
expectedDiags: diag.Diagnostics{
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"+
"Error: Type mismatch between provided value and type of data, expected tftypes.Object[\"name\":tftypes.String], got tftypes.Object[\"not_name\":tftypes.String]"),
},
},
"overwrite": {
data: fwschemadata.Data{
TerraformValue: tftypes.Value{},
Expand All @@ -163,6 +215,7 @@ func TestDataSet(t *testing.T) {
}, map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "newvalue"),
}),
expectedDiags: diag.Diagnostics{},
},
"overwrite-dynamic": {
data: fwschemadata.Data{
Expand Down
2 changes: 2 additions & 0 deletions internal/fwserver/server_getmetadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,8 @@ func TestServerGetMetadata(t *testing.T) {
diag.NewErrorDiagnostic(
"ListResource Type Defined without a Matching Managed Resource Type",
"The test_resource_1 ListResource type name was returned, but no matching managed Resource type was defined. "+
"If the matching managed Resource type is not a framework resource either ProtoV5Schema and ProtoV5IdentitySchema must be specified in the RawV5Schemas method, "+
"or ProtoV6Schema and ProtoV6IdentitySchema must be specified in the RawV6Schemas method. "+
"This is always an issue with the provider and should be reported to the provider developers.",
),
},
Expand Down
8 changes: 4 additions & 4 deletions internal/fwserver/server_listresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,14 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream
logging.FrameworkTrace(ctx, "Called provider defined ListResource")

// If the provider returned a nil results stream, we return an empty stream.
if stream.Results == nil {
stream.Results = list.NoListResults
}

if diagsStream.Results == nil {
diagsStream.Results = list.NoListResults
}

if stream.Results == nil {
stream.Results = list.NoListResults
}

fwStream.Results = processListResults(req, stream.Results, diagsStream.Results)
}

Expand Down
28 changes: 28 additions & 0 deletions internal/fwserver/server_listresource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,34 @@ func TestServerListResource(t *testing.T) {
expectedStreamEvents: []fwserver.ListResult{},
expectedError: "config cannot be nil",
},
"zero-results-with-warning-diagnostic": {
server: &fwserver.Server{
Provider: &testprovider.Provider{},
},
request: &fwserver.ListRequest{
Config: &tfsdk.Config{},
ListResource: &testprovider.ListResourceWithConfigure{
ConfigureMethod: func(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
resp.Diagnostics.AddWarning("Test Warning", "This is a test warning diagnostic")
},
ListResource: &testprovider.ListResource{
ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) {
resp.Results = list.NoListResults
},
},
},
},
expectedStreamEvents: []fwserver.ListResult{
{
Identity: nil,
Resource: nil,
DisplayName: "",
Diagnostics: diag.Diagnostics{
diag.NewWarningDiagnostic("Test Warning", "This is a test warning diagnostic"),
},
},
},
},
"listresource-configure-data": {
server: &fwserver.Server{
ListResourceConfigureData: "test-provider-configure-value",
Expand Down
26 changes: 20 additions & 6 deletions internal/fwserver/server_listresources.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,28 @@ func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list.
continue
}

rawV5SchemasResp := list.RawV5SchemaResponse{}
if listResourceWithSchemas, ok := listResource.(list.ListResourceWithRawV5Schemas); ok {
listResourceWithSchemas.RawV5Schemas(ctx, list.RawV5SchemaRequest{}, &rawV5SchemasResp)
}

rawV6SchemasResp := list.RawV6SchemaResponse{}
if listResourceWithSchemas, ok := listResource.(list.ListResourceWithRawV6Schemas); ok {
listResourceWithSchemas.RawV6Schemas(ctx, list.RawV6SchemaRequest{}, &rawV6SchemasResp)
}

resourceFuncs, _ := s.ResourceFuncs(ctx)
if _, ok := resourceFuncs[typeName]; !ok {
s.listResourceFuncsDiags.AddError(
"ListResource Type Defined without a Matching Managed Resource Type",
fmt.Sprintf("The %s ListResource type name was returned, but no matching managed Resource type was defined. ", typeName)+
"This is always an issue with the provider and should be reported to the provider developers.",
)
continue
if (rawV5SchemasResp.ProtoV5Schema == nil || rawV5SchemasResp.ProtoV5IdentitySchema == nil) && (rawV6SchemasResp.ProtoV6Schema == nil || rawV6SchemasResp.ProtoV6IdentitySchema == nil) {
s.listResourceFuncsDiags.AddError(
"ListResource Type Defined without a Matching Managed Resource Type",
fmt.Sprintf("The %s ListResource type name was returned, but no matching managed Resource type was defined. ", typeName)+
"If the matching managed Resource type is not a framework resource either ProtoV5Schema and ProtoV5IdentitySchema must be specified in the RawV5Schemas method, "+
"or ProtoV6Schema and ProtoV6IdentitySchema must be specified in the RawV6Schemas method. "+
"This is always an issue with the provider and should be reported to the provider developers.",
)
continue
}
}

s.listResourceFuncs[typeName] = listResourceFunc
Expand Down
54 changes: 39 additions & 15 deletions internal/proto5server/server_listresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/internal/fromproto5"
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
"github.com/hashicorp/terraform-plugin-framework/internal/toproto5"
"github.com/hashicorp/terraform-plugin-framework/list"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
)

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

resourceSchema, diags := s.FrameworkServer.ResourceSchema(ctx, protoReq.TypeName)
allDiags.Append(diags...)
if diags.HasError() {
return ListRequestErrorDiagnostics(ctx, allDiags...)
req := &fwserver.ListRequest{
Config: config,
ListResource: listResource,
IncludeResource: protoReq.IncludeResource,
Limit: protoReq.Limit,
}

identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, protoReq.TypeName)
allDiags.Append(diags...)
if diags.HasError() {
return ListRequestErrorDiagnostics(ctx, allDiags...)
schemaResp := list.RawV5SchemaResponse{}
if listResourceWithProtoSchemas, ok := listResource.(list.ListResourceWithRawV5Schemas); ok {
listResourceWithProtoSchemas.RawV5Schemas(ctx, list.RawV5SchemaRequest{}, &schemaResp)
}

req := &fwserver.ListRequest{
Config: config,
ListResource: listResource,
ResourceSchema: resourceSchema,
ResourceIdentitySchema: identitySchema,
IncludeResource: protoReq.IncludeResource,
Limit: protoReq.Limit,
// There's validation in ListResources that ensures both are set if either is provided so it should be sufficient to only nil check Identity
if schemaResp.ProtoV5IdentitySchema != nil {
var err error

req.ResourceSchema, err = fromproto5.ResourceSchema(ctx, schemaResp.ProtoV5Schema)
if err != nil {
diags.AddError("Converting Resource Schema", err.Error())
allDiags.Append(diags...)
return ListRequestErrorDiagnostics(ctx, allDiags...)
}

req.ResourceIdentitySchema, err = fromproto5.IdentitySchema(ctx, schemaResp.ProtoV5IdentitySchema)
if err != nil {
diags.AddError("Converting Resource Identity Schema", err.Error())
allDiags.Append(diags...)
return ListRequestErrorDiagnostics(ctx, allDiags...)
}
} else {
req.ResourceSchema, diags = s.FrameworkServer.ResourceSchema(ctx, protoReq.TypeName)
allDiags.Append(diags...)
if diags.HasError() {
return ListRequestErrorDiagnostics(ctx, allDiags...)
}

req.ResourceIdentitySchema, diags = s.FrameworkServer.ResourceIdentitySchema(ctx, protoReq.TypeName)
allDiags.Append(diags...)
if diags.HasError() {
return ListRequestErrorDiagnostics(ctx, allDiags...)
}
}

stream := &fwserver.ListResultsStream{}

s.FrameworkServer.ListResource(ctx, req, stream)
Expand Down
Loading
Loading