From 7e298db6c8bd49bf7f6c4187d9caa124a4201594 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 09:52:10 -0400 Subject: [PATCH 01/18] Add list RPC types --- resource/list.go | 43 ++++++++++++++++++++++++++++++++ tfsdk/resource_object.go | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 resource/list.go create mode 100644 tfsdk/resource_object.go diff --git a/resource/list.go b/resource/list.go new file mode 100644 index 000000000..a4b842253 --- /dev/null +++ b/resource/list.go @@ -0,0 +1,43 @@ +package resource + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" +) + +type List interface { + ListSchema(context.Context, SchemaRequest, SchemaResponse) + ListResources(context.Context, ListRequest, ListResponse) +} + +type ListWithValidate interface { + List + + ValidateListConfig(context.Context, ValidateListConfigRequest, ValidateListConfigResponse) +} + +type ListRequest struct { + Config tfsdk.Config + IncludeResourceObject bool +} + +type ListResponse struct { + Results []ListResult // TODO: streamify +} + +type ListResult struct { + Identity tfsdk.ResourceIdentity + Resource tfsdk.ResourceObject + DisplayName string + Diagnostics diag.Diagnostics +} + +type ValidateListConfigRequest struct { + Config tfsdk.Config +} + +type ValidateListConfigResponse struct { + Diagnostics diag.Diagnostics +} 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, + } +} From 3047b495ea484192eb66379d4cff0668b6cc6449 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 12:06:00 -0400 Subject: [PATCH 02/18] Add list_test --- resource/list.go | 12 ++++++-- resource/list_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 resource/list_test.go diff --git a/resource/list.go b/resource/list.go index a4b842253..155dfeaa3 100644 --- a/resource/list.go +++ b/resource/list.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package resource import ( @@ -8,14 +11,19 @@ import ( ) type List interface { + Metadata(context.Context, MetadataRequest, *MetadataResponse) ListSchema(context.Context, SchemaRequest, SchemaResponse) ListResources(context.Context, ListRequest, ListResponse) } -type ListWithValidate interface { +type ListWithConfigure interface { List + Configure(context.Context, ConfigureRequest, *ConfigureResponse) +} - ValidateListConfig(context.Context, ValidateListConfigRequest, ValidateListConfigResponse) +type ListWithValidateConfig interface { + List + ValidateListConfig(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) } type ListRequest struct { diff --git a/resource/list_test.go b/resource/list_test.go new file mode 100644 index 000000000..2ac7c70e7 --- /dev/null +++ b/resource/list_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource_test + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type ComputeInstance struct { +} + +func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) ValidateListConfig(_ context.Context, _ resource.ValidateListConfigRequest, _ *resource.ValidateListConfigResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) ListSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) ListResources(_ context.Context, _ resource.ListRequest, _ resource.ListResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Schema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { + panic("not implemented") +} + +func (c *ComputeInstance) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { + panic("not implemented") +} + +// ExampleResource_listable demonstrates a resource.Resource that implements resource.List interfaces +func ExampleResource_listable() { + + var _ resource.List = &ComputeInstance{} + var _ resource.ListWithConfigure = &ComputeInstance{} + var _ resource.ListWithValidateConfig = &ComputeInstance{} + + var _ resource.Resource = &ComputeInstance{} + var _ resource.ResourceWithConfigure = &ComputeInstance{} + + // Output: +} From b69c145874acb8ced7fd6133ea6a8046ab4a410d Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 12:26:45 -0400 Subject: [PATCH 03/18] Expand on validation interfaces --- resource/list.go | 29 +++++++++++++++++++++++++++++ resource/list_test.go | 25 +++++++++++++++++++------ 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/resource/list.go b/resource/list.go index 155dfeaa3..1205ae28f 100644 --- a/resource/list.go +++ b/resource/list.go @@ -10,6 +10,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) +// List represents an implementation of listing instances of a managed resource +// This is the core interface for all list implementations. +// +// List implementations can optionally implement these additional concepts: +// +// - Configure: Include provider-level data or clients. +// - Validation: Schema-based or entire configuration via +// ListWithConfigValidators or ListWithValidateConfig. type List interface { Metadata(context.Context, MetadataRequest, *MetadataResponse) ListSchema(context.Context, SchemaRequest, SchemaResponse) @@ -21,6 +29,11 @@ type ListWithConfigure interface { Configure(context.Context, ConfigureRequest, *ConfigureResponse) } +type ListWithConfigValidators interface { + List + ListConfigValidators(context.Context) []ListConfigValidator +} + type ListWithValidateConfig interface { List ValidateListConfig(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) @@ -42,10 +55,26 @@ type ListResult struct { Diagnostics diag.Diagnostics } +// ValidateListConfigRequest represents a request to validate the +// configuration of a resource. An instance of this request struct is +// supplied as an argument to the Resource ValidateListConfig receiver method +// or automatically passed through to each ListConfigValidator. type ValidateListConfigRequest 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 } +// ValidateListConfigResponse represents a response to a +// ValidateListConfigRequest. An instance of this response struct is +// supplied as an argument to the Resource ValidateListConfig receiver method +// or automatically passed through to each ListConfigValidator. type ValidateListConfigResponse struct { + // Diagnostics report errors or warnings related to validating the resource + // configuration. An empty slice indicates success, with no warnings or + // errors generated. Diagnostics diag.Diagnostics } diff --git a/resource/list_test.go b/resource/list_test.go index 2ac7c70e7..c4074f0ec 100644 --- a/resource/list_test.go +++ b/resource/list_test.go @@ -12,11 +12,15 @@ import ( type ComputeInstance struct { } -func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { - panic("not implemented") +type ComputeInstanceWithValidateConfig struct { + ComputeInstance +} + +type ComputeInstanceWithConfigValidators struct { + ComputeInstance } -func (c *ComputeInstance) ValidateListConfig(_ context.Context, _ resource.ValidateListConfigRequest, _ *resource.ValidateListConfigResponse) { +func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { panic("not implemented") } @@ -52,12 +56,21 @@ func (c *ComputeInstance) Delete(_ context.Context, _ resource.DeleteRequest, _ panic("not implemented") } -// ExampleResource_listable demonstrates a resource.Resource that implements resource.List interfaces -func ExampleResource_listable() { +func (c *ComputeInstanceWithValidateConfig) ValidateListConfig(_ context.Context, _ resource.ValidateListConfigRequest, _ *resource.ValidateListConfigResponse) { + panic("not implemented") +} + +func (c *ComputeInstanceWithConfigValidators) ListConfigValidators(_ context.Context) []resource.ListConfigValidator { + panic("not implemented") +} +// ExampleResource_listable demonstrates a resource.Resource that implements +// resource.List interfaces. +func ExampleResource_listable() { var _ resource.List = &ComputeInstance{} var _ resource.ListWithConfigure = &ComputeInstance{} - var _ resource.ListWithValidateConfig = &ComputeInstance{} + var _ resource.ListWithValidateConfig = &ComputeInstanceWithValidateConfig{} + var _ resource.ListWithConfigValidators = &ComputeInstanceWithConfigValidators{} var _ resource.Resource = &ComputeInstance{} var _ resource.ResourceWithConfigure = &ComputeInstance{} From 0596c033adbdaff61be065155798e7f29d230279 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 12:41:31 -0400 Subject: [PATCH 04/18] Add type comments --- resource/list.go | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/resource/list.go b/resource/list.go index 1205ae28f..910e7093d 100644 --- a/resource/list.go +++ b/resource/list.go @@ -5,6 +5,7 @@ package resource import ( "context" + "iter" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" @@ -24,30 +25,62 @@ type List interface { ListResources(context.Context, ListRequest, ListResponse) } +// ListWithConfigure is an interface type that extends List 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 ListWithConfigure interface { List Configure(context.Context, ConfigureRequest, *ConfigureResponse) } +// ListWithConfigValidators is an interface type that extends List 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 ListConfigValidators and ValidateListConfig, if both +// are implemented, in addition to any Attribute or Type validation. type ListWithConfigValidators interface { List ListConfigValidators(context.Context) []ListConfigValidator } +// ListWithValidateConfig is an interface type that extends List 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 ListConfigValidators and ValidateListConfig, if both +// are implemented, in addition to any Attribute or Type validation. type ListWithValidateConfig interface { List ValidateListConfig(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) } +// 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 rqeuest struct is passed as an argument to the provider's ListResources +// function implementation. type ListRequest struct { Config tfsdk.Config IncludeResourceObject bool } +// ListResponse represents a response to a ListRequest. An instance of this +// response struct is supplied as an argument to the provider's ListResource +// function implementation function. The provider should set an iterator +// function on the response struct. type ListResponse struct { - Results []ListResult // TODO: streamify + Results iter.Seq[ListResult] // Speculative + exploratory use of Go 1.23 iterators } +// ListResult represents a managed resource instance. A provider's ListResource +// function implementation will emit zero or more results for a user-provided +// request. type ListResult struct { Identity tfsdk.ResourceIdentity Resource tfsdk.ResourceObject From 79a7422d8ec08e900a44c3ada1f9a31b257cb204 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 12:53:06 -0400 Subject: [PATCH 05/18] Add field comments --- resource/list.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/resource/list.go b/resource/list.go index 910e7093d..1393e64e7 100644 --- a/resource/list.go +++ b/resource/list.go @@ -20,8 +20,15 @@ import ( // - Validation: Schema-based or entire configuration via // ListWithConfigValidators or ListWithValidateConfig. type List interface { + // Metadata should return the full name of the managed resource to be listed, + // such as examplecloud_thing.. Metadata(context.Context, MetadataRequest, *MetadataResponse) + + // Schema should return the schema for list blocks. ListSchema(context.Context, SchemaRequest, SchemaResponse) + + // ListResources is called when the provider must list instances of a + // managed resource type that satisfy a user-provided request. ListResources(context.Context, ListRequest, ListResponse) } @@ -30,6 +37,10 @@ type List interface { // opportunity to setup any necessary provider-level data or clients. type ListWithConfigure interface { List + + // Configure enables provider-level data or clients to be set in the + // provider-defined Resource type. It is separately executed for each + // ReadResource RPC. Configure(context.Context, ConfigureRequest, *ConfigureResponse) } @@ -44,6 +55,8 @@ type ListWithConfigure interface { // are implemented, in addition to any Attribute or Type validation. type ListWithConfigValidators interface { List + + // ListConfigValidators returns a list of functions which will all be performed during validation. ListConfigValidators(context.Context) []ListConfigValidator } @@ -58,6 +71,8 @@ type ListWithConfigValidators interface { // are implemented, in addition to any Attribute or Type validation. type ListWithValidateConfig interface { List + + // ValidateListConfig performs the validation. ValidateListConfig(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) } @@ -66,8 +81,19 @@ type ListWithValidateConfig interface { // this rqeuest struct is passed as an argument to the provider's ListResources // function implementation. type ListRequest struct { - Config tfsdk.Config + // 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 ListResult struct. IncludeResourceObject bool + + // TODO: consider applicability of: + // + // Private *privatestate.ProviderData + // ProviderMeta tfsdk.Config + // ClientCapabilities ReadClientCapabilities } // ListResponse represents a response to a ListRequest. An instance of this @@ -75,6 +101,8 @@ type ListRequest struct { // function implementation function. The provider should set an iterator // function on the response struct. type ListResponse struct { + // Results is a function that emits ListRequest values via its yield + // function argument. Results iter.Seq[ListResult] // Speculative + exploratory use of Go 1.23 iterators } From 8d92c15a0cc0dc0625bd6026530ab95e3fcc034e Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 16:36:10 -0400 Subject: [PATCH 06/18] Add resource.ListConfigValidator --- resource/list_config_validator.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 resource/list_config_validator.go diff --git a/resource/list_config_validator.go b/resource/list_config_validator.go new file mode 100644 index 000000000..22aa6dbd0 --- /dev/null +++ b/resource/list_config_validator.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import "context" + +// ListConfigValidator describes reusable List configuration validation functionality. +type ListConfigValidator 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. + ValidateList(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) +} From 9b5e17693df86644c1751c242674b88ff7752c4c Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 17:00:08 -0400 Subject: [PATCH 07/18] tidy --- hats.go | 43 +++++++++++++++++++ internal/fwserver/server_getproviderschema.go | 1 + resource/list.go | 22 +++++++--- resource/list_test.go | 2 +- resource/resource.go | 2 +- 5 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 hats.go diff --git a/hats.go b/hats.go new file mode 100644 index 000000000..975830ad3 --- /dev/null +++ b/hats.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package +type ResourceWithList interface { + Resource + + ListSchema(context.Context, SchemaRequest, SchemaResponse) + List(context.Context, ListRequest, ListResponse) +} + +type ResourceWithValidateListConfig interface { + ResourceWithList + + ValidateListConfig( + context.Context, + ValidateListConfigRequest, + ValidateListConfigResponse) +} + +type ListRequest struct { + Config tfsdk.Config + IncludeResourceObject bool +} + +type ListResponse struct { + Results []ListResult +} + +type ListResult struct { + Identity tfsdk.ResourceIdentity + Resource tfsdk.State + DisplayName string + Diagnostics diag.Diagnostics +} + +type ValidateListConfigRequest struct { + Config tfsdk.Config +} + +type ValidateListConfigResponse struct { + Diagnostics diag.Diagnostics +} diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index b8061dd10..26a604d76 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 + ListSchemas map[string]fwschema.Schema FunctionDefinitions map[string]function.Definition Diagnostics diag.Diagnostics } diff --git a/resource/list.go b/resource/list.go index 1393e64e7..c5ebb84ce 100644 --- a/resource/list.go +++ b/resource/list.go @@ -24,8 +24,8 @@ type List interface { // such as examplecloud_thing.. Metadata(context.Context, MetadataRequest, *MetadataResponse) - // Schema should return the schema for list blocks. - ListSchema(context.Context, SchemaRequest, SchemaResponse) + // ListConfigSchema should return the schema for list blocks. + ListConfigSchema(context.Context, SchemaRequest, SchemaResponse) // ListResources is called when the provider must list instances of a // managed resource type that satisfy a user-provided request. @@ -38,9 +38,7 @@ type List interface { type ListWithConfigure interface { List - // Configure enables provider-level data or clients to be set in the - // provider-defined Resource type. It is separately executed for each - // ReadResource RPC. + // Configure enables provider-level data or clients to be set. Configure(context.Context, ConfigureRequest, *ConfigureResponse) } @@ -110,8 +108,18 @@ type ListResponse struct { // function implementation will emit zero or more results for a user-provided // request. type ListResult struct { - Identity tfsdk.ResourceIdentity - Resource tfsdk.ResourceObject + // 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 ListRequest.IncludeResourceObject is true, a nil value will raise + // a warning diagnostic. + Resource *tfsdk.ResourceObject + DisplayName string Diagnostics diag.Diagnostics } diff --git a/resource/list_test.go b/resource/list_test.go index c4074f0ec..7079aebbd 100644 --- a/resource/list_test.go +++ b/resource/list_test.go @@ -24,7 +24,7 @@ func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureReque panic("not implemented") } -func (c *ComputeInstance) ListSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { +func (c *ComputeInstance) ListConfigSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { panic("not implemented") } 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. // From ad82f7fe7f45065235afd9540f3a187633ff03cd Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 17:00:08 -0400 Subject: [PATCH 08/18] tidy --- resource/list.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/resource/list.go b/resource/list.go index c5ebb84ce..9cddc4109 100644 --- a/resource/list.go +++ b/resource/list.go @@ -120,7 +120,13 @@ type ListResult struct { // a warning diagnostic. Resource *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 } @@ -142,8 +148,8 @@ type ValidateListConfigRequest struct { // supplied as an argument to the Resource ValidateListConfig receiver method // or automatically passed through to each ListConfigValidator. type ValidateListConfigResponse struct { - // Diagnostics report errors or warnings related to validating the resource - // configuration. An empty slice indicates success, with no warnings or - // errors generated. + // 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 } From 05619ed04814bf47632d19096eb4364efb2a02bc Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 17:04:38 -0400 Subject: [PATCH 09/18] tidy --- hats.go | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 hats.go diff --git a/hats.go b/hats.go deleted file mode 100644 index 975830ad3..000000000 --- a/hats.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package -type ResourceWithList interface { - Resource - - ListSchema(context.Context, SchemaRequest, SchemaResponse) - List(context.Context, ListRequest, ListResponse) -} - -type ResourceWithValidateListConfig interface { - ResourceWithList - - ValidateListConfig( - context.Context, - ValidateListConfigRequest, - ValidateListConfigResponse) -} - -type ListRequest struct { - Config tfsdk.Config - IncludeResourceObject bool -} - -type ListResponse struct { - Results []ListResult -} - -type ListResult struct { - Identity tfsdk.ResourceIdentity - Resource tfsdk.State - DisplayName string - Diagnostics diag.Diagnostics -} - -type ValidateListConfigRequest struct { - Config tfsdk.Config -} - -type ValidateListConfigResponse struct { - Diagnostics diag.Diagnostics -} From 51fbc82620e111366309663b550d01c746098977 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 17:05:11 -0400 Subject: [PATCH 10/18] tidy --- internal/fwserver/server_getproviderschema.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index 26a604d76..4e78b4901 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -24,7 +24,7 @@ type GetProviderSchemaResponse struct { ResourceSchemas map[string]fwschema.Schema DataSourceSchemas map[string]fwschema.Schema EphemeralResourceSchemas map[string]fwschema.Schema - ListSchemas map[string]fwschema.Schema + ListConfigSchemas map[string]fwschema.Schema FunctionDefinitions map[string]function.Definition Diagnostics diag.Diagnostics } From adb70d1944e08d0dcee48c42d5f5f6dfff31dc5e Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 20 May 2025 17:32:47 -0400 Subject: [PATCH 11/18] tidy --- resource/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource/list.go b/resource/list.go index 9cddc4109..7d059c913 100644 --- a/resource/list.go +++ b/resource/list.go @@ -118,7 +118,7 @@ type ListResult struct { // // If ListRequest.IncludeResourceObject is true, a nil value will raise // a warning diagnostic. - Resource *tfsdk.ResourceObject + ResourceObject *tfsdk.ResourceObject // DisplayName is a provider-defined human-readable description of the // managed resource instance, intended for CLI and browser UIs. From ee84f77889b66c313cfeb907a013f0508236e2c0 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 11:17:31 -0400 Subject: [PATCH 12/18] Rename resource.List to list.ListResource --- {resource => list}/list_config_validator.go | 8 +- resource/list.go => list/list_resource.go | 99 ++++++++++--------- .../list_resource_test.go | 37 +++---- 3 files changed, 68 insertions(+), 76 deletions(-) rename {resource => list}/list_config_validator.go (77%) rename resource/list.go => list/list_resource.go (52%) rename resource/list_test.go => list/list_resource_test.go (51%) diff --git a/resource/list_config_validator.go b/list/list_config_validator.go similarity index 77% rename from resource/list_config_validator.go rename to list/list_config_validator.go index 22aa6dbd0..9801a1fff 100644 --- a/resource/list_config_validator.go +++ b/list/list_config_validator.go @@ -1,12 +1,12 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package resource +package list import "context" -// ListConfigValidator describes reusable List configuration validation functionality. -type ListConfigValidator interface { +// 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 @@ -24,5 +24,5 @@ type ListConfigValidator interface { // This method name is separate from the datasource.ConfigValidator // interface ValidateDataSource method name and provider.ConfigValidator // interface ValidateProvider method name to allow generic validators. - ValidateList(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) + ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } diff --git a/resource/list.go b/list/list_resource.go similarity index 52% rename from resource/list.go rename to list/list_resource.go index 7d059c913..bcc481abd 100644 --- a/resource/list.go +++ b/list/list_resource.go @@ -1,90 +1,91 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package resource +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" ) -// List represents an implementation of listing instances of a managed resource +// ListResource represents an implementation of listing instances of a managed resource // This is the core interface for all list implementations. // -// List implementations can optionally implement these additional concepts: +// ListResource implementations can optionally implement these additional concepts: // // - Configure: Include provider-level data or clients. // - Validation: Schema-based or entire configuration via -// ListWithConfigValidators or ListWithValidateConfig. -type List interface { +// 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, MetadataRequest, *MetadataResponse) + // such as examplecloud_thing. + Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) // ListConfigSchema should return the schema for list blocks. - ListConfigSchema(context.Context, SchemaRequest, SchemaResponse) + ListResourceConfigSchema(context.Context, resource.SchemaRequest, resource.SchemaResponse) - // ListResources is called when the provider must list instances of a + // ListResource is called when the provider must list instances of a // managed resource type that satisfy a user-provided request. - ListResources(context.Context, ListRequest, ListResponse) + ListResource(context.Context, ListResourceRequest, ListResourceResponse) } -// ListWithConfigure is an interface type that extends List to include a method +// 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 ListWithConfigure interface { - List +type ListResourceWithConfigure interface { + ListResource // Configure enables provider-level data or clients to be set. - Configure(context.Context, ConfigureRequest, *ConfigureResponse) + Configure(context.Context, resource.ConfigureRequest, *resource.ConfigureResponse) } -// ListWithConfigValidators is an interface type that extends List to include +// 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 ListConfigValidators and ValidateListConfig, if both +// Validation will include ListResourceConfigValidators and ValidateListResourceConfig, if both // are implemented, in addition to any Attribute or Type validation. -type ListWithConfigValidators interface { - List +type ListResourceWithConfigValidators interface { + ListResource - // ListConfigValidators returns a list of functions which will all be performed during validation. - ListConfigValidators(context.Context) []ListConfigValidator + // ListResourceConfigValidators returns a list of functions which will all be performed during validation. + ListResourceConfigValidators(context.Context) []ConfigValidator } -// ListWithValidateConfig is an interface type that extends List to include +// 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 ListConfigValidators and ValidateListConfig, if both +// Validation will include ListResourceConfigValidators and ValidateListResourceConfig, if both // are implemented, in addition to any Attribute or Type validation. -type ListWithValidateConfig interface { - List +type ListResourceWithValidateConfig interface { + ListResource - // ValidateListConfig performs the validation. - ValidateListConfig(context.Context, ValidateListConfigRequest, *ValidateListConfigResponse) + // ValidateListResourceConfig performs the validation. + ValidateListResourceConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } -// ListRequest represents a request for the provider to list instances of a +// 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 ListResources +// this rqeuest struct is passed as an argument to the provider's ListResourceResources // function implementation. -type ListRequest struct { +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 ListResult struct. + // the ResourceObject field in the ListResourceResult struct. IncludeResourceObject bool // TODO: consider applicability of: @@ -94,20 +95,20 @@ type ListRequest struct { // ClientCapabilities ReadClientCapabilities } -// ListResponse represents a response to a ListRequest. An instance of this -// response struct is supplied as an argument to the provider's ListResource +// 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 ListResponse struct { - // Results is a function that emits ListRequest values via its yield +type ListResourceResponse struct { + // Results is a function that emits ListResourceRequest values via its yield // function argument. - Results iter.Seq[ListResult] // Speculative + exploratory use of Go 1.23 iterators + Results iter.Seq[ListResourceEvent] } -// ListResult represents a managed resource instance. A provider's ListResource -// function implementation will emit zero or more results for a user-provided -// request. -type ListResult struct { +// 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. @@ -116,7 +117,7 @@ type ListResult struct { // ResourceObject is the provider's representation of all attributes of the // managed resource instance. // - // If ListRequest.IncludeResourceObject is true, a nil value will raise + // If ListResourceRequest.IncludeResourceObject is true, a nil value will raise // a warning diagnostic. ResourceObject *tfsdk.ResourceObject @@ -130,11 +131,11 @@ type ListResult struct { Diagnostics diag.Diagnostics } -// ValidateListConfigRequest represents a request to validate the +// 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 ValidateListConfig receiver method -// or automatically passed through to each ListConfigValidator. -type ValidateListConfigRequest struct { +// 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 @@ -143,11 +144,11 @@ type ValidateListConfigRequest struct { Config tfsdk.Config } -// ValidateListConfigResponse represents a response to a -// ValidateListConfigRequest. An instance of this response struct is -// supplied as an argument to the Resource ValidateListConfig receiver method -// or automatically passed through to each ListConfigValidator. -type ValidateListConfigResponse struct { +// 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. diff --git a/resource/list_test.go b/list/list_resource_test.go similarity index 51% rename from resource/list_test.go rename to list/list_resource_test.go index 7079aebbd..96798fcc5 100644 --- a/resource/list_test.go +++ b/list/list_resource_test.go @@ -1,76 +1,67 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 -package resource_test +package list_test import ( "context" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/resource" ) type ComputeInstance struct { } -type ComputeInstanceWithValidateConfig struct { +type ComputeInstanceWithValidateListResourceConfig struct { ComputeInstance } -type ComputeInstanceWithConfigValidators struct { +type ComputeInstanceWithListResourceConfigValidators struct { ComputeInstance } func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { - panic("not implemented") } -func (c *ComputeInstance) ListConfigSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { - panic("not implemented") +func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { } -func (c *ComputeInstance) ListResources(_ context.Context, _ resource.ListRequest, _ resource.ListResponse) { - panic("not implemented") +func (c *ComputeInstance) ListResource(_ context.Context, _ list.ListResourceRequest, _ list.ListResourceResponse) { } func (c *ComputeInstance) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { - panic("not implemented") } func (c *ComputeInstance) Schema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { - panic("not implemented") } func (c *ComputeInstance) Create(_ context.Context, _ resource.CreateRequest, _ *resource.CreateResponse) { - panic("not implemented") } func (c *ComputeInstance) Read(_ context.Context, _ resource.ReadRequest, _ *resource.ReadResponse) { - panic("not implemented") } func (c *ComputeInstance) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { - panic("not implemented") } func (c *ComputeInstance) Delete(_ context.Context, _ resource.DeleteRequest, _ *resource.DeleteResponse) { - panic("not implemented") } -func (c *ComputeInstanceWithValidateConfig) ValidateListConfig(_ context.Context, _ resource.ValidateListConfigRequest, _ *resource.ValidateListConfigResponse) { - panic("not implemented") +func (c *ComputeInstanceWithValidateListResourceConfig) ValidateListResourceConfig(_ context.Context, _ list.ValidateConfigRequest, _ *list.ValidateConfigResponse) { } -func (c *ComputeInstanceWithConfigValidators) ListConfigValidators(_ context.Context) []resource.ListConfigValidator { - panic("not implemented") +func (c *ComputeInstanceWithListResourceConfigValidators) ListResourceConfigValidators(_ context.Context) []list.ConfigValidator { + return nil } // ExampleResource_listable demonstrates a resource.Resource that implements -// resource.List interfaces. +// list.ListResource interfaces. func ExampleResource_listable() { - var _ resource.List = &ComputeInstance{} - var _ resource.ListWithConfigure = &ComputeInstance{} - var _ resource.ListWithValidateConfig = &ComputeInstanceWithValidateConfig{} - var _ resource.ListWithConfigValidators = &ComputeInstanceWithConfigValidators{} + var _ list.ListResource = &ComputeInstance{} + var _ list.ListResourceWithConfigure = &ComputeInstance{} + var _ list.ListResourceWithValidateConfig = &ComputeInstanceWithValidateListResourceConfig{} + var _ list.ListResourceWithConfigValidators = &ComputeInstanceWithListResourceConfigValidators{} var _ resource.Resource = &ComputeInstance{} var _ resource.ResourceWithConfigure = &ComputeInstance{} From ae81738400e4d20372e1c006c91779f07f76ae57 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 11:20:11 -0400 Subject: [PATCH 13/18] Add ProviderWithListResources interface --- provider/provider.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 From ce1fee346e293a4e0ca33e4df413cc0db8188ff1 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 11:26:03 -0400 Subject: [PATCH 14/18] Add list resource function cache --- internal/fwserver/server.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b4b8a779b..779e30b68 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. From 5f48f4106a9c17ef68ee3099df239e1fc2ffb556 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 11:47:23 -0400 Subject: [PATCH 15/18] Update fwserver GetMetadata to cache ListResources --- internal/fwserver/server.go | 76 ++++++++++++++++++++ internal/fwserver/server_getmetadata.go | 16 ++++- internal/fwserver/server_getmetadata_test.go | 58 +++++++++------ internal/logging/keys.go | 3 + 4 files changed, 129 insertions(+), 24 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 779e30b68..f7a3583b8 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -819,3 +819,79 @@ 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}) + + if _, ok := s.listResourceFuncs[typeName]; ok { + s.listResourceTypesDiags.AddError( + "Duplicate ListResource Type Defined", + fmt.Sprintf("The %s ListResource type name was returned for multiple 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 + } + + s.listResourceFuncs[typeName] = listResourceFunc + } + + return s.listResourceFuncs, s.resourceTypesDiags +} + +// 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..1ddfdc8e6 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,36 @@ 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...) + listResourceMetadatas, diags := s.ListResourceMetadatas(ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -83,5 +92,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..1506005ab 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -36,6 +36,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 +80,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 +124,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 +161,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 +199,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 +242,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 +287,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 +324,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 +361,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 +406,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 +450,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 +487,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, @@ -513,6 +525,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 +576,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 +613,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 +647,7 @@ func TestServerGetMetadata(t *testing.T) { DataSources: []fwserver.DataSourceMetadata{}, EphemeralResources: []fwserver.EphemeralResourceMetadata{}, Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{}, Resources: []fwserver.ResourceMetadata{ { TypeName: "testprovidertype_resource", 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" ) From 88c77f0cff11c1956c4c9c183e08ab1e446301de Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 17:18:00 -0400 Subject: [PATCH 16/18] Check that a ListResource type has a matching Resource type --- internal/fwserver/server.go | 15 +- internal/fwserver/server_getmetadata.go | 2 + internal/fwserver/server_getmetadata_test.go | 146 +++++++++++++++++++ internal/testing/testprovider/provider.go | 13 +- internal/testing/testprovider/resource.go | 66 +++------ list/list_resource.go | 4 +- 6 files changed, 192 insertions(+), 54 deletions(-) diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index f7a3583b8..6aed3c085 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -862,22 +862,31 @@ func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list. continue } - logging.FrameworkTrace(ctx, "Found resource type", map[string]interface{}{logging.KeyListResourceType: typeName}) + 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 resources. ", typeName)+ + 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.resourceTypesDiags + return s.listResourceFuncs, s.listResourceTypesDiags } // ListResourceMetadatas returns a slice of ListResourceMetadata for the GetMetadata diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 1ddfdc8e6..aa7ef62db 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -82,6 +82,8 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp 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...) diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 1506005ab..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" ) @@ -497,6 +498,147 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, + "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, + PlanDestroy: true, + }, + }, + }, "resources": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -682,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/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/internal/testing/testprovider/resource.go b/internal/testing/testprovider/resource.go index d2f0ef63c..2b59e43f9 100644 --- a/internal/testing/testprovider/resource.go +++ b/internal/testing/testprovider/resource.go @@ -6,24 +6,22 @@ package testprovider import ( "context" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/resource" ) -var _ resource.Resource = &Resource{} +var _ list.ListResource = &ListResource{} -// Declarative resource.Resource for unit testing. -type Resource struct { - // Resource interface methods - MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) - SchemaMethod func(context.Context, resource.SchemaRequest, *resource.SchemaResponse) - CreateMethod func(context.Context, resource.CreateRequest, *resource.CreateResponse) - DeleteMethod func(context.Context, resource.DeleteRequest, *resource.DeleteResponse) - ReadMethod func(context.Context, resource.ReadRequest, *resource.ReadResponse) - UpdateMethod func(context.Context, resource.UpdateRequest, *resource.UpdateResponse) +// Declarative list.ListResource for unit testing. +type ListResource struct { + // ListResource interface methods + MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) + ListResourceConfigSchemaMethod func(context.Context, resource.SchemaRequest, *resource.SchemaResponse) + ListResourceMethod func(context.Context, list.ListResourceRequest, *list.ListResourceResponse) } -// Metadata satisfies the resource.Resource interface. -func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +// Metadata satisfies the list.ListResource interface. +func (r *ListResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { if r.MetadataMethod == nil { return } @@ -31,47 +29,19 @@ func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, r r.MetadataMethod(ctx, req, resp) } -// Schema satisfies the resource.Resource interface. -func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - if r.SchemaMethod == nil { +// ListResourceConfigSchema satisfies the list.ListResource interface. +func (r *ListResource) ListResourceConfigSchema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + if r.ListResourceConfigSchemaMethod == nil { return } - r.SchemaMethod(ctx, req, resp) + r.ListResourceConfigSchemaMethod(ctx, req, resp) } -// Create satisfies the resource.Resource interface. -func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - if r.CreateMethod == nil { +// 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.CreateMethod(ctx, req, resp) -} - -// Delete satisfies the resource.Resource interface. -func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - if r.DeleteMethod == nil { - return - } - - r.DeleteMethod(ctx, req, resp) -} - -// Read satisfies the resource.Resource interface. -func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - if r.ReadMethod == nil { - return - } - - r.ReadMethod(ctx, req, resp) -} - -// Update satisfies the resource.Resource interface. -func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - if r.UpdateMethod == nil { - return - } - - r.UpdateMethod(ctx, req, resp) + r.ListResourceMethod(ctx, req, resp) } diff --git a/list/list_resource.go b/list/list_resource.go index bcc481abd..9c94ed125 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -26,11 +26,11 @@ type ListResource interface { Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) // ListConfigSchema should return the schema for list blocks. - ListResourceConfigSchema(context.Context, resource.SchemaRequest, resource.SchemaResponse) + ListResourceConfigSchema(context.Context, resource.SchemaRequest, *resource.SchemaResponse) // TODO: list.Schema{Request,Response} // 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) + ListResource(context.Context, ListResourceRequest, *ListResourceResponse) } // ListResourceWithConfigure is an interface type that extends ListResource to include a method From f74e2309914b6d7ae70e312fb9a79db3e81c2e92 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 17:22:18 -0400 Subject: [PATCH 17/18] fixup! Check that a ListResource type has a matching Resource type --- list/list_resource_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/list/list_resource_test.go b/list/list_resource_test.go index 96798fcc5..c4201a3e4 100644 --- a/list/list_resource_test.go +++ b/list/list_resource_test.go @@ -24,10 +24,10 @@ type ComputeInstanceWithListResourceConfigValidators struct { func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { } -func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ resource.SchemaRequest, _ resource.SchemaResponse) { +func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { } -func (c *ComputeInstance) ListResource(_ context.Context, _ list.ListResourceRequest, _ list.ListResourceResponse) { +func (c *ComputeInstance) ListResource(_ context.Context, _ list.ListResourceRequest, _ *list.ListResourceResponse) { } func (c *ComputeInstance) Metadata(_ context.Context, _ resource.MetadataRequest, _ *resource.MetadataResponse) { From 98c3e2f428928ee5b533b9c83d107ba9b23d114e Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Tue, 27 May 2025 17:35:55 -0400 Subject: [PATCH 18/18] fixup! fixup! Check that a ListResource type has a matching Resource type --- .../testing/testprovider/list_resource.go | 47 +++++++++++++ internal/testing/testprovider/resource.go | 66 ++++++++++++++----- list/list_resource.go | 2 +- list/list_resource_test.go | 2 +- list/schema.go | 24 +++++++ list/schema/schema.go | 4 ++ 6 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 internal/testing/testprovider/list_resource.go create mode 100644 list/schema.go create mode 100644 list/schema/schema.go 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/resource.go b/internal/testing/testprovider/resource.go index 2b59e43f9..d2f0ef63c 100644 --- a/internal/testing/testprovider/resource.go +++ b/internal/testing/testprovider/resource.go @@ -6,22 +6,24 @@ package testprovider import ( "context" - "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/resource" ) -var _ list.ListResource = &ListResource{} +var _ resource.Resource = &Resource{} -// Declarative list.ListResource for unit testing. -type ListResource struct { - // ListResource interface methods - MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) - ListResourceConfigSchemaMethod func(context.Context, resource.SchemaRequest, *resource.SchemaResponse) - ListResourceMethod func(context.Context, list.ListResourceRequest, *list.ListResourceResponse) +// Declarative resource.Resource for unit testing. +type Resource struct { + // Resource interface methods + MetadataMethod func(context.Context, resource.MetadataRequest, *resource.MetadataResponse) + SchemaMethod func(context.Context, resource.SchemaRequest, *resource.SchemaResponse) + CreateMethod func(context.Context, resource.CreateRequest, *resource.CreateResponse) + DeleteMethod func(context.Context, resource.DeleteRequest, *resource.DeleteResponse) + ReadMethod func(context.Context, resource.ReadRequest, *resource.ReadResponse) + UpdateMethod func(context.Context, resource.UpdateRequest, *resource.UpdateResponse) } -// Metadata satisfies the list.ListResource interface. -func (r *ListResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +// Metadata satisfies the resource.Resource interface. +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { if r.MetadataMethod == nil { return } @@ -29,19 +31,47 @@ func (r *ListResource) Metadata(ctx context.Context, req resource.MetadataReques r.MetadataMethod(ctx, req, resp) } -// ListResourceConfigSchema satisfies the list.ListResource interface. -func (r *ListResource) ListResourceConfigSchema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - if r.ListResourceConfigSchemaMethod == nil { +// Schema satisfies the resource.Resource interface. +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + if r.SchemaMethod == nil { return } - r.ListResourceConfigSchemaMethod(ctx, req, resp) + r.SchemaMethod(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 { +// Create satisfies the resource.Resource interface. +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + if r.CreateMethod == nil { return } - r.ListResourceMethod(ctx, req, resp) + + r.CreateMethod(ctx, req, resp) +} + +// Delete satisfies the resource.Resource interface. +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + if r.DeleteMethod == nil { + return + } + + r.DeleteMethod(ctx, req, resp) +} + +// Read satisfies the resource.Resource interface. +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if r.ReadMethod == nil { + return + } + + r.ReadMethod(ctx, req, resp) +} + +// Update satisfies the resource.Resource interface. +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + if r.UpdateMethod == nil { + return + } + + r.UpdateMethod(ctx, req, resp) } diff --git a/list/list_resource.go b/list/list_resource.go index 9c94ed125..5f7821ae6 100644 --- a/list/list_resource.go +++ b/list/list_resource.go @@ -26,7 +26,7 @@ type ListResource interface { Metadata(context.Context, resource.MetadataRequest, *resource.MetadataResponse) // ListConfigSchema should return the schema for list blocks. - ListResourceConfigSchema(context.Context, resource.SchemaRequest, *resource.SchemaResponse) // TODO: list.Schema{Request,Response} + 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. diff --git a/list/list_resource_test.go b/list/list_resource_test.go index c4201a3e4..9e0435174 100644 --- a/list/list_resource_test.go +++ b/list/list_resource_test.go @@ -24,7 +24,7 @@ type ComputeInstanceWithListResourceConfigValidators struct { func (c *ComputeInstance) Configure(_ context.Context, _ resource.ConfigureRequest, _ *resource.ConfigureResponse) { } -func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ resource.SchemaRequest, _ *resource.SchemaResponse) { +func (c *ComputeInstance) ListResourceConfigSchema(_ context.Context, _ list.ListResourceSchemaRequest, _ *list.ListResourceSchemaResponse) { } func (c *ComputeInstance) ListResource(_ context.Context, _ list.ListResourceRequest, _ *list.ListResourceResponse) { 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 { +}