diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b4b8a779b..6aed3c085 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -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" ) @@ -172,6 +173,20 @@ type Server struct { // Provider.Resources() method. resourceFuncs map[string]func() resource.Resource + // listResourceFuncs is the cached Resource List functions for RPCs that need to + // access resource lists. If not found, it will be fetched from the + // Provider.ListResources() method. + listResourceFuncs map[string]func() list.ListResource + + // listResourceTypesDiags is the cached Diagnostics obtained while populating + // listResourceTypes. This is to ensure any warnings or errors are also + // returned appropriately when fetching listResourceTypes. + listResourceTypesDiags diag.Diagnostics + + // listResourceTypesMutex is a mutex to protect concurrent listResourceTypes + // access from race conditions. + listResourceTypesMutex sync.Mutex + // resourceTypesDiags is the cached Diagnostics obtained while populating // resourceTypes. This is to ensure any warnings or errors are also // returned appropriately when fetching resourceTypes. @@ -804,3 +819,88 @@ func (s *Server) ResourceIdentitySchemas(ctx context.Context) (map[string]fwsche return resourceIdentitySchemas, diags } + +// ListResourceFuncs returns a map of ListResource functions. The results are +// cached on first use. +func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list.ListResource, diag.Diagnostics) { + provider, ok := s.Provider.(provider.ProviderWithListResources) + if !ok { + return nil, nil + } + + logging.FrameworkTrace(ctx, "Checking ListResourceTypes lock") + s.listResourceTypesMutex.Lock() + defer s.listResourceTypesMutex.Unlock() + + if s.listResourceFuncs != nil { + return s.listResourceFuncs, s.resourceTypesDiags + } + + providerTypeName := s.ProviderTypeName(ctx) + s.listResourceFuncs = make(map[string]func() list.ListResource) + + logging.FrameworkTrace(ctx, "Calling provider defined ListResources") + listResourceFuncSlice := provider.ListResources(ctx) + logging.FrameworkTrace(ctx, "Called provider defined ListResources") + + for _, listResourceFunc := range listResourceFuncSlice { + listResource := listResourceFunc() + + metadataReq := resource.MetadataRequest{ + ProviderTypeName: providerTypeName, + } + metadataResp := resource.MetadataResponse{} + listResource.Metadata(ctx, metadataReq, &metadataResp) + + typeName := metadataResp.TypeName + if typeName == "" { + s.listResourceTypesDiags.AddError( + "ListResource Type Name Missing", + fmt.Sprintf("The %T ListResource returned an empty string from the Metadata method. ", listResource)+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + logging.FrameworkTrace(ctx, "Found resource type", map[string]interface{}{logging.KeyListResourceType: typeName}) // TODO: y? + + if _, ok := s.listResourceFuncs[typeName]; ok { + s.listResourceTypesDiags.AddError( + "Duplicate ListResource Type Defined", + fmt.Sprintf("The %s ListResource type name was returned for multiple list resources. ", typeName)+ + "ListResource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ) + continue + } + + if _, ok := s.resourceFuncs[typeName]; !ok { + s.listResourceTypesDiags.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 + } + + s.listResourceFuncs[typeName] = listResourceFunc + } + + return s.listResourceFuncs, s.listResourceTypesDiags +} + +// ListResourceMetadatas returns a slice of ListResourceMetadata for the GetMetadata +// RPC. +func (s *Server) ListResourceMetadatas(ctx context.Context) ([]ListResourceMetadata, diag.Diagnostics) { + resourceFuncs, diags := s.ListResourceFuncs(ctx) + + resourceMetadatas := make([]ListResourceMetadata, 0, len(resourceFuncs)) + + for typeName := range resourceFuncs { + resourceMetadatas = append(resourceMetadatas, ListResourceMetadata{ + TypeName: typeName, + }) + } + + return resourceMetadatas, diags +} diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 458694f2b..aa7ef62db 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -20,6 +20,7 @@ type GetMetadataResponse struct { Diagnostics diag.Diagnostics EphemeralResources []EphemeralResourceMetadata Functions []FunctionMetadata + ListResources []ListResourceMetadata Resources []ResourceMetadata ServerCapabilities *ServerCapabilities } @@ -52,28 +53,38 @@ 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. + listResourceMetadatas, diags := s.ListResourceMetadatas(ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -83,5 +94,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 } diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 8729f6dbd..ded92b0fe 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -16,6 +16,7 @@ import ( "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" ) @@ -36,6 +37,7 @@ func TestServerGetMetadata(t *testing.T) { DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -79,6 +81,7 @@ func TestServerGetMetadata(t *testing.T) { }, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -122,8 +125,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -158,8 +162,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -195,6 +200,7 @@ func TestServerGetMetadata(t *testing.T) { }, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, @@ -237,8 +243,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "test_ephemeral_resource2", }, }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -281,8 +288,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -317,8 +325,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -353,8 +362,9 @@ func TestServerGetMetadata(t *testing.T) { TypeName: "testprovidertype_ephemeral_resource", }, }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -397,7 +407,8 @@ func TestServerGetMetadata(t *testing.T) { Name: "function2", }, }, - Resources: []fwserver.ResourceMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -440,8 +451,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -476,8 +488,150 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + 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{}, + ListResources: []fwserver.ListResourceMetadata{}, + 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{}, + ListResources: []fwserver.ListResourceMetadata{}, + 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{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -513,6 +667,7 @@ func TestServerGetMetadata(t *testing.T) { DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "test_resource1", @@ -563,8 +718,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -599,8 +755,9 @@ func TestServerGetMetadata(t *testing.T) { "This is always an issue with the provider and should be reported to the provider developers.", ), }, - Functions: []fwserver.FunctionMetadata{}, - Resources: []fwserver.ResourceMetadata{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -632,6 +789,7 @@ func TestServerGetMetadata(t *testing.T) { DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "testprovidertype_resource", @@ -666,6 +824,10 @@ 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 }) diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index b8061dd10..4e78b4901 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -24,6 +24,7 @@ type GetProviderSchemaResponse struct { ResourceSchemas map[string]fwschema.Schema DataSourceSchemas map[string]fwschema.Schema EphemeralResourceSchemas map[string]fwschema.Schema + ListConfigSchemas map[string]fwschema.Schema FunctionDefinitions map[string]function.Definition Diagnostics diag.Diagnostics } diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 1443710c9..c9d2dc9a9 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -37,6 +37,9 @@ const ( // The type of resource being operated on, such as "random_pet" KeyResourceType = "tf_resource_type" + // The type of list resource being operated on, such as "random_pet" + KeyListResourceType = "tf_list_resource_type" + // The type of value being operated on, such as "JSONStringValue". KeyValueType = "tf_value_type" ) diff --git a/internal/testing/testprovider/list_resource.go b/internal/testing/testprovider/list_resource.go new file mode 100644 index 000000000..0ccb0d054 --- /dev/null +++ b/internal/testing/testprovider/list_resource.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ list.ListResource = &ListResource{} + +// Declarative list.ListResource for unit testing. +type ListResource struct { + // ListResource interface methods + MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) + ListResourceConfigSchemaMethod func(context.Context, list.ListResourceSchemaRequest, *list.ListResourceSchemaResponse) + ListResourceMethod func(context.Context, list.ListResourceRequest, *list.ListResourceResponse) +} + +// Metadata satisfies the list.ListResource interface. +func (r *ListResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + if r.MetadataMethod == nil { + return + } + + r.MetadataMethod(ctx, req, resp) +} + +// ListResourceConfigSchema satisfies the list.ListResource interface. +func (r *ListResource) ListResourceConfigSchema(ctx context.Context, req list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + if r.ListResourceConfigSchemaMethod == nil { + return + } + + r.ListResourceConfigSchemaMethod(ctx, req, resp) +} + +// ListResource satisfies the list.ListResource interface. +func (r *ListResource) ListResource(ctx context.Context, req list.ListResourceRequest, resp *list.ListResourceResponse) { + if r.ListResourceMethod == nil { + return + } + r.ListResourceMethod(ctx, req, resp) +} diff --git a/internal/testing/testprovider/provider.go b/internal/testing/testprovider/provider.go index 0b2c536da..efad3b8a4 100644 --- a/internal/testing/testprovider/provider.go +++ b/internal/testing/testprovider/provider.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -21,8 +22,9 @@ type Provider struct { ConfigureMethod func(context.Context, provider.ConfigureRequest, *provider.ConfigureResponse) SchemaMethod func(context.Context, provider.SchemaRequest, *provider.SchemaResponse) DataSourcesMethod func(context.Context) []func() datasource.DataSource - ResourcesMethod func(context.Context) []func() resource.Resource EphemeralResourcesMethod func(context.Context) []func() ephemeral.EphemeralResource + ListResourcesMethod func(context.Context) []func() list.ListResource + ResourcesMethod func(context.Context) []func() resource.Resource } // Configure satisfies the provider.Provider interface. @@ -61,6 +63,15 @@ func (p *Provider) Schema(ctx context.Context, req provider.SchemaRequest, resp p.SchemaMethod(ctx, req, resp) } +// ListResources satisfies the provider.Provider interface. +func (p *Provider) ListResources(ctx context.Context) []func() list.ListResource { + if p == nil || p.ListResourcesMethod == nil { + return nil + } + + return p.ListResourcesMethod(ctx) +} + // Resources satisfies the provider.Provider interface. func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { if p == nil || p.ResourcesMethod == nil { diff --git a/list/list_config_validator.go b/list/list_config_validator.go new file mode 100644 index 000000000..9801a1fff --- /dev/null +++ b/list/list_config_validator.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import "context" + +// ConfigValidator describes reusable List configuration validation functionality. +type ConfigValidator interface { + // Description describes the validation in plain text formatting. + // + // This information may be automatically added to resource plain text + // descriptions by external tooling. + Description(context.Context) string + + // MarkdownDescription describes the validation in Markdown formatting. + // + // This information may be automatically added to resource Markdown + // descriptions by external tooling. + MarkdownDescription(context.Context) string + + // ValidateResource performs the validation. + // + // This method name is separate from the datasource.ConfigValidator + // interface ValidateDataSource method name and provider.ConfigValidator + // interface ValidateProvider method name to allow generic validators. + ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} diff --git a/list/list_resource.go b/list/list_resource.go new file mode 100644 index 000000000..5f7821ae6 --- /dev/null +++ b/list/list_resource.go @@ -0,0 +1,156 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list + +import ( + "context" + "iter" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +// ListResource represents an implementation of listing instances of a managed resource +// This is the core interface for all list implementations. +// +// ListResource implementations can optionally implement these additional concepts: +// +// - Configure: Include provider-level data or clients. +// - Validation: Schema-based or entire configuration via +// ListResourceWithConfigValidators or ListResourceWithValidateConfig. +type ListResource interface { + // Metadata should return the full name of the managed resource to be listed, + // such as examplecloud_thing. + Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) + + // ListConfigSchema should return the schema for list blocks. + ListResourceConfigSchema(context.Context, ListResourceSchemaRequest, *ListResourceSchemaResponse) + + // ListResource is called when the provider must list instances of a + // managed resource type that satisfy a user-provided request. + ListResource(context.Context, ListResourceRequest, *ListResourceResponse) +} + +// 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. +type ListResourceWithConfigure interface { + ListResource + + // Configure enables provider-level data or clients to be set. + Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) +} + +// ListResourceWithConfigValidators is an interface type that extends ListResource to include +// declarative validations. +// +// Declaring validation using this methodology simplifies implementation of +// reusable functionality. These also include descriptions, which can be used +// for automating documentation. +// +// Validation will include ListResourceConfigValidators and ValidateListResourceConfig, if both +// are implemented, in addition to any Attribute or Type validation. +type ListResourceWithConfigValidators interface { + ListResource + + // ListResourceConfigValidators returns a list of functions which will all be performed during validation. + ListResourceConfigValidators(context.Context) []ConfigValidator +} + +// ListResourceWithValidateConfig is an interface type that extends ListResource to include +// imperative validation. +// +// Declaring validation using this methodology simplifies one-off +// functionality that typically applies to a single resource. Any documentation +// of this functionality must be manually added into schema descriptions. +// +// Validation will include ListResourceConfigValidators and ValidateListResourceConfig, if both +// are implemented, in addition to any Attribute or Type validation. +type ListResourceWithValidateConfig interface { + ListResource + + // ValidateListResourceConfig performs the validation. + ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) +} + +// ListResourceRequest represents a request for the provider to list instances of a +// managed resource type that satisfy a user-defined request. An instance of +// this rqeuest struct is passed as an argument to the provider's ListResourceResources +// function implementation. +type ListResourceRequest struct { + // Config is the configuration the user supplied for listing resource + // instances. + Config tfsdk.Config + + // IncludeResourceObject indicates whether the provider should populate + // the ResourceObject field in the ListResourceResult struct. + IncludeResourceObject bool + + // TODO: consider applicability of: + // + // Private *privatestate.ProviderData + // ProviderMeta tfsdk.Config + // ClientCapabilities ReadClientCapabilities +} + +// ListResourceResponse represents a response to a ListResourceRequest. An instance of this +// response struct is supplied as an argument to the provider's ListResourceResource +// function implementation function. The provider should set an iterator +// function on the response struct. +type ListResourceResponse struct { + // Results is a function that emits ListResourceRequest values via its yield + // function argument. + Results iter.Seq[ListResourceEvent] +} + +// ListResourceEvent represents a managed resource instance. A provider's +// ListManagedResources function implementation will emit zero or more results +// for a user-provided request. +type ListResourceEvent struct { + // Identity is the identity of the managed resource instance. + // + // A nil value will raise will raise a diagnostic. + Identity *tfsdk.ResourceIdentity + + // ResourceObject is the provider's representation of all attributes of the + // managed resource instance. + // + // If ListResourceRequest.IncludeResourceObject is true, a nil value will raise + // a warning diagnostic. + ResourceObject *tfsdk.ResourceObject + + // DisplayName is a provider-defined human-readable description of the + // managed resource instance, intended for CLI and browser UIs. + DisplayName string + + // Diagnostics report errors or warnings related to listing the + // resource. An empty slice indicates a successful operation with no + // warnings or errors generated. + Diagnostics diag.Diagnostics +} + +// ValidateConfigRequest represents a request to validate the +// configuration of a resource. An instance of this request struct is +// supplied as an argument to the Resource ValidateListResourceConfig receiver method +// or automatically passed through to each ListResourceConfigValidator. +type ValidateConfigRequest struct { + // Config is the configuration the user supplied for the resource. + // + // This configuration may contain unknown values if a user uses + // interpolation or other functionality that would prevent Terraform + // from knowing the value at request time. + Config tfsdk.Config +} + +// ValidateConfigResponse represents a response to a +// ValidateConfigRequest. An instance of this response struct is +// supplied as an argument to the list.ValidateConfig receiver method +// or automatically passed through to each ConfigValidator. +type ValidateConfigResponse struct { + // Diagnostics report errors or warnings related to validating the list + // configuration. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/list/list_resource_test.go b/list/list_resource_test.go new file mode 100644 index 000000000..9e0435174 --- /dev/null +++ b/list/list_resource_test.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package list_test + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type ComputeInstance struct { +} + +type ComputeInstanceWithValidateListResourceConfig struct { + ComputeInstance +} + +type ComputeInstanceWithListResourceConfigValidators struct { + ComputeInstance +} + +func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { +} + +func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ list.ListResourceSchemaRequest, _ *list.ListResourceSchemaResponse) { +} + +func (c *ComputeInstance) ListResource(_ context.Context, _ list.ListResourceRequest, _ *list.ListResourceResponse) { +} + +func (c *ComputeInstance) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { +} + +func (c *ComputeInstance) Schema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { +} + +func (c *ComputeInstance) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { +} + +func (c *ComputeInstance) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { +} + +func (c *ComputeInstance) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { +} + +func (c *ComputeInstance) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { +} + +func (c *ComputeInstanceWithValidateListResourceConfig) ValidateListResourceConfig(_ context.Context, _ list.ValidateConfigRequest, _ *list.ValidateConfigResponse) { +} + +func (c *ComputeInstanceWithListResourceConfigValidators) ListResourceConfigValidators(_ context.Context) []list.ConfigValidator { + return nil +} + +// ExampleResource_listable demonstrates a resource.Resource that implements +// list.ListResource interfaces. +func ExampleResource_listable() { + var _ list.ListResource = &ComputeInstance{} + var _ list.ListResourceWithConfigure = &ComputeInstance{} + var _ list.ListResourceWithValidateConfig = &ComputeInstanceWithValidateListResourceConfig{} + var _ list.ListResourceWithConfigValidators = &ComputeInstanceWithListResourceConfigValidators{} + + var _ resource.Resource = &ComputeInstance{} + var _ resource.ResourceWithConfigure = &ComputeInstance{} + + // Output: +} diff --git a/list/schema.go b/list/schema.go new file mode 100644 index 000000000..a0b9cb1ca --- /dev/null +++ b/list/schema.go @@ -0,0 +1,24 @@ +package list + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" +) + +// ListResourceSchemaRequest represents a request for the ListResource to +// return its schema. An instance of this request struct is supplied as an +// argument to the ListResource type ListResourceSchema method. +type ListResourceSchemaRequest struct{} + +// ListResourceSchemaResponse represents a response to a +// ListResourceSchemaRequest. An instance of this response struct is supplied +// as an argument to the ListResource type ListResourceResourceSchema method. +type ListResourceSchemaResponse struct { + // Schema is the schema of the list resource. + Schema schema.Schema + + // Diagnostics report errors or warnings related to retrieving the list + // resource schema. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/list/schema/schema.go b/list/schema/schema.go new file mode 100644 index 000000000..d77714cf2 --- /dev/null +++ b/list/schema/schema.go @@ -0,0 +1,4 @@ +package schema + +type Schema struct { +} diff --git a/provider/provider.go b/provider/provider.go index 61dfd39fe..a4e861647 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -122,6 +123,17 @@ type ProviderWithMetaSchema interface { MetaSchema(context.Context, MetaSchemaRequest, *MetaSchemaResponse) } +type ProviderWithListResources interface { + Provider + + // ListResources returns a slice of functions to instantiate each Resource + // List implementation. + // + // The resource type name is determined by the Resource List implementing + // the Metadata method. All resource lists must have unique names. + ListResources(context.Context) []func() list.ListResource +} + // ProviderWithValidateConfig is an interface type that extends Provider to include imperative validation. // // Declaring validation using this methodology simplifies one-off diff --git a/resource/resource.go b/resource/resource.go index b15bbb80b..73ed95df6 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -75,7 +75,7 @@ type ResourceWithConfigure interface { // ResourceWithConfigValidators is an interface type that extends Resource to include declarative validations. // -// Declaring validation using this methodology simplifies implmentation of +// Declaring validation using this methodology simplifies implementation of // reusable functionality. These also include descriptions, which can be used // for automating documentation. // diff --git a/tfsdk/resource_object.go b/tfsdk/resource_object.go new file mode 100644 index 000000000..14cc4da7f --- /dev/null +++ b/tfsdk/resource_object.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ResourceObject represents a Terraform resource. +type ResourceObject struct { + Raw tftypes.Value + Schema fwschema.Schema +} + +// Get populates the struct passed as `target` with the resource. +func (c ResourceObject) Get(ctx context.Context, target interface{}) diag.Diagnostics { + return c.data().Get(ctx, target) +} + +// GetAttribute retrieves the attribute or block found at `path` and populates +// the `target` with the value. This method is intended for top level schema +// attributes or blocks. Use `types` package methods or custom types to step +// into collections. +// +// Attributes or elements under null or unknown collections return null +// values, however this behavior is not protected by compatibility promises. +func (c ResourceObject) GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics { + return c.data().GetAtPath(ctx, path, target) +} + +// PathMatches returns all matching path.Paths from the given path.Expression. +// +// If a parent path is null or unknown, which would prevent a full expression +// from matching, the parent path is returned rather than no match to prevent +// false positives. +func (c ResourceObject) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return c.data().PathMatches(ctx, pathExpr) +} + +func (c ResourceObject) data() fwschemadata.Data { + return fwschemadata.Data{ + Description: fwschemadata.DataDescriptionConfiguration, + Schema: c.Schema, + TerraformValue: c.Raw, + } +}