Skip to content

list: framework changes to support list with sdkv2 resources #1198

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

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
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", d.Description.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 %s", path.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
33 changes: 33 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,39 @@ 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),
}),
},
"AttrTypeWithValidateError": {
data: fwschemadata.Data{
TerraformValue: tftypes.NewValue(tftypes.Object{
Expand Down
33 changes: 33 additions & 0 deletions internal/fwschemadata/data_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,39 @@ func TestDataSet(t *testing.T) {
),
}),
},
"write-tftypes-values": {
data: fwschemadata.Data{
TerraformValue: tftypes.NewValue(tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
},
}, map[string]tftypes.Value{
"name": tftypes.NewValue(tftypes.String, "oldvalue"),
}),
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"),
}),
},
"overwrite": {
data: fwschemadata.Data{
TerraformValue: tftypes.Value{},
Expand Down
1 change: 1 addition & 0 deletions internal/fwserver/server_getmetadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,7 @@ 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 a legacy resource, ProtoV5Schema and ProtoV5IdentitySchema must be specified in the Metadata method. "+
"This is always an issue with the provider and should be reported to the provider developers.",
),
},
Expand Down
12 changes: 7 additions & 5 deletions internal/fwserver/server_listresource.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,17 @@ 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
}

fwStream.Results = processListResults(req, stream.Results, diagsStream.Results)
if stream.Results == nil {
fwStream.Results = processListResults(req, list.NoListResults, diagsStream.Results)
}

if stream.Results != nil {
fwStream.Results = processListResults(req, stream.Results, diagsStream.Results)
}
}

func processListResults(req list.ListRequest, streams ...iter.Seq[list.ListResult]) iter.Seq[ListResult] {
Expand Down
20 changes: 14 additions & 6 deletions internal/fwserver/server_listresources.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,22 @@ func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list.
continue
}

schemasResp := list.SchemaResponse{}
if listResourceWithSchemas, ok := listResource.(list.ListResourceWithProtoSchemas); ok {
listResourceWithSchemas.Schemas(ctx, &schemasResp)
}

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 schemasResp.ProtoV5Schema == nil || schemasResp.ProtoV5IdentitySchema == 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 a legacy resource, ProtoV5Schema and ProtoV5IdentitySchema must be specified in the Metadata method. "+
"This is always an issue with the provider and should be reported to the provider developers.",
)
continue
}
}

s.listResourceFuncs[typeName] = listResourceFunc
Expand Down
41 changes: 26 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,36 @@ 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.SchemaResponse{}
if listResourceWithProtoSchemas, ok := listResource.(list.ListResourceWithProtoSchemas); ok {
listResourceWithProtoSchemas.Schemas(ctx, &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 {
req.ResourceSchema, _ = fromproto5.ResourceSchema(ctx, schemaResp.ProtoV5Schema())
req.ResourceIdentitySchema, _ = fromproto5.IdentitySchema(ctx, schemaResp.ProtoV5IdentitySchema())
} 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
25 changes: 25 additions & 0 deletions list/list_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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"
)

// ListResource represents an implementation of listing instances of a managed resource
Expand Down Expand Up @@ -40,6 +41,16 @@ type ListResource interface {
List(context.Context, ListRequest, *ListResultsStream)
}

// ListResourceWithProtoSchemas 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 legacy resource.
type ListResourceWithProtoSchemas interface {
Copy link
Member Author

Choose a reason for hiding this comment

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

These probably need a better name...

ListResource

// Schemas is called to provide the ProtoV5 representations of the resource and resource identity schemas.
Schemas(context.Context, *SchemaResponse)
}

// 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.
Expand Down Expand Up @@ -175,6 +186,20 @@ type ListResult struct {
Diagnostics diag.Diagnostics
}

// SchemaResponse represents a response that is populated by the Schemas method
// and is used to pass along the ProtoV5 representations of the resource and resource identity schemas.
type SchemaResponse 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 func() *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 func() *tfprotov5.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]
Expand Down
Loading