From 7a0dfb34c0d4600000af667eff52c0622be1cdf5 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Fri, 30 May 2025 17:00:19 -0400 Subject: [PATCH 1/4] fwserver: add list resources to GetMetadata --- internal/fwserver/server.go | 12 ++ internal/fwserver/server_getmetadata.go | 19 +- internal/fwserver/server_getmetadata_test.go | 196 +++++++++++++++++- internal/fwserver/server_listresources.go | 100 +++++++++ internal/logging/keys.go | 3 + .../testing/testprovider/list_resource.go | 47 +++++ internal/testing/testprovider/provider.go | 13 +- provider/provider.go | 12 ++ 8 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 internal/fwserver/server_listresources.go create mode 100644 internal/testing/testprovider/list_resource.go diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b4b8a779b..e3cb1aefb 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" ) @@ -113,6 +114,17 @@ type Server struct { // access from race conditions. functionFuncsMutex sync.Mutex + // listResourceFuncs is a map of known ListResource factory functions. + listResourceFuncs map[string]func() list.ListResource + + // listResourceFuncsDiags are the cached Diagnostics obtained while + // populating listResourceFuncs. + listResourceFuncsDiags diag.Diagnostics + + // listResourceFuncsMutex is a mutex to protect concurrent listResourceFuncs + // access from race conditions. + listResourceFuncsMutex sync.Mutex + // providerSchema is the cached Provider Schema for RPCs that need to // convert configuration data from the protocol. If not found, it will be // fetched from the Provider.GetSchema() method. diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 458694f2b..198a7f50c 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,39 @@ type ResourceMetadata struct { TypeName string } +// ListResourceMetadata is the framework server equivalent of the +// tfprotov5.ListResourceMetadata and tfprotov6.ListResourceMetadata types. +type ListResourceMetadata struct { + // TypeName is the name of the list resource. + TypeName string +} + // GetMetadata implements the framework server GetMetadata RPC. func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp *GetMetadataResponse) { resp.DataSources = []DataSourceMetadata{} resp.EphemeralResources = []EphemeralResourceMetadata{} resp.Functions = []FunctionMetadata{} + resp.ListResources = []ListResourceMetadata{} resp.Resources = []ResourceMetadata{} + resp.ServerCapabilities = s.ServerCapabilities() datasourceMetadatas, diags := s.DataSourceMetadatas(ctx) - resp.Diagnostics.Append(diags...) ephemeralResourceMetadatas, diags := s.EphemeralResourceMetadatas(ctx) - resp.Diagnostics.Append(diags...) functionMetadatas, diags := s.FunctionMetadatas(ctx) - resp.Diagnostics.Append(diags...) resourceMetadatas, diags := s.ResourceMetadatas(ctx) + resp.Diagnostics.Append(diags...) + // Metadata for list resources must be retrieved after metadata for managed + // resources. Server.ListResourceMetadatas checks that each list resource + // type nane matches a known managed Resource type name. + listResourceMetadatas, diags := s.ListResourceMetadatas(ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -83,5 +95,6 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.DataSources = datasourceMetadatas resp.EphemeralResources = ephemeralResourceMetadatas resp.Functions = functionMetadatas + resp.ListResources = listResourceMetadatas resp.Resources = resourceMetadatas } diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go index 8729f6dbd..dff3b7378 100644 --- a/internal/fwserver/server_getmetadata_test.go +++ b/internal/fwserver/server_getmetadata_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -16,6 +17,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" ) @@ -485,6 +487,190 @@ func TestServerGetMetadata(t *testing.T) { }, }, }, + "listresources": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource_1" + }, + } + }, + } + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource_1" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{}, + Functions: []fwserver.FunctionMetadata{}, + ListResources: []fwserver.ListResourceMetadata{ + {TypeName: "test_resource_1"}, + }, + Resources: []fwserver.ResourceMetadata{ + {TypeName: "test_resource_1"}, + }, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "list-resources-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "ListResource Type Name Missing", + "The *testprovider.ListResource ListResource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "list-resources-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate ListResource Type Defined", + "The test_resource ListResource type name was returned for multiple list resources. "+ + "ListResource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "list-resources-no-matching-managed-resource-type": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource_1" + }, + } + }, + } + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource_2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + EphemeralResources: []fwserver.EphemeralResourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "ListResource Type Defined without a Matching Managed Resource Type", + "The test_resource_1 ListResource type name was returned, but no matching managed Resource type was defined. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Functions: []fwserver.FunctionMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, "resources": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -666,11 +852,19 @@ func TestServerGetMetadata(t *testing.T) { return response.Functions[i].Name < response.Functions[j].Name }) + sort.Slice(response.ListResources, func(i int, j int) bool { + return response.ListResources[i].TypeName < response.ListResources[j].TypeName + }) + sort.Slice(response.Resources, func(i int, j int) bool { return response.Resources[i].TypeName < response.Resources[j].TypeName }) - if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + opts := cmp.Options{ + cmpopts.EquateEmpty(), + } + + if diff := cmp.Diff(response, testCase.expectedResponse, opts...); diff != "" { t.Errorf("unexpected difference: %s", diff) } }) diff --git a/internal/fwserver/server_listresources.go b/internal/fwserver/server_listresources.go new file mode 100644 index 000000000..d7deb5c7d --- /dev/null +++ b/internal/fwserver/server_listresources.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +// 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.listResourceFuncsMutex.Lock() + defer s.listResourceFuncsMutex.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.listResourceFuncsDiags.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.listResourceFuncsDiags.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.listResourceFuncsDiags.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.listResourceFuncsDiags +} + +// 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/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..857ea0d97 --- /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) + ListMethod func(context.Context, list.ListRequest, *list.ListResultsStream) +} + +// 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) List(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { + if r.ListMethod == nil { + return + } + r.ListMethod(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/provider/provider.go b/provider/provider.go index 61dfd39fe..a7dc583f6 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 ListResource implementing + // the Metadata method. All ListResources 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 7691bb893099b707e73f6dbf67038838de7d3f37 Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Mon, 2 Jun 2025 12:27:12 -0400 Subject: [PATCH 2/4] fwserver: add list resources to GetProviderSchema --- internal/fwserver/server_getproviderschema.go | 26 +- .../fwserver/server_getproviderschema_test.go | 168 +++- internal/fwserver/server_listresources.go | 39 +- .../server_getproviderschema_test.go | 30 + .../server_getproviderschema_test.go | 30 + list/schema/attribute.go | 20 + list/schema/block.go | 27 + list/schema/doc.go | 7 + list/schema/schema.go | 190 ++++ list/schema/schema_test.go | 905 ++++++++++++++++++ list/schema/string_attribute.go | 190 ++++ list/schema/string_attribute_test.go | 634 ++++++++++++ 12 files changed, 2244 insertions(+), 22 deletions(-) create mode 100644 list/schema/attribute.go create mode 100644 list/schema/block.go create mode 100644 list/schema/doc.go create mode 100644 list/schema/schema_test.go create mode 100644 list/schema/string_attribute.go create mode 100644 list/schema/string_attribute_test.go diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index b8061dd10..1b6b75a62 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -25,6 +25,7 @@ type GetProviderSchemaResponse struct { DataSourceSchemas map[string]fwschema.Schema EphemeralResourceSchemas map[string]fwschema.Schema FunctionDefinitions map[string]function.Definition + ListResourceSchemas map[string]fwschema.Schema Diagnostics diag.Diagnostics } @@ -33,62 +34,51 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe resp.ServerCapabilities = s.ServerCapabilities() providerSchema, diags := s.ProviderSchema(ctx) - resp.Diagnostics.Append(diags...) - if diags.HasError() { return } - resp.Provider = providerSchema providerMetaSchema, diags := s.ProviderMetaSchema(ctx) - resp.Diagnostics.Append(diags...) - if diags.HasError() { return } - resp.ProviderMeta = providerMetaSchema resourceSchemas, diags := s.ResourceSchemas(ctx) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - resp.ResourceSchemas = resourceSchemas dataSourceSchemas, diags := s.DataSourceSchemas(ctx) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - resp.DataSourceSchemas = dataSourceSchemas functions, diags := s.FunctionDefinitions(ctx) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - resp.FunctionDefinitions = functions ephemeralResourceSchemas, diags := s.EphemeralResourceSchemas(ctx) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - resp.EphemeralResourceSchemas = ephemeralResourceSchemas + + listResourceSchemas, diags := s.ListResourceSchemas(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + resp.ListResourceSchemas = listResourceSchemas } diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index e5e17bc89..6caecd976 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -18,6 +18,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/list" + listschema "github.com/hashicorp/terraform-plugin-framework/list/schema" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" providerschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -41,6 +43,7 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -111,6 +114,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -318,6 +322,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -388,6 +393,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -601,6 +607,7 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ @@ -655,8 +662,9 @@ func TestServerGetProviderSchema(t *testing.T) { Return: function.StringReturn{}, }, }, - Provider: providerschema.Schema{}, - ResourceSchemas: map[string]fwschema.Schema{}, + ListResourceSchemas: map[string]fwschema.Schema{}, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ GetProviderSchemaOptional: true, MoveResourceState: true, @@ -808,6 +816,158 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, }, + "listresource-schemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = listschema.Schema{ + Attributes: map[string]listschema.Attribute{ + "test1": listschema.StringAttribute{ + Required: true, + }, + }, + } + }, + 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{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{ + "test_resource": listschema.Schema{ + Attributes: map[string]listschema.Attribute{ + "test1": listschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + Provider: providerschema.Schema{}, + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "test1": resourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + }, + }, + "listresource-schemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ListResourcesMethod: func(_ context.Context) []func() list.ListResource { + return []func() list.ListResource{ + func() list.ListResource { + return &testprovider.ListResource{ + ListResourceConfigSchemaMethod: func(_ context.Context, _ list.ListResourceSchemaRequest, resp *list.ListResourceSchemaResponse) { + resp.Schema = listschema.Schema{ + Attributes: map[string]listschema.Attribute{ + "$filter": listschema.StringAttribute{ + Required: true, + }, + }, + } + }, + 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{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "name": resourceschema.StringAttribute{ + Required: true, + }, + }, + } + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetProviderSchemaRequest{}, + expectedResponse: &fwserver.GetProviderSchemaResponse{ + Provider: providerschema.Schema{}, + DataSourceSchemas: map[string]fwschema.Schema{}, + EphemeralResourceSchemas: map[string]fwschema.Schema{}, + FunctionDefinitions: map[string]function.Definition{}, + ResourceSchemas: map[string]fwschema.Schema{ + "test_resource": resourceschema.Schema{ + Attributes: map[string]resourceschema.Attribute{ + "name": resourceschema.StringAttribute{ + Required: true, + }, + }, + }, + }, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + MoveResourceState: true, + PlanDestroy: true, + }, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$filter\" at schema path \"$filter\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + }, "provider": { server: &fwserver.Server{ Provider: &testprovider.Provider{ @@ -827,6 +987,7 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{ Attributes: map[string]providerschema.Attribute{ "test": providerschema.StringAttribute{ @@ -894,6 +1055,7 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ProviderMeta: metaschema.Schema{ Attributes: map[string]metaschema.Attribute{ @@ -990,6 +1152,7 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "test_resource1": resourceschema.Schema{ @@ -1203,6 +1366,7 @@ func TestServerGetProviderSchema(t *testing.T) { DataSourceSchemas: map[string]fwschema.Schema{}, EphemeralResourceSchemas: map[string]fwschema.Schema{}, FunctionDefinitions: map[string]function.Definition{}, + ListResourceSchemas: map[string]fwschema.Schema{}, Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{ "testprovidertype_resource": resourceschema.Schema{ diff --git a/internal/fwserver/server_listresources.go b/internal/fwserver/server_listresources.go index d7deb5c7d..569e43bfc 100644 --- a/internal/fwserver/server_listresources.go +++ b/internal/fwserver/server_listresources.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework/diag" + "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" @@ -22,12 +23,12 @@ func (s *Server) ListResourceFuncs(ctx context.Context) (map[string]func() list. return nil, nil } - logging.FrameworkTrace(ctx, "Checking ListResourceTypes lock") + logging.FrameworkTrace(ctx, "Checking ListResourceFuncs lock") s.listResourceFuncsMutex.Lock() defer s.listResourceFuncsMutex.Unlock() if s.listResourceFuncs != nil { - return s.listResourceFuncs, s.resourceTypesDiags + return s.listResourceFuncs, s.listResourceFuncsDiags } providerTypeName := s.ProviderTypeName(ctx) @@ -98,3 +99,37 @@ func (s *Server) ListResourceMetadatas(ctx context.Context) ([]ListResourceMetad return resourceMetadatas, diags } + +// ListResourceSchemas returns a map of ListResource Schemas for the +// GetProviderSchema RPC without caching since not all schemas are guaranteed to +// be necessary for later provider operations. The schema implementations are +// also validated. +func (s *Server) ListResourceSchemas(ctx context.Context) (map[string]fwschema.Schema, diag.Diagnostics) { + listResourceSchemas := make(map[string]fwschema.Schema) + listResourceFuncs, diags := s.ListResourceFuncs(ctx) + + for typeName, listResourceFunc := range listResourceFuncs { + listResource := listResourceFunc() + schemaReq := list.ListResourceSchemaRequest{} + schemaResp := list.ListResourceSchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined ListResource Schemas", map[string]interface{}{logging.KeyListResourceType: typeName}) + listResource.ListResourceConfigSchema(ctx, schemaReq, &schemaResp) + logging.FrameworkTrace(ctx, "Called provider defined ListResource Schemas", map[string]interface{}{logging.KeyListResourceType: typeName}) + + diags.Append(schemaResp.Diagnostics...) + if schemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := schemaResp.Schema.ValidateImplementation(ctx) + diags.Append(validateDiags...) + if validateDiags.HasError() { + continue + } + + listResourceSchemas[typeName] = schemaResp.Schema + } + + return listResourceSchemas, diags +} diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 590d87a62..884084b42 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -1018,6 +1018,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Called provider defined Provider EphemeralResources", "@module": "sdk.framework", }, + { + "@level": string("trace"), + "@message": string("Checking ListResourceFuncs lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Checking ProviderTypeName lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined ListResources"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined ListResources"), + "@module": string("sdk.framework"), + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index 0b60159de..7501e7434 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -1018,6 +1018,36 @@ func TestServerGetProviderSchema_logging(t *testing.T) { "@message": "Called provider defined Provider EphemeralResources", "@module": "sdk.framework", }, + { + "@level": string("trace"), + "@message": string("Checking ListResourceFuncs lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Checking ProviderTypeName lock"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined Provider Metadata"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Calling provider defined ListResources"), + "@module": string("sdk.framework"), + }, + { + "@level": string("trace"), + "@message": string("Called provider defined ListResources"), + "@module": string("sdk.framework"), + }, } if diff := cmp.Diff(entries, expectedEntries); diff != "" { diff --git a/list/schema/attribute.go b/list/schema/attribute.go new file mode 100644 index 000000000..00a674338 --- /dev/null +++ b/list/schema/attribute.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute define a value field inside the Schema. Implementations in this +// package include: +// - StringAttribute +// +// In practitioner configurations, an equals sign (=) is required to set +// the value. [Configuration Reference] +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Attribute interface { + fwschema.Attribute +} diff --git a/list/schema/block.go b/list/schema/block.go new file mode 100644 index 000000000..2a4a88ef7 --- /dev/null +++ b/list/schema/block.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Block defines a structural field inside a Schema. Implementations in this +// package include: +// +// In practitioner configurations, an equals sign (=) cannot be used to set the +// value. Blocks are instead repeated as necessary, or require the use of +// [Dynamic Block Expressions]. +// +// Prefer NestedAttribute over Block. Blocks should typically be used for +// configuration compatibility with previously existing schemas from an older +// Terraform Plugin SDK. Efforts should be made to convert from Block to +// NestedAttribute as a breaking change for practitioners. +// +// [Dynamic Block Expressions]: https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks +// +// [Configuration Reference]: https://developer.hashicorp.com/terraform/language/syntax/configuration +type Block interface { + fwschema.Block +} diff --git a/list/schema/doc.go b/list/schema/doc.go new file mode 100644 index 000000000..4c658221e --- /dev/null +++ b/list/schema/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package schema contains all available schema functionality for list +// resources. List resource schemas define the structure of a list block. +// Schemas are implemented via the list.ListResource type Schema method. +package schema diff --git a/list/schema/schema.go b/list/schema/schema.go index f1c142dd6..759557c16 100644 --- a/list/schema/schema.go +++ b/list/schema/schema.go @@ -3,5 +3,195 @@ package schema +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Schema must satify the fwschema.Schema interface. +var _ fwschema.Schema = Schema{} + +// Schema defines the structure and value types of resource data. This type +// is used as the resource.SchemaResponse type Schema field, which is +// implemented by the resource.DataSource type Schema method. type Schema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Blocks names. + Attributes map[string]Attribute + + // Blocks is the mapping of underlying block names to block definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + // Names must not collide with any Attributes names. + Blocks map[string]Block + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this resource is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this resource is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this resource. The warning diagnostic + // summary is automatically set to "Resource Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Use examplecloud_other resource instead. This resource + // will be removed in the next major version of the provider." + // - "Remove this resource as it no longer is valid and + // will be removed in the next major version of the provider." + // + DeprecationMessage string + + // Version indicates the current version of the resource schema. Resource + // schema versioning enables state upgrades in conjunction with the + // [resource.ResourceWithStateUpgrades] interface. Versioning is only + // required if there is a breaking change involving existing state data, + // such as changing an attribute or block type in a manner that is + // incompatible with the Terraform type. + // + // Versions are conventionally only incremented by one each release. + Version int64 +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks returns the Blocks field value. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return schemaBlocks(s.Blocks) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (s Schema) GetDeprecationMessage() string { + return s.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (s Schema) GetDescription() string { + return s.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (s Schema) GetMarkdownDescription() string { + return s.MarkdownDescription +} + +// GetVersion returns the Version field value. +func (s Schema) GetVersion() int64 { + return s.Version +} + +// Type returns the framework type of the schema. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath returns the framework type at the given tftypes path. +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +// +// Deprecated: Use the ValidateImplementation method instead. +func (s Schema) Validate() diag.Diagnostics { + return s.ValidateImplementation(context.Background()) +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the schema and underlying attributes and blocks to prevent +// unexpected errors or panics. This logic runs during the +// ValidateResourceConfig RPC, or via provider-defined unit testing, and should +// never include false positives. +func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + for attributeName, attribute := range s.GetAttributes() { + req := fwschema.ValidateImplementationRequest{ + Name: attributeName, + Path: path.Root(attributeName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...) + } + + for blockName, block := range s.GetBlocks() { + req := fwschema.ValidateImplementationRequest{ + Name: blockName, + Path: path.Root(blockName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateBlockImplementation(ctx, block, req)...) + } + + return diags +} + +// schemaAttributes is a resource to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} + +// schemaBlocks is a resource to fwschema type conversion function. +func schemaBlocks(blocks map[string]Block) map[string]fwschema.Block { + result := make(map[string]fwschema.Block, len(blocks)) + + for name, block := range blocks { + result[name] = block + } + + return result } diff --git a/list/schema/schema_test.go b/list/schema/schema_test.go new file mode 100644 index 000000000..9e7056a2a --- /dev/null +++ b/list/schema/schema_test.go @@ -0,0 +1,905 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: schema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: schema.Schema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.Schema", + ), + }, + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type schema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: schema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "empty-nil": { + schema: schema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "root": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "nil": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type schema.Schema", + }, + "WithAttributeName-attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: schema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "test": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: schema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": schema.StringAttribute{}, + "testattr2": schema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: schema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-deprecation-message": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "deprecation-message": { + schema: schema.Schema{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "description": { + schema: schema.Schema{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected string + }{ + "no-markdown-description": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: "", + }, + "markdown-description": { + schema: schema.Schema{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected int64 + }{ + "no-version": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: 0, + }, + "version": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + Version: 1, + }, + expected: 1, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expected attr.Type + }{ + "base": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "testattr": schema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string": schema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string": schema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: schema.Schema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: schema.Schema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: schema.Schema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: schema.Schema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: schema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string": schema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "string": schema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: schema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "validate-implementation-error": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema schema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: schema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "depends_on": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: schema.Schema{ + Attributes: map[string]schema.Attribute{ + "^": schema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/list/schema/string_attribute.go b/list/schema/string_attribute.go new file mode 100644 index 000000000..d0e1cadac --- /dev/null +++ b/list/schema/string_attribute.go @@ -0,0 +1,190 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema/fwxschema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = StringAttribute{} + _ fwxschema.AttributeWithStringValidators = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +// +// Terraform configurations reference this attribute using the attribute name. +// +// .example_attribute +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.StringType. When retrieving data, the basetypes.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType basetypes.StringTypable + + // Required indicates whether the practitioner must enter a value for + // this attribute or not. Required and Optional cannot both be true. + Required bool + + // Optional indicates whether the practitioner can choose to enter a value + // for this attribute or not. Optional and Required cannot both be true. + Optional bool + + // Description is used in various tooling, like the language server, to + // give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It should be written as + // plain text, with no special formatting. + Description string + + // MarkdownDescription is used in various tooling, like the + // documentation generator, to give practitioners more information + // about what this attribute is, what it's for, and how it should be + // used. It should be formatted using Markdown. + MarkdownDescription string + + // DeprecationMessage defines warning diagnostic details to display when + // practitioner configurations use this Attribute. The warning diagnostic + // summary is automatically set to "Attribute Deprecated" along with + // configuration source file and line information. + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // + // In Terraform 1.2.7 and later, this warning diagnostic is displayed any + // time a practitioner attempts to configure a value for this attribute and + // certain scenarios where this attribute is referenced. + // + // In Terraform 1.2.6 and earlier, this warning diagnostic is only + // displayed when the Attribute is Required or Optional, and if the + // practitioner configuration sets the value to a known or unknown value + // (which may eventually be null). It has no effect when the Attribute is + // Computed-only (read-only; not Required or Optional). + // + // Across any Terraform version, there are no warnings raised for + // practitioner configuration values set directly to null, as there is no + // way for the framework to differentiate between an unset and null + // configuration due to how Terraform sends configuration information + // across the protocol. + // + // Additional information about deprecation enhancements for read-only + // attributes can be found in: + // + // - https://github.com/hashicorp/terraform/issues/7569 + // + DeprecationMessage string + + // Validators define value validation functionality for the attribute. All + // elements of the slice of AttributeValidator are run, regardless of any + // previous error diagnostics. + // + // Many common use case validators can be found in the + // github.com/hashicorp/terraform-plugin-framework-validators Go module. + // + // If the Type field points to a custom type that implements the + // xattr.TypeWithValidate interface, the validators defined in this field + // are run in addition to the validation defined by the type. + Validators []validator.String +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns the DeprecationMessage field value. +func (a StringAttribute) GetDeprecationMessage() string { + return a.DeprecationMessage +} + +// GetDescription returns the Description field value. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the MarkdownDescription field value. +func (a StringAttribute) GetMarkdownDescription() string { + return a.MarkdownDescription +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed returns false because it does not apply to ListResource schemas. +func (a StringAttribute) IsComputed() bool { + return false +} + +// IsOptional returns the Optional field value. +func (a StringAttribute) IsOptional() bool { + return a.Optional +} + +// IsRequired returns the Required field value. +func (a StringAttribute) IsRequired() bool { + return a.Required +} + +// IsSensitive returns false because it does not apply to ListResource schemas. +func (a StringAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false because it does not apply to ListResource schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only relevant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + +// StringValidators returns the Validators field value. +func (a StringAttribute) StringValidators() []validator.String { + return a.Validators +} diff --git a/list/schema/string_attribute_test.go b/list/schema/string_attribute_test.go new file mode 100644 index 000000000..c1dffa2f2 --- /dev/null +++ b/list/schema/string_attribute_test.go @@ -0,0 +1,634 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package schema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: schema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: schema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.StringType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "deprecation-message": { + attribute: schema.StringAttribute{ + DeprecationMessage: "test deprecation message", + }, + expected: "test deprecation message", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: schema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: schema.StringAttribute{}, + other: schema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: schema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: schema.StringAttribute{}, + expected: "", + }, + "markdown-description": { + attribute: schema.StringAttribute{ + MarkdownDescription: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: schema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: schema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "computed": { + attribute: schema.StringAttribute{ + Computed: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "optional": { + attribute: schema.StringAttribute{ + Optional: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-required": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "required": { + attribute: schema.StringAttribute{ + Required: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "sensitive": { + attribute: schema.StringAttribute{ + Sensitive: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: schema.StringAttribute{}, + expected: false, + }, + "writeOnly": { + attribute: schema.StringAttribute{ + WriteOnly: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeStringDefaultValue(t *testing.T) { + t.Parallel() + + opt := cmp.Comparer(func(x, y defaults.String) bool { + ctx := context.Background() + req := defaults.StringRequest{} + + xResp := defaults.StringResponse{} + x.DefaultString(ctx, req, &xResp) + + yResp := defaults.StringResponse{} + y.DefaultString(ctx, req, &yResp) + + return xResp.PlanValue.Equal(yResp.PlanValue) + }) + + testCases := map[string]struct { + attribute schema.StringAttribute + expected defaults.String + }{ + "no-default": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "default": { + attribute: schema.StringAttribute{ + Default: stringdefault.StaticString("test-value"), + }, + expected: stringdefault.StaticString("test-value"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringDefaultValue() + + if diff := cmp.Diff(got, testCase.expected, opt); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeStringPlanModifiers(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []planmodifier.String + }{ + "no-planmodifiers": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "planmodifiers": { + attribute: schema.StringAttribute{ + PlanModifiers: []planmodifier.String{}, + }, + expected: []planmodifier.String{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringPlanModifiers() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeStringValidators(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected []validator.String + }{ + "no-validators": { + attribute: schema.StringAttribute{}, + expected: nil, + }, + "validators": { + attribute: schema.StringAttribute{ + Validators: []validator.String{}, + }, + expected: []validator.String{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.StringValidators() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "computed": { + attribute: schema.StringAttribute{ + Computed: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + "default-without-computed": { + attribute: schema.StringAttribute{ + Default: stringdefault.StaticString("test"), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Schema Using Attribute Default For Non-Computed Attribute", + "Attribute \"test\" must be computed when using default. "+ + "This is an issue with the provider and should be reported to the provider developers.", + ), + }, + }, + }, + "default-with-computed": { + attribute: schema.StringAttribute{ + Computed: true, + Default: stringdefault.StaticString("test"), + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From c4204cec3686b525306df8eaaabfafe0cc2907da Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 5 Jun 2025 09:03:55 -0400 Subject: [PATCH 3/4] tidy: remove list.Schema.Version --- internal/fwserver/server_listresources.go | 2 +- list/schema/schema.go | 14 ++------------ list/schema/schema_test.go | 9 --------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/internal/fwserver/server_listresources.go b/internal/fwserver/server_listresources.go index f5a46014e..569e43bfc 100644 --- a/internal/fwserver/server_listresources.go +++ b/internal/fwserver/server_listresources.go @@ -132,4 +132,4 @@ func (s *Server) ListResourceSchemas(ctx context.Context) (map[string]fwschema.S } return listResourceSchemas, diags -} \ No newline at end of file +} diff --git a/list/schema/schema.go b/list/schema/schema.go index 759557c16..cdf07d662 100644 --- a/list/schema/schema.go +++ b/list/schema/schema.go @@ -59,16 +59,6 @@ type Schema struct { // will be removed in the next major version of the provider." // DeprecationMessage string - - // Version indicates the current version of the resource schema. Resource - // schema versioning enables state upgrades in conjunction with the - // [resource.ResourceWithStateUpgrades] interface. Versioning is only - // required if there is a breaking change involving existing state data, - // such as changing an attribute or block type in a manner that is - // incompatible with the Terraform type. - // - // Versions are conventionally only incremented by one each release. - Version int64 } // ApplyTerraform5AttributePathStep applies the given AttributePathStep to the @@ -116,9 +106,9 @@ func (s Schema) GetMarkdownDescription() string { return s.MarkdownDescription } -// GetVersion returns the Version field value. +// GetVersion returns zero because list resource schemas do not have a version. func (s Schema) GetVersion() int64 { - return s.Version + return 0 } // Type returns the framework type of the schema. diff --git a/list/schema/schema_test.go b/list/schema/schema_test.go index 9e7056a2a..14fb2fff0 100644 --- a/list/schema/schema_test.go +++ b/list/schema/schema_test.go @@ -538,15 +538,6 @@ func TestSchemaGetVersion(t *testing.T) { }, expected: 0, }, - "version": { - schema: schema.Schema{ - Attributes: map[string]schema.Attribute{ - "testattr": schema.StringAttribute{}, - }, - Version: 1, - }, - expected: 1, - }, } for name, testCase := range testCases { From ee50ee753cd3952e859fd1ed8ac8173607d8d99c Mon Sep 17 00:00:00 2001 From: Baraa Basata Date: Thu, 5 Jun 2025 09:10:55 -0400 Subject: [PATCH 4/4] tidy: consistent comments --- internal/fwserver/server_getmetadata.go | 4 ++-- internal/fwserver/server_getproviderschema.go | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go index 198a7f50c..33b675865 100644 --- a/internal/fwserver/server_getmetadata.go +++ b/internal/fwserver/server_getmetadata.go @@ -83,8 +83,8 @@ func (s *Server) GetMetadata(ctx context.Context, req *GetMetadataRequest, resp resp.Diagnostics.Append(diags...) // Metadata for list resources must be retrieved after metadata for managed - // resources. Server.ListResourceMetadatas checks that each list resource - // type nane matches a known managed Resource type name. + // resources. Server.ListResourceFuncs checks that each list resource type + // name matches a known managed resource type name. listResourceMetadatas, diags := s.ListResourceMetadatas(ctx) resp.Diagnostics.Append(diags...) diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index 1b6b75a62..c695a7530 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -75,6 +75,9 @@ func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRe } resp.EphemeralResourceSchemas = ephemeralResourceSchemas + // Schemas for list resources must be retrieved after schemas for managed + // resources. Server.ListResourceFuncs checks that each list resource type + // name matches a known managed resource type name. listResourceSchemas, diags := s.ListResourceSchemas(ctx) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() {