diff --git a/internal/fwschemadata/data_set.go b/internal/fwschemadata/data_set.go index b53604b31..0fbdccdb8 100644 --- a/internal/fwschemadata/data_set.go +++ b/internal/fwschemadata/data_set.go @@ -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() { diff --git a/internal/fwschemadata/data_set_at_path.go b/internal/fwschemadata/data_set_at_path.go index 211e75c1c..0fdf47e98 100644 --- a/internal/fwschemadata/data_set_at_path.go +++ b/internal/fwschemadata/data_set_at_path.go @@ -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...) diff --git a/internal/fwschemadata/data_set_at_path_test.go b/internal/fwschemadata/data_set_at_path_test.go index 95a41fee9..ca257f5da 100644 --- a/internal/fwschemadata/data_set_at_path_test.go +++ b/internal/fwschemadata/data_set_at_path_test.go @@ -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{ diff --git a/internal/fwschemadata/data_set_test.go b/internal/fwschemadata/data_set_test.go index 995923187..64998d39f 100644 --- a/internal/fwschemadata/data_set_test.go +++ b/internal/fwschemadata/data_set_test.go @@ -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{}, @@ -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{ diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 96aebfc22..605e9fc49 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -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.", ), }, diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 74b0955c1..828382338 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -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) } diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index a4aeba813..5c4315b94 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -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", diff --git a/internal/fwserver/server_listresources.go b/internal/fwserver/server_listresources.go index 94760afb3..b1819a6d4 100644 --- a/internal/fwserver/server_listresources.go +++ b/internal/fwserver/server_listresources.go @@ -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 diff --git a/internal/proto5server/server_listresource.go b/internal/proto5server/server_listresource.go index 54bc34b37..a817f20e5 100644 --- a/internal/proto5server/server_listresource.go +++ b/internal/proto5server/server_listresource.go @@ -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" ) @@ -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) diff --git a/internal/proto6server/server_listresource.go b/internal/proto6server/server_listresource.go index b180c4fbc..53512effb 100644 --- a/internal/proto6server/server_listresource.go +++ b/internal/proto6server/server_listresource.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) @@ -47,26 +48,48 @@ func (s *Server) ListResource(ctx context.Context, protoReq *tfprotov6.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.RawV6SchemaResponse{} + if listResourceWithProtoSchemas, ok := listResource.(list.ListResourceWithRawV6Schemas); ok { + listResourceWithProtoSchemas.RawV6Schemas(ctx, list.RawV6SchemaRequest{}, &schemaResp) } - req := &fwserver.ListRequest{ - Config: config, - ListResource: listResource, - ResourceSchema: resourceSchema, - ResourceIdentitySchema: identitySchema, - IncludeResource: protoReq.IncludeResource, - Limit: protoReq.Limit, + if schemaResp.ProtoV6IdentitySchema != nil { + var err error + + req.ResourceSchema, err = fromproto6.ResourceSchema(ctx, schemaResp.ProtoV6Schema) + if err != nil { + diags.AddError("Converting Resource Schema", err.Error()) + allDiags.Append(diags...) + return ListRequestErrorDiagnostics(ctx, allDiags...) + } + + req.ResourceIdentitySchema, err = fromproto6.IdentitySchema(ctx, schemaResp.ProtoV6IdentitySchema) + 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) diff --git a/list/list_resource.go b/list/list_resource.go index 3dbdbee17..ea71cf49e 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" ) @@ -41,6 +43,26 @@ type ListResource interface { List(context.Context, ListRequest, *ListResultsStream) } +// ListResourceWithRawV5Schemas is an interface type that extends ListResource to include a method +// which allows provider developers to supply the ProtoV5 representations of resource and resource identity +// schemas. This is necessary if list functionality is being used with a resource that is not defined with Framework. +type ListResourceWithRawV5Schemas interface { + ListResource + + // RawV5Schemas is called to provide the ProtoV5 representations of the resource and resource identity schemas. + RawV5Schemas(context.Context, RawV5SchemaRequest, *RawV5SchemaResponse) +} + +// ListResourceWithRawV6Schemas is an interface type that extends ListResource to include a method +// which allows provider developers to supply the ProtoV6 representations of resource and resource identity +// schemas. This is necessary if list functionality is being used with a resource that is not defined with Framework. +type ListResourceWithRawV6Schemas interface { + ListResource + + // RawV6Schemas is called to provide the ProtoV6 representations of the resource and resource identity schemas. + RawV6Schemas(context.Context, RawV6SchemaRequest, *RawV6SchemaResponse) +} + // ListResourceWithConfigure is an interface type that extends ListResource to include a method // which the framework will automatically call so provider developers have the // opportunity to setup any necessary provider-level data or clients. @@ -182,6 +204,44 @@ type ListResult struct { Diagnostics diag.Diagnostics } +// RawV5SchemaRequest represents a request for the ListResource to return the +// ProtoV5 schemas. An instance of this request struct is supplied as an argument +// to the ListResource type RawV5Schemas method. +type RawV5SchemaRequest struct{} + +// RawV5SchemaResponse represents a response that is populated by the RawV5Schemas method +// and is used to pass along the ProtoV5 representations of the resource and resource identity schemas. +type RawV5SchemaResponse struct { + // ProtoV5IdentitySchema is the ProtoV5 representation of the resource identity + // schema. This should only be supplied if framework functionality is being used + // with a legacy resource. Currently, this only applies to list. + ProtoV5IdentitySchema *tfprotov5.ResourceIdentitySchema + + // ProtoV5Schema is the ProtoV5 representation of the resource schema + // This should only be supplied if framework functionality is being used + // with a legacy resource. Currently, this only applies to list. + ProtoV5Schema *tfprotov5.Schema +} + +// RawV6SchemaRequest represents a request for the ListResource to return the +// ProtoV6 schemas. An instance of this request struct is supplied as an argument +// to the ListResource type RawV6Schemas method. +type RawV6SchemaRequest struct{} + +// RawV6SchemaResponse represents a response that is populated by the RawV6Schemas method +// and is used to pass along the ProtoV6 representations of the resource and resource identity schemas. +type RawV6SchemaResponse struct { + // ProtoV6IdentitySchema is the ProtoV6 representation of the resource identity + // schema. This should only be supplied if framework functionality is being used + // with a legacy resource. Currently, this only applies to list. + ProtoV6IdentitySchema *tfprotov6.ResourceIdentitySchema + + // ProtoV6Schema is the ProtoV6 representation of the resource schema + // This should only be supplied if framework functionality is being used + // with a legacy resource. Currently, this only applies to list. + ProtoV6Schema *tfprotov6.Schema +} + // ValidateConfigRequest represents a request to validate the configuration of // a list resource. An instance of this request struct is supplied as an // argument to the [ListResourceWithValidateConfig.ValidateListResourceConfig]