diff --git a/.changes/unreleased/FEATURES-20230825-115848.yaml b/.changes/unreleased/FEATURES-20230825-115848.yaml new file mode 100644 index 000000000..33a52084c --- /dev/null +++ b/.changes/unreleased/FEATURES-20230825-115848.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'providerserver: Upgrade to protocol versions 5.4 and 6.4, which can + significantly reduce memory usage with Terraform 1.6 and later when a + configuration includes multiple instances of the same provider' +time: 2023-08-25T11:58:48.820178-04:00 +custom: + Issue: "828" diff --git a/go.mod b/go.mod index 6a46cfc39..9f02b5956 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/google/go-cmp v0.5.9 - github.com/hashicorp/terraform-plugin-go v0.18.0 + github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -12,9 +12,9 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.2 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect github.com/mattn/go-colorable v0.1.12 // indirect @@ -23,10 +23,10 @@ require ( github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/net v0.11.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.1 // indirect + golang.org/x/net v0.13.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/go.sum b/go.sum index 6baecccf9..571b1dd6b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,21 +12,21 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= +github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.18.0 h1:IwTkOS9cOW1ehLd/rG0y+u/TGLK9y6fGoBjXVUquzpE= -github.com/hashicorp/terraform-plugin-go v0.18.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= +github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= +github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -46,22 +47,22 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ= -google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/internal/fromproto5/getmetadata.go b/internal/fromproto5/getmetadata.go new file mode 100644 index 000000000..c80ad2b82 --- /dev/null +++ b/internal/fromproto5/getmetadata.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetMetadataRequest returns the *fwserver.GetMetadataRequest +// equivalent of a *tfprotov5.GetMetadataRequest. +func GetMetadataRequest(ctx context.Context, proto5 *tfprotov5.GetMetadataRequest) *fwserver.GetMetadataRequest { + if proto5 == nil { + return nil + } + + fw := &fwserver.GetMetadataRequest{} + + return fw +} diff --git a/internal/fromproto5/getmetadata_test.go b/internal/fromproto5/getmetadata_test.go new file mode 100644 index 000000000..e92a5acef --- /dev/null +++ b/internal/fromproto5/getmetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestGetMetadataRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.GetMetadataRequest + expected *fwserver.GetMetadataRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.GetMetadataRequest{}, + expected: &fwserver.GetMetadataRequest{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto5.GetMetadataRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fromproto6/getmetadata.go b/internal/fromproto6/getmetadata.go new file mode 100644 index 000000000..a9eedc568 --- /dev/null +++ b/internal/fromproto6/getmetadata.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetMetadataRequest returns the *fwserver.GetMetadataRequest +// equivalent of a *tfprotov6.GetMetadataRequest. +func GetMetadataRequest(ctx context.Context, proto6 *tfprotov6.GetMetadataRequest) *fwserver.GetMetadataRequest { + if proto6 == nil { + return nil + } + + fw := &fwserver.GetMetadataRequest{} + + return fw +} diff --git a/internal/fromproto6/getmetadata_test.go b/internal/fromproto6/getmetadata_test.go new file mode 100644 index 000000000..20944d614 --- /dev/null +++ b/internal/fromproto6/getmetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestGetMetadataRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.GetMetadataRequest + expected *fwserver.GetMetadataRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.GetMetadataRequest{}, + expected: &fwserver.GetMetadataRequest{}, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto6.GetMetadataRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 8a623a800..31e75bd6b 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -183,6 +183,22 @@ func (s *Server) DataSourceFuncs(ctx context.Context) (map[string]func() datasou return s.dataSourceFuncs, s.dataSourceTypesDiags } +// DataSourceMetadatas returns a slice of DataSourceMetadata for the GetMetadata +// RPC. +func (s *Server) DataSourceMetadatas(ctx context.Context) ([]DataSourceMetadata, diag.Diagnostics) { + datasourceFuncs, diags := s.DataSourceFuncs(ctx) + + datasourceMetadatas := make([]DataSourceMetadata, 0, len(datasourceFuncs)) + + for typeName := range datasourceFuncs { + datasourceMetadatas = append(datasourceMetadatas, DataSourceMetadata{ + TypeName: typeName, + }) + } + + return datasourceMetadatas, diags +} + // DataSourceSchema returns the DataSource Schema for the given type name and // caches the result for later DataSource operations. func (s *Server) DataSourceSchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { @@ -401,6 +417,22 @@ func (s *Server) ResourceFuncs(ctx context.Context) (map[string]func() resource. return s.resourceFuncs, s.resourceTypesDiags } +// ResourceMetadatas returns a slice of ResourceMetadata for the GetMetadata +// RPC. +func (s *Server) ResourceMetadatas(ctx context.Context) ([]ResourceMetadata, diag.Diagnostics) { + resourceFuncs, diags := s.ResourceFuncs(ctx) + + resourceMetadatas := make([]ResourceMetadata, 0, len(resourceFuncs)) + + for typeName := range resourceFuncs { + resourceMetadatas = append(resourceMetadatas, ResourceMetadata{ + TypeName: typeName, + }) + } + + return resourceMetadatas, diags +} + // ResourceSchema returns the Resource Schema for the given type name and // caches the result for later Resource operations. func (s *Server) ResourceSchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { diff --git a/internal/fwserver/server_capabilities.go b/internal/fwserver/server_capabilities.go index 4b8fd607d..23b46f590 100644 --- a/internal/fwserver/server_capabilities.go +++ b/internal/fwserver/server_capabilities.go @@ -8,6 +8,13 @@ package fwserver // the toproto5 conversion logic will handle the appropriate filtering and the // proto5server/fwserver logic will need to account for missing features. type ServerCapabilities struct { + // GetProviderSchemaOptional signals that the provider does not require the + // GetProviderSchema RPC before other RPCs. + // + // This should always be enabled in framework providers and requires + // Terraform 1.6 or later. + GetProviderSchemaOptional bool + // PlanDestroy signals that the provider is ready for the // PlanResourceChange RPC on resource destruction. // @@ -15,3 +22,11 @@ type ServerCapabilities struct { // Terraform 1.3 or later. PlanDestroy bool } + +// ServerCapabilities returns the server capabilities. +func (s *Server) ServerCapabilities() *ServerCapabilities { + return &ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + } +} diff --git a/internal/fwserver/server_getmetadata.go b/internal/fwserver/server_getmetadata.go new file mode 100644 index 000000000..a4abd895c --- /dev/null +++ b/internal/fwserver/server_getmetadata.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/provider" +) + +// GetMetadataRequest is the framework server request for the +// GetMetadata RPC. +type GetMetadataRequest struct{} + +// GetMetadataResponse is the framework server response for the +// GetMetadata RPC. +type GetMetadataResponse struct { + DataSources []DataSourceMetadata + Diagnostics diag.Diagnostics + Resources []ResourceMetadata + ServerCapabilities *ServerCapabilities +} + +// DataSourceMetadata is the framework server equivalent of the +// tfprotov5.DataSourceMetadata and tfprotov6.DataSourceMetadata types. +type DataSourceMetadata struct { + // TypeName is the name of the data resource. + TypeName string +} + +// ResourceMetadata is the framework server equivalent of the +// tfprotov5.ResourceMetadata and tfprotov6.ResourceMetadata types. +type ResourceMetadata struct { + // TypeName is the name of the managed 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.Resources = []ResourceMetadata{} + resp.ServerCapabilities = s.ServerCapabilities() + + metadataReq := provider.MetadataRequest{} + metadataResp := provider.MetadataResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Provider Metadata") + s.Provider.Metadata(ctx, metadataReq, &metadataResp) + logging.FrameworkTrace(ctx, "Called provider defined Provider Metadata") + + s.providerTypeName = metadataResp.TypeName + + datasourceMetadatas, diags := s.DataSourceMetadatas(ctx) + + resp.Diagnostics.Append(diags...) + + resourceMetadatas, diags := s.ResourceMetadatas(ctx) + + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.DataSources = datasourceMetadatas + resp.Resources = resourceMetadatas +} diff --git a/internal/fwserver/server_getmetadata_test.go b/internal/fwserver/server_getmetadata_test.go new file mode 100644 index 000000000..7ba504723 --- /dev/null +++ b/internal/fwserver/server_getmetadata_test.go @@ -0,0 +1,361 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +func TestServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.GetMetadataRequest + expectedResponse *fwserver.GetMetadataResponse + }{ + "empty-provider": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source1" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{ + { + TypeName: "test_data_source1", + }, + { + TypeName: "test_data_source2", + }, + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Data Source Type Defined", + "The test_data_source data source type name was returned for multiple data sources. "+ + "Data source type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Data Source Type Name Missing", + "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_data_source" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{ + { + TypeName: "testprovidertype_data_source", + }, + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + 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_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Resources: []fwserver.ResourceMetadata{ + { + TypeName: "test_resource1", + }, + { + TypeName: "test_resource2", + }, + }, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-duplicate-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + 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" + }, + } + }, + 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{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Resource Type Defined", + "The test_resource resource type name was returned for multiple resources. "+ + "Resource type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-empty-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + 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 = "" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Resource Type Name Missing", + "The *testprovider.Resource Resource returned an empty string from the Metadata method. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + Resources: []fwserver.ResourceMetadata{}, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-provider-type-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + MetadataMethod: func(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "testprovidertype" + }, + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_resource" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetMetadataRequest{}, + expectedResponse: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{}, + Resources: []fwserver.ResourceMetadata{ + { + TypeName: "testprovidertype_resource", + }, + }, + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.GetMetadataResponse{} + testCase.server.GetMetadata(context.Background(), testCase.request, response) + + // Prevent false positives with random map access in testing + sort.Slice(response.DataSources, func(i int, j int) bool { + return response.DataSources[i].TypeName < response.DataSources[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 != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server_getproviderschema.go b/internal/fwserver/server_getproviderschema.go index db12bd32d..e4e7d645f 100644 --- a/internal/fwserver/server_getproviderschema.go +++ b/internal/fwserver/server_getproviderschema.go @@ -29,9 +29,7 @@ type GetProviderSchemaResponse struct { // GetProviderSchema implements the framework server GetProviderSchema RPC. func (s *Server) GetProviderSchema(ctx context.Context, req *GetProviderSchemaRequest, resp *GetProviderSchemaResponse) { - resp.ServerCapabilities = &ServerCapabilities{ - PlanDestroy: true, - } + resp.ServerCapabilities = s.ServerCapabilities() metadataReq := provider.MetadataRequest{} metadataResp := provider.MetadataResponse{} diff --git a/internal/fwserver/server_getproviderschema_test.go b/internal/fwserver/server_getproviderschema_test.go index c043c2ca4..b956d47c4 100644 --- a/internal/fwserver/server_getproviderschema_test.go +++ b/internal/fwserver/server_getproviderschema_test.go @@ -39,7 +39,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -105,7 +106,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -155,7 +157,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( @@ -223,7 +226,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -256,7 +260,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -302,7 +307,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -332,7 +338,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -353,7 +360,8 @@ func TestServerGetProviderSchema(t *testing.T) { request: &fwserver.GetProviderSchemaRequest{}, expectedResponse: &fwserver.GetProviderSchemaResponse{ ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( @@ -394,7 +402,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]fwschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -417,7 +426,8 @@ func TestServerGetProviderSchema(t *testing.T) { expectedResponse: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( @@ -492,7 +502,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -541,7 +552,8 @@ func TestServerGetProviderSchema(t *testing.T) { expectedResponse: &fwserver.GetProviderSchemaResponse{ Provider: providerschema.Schema{}, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, Diagnostics: diag.Diagnostics{ diag.NewErrorDiagnostic( @@ -609,7 +621,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: nil, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -642,7 +655,8 @@ func TestServerGetProviderSchema(t *testing.T) { Provider: providerschema.Schema{}, ResourceSchemas: nil, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -688,7 +702,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, ServerCapabilities: &fwserver.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, diff --git a/internal/proto5server/server_getmetadata.go b/internal/proto5server/server_getmetadata.go new file mode 100644 index 000000000..8888d1a86 --- /dev/null +++ b/internal/proto5server/server_getmetadata.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetMetadata satisfies the tfprotov5.ProviderServer interface. +func (s *Server) GetMetadata(ctx context.Context, proto6Req *tfprotov5.GetMetadataRequest) (*tfprotov5.GetMetadataResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto5.GetMetadataRequest(ctx, proto6Req) + fwResp := &fwserver.GetMetadataResponse{} + + s.FrameworkServer.GetMetadata(ctx, fwReq, fwResp) + + return toproto5.GetMetadataResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_getmetadata_test.go b/internal/proto5server/server_getmetadata_test.go new file mode 100644 index 000000000..a6b090759 --- /dev/null +++ b/internal/proto5server/server_getmetadata_test.go @@ -0,0 +1,300 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov5.GetMetadataRequest + expectedError error + expectedResponse *tfprotov5.GetMetadataResponse + }{ + "datasources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source1" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_data_source1", + }, + { + TypeName: "test_data_source2", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Data Source Type Defined", + Detail: "The test_data_source data source type name was returned for multiple data sources. " + + "Data source type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Data Source Type Name Missing", + Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_resource1", + }, + { + TypeName: "test_resource2", + }, + }, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Resource Type Defined", + Detail: "The test_resource resource type name was returned for multiple resources. " + + "Resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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 = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetMetadataRequest{}, + expectedResponse: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Resource Type Name Missing", + Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetMetadata(context.Background(), new(tfprotov5.GetMetadataRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + // Prevent false positives with random map access in testing + sort.Slice(got.DataSources, func(i int, j int) bool { + return got.DataSources[i].TypeName < got.DataSources[j].TypeName + }) + + sort.Slice(got.Resources, func(i int, j int) bool { + return got.Resources[i].TypeName < got.Resources[j].TypeName + }) + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_getproviderschema_test.go b/internal/proto5server/server_getproviderschema_test.go index 28dcf128b..877552fd2 100644 --- a/internal/proto5server/server_getproviderschema_test.go +++ b/internal/proto5server/server_getproviderschema_test.go @@ -107,7 +107,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -171,7 +172,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -209,7 +211,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -245,7 +248,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -285,7 +289,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -363,7 +368,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -427,7 +433,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -465,7 +472,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov5.Schema{}, ServerCapabilities: &tfprotov5.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, diff --git a/internal/proto6server/server_getmetadata.go b/internal/proto6server/server_getmetadata.go new file mode 100644 index 000000000..589f6682a --- /dev/null +++ b/internal/proto6server/server_getmetadata.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetMetadata satisfies the tfprotov6.ProviderServer interface. +func (s *Server) GetMetadata(ctx context.Context, proto6Req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto6.GetMetadataRequest(ctx, proto6Req) + fwResp := &fwserver.GetMetadataResponse{} + + s.FrameworkServer.GetMetadata(ctx, fwReq, fwResp) + + return toproto6.GetMetadataResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_getmetadata_test.go b/internal/proto6server/server_getmetadata_test.go new file mode 100644 index 000000000..1c65ab30b --- /dev/null +++ b/internal/proto6server/server_getmetadata_test.go @@ -0,0 +1,300 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestServerGetMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov6.GetMetadataRequest + expectedError error + expectedResponse *tfprotov6.GetMetadataResponse + }{ + "datasources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source1" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_data_source1", + }, + { + TypeName: "test_data_source2", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "test_data_source" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Data Source Type Defined", + Detail: "The test_data_source data source type name was returned for multiple data sources. " + + "Data source type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "datasources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + DataSourcesMethod: func(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + func() datasource.DataSource { + return &testprovider.DataSource{ + MetadataMethod: func(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Data Source Type Name Missing", + Detail: "The *testprovider.DataSource DataSource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_resource1", + }, + { + TypeName: "test_resource2", + }, + }, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-duplicate-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Resource Type Defined", + Detail: "The test_resource resource type name was returned for multiple resources. " + + "Resource type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + "resources-empty-type-name": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + 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 = "" + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetMetadataRequest{}, + expectedResponse: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Resource Type Name Missing", + Detail: "The *testprovider.Resource Resource returned an empty string from the Metadata method. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetMetadata(context.Background(), new(tfprotov6.GetMetadataRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + // Prevent false positives with random map access in testing + sort.Slice(got.DataSources, func(i int, j int) bool { + return got.DataSources[i].TypeName < got.DataSources[j].TypeName + }) + + sort.Slice(got.Resources, func(i int, j int) bool { + return got.Resources[i].TypeName < got.Resources[j].TypeName + }) + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_getproviderschema_test.go b/internal/proto6server/server_getproviderschema_test.go index f4e581529..d5cb9c8bc 100644 --- a/internal/proto6server/server_getproviderschema_test.go +++ b/internal/proto6server/server_getproviderschema_test.go @@ -107,7 +107,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -171,7 +172,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -209,7 +211,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -245,7 +248,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -285,7 +289,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -363,7 +368,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, }, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -427,7 +433,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, @@ -465,7 +472,8 @@ func TestServerGetProviderSchema(t *testing.T) { }, ResourceSchemas: map[string]*tfprotov6.Schema{}, ServerCapabilities: &tfprotov6.ServerCapabilities{ - PlanDestroy: true, + GetProviderSchemaOptional: true, + PlanDestroy: true, }, }, }, diff --git a/internal/toproto5/datasourcemetadata.go b/internal/toproto5/datasourcemetadata.go new file mode 100644 index 000000000..98e6c4f44 --- /dev/null +++ b/internal/toproto5/datasourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// DataSourceMetadata returns the tfprotov5.DataSourceMetadata for a +// fwserver.DataSourceMetadata. +func DataSourceMetadata(ctx context.Context, fw fwserver.DataSourceMetadata) tfprotov5.DataSourceMetadata { + return tfprotov5.DataSourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto5/datasourcemetadata_test.go b/internal/toproto5/datasourcemetadata_test.go new file mode 100644 index 000000000..882de7469 --- /dev/null +++ b/internal/toproto5/datasourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestDataSourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.DataSourceMetadata + expected tfprotov5.DataSourceMetadata + }{ + "TypeName": { + fw: fwserver.DataSourceMetadata{ + TypeName: "test", + }, + expected: tfprotov5.DataSourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.DataSourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/getmetadata.go b/internal/toproto5/getmetadata.go new file mode 100644 index 000000000..2162635c8 --- /dev/null +++ b/internal/toproto5/getmetadata.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetMetadataResponse returns the *tfprotov5.GetMetadataResponse +// equivalent of a *fwserver.GetMetadataResponse. +func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) *tfprotov5.GetMetadataResponse { + if fw == nil { + return nil + } + + protov6 := &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + } + + for _, datasource := range fw.DataSources { + protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) + } + + for _, resource := range fw.Resources { + protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) + } + + return protov6 +} diff --git a/internal/toproto5/getmetadata_test.go b/internal/toproto5/getmetadata_test.go new file mode 100644 index 000000000..d363d260e --- /dev/null +++ b/internal/toproto5/getmetadata_test.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestGetMetadataResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetMetadataResponse + expected *tfprotov5.GetMetadataResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "datasources": { + input: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{ + { + TypeName: "test_data_source_1", + }, + { + TypeName: "test_data_source_2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{ + { + TypeName: "test_data_source_1", + }, + { + TypeName: "test_data_source_2", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, + "diagnostics": { + input: &fwserver.GetMetadataResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Data Source Type Defined", + "The test_data_source data source type name was returned for multiple data sources. "+ + "Data source type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Duplicate Data Source Type Defined", + Detail: "The test_data_source data source type name was returned for multiple data sources. " + + "Data source type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov5.ResourceMetadata{}, + }, + }, + "resources": { + input: &fwserver.GetMetadataResponse{ + Resources: []fwserver.ResourceMetadata{ + { + TypeName: "test_resource_1", + }, + { + TypeName: "test_resource_2", + }, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Resources: []tfprotov5.ResourceMetadata{ + { + TypeName: "test_resource_1", + }, + { + TypeName: "test_resource_2", + }, + }, + }, + }, + "servercapabilities": { + input: &fwserver.GetMetadataResponse{ + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + expected: &tfprotov5.GetMetadataResponse{ + DataSources: []tfprotov5.DataSourceMetadata{}, + Resources: []tfprotov5.ResourceMetadata{}, + ServerCapabilities: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.GetMetadataResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/resourcemetadata.go b/internal/toproto5/resourcemetadata.go new file mode 100644 index 000000000..e13af0c50 --- /dev/null +++ b/internal/toproto5/resourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ResourceMetadata returns the tfprotov5.ResourceMetadata for a +// fwserver.ResourceMetadata. +func ResourceMetadata(ctx context.Context, fw fwserver.ResourceMetadata) tfprotov5.ResourceMetadata { + return tfprotov5.ResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto5/resourcemetadata_test.go b/internal/toproto5/resourcemetadata_test.go new file mode 100644 index 000000000..d8572f227 --- /dev/null +++ b/internal/toproto5/resourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ResourceMetadata + expected tfprotov5.ResourceMetadata + }{ + "TypeName": { + fw: fwserver.ResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov5.ResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.ResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/server_capabilities.go b/internal/toproto5/server_capabilities.go index 275cec958..fd968906d 100644 --- a/internal/toproto5/server_capabilities.go +++ b/internal/toproto5/server_capabilities.go @@ -18,6 +18,7 @@ func ServerCapabilities(ctx context.Context, fw *fwserver.ServerCapabilities) *t } return &tfprotov5.ServerCapabilities{ - PlanDestroy: fw.PlanDestroy, + GetProviderSchemaOptional: fw.GetProviderSchemaOptional, + PlanDestroy: fw.PlanDestroy, } } diff --git a/internal/toproto5/server_capabilities_test.go b/internal/toproto5/server_capabilities_test.go index 90c74be88..772909e56 100644 --- a/internal/toproto5/server_capabilities_test.go +++ b/internal/toproto5/server_capabilities_test.go @@ -24,6 +24,14 @@ func TestServerCapabilities(t *testing.T) { fw: nil, expected: nil, }, + "GetProviderSchemaOptional": { + fw: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + expected: &tfprotov5.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + }, "PlanDestroy": { fw: &fwserver.ServerCapabilities{ PlanDestroy: true, diff --git a/internal/toproto6/datasourcemetadata.go b/internal/toproto6/datasourcemetadata.go new file mode 100644 index 000000000..24c8bbb09 --- /dev/null +++ b/internal/toproto6/datasourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// DataSourceMetadata returns the tfprotov6.DataSourceMetadata for a +// fwserver.DataSourceMetadata. +func DataSourceMetadata(ctx context.Context, fw fwserver.DataSourceMetadata) tfprotov6.DataSourceMetadata { + return tfprotov6.DataSourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto6/datasourcemetadata_test.go b/internal/toproto6/datasourcemetadata_test.go new file mode 100644 index 000000000..af81c1f80 --- /dev/null +++ b/internal/toproto6/datasourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestDataSourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.DataSourceMetadata + expected tfprotov6.DataSourceMetadata + }{ + "TypeName": { + fw: fwserver.DataSourceMetadata{ + TypeName: "test", + }, + expected: tfprotov6.DataSourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.DataSourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/getmetadata.go b/internal/toproto6/getmetadata.go new file mode 100644 index 000000000..87e1af5d3 --- /dev/null +++ b/internal/toproto6/getmetadata.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetMetadataResponse returns the *tfprotov6.GetMetadataResponse +// equivalent of a *fwserver.GetMetadataResponse. +func GetMetadataResponse(ctx context.Context, fw *fwserver.GetMetadataResponse) *tfprotov6.GetMetadataResponse { + if fw == nil { + return nil + } + + protov6 := &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: ServerCapabilities(ctx, fw.ServerCapabilities), + } + + for _, datasource := range fw.DataSources { + protov6.DataSources = append(protov6.DataSources, DataSourceMetadata(ctx, datasource)) + } + + for _, resource := range fw.Resources { + protov6.Resources = append(protov6.Resources, ResourceMetadata(ctx, resource)) + } + + return protov6 +} diff --git a/internal/toproto6/getmetadata_test.go b/internal/toproto6/getmetadata_test.go new file mode 100644 index 000000000..af0654095 --- /dev/null +++ b/internal/toproto6/getmetadata_test.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestGetMetadataResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetMetadataResponse + expected *tfprotov6.GetMetadataResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "datasources": { + input: &fwserver.GetMetadataResponse{ + DataSources: []fwserver.DataSourceMetadata{ + { + TypeName: "test_data_source_1", + }, + { + TypeName: "test_data_source_2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{ + { + TypeName: "test_data_source_1", + }, + { + TypeName: "test_data_source_2", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, + "diagnostics": { + input: &fwserver.GetMetadataResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Duplicate Data Source Type Defined", + "The test_data_source data source type name was returned for multiple data sources. "+ + "Data source type names must be unique. "+ + "This is always an issue with the provider and should be reported to the provider developers.", + ), + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Duplicate Data Source Type Defined", + Detail: "The test_data_source data source type name was returned for multiple data sources. " + + "Data source type names must be unique. " + + "This is always an issue with the provider and should be reported to the provider developers.", + }, + }, + Resources: []tfprotov6.ResourceMetadata{}, + }, + }, + "resources": { + input: &fwserver.GetMetadataResponse{ + Resources: []fwserver.ResourceMetadata{ + { + TypeName: "test_resource_1", + }, + { + TypeName: "test_resource_2", + }, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{ + { + TypeName: "test_resource_1", + }, + { + TypeName: "test_resource_2", + }, + }, + }, + }, + "servercapabilities": { + input: &fwserver.GetMetadataResponse{ + ServerCapabilities: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + expected: &tfprotov6.GetMetadataResponse{ + DataSources: []tfprotov6.DataSourceMetadata{}, + Resources: []tfprotov6.ResourceMetadata{}, + ServerCapabilities: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + PlanDestroy: true, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.GetMetadataResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/resourcemetadata.go b/internal/toproto6/resourcemetadata.go new file mode 100644 index 000000000..3f54952bb --- /dev/null +++ b/internal/toproto6/resourcemetadata.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ResourceMetadata returns the tfprotov6.ResourceMetadata for a +// fwserver.ResourceMetadata. +func ResourceMetadata(ctx context.Context, fw fwserver.ResourceMetadata) tfprotov6.ResourceMetadata { + return tfprotov6.ResourceMetadata{ + TypeName: fw.TypeName, + } +} diff --git a/internal/toproto6/resourcemetadata_test.go b/internal/toproto6/resourcemetadata_test.go new file mode 100644 index 000000000..17f9463e1 --- /dev/null +++ b/internal/toproto6/resourcemetadata_test.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestResourceMetadata(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + fw fwserver.ResourceMetadata + expected tfprotov6.ResourceMetadata + }{ + "TypeName": { + fw: fwserver.ResourceMetadata{ + TypeName: "test", + }, + expected: tfprotov6.ResourceMetadata{ + TypeName: "test", + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.ResourceMetadata(context.Background(), testCase.fw) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/server_capabilities.go b/internal/toproto6/server_capabilities.go index 4b614314c..ef46cbf16 100644 --- a/internal/toproto6/server_capabilities.go +++ b/internal/toproto6/server_capabilities.go @@ -18,6 +18,7 @@ func ServerCapabilities(ctx context.Context, fw *fwserver.ServerCapabilities) *t } return &tfprotov6.ServerCapabilities{ - PlanDestroy: fw.PlanDestroy, + GetProviderSchemaOptional: fw.GetProviderSchemaOptional, + PlanDestroy: fw.PlanDestroy, } } diff --git a/internal/toproto6/server_capabilities_test.go b/internal/toproto6/server_capabilities_test.go index 64b3a6a99..f3a91ac03 100644 --- a/internal/toproto6/server_capabilities_test.go +++ b/internal/toproto6/server_capabilities_test.go @@ -24,6 +24,14 @@ func TestServerCapabilities(t *testing.T) { fw: nil, expected: nil, }, + "GetProviderSchemaOptional": { + fw: &fwserver.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + expected: &tfprotov6.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + }, "PlanDestroy": { fw: &fwserver.ServerCapabilities{ PlanDestroy: true,