Skip to content

Commit 877b2b8

Browse files
committed
fwserver: add ListResource method
1 parent 7691bb8 commit 877b2b8

File tree

6 files changed

+301
-246
lines changed

6 files changed

+301
-246
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fwserver
5+
6+
import (
7+
"context"
8+
"iter"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
12+
"github.com/hashicorp/terraform-plugin-framework/list"
13+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
14+
)
15+
16+
// ListRequest represents a request for the provider to list instances of a
17+
// managed resource type that satisfy a user-defined request. An instance of
18+
// this reqeuest struct is passed as an argument to the provider's List
19+
// function implementation.
20+
type ListRequest struct {
21+
// ListResource is an instance of the provider's ListResource
22+
// implementation for a specific managed resource type.
23+
ListResource list.ListResource
24+
25+
// Config is the configuration the user supplied for listing resource
26+
// instances.
27+
Config tfsdk.Config
28+
29+
// IncludeResource indicates whether the provider should populate the
30+
// Resource field in the ListResult struct.
31+
IncludeResource bool
32+
}
33+
34+
// ListResultsStream represents a streaming response to a ListRequest. An
35+
// instance of this struct is supplied as an argument to the provider's List
36+
// function. The provider should set a Results iterator function that yields
37+
// zero or more results of type ListResult.
38+
//
39+
// For convenience, a provider implementation may choose to convert a slice of
40+
// results into an iterator using [slices.Values].
41+
//
42+
// [slices.Values]: https://pkg.go.dev/slices#Values
43+
type ListResourceStream struct {
44+
// Results is a function that emits ListResult values via its yield
45+
// function argument.
46+
Results iter.Seq[ListResult]
47+
}
48+
49+
// ListResult represents a listed managed resource instance.
50+
type ListResult struct {
51+
// Identity is the identity of the managed resource instance. A nil value
52+
// will raise will raise a diagnostic.
53+
Identity *tfsdk.ResourceIdentity
54+
55+
// Resource is the provider's representation of the attributes of the
56+
// listed managed resource instance.
57+
//
58+
// If ListRequest.IncludeResource is true, a nil value will raise
59+
// a warning diagnostic.
60+
Resource *tfsdk.Resource
61+
62+
// DisplayName is a provider-defined human-readable description of the
63+
// listed managed resource instance, intended for CLI and browser UIs.
64+
DisplayName string
65+
66+
// Diagnostics report errors or warnings related to the listed managed
67+
// resource instance. An empty slice indicates a successful operation with
68+
// no warnings or errors generated.
69+
Diagnostics diag.Diagnostics
70+
}
71+
72+
// ListResource implements the framework server ListResource RPC.
73+
func (s *Server) ListResource(ctx context.Context, fwReq *ListRequest, fwStream *ListResourceStream) {
74+
listResource := fwReq.ListResource
75+
76+
req := list.ListRequest{
77+
Config: fwReq.Config,
78+
IncludeResource: fwReq.IncludeResource,
79+
}
80+
81+
stream := &list.ListResultsStream{}
82+
83+
logging.FrameworkTrace(ctx, "Calling provider defined ListResource")
84+
listResource.List(ctx, req, stream)
85+
logging.FrameworkTrace(ctx, "Called provider defined ListResource")
86+
87+
if stream.Results == nil {
88+
// If the provider returned a nil results stream, we treat it as an empty stream.
89+
stream.Results = func(func(list.ListResult) bool) {}
90+
}
91+
92+
fwStream.Results = listResourceEventStreamAdapter(stream.Results)
93+
}
94+
95+
func listResourceEventStreamAdapter(stream iter.Seq[list.ListResult]) iter.Seq[ListResult] {
96+
// TODO: is this any more efficient than a for-range?
97+
return func(yieldFw func(ListResult) bool) {
98+
yield := func(event list.ListResult) bool {
99+
return yieldFw(ListResult(event))
100+
}
101+
stream(yield)
102+
}
103+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package fwserver_test
5+
6+
import (
7+
"context"
8+
"slices"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/hashicorp/terraform-plugin-framework/diag"
13+
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
14+
"github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider"
15+
"github.com/hashicorp/terraform-plugin-framework/list"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
19+
"github.com/hashicorp/terraform-plugin-go/tftypes"
20+
)
21+
22+
func TestServerListResource(t *testing.T) {
23+
t.Parallel()
24+
25+
testSchema := schema.Schema{
26+
Attributes: map[string]schema.Attribute{
27+
"test_computed": schema.StringAttribute{
28+
Computed: true,
29+
},
30+
"test_required": schema.StringAttribute{
31+
Required: true,
32+
},
33+
},
34+
}
35+
36+
testType := tftypes.Object{
37+
AttributeTypes: map[string]tftypes.Type{
38+
"test_attribute": tftypes.String,
39+
},
40+
}
41+
42+
testResourceValue1 := tftypes.NewValue(testType, map[string]tftypes.Value{
43+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value-1"),
44+
})
45+
46+
testResourceValue2 := tftypes.NewValue(testType, map[string]tftypes.Value{
47+
"test_attribute": tftypes.NewValue(tftypes.String, "test-value-2"),
48+
})
49+
50+
testIdentitySchema := identityschema.Schema{
51+
Attributes: map[string]identityschema.Attribute{
52+
"test_id": identityschema.StringAttribute{
53+
RequiredForImport: true,
54+
},
55+
},
56+
}
57+
58+
testIdentityType := tftypes.Object{
59+
AttributeTypes: map[string]tftypes.Type{
60+
"test_id": tftypes.String,
61+
},
62+
}
63+
64+
testIdentityValue1 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{
65+
"test_id": tftypes.NewValue(tftypes.String, "new-id-123"),
66+
})
67+
68+
testIdentityValue2 := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{
69+
"test_id": tftypes.NewValue(tftypes.String, "new-id-456"),
70+
})
71+
72+
// nilIdentityValue := tftypes.NewValue(testIdentityType, nil)
73+
74+
testCases := map[string]struct {
75+
server *fwserver.Server
76+
request *fwserver.ListRequest
77+
expectedStreamEvents []fwserver.ListResult
78+
}{
79+
"success-with-zero-results": {
80+
server: &fwserver.Server{
81+
Provider: &testprovider.Provider{},
82+
},
83+
request: &fwserver.ListRequest{
84+
ListResource: &testprovider.ListResource{
85+
ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO
86+
resp.Results = slices.Values([]list.ListResult{})
87+
},
88+
},
89+
},
90+
expectedStreamEvents: []fwserver.ListResult{},
91+
},
92+
"success-with-nil-results": {
93+
server: &fwserver.Server{
94+
Provider: &testprovider.Provider{},
95+
},
96+
request: &fwserver.ListRequest{
97+
ListResource: &testprovider.ListResource{
98+
ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO
99+
// Do nothing, so that resp.Results is nil
100+
},
101+
},
102+
},
103+
expectedStreamEvents: []fwserver.ListResult{},
104+
},
105+
106+
"success-with-multiple-results": {
107+
server: &fwserver.Server{
108+
Provider: &testprovider.Provider{},
109+
},
110+
request: &fwserver.ListRequest{
111+
ListResource: &testprovider.ListResource{
112+
ListMethod: func(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { // TODO
113+
resp.Results = slices.Values([]list.ListResult{
114+
{
115+
Identity: &tfsdk.ResourceIdentity{
116+
Schema: testIdentitySchema,
117+
Raw: testIdentityValue1,
118+
},
119+
Resource: &tfsdk.Resource{
120+
Schema: testSchema,
121+
Raw: testResourceValue1,
122+
},
123+
DisplayName: "Test Resource 1",
124+
Diagnostics: diag.Diagnostics{},
125+
},
126+
{
127+
Identity: &tfsdk.ResourceIdentity{
128+
Schema: testIdentitySchema,
129+
Raw: testIdentityValue2,
130+
},
131+
Resource: &tfsdk.Resource{
132+
Schema: testSchema,
133+
Raw: testResourceValue2,
134+
},
135+
DisplayName: "Test Resource 2",
136+
Diagnostics: diag.Diagnostics{},
137+
},
138+
})
139+
},
140+
},
141+
},
142+
expectedStreamEvents: []fwserver.ListResult{
143+
{
144+
Identity: &tfsdk.ResourceIdentity{
145+
Schema: testIdentitySchema,
146+
Raw: testIdentityValue1,
147+
},
148+
Resource: &tfsdk.Resource{
149+
Schema: testSchema,
150+
Raw: testResourceValue1,
151+
},
152+
DisplayName: "Test Resource 1",
153+
Diagnostics: diag.Diagnostics{},
154+
},
155+
{
156+
Identity: &tfsdk.ResourceIdentity{
157+
Schema: testIdentitySchema,
158+
Raw: testIdentityValue2,
159+
},
160+
Resource: &tfsdk.Resource{
161+
Schema: testSchema,
162+
Raw: testResourceValue2,
163+
},
164+
DisplayName: "Test Resource 2",
165+
Diagnostics: diag.Diagnostics{},
166+
},
167+
},
168+
},
169+
}
170+
171+
for name, testCase := range testCases {
172+
t.Run(name, func(t *testing.T) {
173+
t.Parallel()
174+
175+
response := &fwserver.ListResourceStream{}
176+
testCase.server.ListResource(context.Background(), testCase.request, response)
177+
178+
events := slices.AppendSeq([]fwserver.ListResult{}, response.Results)
179+
if diff := cmp.Diff(events, testCase.expectedStreamEvents); diff != "" {
180+
t.Errorf("unexpected difference: %s", diff)
181+
}
182+
})
183+
}
184+
}

list/list_resource.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ type ListResourceWithValidateConfig interface {
8585
ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse)
8686
}
8787

88-
// ListRequest represents a request for the provider to list instances
89-
// of a managed resource type that satisfy a user-defined request. An instance
90-
// of this reqeuest struct is passed as an argument to the provider's
91-
// ListResource function implementation.
88+
// ListRequest represents a request for the provider to list instances of a
89+
// managed resource type that satisfy a user-defined request. An instance of
90+
// this reqeuest struct is passed as an argument to the provider's List
91+
// function implementation.
9292
type ListRequest struct {
9393
// Config is the configuration the user supplied for listing resource
9494
// instances.
@@ -99,10 +99,10 @@ type ListRequest struct {
9999
IncludeResource bool
100100
}
101101

102-
// ListResultsStream represents a streaming response to a ListRequest.
103-
// An instance of this struct is supplied as an argument to the provider's
104-
// ListResource function implementation function. The provider should set a Results
105-
// iterator function that yields zero or more results of type ListResult.
102+
// ListResultsStream represents a streaming response to a ListRequest. An
103+
// instance of this struct is supplied as an argument to the provider's
104+
// ListResource function. The provider should set a Results iterator function
105+
// that yields zero or more results of type ListResult.
106106
//
107107
// For convenience, a provider implementation may choose to convert a slice of
108108
// results into an iterator using [slices.Values].

list/schema/schema.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ import (
1717
// Schema must satify the fwschema.Schema interface.
1818
var _ fwschema.Schema = Schema{}
1919

20-
// Schema defines the structure and value types of resource data. This type
21-
// is used as the resource.SchemaResponse type Schema field, which is
22-
// implemented by the resource.DataSource type Schema method.
20+
// Schema defines the structure and value types of a list block. This is returned as a ListResourceSchemas map value by
21+
// the GetProviderSchemas RPC.
2322
type Schema struct {
2423
// Attributes is the mapping of underlying attribute names to attribute
2524
// definitions.
@@ -59,16 +58,6 @@ type Schema struct {
5958
// will be removed in the next major version of the provider."
6059
//
6160
DeprecationMessage string
62-
63-
// Version indicates the current version of the resource schema. Resource
64-
// schema versioning enables state upgrades in conjunction with the
65-
// [resource.ResourceWithStateUpgrades] interface. Versioning is only
66-
// required if there is a breaking change involving existing state data,
67-
// such as changing an attribute or block type in a manner that is
68-
// incompatible with the Terraform type.
69-
//
70-
// Versions are conventionally only incremented by one each release.
71-
Version int64
7261
}
7362

7463
// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the
@@ -116,9 +105,9 @@ func (s Schema) GetMarkdownDescription() string {
116105
return s.MarkdownDescription
117106
}
118107

119-
// GetVersion returns the Version field value.
108+
// GetVersion always returns 0 as list resource schemas cannot be versioned.
120109
func (s Schema) GetVersion() int64 {
121-
return s.Version
110+
return 0
122111
}
123112

124113
// Type returns the framework type of the schema.
@@ -136,17 +125,10 @@ func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePat
136125
return fwschema.SchemaTypeAtTerraformPath(ctx, s, p)
137126
}
138127

139-
// Validate verifies that the schema is not using a reserved field name for a top-level attribute.
140-
//
141-
// Deprecated: Use the ValidateImplementation method instead.
142-
func (s Schema) Validate() diag.Diagnostics {
143-
return s.ValidateImplementation(context.Background())
144-
}
145-
146128
// ValidateImplementation contains logic for validating the provider-defined
147129
// implementation of the schema and underlying attributes and blocks to prevent
148130
// unexpected errors or panics. This logic runs during the
149-
// ValidateResourceConfig RPC, or via provider-defined unit testing, and should
131+
// ValidateListResourceConfig RPC, or via provider-defined unit testing, and should
150132
// never include false positives.
151133
func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics {
152134
var diags diag.Diagnostics

0 commit comments

Comments
 (0)