Skip to content
Merged
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
12 changes: 12 additions & 0 deletions internal/fwserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
"github.com/hashicorp/terraform-plugin-framework/list"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
Expand Down Expand Up @@ -113,6 +114,17 @@ type Server struct {
// access from race conditions.
functionFuncsMutex sync.Mutex

// listResourceFuncs is a map of known ListResource factory functions.
listResourceFuncs map[string]func() list.ListResource

// listResourceFuncsDiags are the cached Diagnostics obtained while
// populating listResourceFuncs.
listResourceFuncsDiags diag.Diagnostics

// listResourceFuncsMutex is a mutex to protect concurrent listResourceFuncs
// access from race conditions.
listResourceFuncsMutex sync.Mutex

// providerSchema is the cached Provider Schema for RPCs that need to
// convert configuration data from the protocol. If not found, it will be
// fetched from the Provider.GetSchema() method.
Expand Down
19 changes: 16 additions & 3 deletions internal/fwserver/server_getmetadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type GetMetadataResponse struct {
Diagnostics diag.Diagnostics
EphemeralResources []EphemeralResourceMetadata
Functions []FunctionMetadata
ListResources []ListResourceMetadata
Resources []ResourceMetadata
ServerCapabilities *ServerCapabilities
}
Expand Down Expand Up @@ -52,28 +53,39 @@ type ResourceMetadata struct {
TypeName string
}

// ListResourceMetadata is the framework server equivalent of the
// tfprotov5.ListResourceMetadata and tfprotov6.ListResourceMetadata types.
type ListResourceMetadata struct {
// TypeName is the name of the list resource.
TypeName string
}

// GetMetadata implements the framework server GetMetadata RPC.
func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) {
resp.DataSources = []DataSourceMetadata{}
resp.EphemeralResources = []EphemeralResourceMetadata{}
resp.Functions = []FunctionMetadata{}
resp.ListResources = []ListResourceMetadata{}
resp.Resources = []ResourceMetadata{}

resp.ServerCapabilities = s.ServerCapabilities()

datasourceMetadatas, diags := s.DataSourceMetadatas(ctx)

resp.Diagnostics.Append(diags...)

ephemeralResourceMetadatas, diags := s.EphemeralResourceMetadatas(ctx)

resp.Diagnostics.Append(diags...)

functionMetadatas, diags := s.FunctionMetadatas(ctx)

resp.Diagnostics.Append(diags...)

resourceMetadatas, diags := s.ResourceMetadatas(ctx)
resp.Diagnostics.Append(diags...)

// Metadata for list resources must be retrieved after metadata for managed
// resources. Server.ListResourceMetadatas checks that each list resource
// type nane matches a known managed Resource type name.
listResourceMetadatas, diags := s.ListResourceMetadatas(ctx)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
Expand All @@ -83,5 +95,6 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp
resp.DataSources = datasourceMetadatas
resp.EphemeralResources = ephemeralResourceMetadatas
resp.Functions = functionMetadatas
resp.ListResources = listResourceMetadatas
resp.Resources = resourceMetadatas
}
196 changes: 195 additions & 1 deletion internal/fwserver/server_getmetadata_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/function"
"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/provider"
"github.com/hashicorp/terraform-plugin-framework/resource"
)
Expand Down Expand Up @@ -485,6 +487,190 @@ func TestServerGetMetadata(t *testing.T) {
},
},
},
"listresources": {
server: &fwserver.Server{
Provider: &testprovider.Provider{
ListResourcesMethod: func(_ context.Context) []func() list.ListResource {
return []func() list.ListResource{
func() list.ListResource {
return &testprovider.ListResource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource_1"
},
}
},
}
},
ResourcesMethod: func(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource_1"
},
}
},
}
},
},
},
request: &fwserver.GetMetadataRequest{},
expectedResponse: &fwserver.GetMetadataResponse{
DataSources: []fwserver.DataSourceMetadata{},
EphemeralResources: []fwserver.EphemeralResourceMetadata{},
Diagnostics: diag.Diagnostics{},
Functions: []fwserver.FunctionMetadata{},
ListResources: []fwserver.ListResourceMetadata{
{TypeName: "test_resource_1"},
},
Resources: []fwserver.ResourceMetadata{
{TypeName: "test_resource_1"},
},
ServerCapabilities: &fwserver.ServerCapabilities{
GetProviderSchemaOptional: true,
MoveResourceState: true,
PlanDestroy: true,
},
},
},
"list-resources-empty-type-name": {
server: &fwserver.Server{
Provider: &testprovider.Provider{
ListResourcesMethod: func(_ context.Context) []func() list.ListResource {
return []func() list.ListResource{
func() list.ListResource {
return &testprovider.ListResource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = ""
},
}
},
}
},
},
},
request: &fwserver.GetMetadataRequest{},
expectedResponse: &fwserver.GetMetadataResponse{
DataSources: []fwserver.DataSourceMetadata{},
EphemeralResources: []fwserver.EphemeralResourceMetadata{},
Diagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic(
"ListResource Type Name Missing",
"The *testprovider.ListResource ListResource returned an empty string from the Metadata method. "+
"This is always an issue with the provider and should be reported to the provider developers.",
),
},
Functions: []fwserver.FunctionMetadata{},
Resources: []fwserver.ResourceMetadata{},
ServerCapabilities: &fwserver.ServerCapabilities{
GetProviderSchemaOptional: true,
MoveResourceState: true,
PlanDestroy: true,
},
},
},
"list-resources-duplicate-type-name": {
server: &fwserver.Server{
Provider: &testprovider.Provider{
ListResourcesMethod: func(_ context.Context) []func() list.ListResource {
return []func() list.ListResource{
func() list.ListResource {
return &testprovider.ListResource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource"
},
}
},
func() list.ListResource {
return &testprovider.ListResource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource"
},
}
},
}
},
ResourcesMethod: func(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource"
},
}
},
}
},
},
},
request: &fwserver.GetMetadataRequest{},
expectedResponse: &fwserver.GetMetadataResponse{
DataSources: []fwserver.DataSourceMetadata{},
EphemeralResources: []fwserver.EphemeralResourceMetadata{},
Diagnostics: diag.Diagnostics{
diag.NewErrorDiagnostic(
"Duplicate ListResource Type Defined",
"The test_resource ListResource type name was returned for multiple list resources. "+
"ListResource type names must be unique. "+
"This is always an issue with the provider and should be reported to the provider developers.",
),
},
Functions: []fwserver.FunctionMetadata{},
Resources: []fwserver.ResourceMetadata{},
ServerCapabilities: &fwserver.ServerCapabilities{
GetProviderSchemaOptional: true,
MoveResourceState: true,
PlanDestroy: true,
},
},
},
"list-resources-no-matching-managed-resource-type": {
server: &fwserver.Server{
Provider: &testprovider.Provider{
ListResourcesMethod: func(_ context.Context) []func() list.ListResource {
return []func() list.ListResource{
func() list.ListResource {
return &testprovider.ListResource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource_1"
},
}
},
}
},
ResourcesMethod: func(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
func() resource.Resource {
return &testprovider.Resource{
MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = "test_resource_2"
},
}
},
}
},
},
},
request: &fwserver.GetMetadataRequest{},
expectedResponse: &fwserver.GetMetadataResponse{
DataSources: []fwserver.DataSourceMetadata{},
EphemeralResources: []fwserver.EphemeralResourceMetadata{},
Diagnostics: diag.Diagnostics{
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. "+
"This is always an issue with the provider and should be reported to the provider developers.",
),
},
Functions: []fwserver.FunctionMetadata{},
Resources: []fwserver.ResourceMetadata{},
ServerCapabilities: &fwserver.ServerCapabilities{
GetProviderSchemaOptional: true,
MoveResourceState: true,
PlanDestroy: true,
},
},
},
"resources": {
server: &fwserver.Server{
Provider: &testprovider.Provider{
Expand Down Expand Up @@ -666,11 +852,19 @@ func TestServerGetMetadata(t *testing.T) {
return response.Functions[i].Name < response.Functions[j].Name
})

sort.Slice(response.ListResources, func(i int, j int) bool {
return response.ListResources[i].TypeName < response.ListResources[j].TypeName
})

sort.Slice(response.Resources, func(i int, j int) bool {
return response.Resources[i].TypeName < response.Resources[j].TypeName
})

if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" {
opts := cmp.Options{
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, what effect does adding this have?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test will ignore the difference between []ListResource{} and nil. So it ignores the difference between the empty slice that GetMetadata returns by default and the default zero value for GetMetadataResponse.ListResources.

So the effect is: no need to add []ListResource{} to every test.

cmpopts.EquateEmpty(),
}

if diff := cmp.Diff(response, testCase.expectedResponse, opts...); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
Expand Down
Loading
Loading