From 877b2b82272ff548199d6c57e8b3d67ef97691f2 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 3 Jun 2025 15:58:42 -0400 Subject: [PATCH 1/3] fwserver: add ListResource method --- internal/fwserver/server_listresource.go | 103 ++++++++++ internal/fwserver/server_listresource_test.go | 184 ++++++++++++++++++ list/list_resource.go | 16 +- list/schema/schema.go | 28 +-- list/schema/schema_test.go | 50 ----- list/schema/string_attribute_test.go | 166 +--------------- 6 files changed, 301 insertions(+), 246 deletions(-) create mode 100644 internal/fwserver/server_listresource.go create mode 100644 internal/fwserver/server_listresource_test.go diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go new file mode 100644 index 000000000..5206a0a41 --- /dev/null +++ b/internal/fwserver/server_listresource.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "iter" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ListRequest represents a request for the provider to list instances of a +// managed resource type that satisfy a user-defined request. An instance of +// this reqeuest struct is passed as an argument to the provider's List +// function implementation. +type ListRequest struct { + // ListResource is an instance of the provider's ListResource + // implementation for a specific managed resource type. + ListResource list.ListResource + + // Config is the configuration the user supplied for listing resource + // instances. + Config tfsdk.Config + + // IncludeResource indicates whether the provider should populate the + // Resource field in the ListResult struct. + IncludeResource bool +} + +// ListResultsStream represents a streaming response to a ListRequest. An +// instance of this struct is supplied as an argument to the provider's List +// function. The provider should set a Results iterator function that yields +// zero or more results of type ListResult. +// +// For convenience, a provider implementation may choose to convert a slice of +// results into an iterator using [slices.Values]. +// +// [slices.Values]: https://pkg.go.dev/slices#Values +type ListResourceStream struct { + // Results is a function that emits ListResult values via its yield + // function argument. + Results iter.Seq[ListResult] +} + +// ListResult represents a listed managed resource instance. +type ListResult struct { + // Identity is the identity of the managed resource instance. A nil value + // will raise will raise a diagnostic. + Identity *tfsdk.ResourceIdentity + + // Resource is the provider's representation of the attributes of the + // listed managed resource instance. + // + // If ListRequest.IncludeResource is true, a nil value will raise + // a warning diagnostic. + Resource *tfsdk.Resource + + // DisplayName is a provider-defined human-readable description of the + // listed managed resource instance, intended for CLI and browser UIs. + DisplayName string + + // Diagnostics report errors or warnings related to the listed managed + // resource instance. An empty slice indicates a successful operation with + // no warnings or errors generated. + Diagnostics diag.Diagnostics +} + +// ListResource implements the framework server ListResource RPC. +func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResourceStream) { + listResource := fwReq.ListResource + + req := list.ListRequest{ + Config: fwReq.Config, + IncludeResource: fwReq.IncludeResource, + } + + stream := &list.ListResultsStream{} + + logging.FrameworkTrace(ctx, "Calling provider defined ListResource") + listResource.List(ctx, req, stream) + logging.FrameworkTrace(ctx, "Called provider defined ListResource") + + if stream.Results == nil { + // If the provider returned a nil results stream, we treat it as an empty stream. + stream.Results = func(func(list.ListResult) bool) {} + } + + fwStream.Results = listResourceEventStreamAdapter(stream.Results) +} + +func listResourceEventStreamAdapter(stream iter.Seq[list.ListResult]) iter.Seq[ListResult] { + // TODO: is this any more efficient than a for-range? + return func(yieldFw func(ListResult) bool) { + yield := func(event list.ListResult) bool { + return yieldFw(ListResult(event)) + } + stream(yield) + } +} diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go new file mode 100644 index 000000000..2537b2db1 --- /dev/null +++ b/internal/fwserver/server_listresource_test.go @@ -0,0 +1,184 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestServerListResource(t *testing.T) { + t.Parallel() + + testSchema := schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test_computed": schema.StringAttribute{ + Computed: true, + }, + "test_required": schema.StringAttribute{ + Required: true, + }, + }, + } + + testType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testResourceValue1 := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value-1"), + }) + + testResourceValue2 := tftypes.NewValue(testType, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value-2"), + }) + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityValue1 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + + testIdentityValue2 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-456"), + }) + + // nilIdentityValue := tftypes.NewValue(testIdentityType, nil) + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.ListRequest + expectedStreamEvents []fwserver.ListResult + }{ + "success-with-zero-results": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ListRequest{ + ListResource: &testprovider.ListResource{ + ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO + resp.Results = slices.Values([]list.ListResult{}) + }, + }, + }, + expectedStreamEvents: []fwserver.ListResult{}, + }, + "success-with-nil-results": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ListRequest{ + ListResource: &testprovider.ListResource{ + ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO + // Do nothing, so that resp.Results is nil + }, + }, + }, + expectedStreamEvents: []fwserver.ListResult{}, + }, + + "success-with-multiple-results": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ListRequest{ + ListResource: &testprovider.ListResource{ + ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO + resp.Results = slices.Values([]list.ListResult{ + { + Identity: &tfsdk.ResourceIdentity{ + Schema: testIdentitySchema, + Raw: testIdentityValue1, + }, + Resource: &tfsdk.Resource{ + Schema: testSchema, + Raw: testResourceValue1, + }, + DisplayName: "Test Resource 1", + Diagnostics: diag.Diagnostics{}, + }, + { + Identity: &tfsdk.ResourceIdentity{ + Schema: testIdentitySchema, + Raw: testIdentityValue2, + }, + Resource: &tfsdk.Resource{ + Schema: testSchema, + Raw: testResourceValue2, + }, + DisplayName: "Test Resource 2", + Diagnostics: diag.Diagnostics{}, + }, + }) + }, + }, + }, + expectedStreamEvents: []fwserver.ListResult{ + { + Identity: &tfsdk.ResourceIdentity{ + Schema: testIdentitySchema, + Raw: testIdentityValue1, + }, + Resource: &tfsdk.Resource{ + Schema: testSchema, + Raw: testResourceValue1, + }, + DisplayName: "Test Resource 1", + Diagnostics: diag.Diagnostics{}, + }, + { + Identity: &tfsdk.ResourceIdentity{ + Schema: testIdentitySchema, + Raw: testIdentityValue2, + }, + Resource: &tfsdk.Resource{ + Schema: testSchema, + Raw: testResourceValue2, + }, + DisplayName: "Test Resource 2", + Diagnostics: diag.Diagnostics{}, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.ListResourceStream{} + testCase.server.ListResource(context.Background(), testCase.request, response) + + events := slices.AppendSeq([]fwserver.ListResult{}, response.Results) + if diff := cmp.Diff(events, testCase.expectedStreamEvents); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/list/list_resource.go b/list/list_resource.go index def1abfd3..385ece0b0 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -85,10 +85,10 @@ type ListResourceWithValidateConfig interface { ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } -// ListRequest represents a request for the provider to list instances -// of a managed resource type that satisfy a user-defined request. An instance -// of this reqeuest struct is passed as an argument to the provider's -// ListResource function implementation. +// ListRequest represents a request for the provider to list instances of a +// managed resource type that satisfy a user-defined request. An instance of +// this reqeuest struct is passed as an argument to the provider's List +// function implementation. type ListRequest struct { // Config is the configuration the user supplied for listing resource // instances. @@ -99,10 +99,10 @@ type ListRequest struct { IncludeResource bool } -// ListResultsStream represents a streaming response to a ListRequest. -// An instance of this struct is supplied as an argument to the provider's -// ListResource function implementation function. The provider should set a Results -// iterator function that yields zero or more results of type ListResult. +// ListResultsStream represents a streaming response to a ListRequest. An +// instance of this struct is supplied as an argument to the provider's +// ListResource function. The provider should set a Results iterator function +// that yields zero or more results of type ListResult. // // For convenience, a provider implementation may choose to convert a slice of // results into an iterator using [slices.Values]. diff --git a/list/schema/schema.go b/list/schema/schema.go index 759557c16..8ff73ecf7 100644 --- a/list/schema/schema.go +++ b/list/schema/schema.go @@ -17,9 +17,8 @@ import ( // Schema must satify the fwschema.Schema interface. var _ fwschema.Schema = Schema{} -// Schema defines the structure and value types of resource data. This type -// is used as the resource.SchemaResponse type Schema field, which is -// implemented by the resource.DataSource type Schema method. +// Schema defines the structure and value types of a list block. This is returned as a ListResourceSchemas map value by +// the GetProviderSchemas RPC. type Schema struct { // Attributes is the mapping of underlying attribute names to attribute // definitions. @@ -59,16 +58,6 @@ type Schema struct { // will be removed in the next major version of the provider." // DeprecationMessage string - - // Version indicates the current version of the resource schema. Resource - // schema versioning enables state upgrades in conjunction with the - // [resource.ResourceWithStateUpgrades] interface. Versioning is only - // required if there is a breaking change involving existing state data, - // such as changing an attribute or block type in a manner that is - // incompatible with the Terraform type. - // - // Versions are conventionally only incremented by one each release. - Version int64 } // ApplyTerraform5AttributePathStep applies the given AttributePathStep to the @@ -116,9 +105,9 @@ func (s Schema) GetMarkdownDescription() string { return s.MarkdownDescription } -// GetVersion returns the Version field value. +// GetVersion always returns 0 as list resource schemas cannot be versioned. func (s Schema) GetVersion() int64 { - return s.Version + return 0 } // Type returns the framework type of the schema. @@ -136,17 +125,10 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) } -// Validate verifies that the schema is not using a reserved field name for a top-level attribute. -// -// Deprecated: Use the ValidateImplementation method instead. -func (s Schema) Validate() diag.Diagnostics { - return s.ValidateImplementation(context.Background()) -} - // ValidateImplementation contains logic for validating the provider-defined // implementation of the schema and underlying attributes and blocks to prevent // unexpected errors or panics. This logic runs during the -// ValidateResourceConfig RPC, or via provider-defined unit testing, and should +// ValidateListResourceConfig RPC, or via provider-defined unit testing, and should // never include false positives. func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics { var diags diag.Diagnostics diff --git a/list/schema/schema_test.go b/list/schema/schema_test.go index 9e7056a2a..b556ac7df 100644 --- a/list/schema/schema_test.go +++ b/list/schema/schema_test.go @@ -538,15 +538,6 @@ func TestSchemaGetVersion(t *testing.T) { }, expected: 0, }, - "version": { - schema: schema.Schema{ - Attributes: map[string]schema.Attribute{ - "testattr": schema.StringAttribute{}, - }, - Version: 1, - }, - expected: 1, - }, } for name, testCase := range testCases { @@ -806,47 +797,6 @@ func TestSchemaTypeAtTerraformPath(t *testing.T) { } } -func TestSchemaValidate(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - schema schema.Schema - expectedDiags diag.Diagnostics - }{ - "empty-schema": { - schema: schema.Schema{}, - }, - "validate-implementation-error": { - schema: schema.Schema{ - Attributes: map[string]schema.Attribute{ - "depends_on": schema.StringAttribute{}, - }, - }, - expectedDiags: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Reserved Root Attribute/Block Name", - "When validating the resource or data source schema, an implementation issue was found. "+ - "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - "\"depends_on\" is a reserved root attribute/block name. "+ - "This is to prevent practitioners from needing special Terraform configuration syntax.", - ), - }, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - diags := testCase.schema.Validate() - - if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { - t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) - } - }) - } -} - func TestSchemaValidateImplementation(t *testing.T) { t.Parallel() diff --git a/list/schema/string_attribute_test.go b/list/schema/string_attribute_test.go index c1dffa2f2..77df7c025 100644 --- a/list/schema/string_attribute_test.go +++ b/list/schema/string_attribute_test.go @@ -4,7 +4,6 @@ package schema_test import ( - "context" "fmt" "strings" "testing" @@ -13,15 +12,10 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/list/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -260,12 +254,6 @@ func TestStringAttributeIsComputed(t *testing.T) { attribute: schema.StringAttribute{}, expected: false, }, - "computed": { - attribute: schema.StringAttribute{ - Computed: true, - }, - expected: true, - }, } for name, testCase := range testCases { @@ -356,12 +344,6 @@ func TestStringAttributeIsSensitive(t *testing.T) { attribute: schema.StringAttribute{}, expected: false, }, - "sensitive": { - attribute: schema.StringAttribute{ - Sensitive: true, - }, - expected: true, - }, } for name, testCase := range testCases { @@ -388,12 +370,6 @@ func TestStringAttributeIsWriteOnly(t *testing.T) { attribute: schema.StringAttribute{}, expected: false, }, - "writeOnly": { - attribute: schema.StringAttribute{ - WriteOnly: true, - }, - expected: true, - }, } for name, testCase := range testCases { @@ -409,83 +385,6 @@ func TestStringAttributeIsWriteOnly(t *testing.T) { } } -func TestStringAttributeStringDefaultValue(t *testing.T) { - t.Parallel() - - opt := cmp.Comparer(func(x, y defaults.String) bool { - ctx := context.Background() - req := defaults.StringRequest{} - - xResp := defaults.StringResponse{} - x.DefaultString(ctx, req, &xResp) - - yResp := defaults.StringResponse{} - y.DefaultString(ctx, req, &yResp) - - return xResp.PlanValue.Equal(yResp.PlanValue) - }) - - testCases := map[string]struct { - attribute schema.StringAttribute - expected defaults.String - }{ - "no-default": { - attribute: schema.StringAttribute{}, - expected: nil, - }, - "default": { - attribute: schema.StringAttribute{ - Default: stringdefault.StaticString("test-value"), - }, - expected: stringdefault.StaticString("test-value"), - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testCase.attribute.StringDefaultValue() - - if diff := cmp.Diff(got, testCase.expected, opt); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} - -func TestStringAttributeStringPlanModifiers(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - attribute schema.StringAttribute - expected []planmodifier.String - }{ - "no-planmodifiers": { - attribute: schema.StringAttribute{}, - expected: nil, - }, - "planmodifiers": { - attribute: schema.StringAttribute{ - PlanModifiers: []planmodifier.String{}, - }, - expected: []planmodifier.String{}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := testCase.attribute.StringPlanModifiers() - - if diff := cmp.Diff(got, testCase.expected); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} - func TestStringAttributeStringValidators(t *testing.T) { t.Parallel() @@ -518,69 +417,6 @@ func TestStringAttributeStringValidators(t *testing.T) { } } -func TestStringAttributeValidateImplementation(t *testing.T) { - t.Parallel() - - testCases := map[string]struct { - attribute schema.StringAttribute - request fwschema.ValidateImplementationRequest - expected *fwschema.ValidateImplementationResponse - }{ - "computed": { - attribute: schema.StringAttribute{ - Computed: true, - }, - request: fwschema.ValidateImplementationRequest{ - Name: "test", - Path: path.Root("test"), - }, - expected: &fwschema.ValidateImplementationResponse{}, - }, - "default-without-computed": { - attribute: schema.StringAttribute{ - Default: stringdefault.StaticString("test"), - }, - request: fwschema.ValidateImplementationRequest{ - Name: "test", - Path: path.Root("test"), - }, - expected: &fwschema.ValidateImplementationResponse{ - Diagnostics: diag.Diagnostics{ - diag.NewErrorDiagnostic( - "Schema Using Attribute Default For Non-Computed Attribute", - "Attribute \"test\" must be computed when using default. "+ - "This is an issue with the provider and should be reported to the provider developers.", - ), - }, - }, - }, - "default-with-computed": { - attribute: schema.StringAttribute{ - Computed: true, - Default: stringdefault.StaticString("test"), - }, - request: fwschema.ValidateImplementationRequest{ - Name: "test", - Path: path.Root("test"), - }, - expected: &fwschema.ValidateImplementationResponse{}, - }, - } - - for name, testCase := range testCases { - t.Run(name, func(t *testing.T) { - t.Parallel() - - got := &fwschema.ValidateImplementationResponse{} - testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) - - if diff := cmp.Diff(got, testCase.expected); diff != "" { - t.Errorf("unexpected difference: %s", diff) - } - }) - } -} - func TestStringAttributeIsRequiredForImport(t *testing.T) { t.Parallel() From 78fc1485e68eab2a06da7e58a96d1d20299469f9 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 3 Jun 2025 16:37:44 -0400 Subject: [PATCH 2/3] fwserver: validate each ListResult --- internal/fwserver/server_listresource.go | 43 ++++++++++++++----- internal/fwserver/server_listresource_test.go | 2 +- list/list_resource.go | 9 ++-- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 5206a0a41..9f3d2e3c3 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -33,7 +33,7 @@ type ListRequest struct { // ListResultsStream represents a streaming response to a ListRequest. An // instance of this struct is supplied as an argument to the provider's List -// function. The provider should set a Results iterator function that yields +// function. The provider should set a Results iterator function that pushes // zero or more results of type ListResult. // // For convenience, a provider implementation may choose to convert a slice of @@ -41,7 +41,7 @@ type ListRequest struct { // // [slices.Values]: https://pkg.go.dev/slices#Values type ListResourceStream struct { - // Results is a function that emits ListResult values via its yield + // Results is a function that emits ListResult values via its push // function argument. Results iter.Seq[ListResult] } @@ -85,19 +85,40 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream logging.FrameworkTrace(ctx, "Called provider defined ListResource") if stream.Results == nil { - // If the provider returned a nil results stream, we treat it as an empty stream. - stream.Results = func(func(list.ListResult) bool) {} + // If the provider returned a nil results stream, we return an empty stream. + stream.Results = list.NoListResults } - fwStream.Results = listResourceEventStreamAdapter(stream.Results) + fwStream.Results = processListResults(req, stream.Results) } -func listResourceEventStreamAdapter(stream iter.Seq[list.ListResult]) iter.Seq[ListResult] { - // TODO: is this any more efficient than a for-range? - return func(yieldFw func(ListResult) bool) { - yield := func(event list.ListResult) bool { - return yieldFw(ListResult(event)) +func processListResults(req list.ListRequest, stream iter.Seq[list.ListResult]) iter.Seq[ListResult] { + return func(push func(ListResult) bool) { + for result := range stream { + if !push(processListResult(req, result)) { + return + } } - stream(yield) } } + +// processListResult validates the content of a list.ListResult and returns a +// ListResult +func processListResult(req list.ListRequest, result list.ListResult) ListResult { + if result.Identity == nil { + return ListResult{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic("Incomplete List Result", "ListResult.Identity is nil."), + }, + } + } + + if req.IncludeResource && result.Resource == nil { + result.Diagnostics.AddWarning( + "Incomplete List Result", + "ListRequest.IncludeResource is true and ListResult.Resource is nil.", + ) + } + + return ListResult(result) +} diff --git a/internal/fwserver/server_listresource_test.go b/internal/fwserver/server_listresource_test.go index 2537b2db1..14ad8b13d 100644 --- a/internal/fwserver/server_listresource_test.go +++ b/internal/fwserver/server_listresource_test.go @@ -83,7 +83,7 @@ func TestServerListResource(t *testing.T) { request: &fwserver.ListRequest{ ListResource: &testprovider.ListResource{ ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO - resp.Results = slices.Values([]list.ListResult{}) + resp.Results = list.NoListResults }, }, }, diff --git a/list/list_resource.go b/list/list_resource.go index 385ece0b0..da7e757fe 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -102,18 +102,19 @@ type ListRequest struct { // ListResultsStream represents a streaming response to a ListRequest. An // instance of this struct is supplied as an argument to the provider's // ListResource function. The provider should set a Results iterator function -// that yields zero or more results of type ListResult. +// that pushes zero or more results of type ListResult. // // For convenience, a provider implementation may choose to convert a slice of // results into an iterator using [slices.Values]. -// -// [slices.Values]: https://pkg.go.dev/slices#Values type ListResultsStream struct { - // Results is a function that emits ListResult values via its yield + // Results is a function that emits ListResult values via its push // function argument. Results iter.Seq[ListResult] } +// NoListResults is an iterator that pushes zero results. +var NoListResults = func(func(ListResult) bool) {} + // ListResult represents a listed managed resource instance. type ListResult struct { // Identity is the identity of the managed resource instance. From 8f1988920481508b902916dac19a919a70100675 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 3 Jun 2025 17:32:39 -0400 Subject: [PATCH 3/3] ListResource: anticipate RPC-level errors --- internal/fwserver/server_listresource.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/fwserver/server_listresource.go b/internal/fwserver/server_listresource.go index 9f3d2e3c3..843f17be9 100644 --- a/internal/fwserver/server_listresource.go +++ b/internal/fwserver/server_listresource.go @@ -70,7 +70,7 @@ type ListResult struct { } // ListResource implements the framework server ListResource RPC. -func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResourceStream) { +func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResourceStream) error { listResource := fwReq.ListResource req := list.ListRequest{ @@ -90,6 +90,7 @@ func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream } fwStream.Results = processListResults(req, stream.Results) + return nil } func processListResults(req list.ListRequest, stream iter.Seq[list.ListResult]) iter.Seq[ListResult] {