diff --git a/.changelog/39.txt b/.changelog/39.txt new file mode 100644 index 0000000..4723af8 --- /dev/null +++ b/.changelog/39.txt @@ -0,0 +1,3 @@ +```release-note:breaking-change +The root package `SchemaServer` types and `NewSchemaServerFactory` function have been migrated to the `tf5muxserver` package. To upgrade, replace `tfmux.NewSchemaServerFactory` with `tf5muxserver.NewMuxServer` and replace any invocations of the previous `SchemaServerFactory` type `Server()` method with `ProviderServer()`. The underlying types are no longer exported. +``` diff --git a/README.md b/README.md index 7b6e030..c24229b 100644 --- a/README.md +++ b/README.md @@ -77,40 +77,33 @@ func main() { ### Protocol Version 5 -Protocol version 5 providers can be combined using the root package [`NewSchemaServerFactory()` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-mux#NewSchemaServerFactory): +Protocol version 5 providers can be combined using the [`tf5muxserver.NewMuxServer` function](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-mux/tf5muxserver#NewMuxServer): ```go func main() { ctx := context.Background() - - // the ProviderServer from SDKv2 - sdkv2 := sdkv2provider.Provider().GRPCProvider - - // the terraform-plugin-go provider - tpg := protoprovider.Provider - - factory, err := tfmux.NewSchemaServerFactory(ctx, sdkv2, tpg) + providers := []func() tfprotov5.ProviderServer{ + // Example terraform-plugin-sdk ProviderServer function + // sdkprovider.Provider().ProviderServer, + // + // Example terraform-plugin-go ProviderServer function + // goprovider.Provider(), + } + muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) if err != nil { - log.Println(err.Error()) - os.Exit(1) + log.Fatalln(err.Error()) + } + // Use the result to start a muxed provider + err = tf5server.Serve("registry.terraform.io/namespace/example", muxServer.ProviderServer) + if err != nil { + log.Fatalln(err.Error()) } - - tf5server.Serve("registry.terraform.io/myorg/myprovider", factory.Server) } ``` -Each server needs a function that returns a -[`tfprotov5.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov5#ProviderServer). -Those get passed into a -[`NewSchemaServerFactory`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-mux#NewSchemaServerFactory) -function, which returns a factory capable of standing up Terraform provider -servers. Passing that factory into the -[`tf5server.Serve`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov5/server#Serve) -function starts the server and lets Terraform connect to it. - ## Testing -The Terraform Plugin SDK's [`helper/resource`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource) package can be used to test any provider that implements the [`tfprotov5.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov5#ProviderServer) interface, which includes muxed providers created using `tfmux.NewSchemaServerFactory`. +The Terraform Plugin SDK's [`helper/resource`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource) package can be used to test any provider that implements the [`tfprotov5.ProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-go/tfprotov5#ProviderServer) interface, which includes muxed providers created using `tf5muxserver.NewMuxServer`. You may wish to test a terraform-plugin-go provider's resources by supplying only that provider, and not the muxed provider, to the test framework: please see the example in https://github.com/hashicorp/terraform-plugin-go#testing in this case. @@ -129,11 +122,11 @@ func init() { // the terraform-plugin-go provider tpg := protoprovider.Provider - factory, err := tfmux.NewSchemaServerFactory(ctx, sdkv2, tpg) + muxServer, err := tf5muxserver.NewMuxServer(ctx, sdkv2, tpg) if err != nil { return nil, err } - return factory.Server(), nil + return muxServer.ProviderServer(), nil } } ``` diff --git a/internal/tf5testserver/tf5testserver.go b/internal/tf5testserver/tf5testserver.go new file mode 100644 index 0000000..4c4e550 --- /dev/null +++ b/internal/tf5testserver/tf5testserver.go @@ -0,0 +1,155 @@ +package tf5testserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +var _ tfprotov5.ProviderServer = &TestServer{} + +type TestServer struct { + DataSourceSchemas map[string]*tfprotov5.Schema + ProviderMetaSchema *tfprotov5.Schema + ProviderSchema *tfprotov5.Schema + ResourceSchemas map[string]*tfprotov5.Schema + + ApplyResourceChangeCalled map[string]bool + + ConfigureProviderCalled bool + + ImportResourceStateCalled map[string]bool + + PlanResourceChangeCalled map[string]bool + + PrepareProviderConfigCalled bool + PrepareProviderConfigResponse *tfprotov5.PrepareProviderConfigResponse + + ReadDataSourceCalled map[string]bool + + ReadResourceCalled map[string]bool + + StopProviderCalled bool + StopProviderError string + + UpgradeResourceStateCalled map[string]bool + + ValidateDataSourceConfigCalled map[string]bool + + ValidateResourceTypeConfigCalled map[string]bool +} + +func (s *TestServer) ProviderServer() tfprotov5.ProviderServer { + return s +} + +func (s *TestServer) ApplyResourceChange(_ context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { + if s.ApplyResourceChangeCalled == nil { + s.ApplyResourceChangeCalled = make(map[string]bool) + } + + s.ApplyResourceChangeCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) ConfigureProvider(_ context.Context, _ *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { + s.ConfigureProviderCalled = true + return &tfprotov5.ConfigureProviderResponse{}, nil +} + +func (s *TestServer) GetProviderSchema(_ context.Context, _ *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { + if s.DataSourceSchemas == nil { + s.DataSourceSchemas = make(map[string]*tfprotov5.Schema) + } + + if s.ResourceSchemas == nil { + s.ResourceSchemas = make(map[string]*tfprotov5.Schema) + } + + return &tfprotov5.GetProviderSchemaResponse{ + Provider: s.ProviderSchema, + ProviderMeta: s.ProviderMetaSchema, + ResourceSchemas: s.ResourceSchemas, + DataSourceSchemas: s.DataSourceSchemas, + }, nil +} + +func (s *TestServer) ImportResourceState(_ context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { + if s.ImportResourceStateCalled == nil { + s.ImportResourceStateCalled = make(map[string]bool) + } + + s.ImportResourceStateCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) PlanResourceChange(_ context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { + if s.PlanResourceChangeCalled == nil { + s.PlanResourceChangeCalled = make(map[string]bool) + } + + s.PlanResourceChangeCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) ReadDataSource(_ context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { + if s.ReadDataSourceCalled == nil { + s.ReadDataSourceCalled = make(map[string]bool) + } + + s.ReadDataSourceCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) ReadResource(_ context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { + if s.ReadResourceCalled == nil { + s.ReadResourceCalled = make(map[string]bool) + } + + s.ReadResourceCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) StopProvider(_ context.Context, _ *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { + s.StopProviderCalled = true + + if s.StopProviderError != "" { + return &tfprotov5.StopProviderResponse{ + Error: s.StopProviderError, + }, nil + } + + return &tfprotov5.StopProviderResponse{}, nil +} + +func (s *TestServer) UpgradeResourceState(_ context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { + if s.UpgradeResourceStateCalled == nil { + s.UpgradeResourceStateCalled = make(map[string]bool) + } + + s.UpgradeResourceStateCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) ValidateDataSourceConfig(_ context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { + if s.ValidateDataSourceConfigCalled == nil { + s.ValidateDataSourceConfigCalled = make(map[string]bool) + } + + s.ValidateDataSourceConfigCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) ValidateResourceTypeConfig(_ context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { + if s.ValidateResourceTypeConfigCalled == nil { + s.ValidateResourceTypeConfigCalled = make(map[string]bool) + } + + s.ValidateResourceTypeConfigCalled[req.TypeName] = true + return nil, nil +} + +func (s *TestServer) PrepareProviderConfig(_ context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { + s.PrepareProviderConfigCalled = true + return s.PrepareProviderConfigResponse, nil +} diff --git a/tf5muxserver/doc.go b/tf5muxserver/doc.go index 6a474f3..ad23bbd 100644 --- a/tf5muxserver/doc.go +++ b/tf5muxserver/doc.go @@ -1,8 +1,8 @@ -// Package tfmux provides a multiplexer that allows joining multiple Terraform +// Package tf5muxserver provides a multiplexer that allows joining multiple Terraform // provider servers into a single gRPC server. // // This allows providers to use any framework or SDK built on // github.com/hashicorp/terraform-plugin-go to build resources for their // provider, and to join all the resources into a single logical provider even // though they're implemented in different SDKs or frameworks. -package tfmux +package tf5muxserver diff --git a/tf5muxserver/mux_server.go b/tf5muxserver/mux_server.go index 03d7781..85ae57b 100644 --- a/tf5muxserver/mux_server.go +++ b/tf5muxserver/mux_server.go @@ -1,372 +1,118 @@ -package tfmux +package tf5muxserver import ( "context" "fmt" - "strings" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-mux/internal/logging" ) -var _ tfprotov5.ProviderServer = SchemaServer{} +var _ tfprotov5.ProviderServer = muxServer{} -var cmpOptions = []cmp.Option{ - cmpopts.SortSlices(func(i, j *tfprotov5.SchemaAttribute) bool { - return i.Name < j.Name - }), - cmpopts.SortSlices(func(i, j *tfprotov5.SchemaNestedBlock) bool { - return i.TypeName < j.TypeName - }), -} +// muxServer is a gRPC server implementation that stands in front of other +// gRPC servers, routing requests to them as if they were a single server. It +// should always be instantiated by calling NewMuxServer(). +type muxServer struct { + // Routing for data source types + dataSources map[string]tfprotov5.ProviderServer -// SchemaServerFactory is a generator for SchemaServers, which are Terraform -// gRPC servers that route requests to different gRPC provider implementations -// based on which gRPC provider implementation supports the resource the -// request is for. -// -// SchemaServerFactory should always be instantiated by NewSchemaServerFactory. -type SchemaServerFactory struct { - // determine which servers will respond to which requests - resources map[string]int - dataSources map[string]int - servers []func() tfprotov5.ProviderServer + // Routing for resource types + resources map[string]tfprotov5.ProviderServer - // we respond to GetSchema requests using these schemas - resourceSchemas map[string]*tfprotov5.Schema + // Underlying servers for requests that should be handled by all servers + servers []tfprotov5.ProviderServer + + // Schemas are cached during server creation dataSourceSchemas map[string]*tfprotov5.Schema - providerSchema *tfprotov5.Schema providerMetaSchema *tfprotov5.Schema + providerSchema *tfprotov5.Schema + resourceSchemas map[string]*tfprotov5.Schema +} - // any non-error diagnostics should get bubbled up, so we store them here - diagnostics []*tfprotov5.Diagnostic - - // we just store these to surface better errors - // track which server we got the provider schema and provider meta - // schema from - providerSchemaFrom int - providerMetaSchemaFrom int +// ProviderServer is a function compatible with tf6server.Serve. +func (s muxServer) ProviderServer() tfprotov5.ProviderServer { + return s } -// NewSchemaServerFactory returns a SchemaServerFactory that will route gRPC -// requests between the tfprotov5.ProviderServers specified. Each function -// specified is called, and the tfprotov5.ProviderServer has its -// GetProviderSchema method called. The schemas are used to determine which -// server handles each request, with requests for resources and data sources -// directed to the server that specified that data source or resource in its -// schema. Data sources and resources can only be specified in the schema of -// one ProviderServer. -func NewSchemaServerFactory(ctx context.Context, servers ...func() tfprotov5.ProviderServer) (SchemaServerFactory, error) { +// NewMuxServer returns a muxed server that will route gRPC requests between +// tfprotov5.ProviderServers specified. The GetProviderSchema method of each +// is called to verify that the overall muxed server is compatible by ensuring: +// +// - All provider schemas exactly match +// - All provider meta schemas exactly match +// - Only one provider implements each managed resource +// - Only one provider implements each data source +// +// The various schemas are cached and used to respond to the GetProviderSchema +// method of the muxed server. +func NewMuxServer(ctx context.Context, servers ...func() tfprotov5.ProviderServer) (muxServer, error) { ctx = logging.InitContext(ctx) + result := muxServer{ + dataSources: make(map[string]tfprotov5.ProviderServer), + dataSourceSchemas: make(map[string]*tfprotov5.Schema), + resources: make(map[string]tfprotov5.ProviderServer), + resourceSchemas: make(map[string]*tfprotov5.Schema), + } - var factory SchemaServerFactory + for _, serverFunc := range servers { + server := serverFunc() - // know when these are unset vs set to the element in pos 0 - factory.providerSchemaFrom = -1 - factory.providerMetaSchemaFrom = -1 + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") - factory.servers = make([]func() tfprotov5.ProviderServer, len(servers)) - factory.resources = make(map[string]int) - factory.resourceSchemas = make(map[string]*tfprotov5.Schema) - factory.dataSources = make(map[string]int) - factory.dataSourceSchemas = make(map[string]*tfprotov5.Schema) + resp, err := server.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) - for pos, server := range servers { - s := server() - ctx = logging.Tfprotov5ProviderServerContext(ctx, s) - logging.MuxTrace(ctx, "getting provider schema to build server factory") - resp, err := s.GetProviderSchema(ctx, &tfprotov5.GetProviderSchemaRequest{}) if err != nil { - return factory, fmt.Errorf("error retrieving schema for %T: %w", s, err) + return result, fmt.Errorf("error retrieving schema for %T: %w", server, err) } - factory.servers[pos] = server - for _, diag := range resp.Diagnostics { if diag == nil { continue } if diag.Severity != tfprotov5.DiagnosticSeverityError { - factory.diagnostics = append(factory.diagnostics, diag) continue } - return factory, fmt.Errorf("error retrieving schema for %T:\n\n\tAttribute: %s\n\tSummary: %s\n\tDetail: %s", s, diag.Attribute, diag.Summary, diag.Detail) - } - if resp.Provider != nil && factory.providerSchema != nil && !cmp.Equal(resp.Provider, factory.providerSchema, cmpOptions...) { - return factory, fmt.Errorf("got a different provider schema from two servers (%T, %T). Provider schemas must be identical across providers. Diff: %s", factory.servers[factory.providerSchemaFrom](), s, cmp.Diff(resp.Provider, factory.providerSchema, cmpOptions...)) - } else if resp.Provider != nil { - factory.providerSchemaFrom = pos - factory.providerSchema = resp.Provider - } - if resp.ProviderMeta != nil && factory.providerMetaSchema != nil && !cmp.Equal(resp.ProviderMeta, factory.providerMetaSchema, cmpOptions...) { - return factory, fmt.Errorf("got a different provider_meta schema from two servers (%T, %T). Provider metadata schemas must be identical across providers. Diff: %s", factory.servers[factory.providerMetaSchemaFrom](), s, cmp.Diff(resp.ProviderMeta, factory.providerMetaSchema, cmpOptions...)) - } else if resp.ProviderMeta != nil { - factory.providerMetaSchemaFrom = pos - factory.providerMetaSchema = resp.ProviderMeta - } - for resource, schema := range resp.ResourceSchemas { - logging.MuxTrace(ctx, "getting resource schema to build server factory", "resource", resource) - if v, ok := factory.resources[resource]; ok { - return factory, fmt.Errorf("resource %q supported by multiple server implementations (%T, %T); remove support from one", resource, factory.servers[v], s) - } - factory.resources[resource] = pos - factory.resourceSchemas[resource] = schema - } - for data, schema := range resp.DataSourceSchemas { - logging.MuxTrace(ctx, "getting data source schema to build server factory", "data_source", data) - if v, ok := factory.dataSources[data]; ok { - return factory, fmt.Errorf("data source %q supported by multiple server implementations (%T, %T); remove support from one", data, factory.servers[v], s) - } - factory.dataSources[data] = pos - factory.dataSourceSchemas[data] = schema + return result, fmt.Errorf("error retrieving schema for %T:\n\n\tAttribute: %s\n\tSummary: %s\n\tDetail: %s", server, diag.Attribute, diag.Summary, diag.Detail) } - } - return factory, nil -} -func (s SchemaServerFactory) getSchemaHandler(ctx context.Context, _ *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { - logging.MuxTrace(ctx, "serving cached schemas") - return &tfprotov5.GetProviderSchemaResponse{ - Provider: s.providerSchema, - ResourceSchemas: s.resourceSchemas, - DataSourceSchemas: s.dataSourceSchemas, - ProviderMeta: s.providerMetaSchema, - }, nil -} - -// Server returns the SchemaServer that muxes between the -// tfprotov5.ProviderServers associated with the SchemaServerFactory. -func (s SchemaServerFactory) Server() SchemaServer { - res := SchemaServer{ - getSchemaHandler: s.getSchemaHandler, - prepareProviderConfigServer: s.providerSchemaFrom, - servers: make([]tfprotov5.ProviderServer, len(s.servers)), - } - for pos, server := range s.servers { - res.servers[pos] = server() - } - res.resources = make(map[string]tfprotov5.ProviderServer) - for r, pos := range s.resources { - res.resources[r] = res.servers[pos] - } - res.dataSources = make(map[string]tfprotov5.ProviderServer) - for ds, pos := range s.dataSources { - res.dataSources[ds] = res.servers[pos] - } - return res -} - -// SchemaServer is a gRPC server implementation that stands in front of other -// gRPC servers, routing requests to them as if they were a single server. It -// should always be instantiated by calling SchemaServerFactory.Server(). -type SchemaServer struct { - resources map[string]tfprotov5.ProviderServer - dataSources map[string]tfprotov5.ProviderServer - servers []tfprotov5.ProviderServer - - getSchemaHandler func(context.Context, *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) - prepareProviderConfigServer int -} - -// GetProviderSchema merges the schemas returned by the -// tfprotov5.ProviderServers associated with SchemaServer into a single schema. -// Resources and data sources must be returned from only one server. Provider -// and ProviderMeta schemas must be identical between all servers. -func (s SchemaServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { - ctx = logging.InitContext(ctx) - return s.getSchemaHandler(ctx, req) -} - -// PrepareProviderConfig calls the PrepareProviderConfig method on each server -// in order, passing `req`. Only one may respond with a non-nil PreparedConfig -// or a non-empty Diagnostics. -func (s SchemaServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { - ctx = logging.InitContext(ctx) - respondedServer := -1 - var resp *tfprotov5.PrepareProviderConfigResponse - for pos, server := range s.servers { - ctx = logging.Tfprotov5ProviderServerContext(ctx, server) - res, err := server.PrepareProviderConfig(ctx, req) - if err != nil { - return resp, fmt.Errorf("error from %T preparing provider config: %w", server, err) - } - if res == nil { - continue - } - if res.PreparedConfig != nil || len(res.Diagnostics) > 0 { - logging.MuxTrace(ctx, "found a server that supports PrepareProviderConfig") - if respondedServer >= 0 { - return nil, fmt.Errorf("got a PrepareProviderConfig response from multiple servers, %d and %d, not sure which to use", respondedServer, pos) + if resp.Provider != nil { + if result.providerSchema != nil && !schemaEquals(resp.Provider, result.providerSchema) { + return result, fmt.Errorf("got a different provider schema across servers. Provider schemas must be identical across providers. Diff: %s", schemaDiff(resp.Provider, result.providerSchema)) } - resp = res - respondedServer = pos - continue - } - } - return resp, nil -} -// ValidateResourceTypeConfig calls the ValidateResourceTypeConfig method, -// passing `req`, on the provider that returned the resource specified by -// req.TypeName in its schema. -func (s SchemaServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "validating resource type config") - return h.ValidateResourceTypeConfig(ctx, req) -} - -// ValidateDataSourceConfig calls the ValidateDataSourceConfig method, passing -// `req`, on the provider that returned the data source specified by -// req.TypeName in its schema. -func (s SchemaServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "validating data source config") - return h.ValidateDataSourceConfig(ctx, req) -} - -// UpgradeResourceState calls the UpgradeResourceState method, passing `req`, -// on the provider that returned the resource specified by req.TypeName in its -// schema. -func (s SchemaServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "upgrading resource state") - return h.UpgradeResourceState(ctx, req) -} - -// ConfigureProvider calls each provider's ConfigureProvider method, one at a -// time, passing `req`. Any Diagnostic with severity error will abort the -// process and return immediately; non-Error severity Diagnostics will be -// combined and returned. -func (s SchemaServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { - ctx = logging.InitContext(ctx) - var diags []*tfprotov5.Diagnostic - for _, server := range s.servers { - ctx = logging.Tfprotov5ProviderServerContext(ctx, server) - logging.MuxTrace(ctx, "configuring provider") - resp, err := server.ConfigureProvider(ctx, req) - if err != nil { - return resp, fmt.Errorf("error configuring %T: %w", server, err) + result.providerSchema = resp.Provider } - for _, diag := range resp.Diagnostics { - if diag == nil { - continue - } - diags = append(diags, diag) - if diag.Severity != tfprotov5.DiagnosticSeverityError { - continue + + if resp.ProviderMeta != nil { + if result.providerMetaSchema != nil && !schemaEquals(resp.ProviderMeta, result.providerMetaSchema) { + return result, fmt.Errorf("got a different provider meta schema across servers. Provider metadata schemas must be identical across providers. Diff: %s", schemaDiff(resp.ProviderMeta, result.providerMetaSchema)) } - resp.Diagnostics = diags - return resp, err + + result.providerMetaSchema = resp.ProviderMeta } - } - return &tfprotov5.ConfigureProviderResponse{Diagnostics: diags}, nil -} -// ReadResource calls the ReadResource method, passing `req`, on the provider -// that returned the resource specified by req.TypeName in its schema. -func (s SchemaServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "reading resource state") - return h.ReadResource(ctx, req) -} + for resourceType, schema := range resp.ResourceSchemas { + if _, ok := result.resources[resourceType]; ok { + return result, fmt.Errorf("resource %q is implemented by multiple servers; only one implementation allowed", resourceType) + } -// PlanResourceChange calls the PlanResourceChange method, passing `req`, on -// the provider that returned the resource specified by req.TypeName in its -// schema. -func (s SchemaServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "planning resource change") - return h.PlanResourceChange(ctx, req) -} + result.resources[resourceType] = server + result.resourceSchemas[resourceType] = schema + } -// ApplyResourceChange calls the ApplyResourceChange method, passing `req`, on -// the provider that returned the resource specified by req.TypeName in its -// schema. -func (s SchemaServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "applying resource change") - return h.ApplyResourceChange(ctx, req) -} + for dataSourceType, schema := range resp.DataSourceSchemas { + if _, ok := result.dataSources[dataSourceType]; ok { + return result, fmt.Errorf("data source %q is implemented by multiple servers; only one implementation allowed", dataSourceType) + } -// ImportResourceState calls the ImportResourceState method, passing `req`, on -// the provider that returned the resource specified by req.TypeName in its -// schema. -func (s SchemaServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.resources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) - } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "importing resource") - return h.ImportResourceState(ctx, req) -} + result.dataSources[dataSourceType] = server + result.dataSourceSchemas[dataSourceType] = schema + } -// ReadDataSource calls the ReadDataSource method, passing `req`, on the -// provider that returned the data source specified by req.TypeName in its -// schema. -func (s SchemaServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { - ctx = logging.InitContext(ctx) - h, ok := s.dataSources[req.TypeName] - if !ok { - return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + result.servers = append(result.servers, server) } - ctx = logging.Tfprotov5ProviderServerContext(ctx, h) - logging.MuxTrace(ctx, "reading data source") - return h.ReadDataSource(ctx, req) -} -// StopProvider calls the StopProvider function for each provider associated -// with the SchemaServer, one at a time. All Error fields will be joined -// together and returned, but will not prevent the rest of the providers' -// StopProvider methods from being called. -func (s SchemaServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { - ctx = logging.InitContext(ctx) - var errs []string - for _, server := range s.servers { - ctx = logging.Tfprotov5ProviderServerContext(ctx, server) - logging.MuxTrace(ctx, "stopping provider") - resp, err := server.StopProvider(ctx, req) - if err != nil { - return resp, fmt.Errorf("error stopping %T: %w", server, err) - } - if resp.Error != "" { - errs = append(errs, resp.Error) - } - } - return &tfprotov5.StopProviderResponse{ - Error: strings.Join(errs, "\n"), - }, nil + return result, nil } diff --git a/tf5muxserver/mux_server_ApplyResourceChange.go b/tf5muxserver/mux_server_ApplyResourceChange.go new file mode 100644 index 0000000..85c78c2 --- /dev/null +++ b/tf5muxserver/mux_server_ApplyResourceChange.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ApplyResourceChange calls the ApplyResourceChange method, passing `req`, on +// the provider that returned the resource specified by req.TypeName in its +// schema. +func (s muxServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { + rpc := "ApplyResourceChange" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ApplyResourceChange(ctx, req) +} diff --git a/tf5muxserver/mux_server_ApplyResourceChange_test.go b/tf5muxserver/mux_server_ApplyResourceChange_test.go new file mode 100644 index 0000000..b1b2bb7 --- /dev/null +++ b/tf5muxserver/mux_server_ApplyResourceChange_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerApplyResourceChange(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ApplyResourceChange(ctx, &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ApplyResourceChangeCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 ApplyResourceChange to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ApplyResourceChangeCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 ApplyResourceChange called on server2") + } + + _, err = muxServer.ProviderServer().ApplyResourceChange(ctx, &tfprotov5.ApplyResourceChangeRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ApplyResourceChangeCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 ApplyResourceChange called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ApplyResourceChangeCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 ApplyResourceChange to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_ConfigureProvider.go b/tf5muxserver/mux_server_ConfigureProvider.go new file mode 100644 index 0000000..f5f3093 --- /dev/null +++ b/tf5muxserver/mux_server_ConfigureProvider.go @@ -0,0 +1,49 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ConfigureProvider calls each provider's ConfigureProvider method, one at a +// time, passing `req`. Any Diagnostic with severity error will abort the +// process and return immediately; non-Error severity Diagnostics will be +// combined and returned. +func (s muxServer) ConfigureProvider(ctx context.Context, req *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { + rpc := "ConfigureProvider" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + var diags []*tfprotov5.Diagnostic + + for _, server := range s.servers { + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + resp, err := server.ConfigureProvider(ctx, req) + + if err != nil { + return resp, fmt.Errorf("error configuring %T: %w", server, err) + } + + for _, diag := range resp.Diagnostics { + if diag == nil { + continue + } + + diags = append(diags, diag) + + if diag.Severity != tfprotov5.DiagnosticSeverityError { + continue + } + + resp.Diagnostics = diags + + return resp, err + } + } + + return &tfprotov5.ConfigureProviderResponse{Diagnostics: diags}, nil +} diff --git a/tf5muxserver/mux_server_ConfigureProvider_test.go b/tf5muxserver/mux_server_ConfigureProvider_test.go new file mode 100644 index 0000000..003c9e8 --- /dev/null +++ b/tf5muxserver/mux_server_ConfigureProvider_test.go @@ -0,0 +1,40 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerConfigureProvider(t *testing.T) { + t.Parallel() + + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), servers...) + + if err != nil { + t.Fatalf("error setting up muxer: %s", err) + } + + _, err = muxServer.ProviderServer().ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{}) + + if err != nil { + t.Fatalf("error calling ConfigureProvider: %s", err) + } + + for num, server := range servers { + if !server().(*tf5testserver.TestServer).ConfigureProviderCalled { + t.Errorf("configure not called on server%d", num+1) + } + } +} diff --git a/tf5muxserver/mux_server_GetProviderSchema.go b/tf5muxserver/mux_server_GetProviderSchema.go new file mode 100644 index 0000000..8bd6ba5 --- /dev/null +++ b/tf5muxserver/mux_server_GetProviderSchema.go @@ -0,0 +1,26 @@ +package tf5muxserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// GetProviderSchema merges the schemas returned by the +// tfprotov5.ProviderServers associated with muxServer into a single schema. +// Resources and data sources must be returned from only one server. Provider +// and ProviderMeta schemas must be identical between all servers. +func (s muxServer) GetProviderSchema(ctx context.Context, req *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { + rpc := "GetProviderSchema" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + logging.MuxTrace(ctx, "serving cached schema information") + + return &tfprotov5.GetProviderSchemaResponse{ + Provider: s.providerSchema, + ResourceSchemas: s.resourceSchemas, + DataSourceSchemas: s.dataSourceSchemas, + ProviderMeta: s.providerMetaSchema, + }, nil +} diff --git a/tf5muxserver/mux_server_GetProviderSchema_test.go b/tf5muxserver/mux_server_GetProviderSchema_test.go new file mode 100644 index 0000000..fb61bd5 --- /dev/null +++ b/tf5muxserver/mux_server_GetProviderSchema_test.go @@ -0,0 +1,456 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerGetProviderSchema(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []func() tfprotov5.ProviderServer + expectedDataSourceSchemas map[string]*tfprotov5.Schema + expectedProviderSchema *tfprotov5.Schema + expectedProviderMetaSchema *tfprotov5.Schema + expectedResourceSchemas map[string]*tfprotov5.Schema + }{ + "combined": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "account_id", + Type: tftypes.String, + Required: true, + Description: "the account ID to make requests for", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Optional: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, + }, + }, + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 4, + Block: &tfprotov5.SchemaBlock{ + Version: 4, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "module_id", + Type: tftypes.String, + Optional: true, + Description: "a unique identifier for the module", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "airspeed_velocity", + Type: tftypes.Number, + Required: true, + Description: "the airspeed velocity of a swallow", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Optional: true, + Description: "your name", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "color", + Type: tftypes.String, + Optional: true, + Description: "your favorite color", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "current_time", + Type: tftypes.String, + Computed: true, + Description: "the current time in RFC 3339 format", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "account_id", + Type: tftypes.String, + Required: true, + Description: "the account ID to make requests for", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Optional: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, + }, + }, + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 4, + Block: &tfprotov5.SchemaBlock{ + Version: 4, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "module_id", + Type: tftypes.String, + Optional: true, + Description: "a unique identifier for the module", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_quux": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "a", + Type: tftypes.String, + Required: true, + Description: "the account ID to make requests for", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "b", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "a", + Type: tftypes.Number, + Computed: true, + Description: "some field that's set by the provider", + DescriptionKind: tfprotov5.StringKindMarkdown, + }, + }, + }, + }, + "test_quux": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "abc", + Type: tftypes.Number, + Computed: true, + Description: "some other field that's set by the provider", + DescriptionKind: tfprotov5.StringKindMarkdown, + }, + }, + }, + }, + }, + }).ProviderServer, + }, + expectedProviderSchema: &tfprotov5.Schema{ + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "account_id", + Type: tftypes.String, + Required: true, + Description: "the account ID to make requests for", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ + { + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Optional: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + }, + }, + }, + expectedProviderMetaSchema: &tfprotov5.Schema{ + Version: 4, + Block: &tfprotov5.SchemaBlock{ + Version: 4, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "module_id", + Type: tftypes.String, + Optional: true, + Description: "a unique identifier for the module", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + expectedResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "airspeed_velocity", + Type: tftypes.Number, + Required: true, + Description: "the airspeed velocity of a swallow", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "name", + Type: tftypes.String, + Optional: true, + Description: "your name", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "color", + Type: tftypes.String, + Optional: true, + Description: "your favorite color", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_quux": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "a", + Type: tftypes.String, + Required: true, + Description: "the account ID to make requests for", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "b", + Type: tftypes.String, + Required: true, + }, + }, + }, + }, + }, + expectedDataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "current_time", + Type: tftypes.String, + Computed: true, + Description: "the current time in RFC 3339 format", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, + }, + "test_bar": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "a", + Type: tftypes.Number, + Computed: true, + Description: "some field that's set by the provider", + DescriptionKind: tfprotov5.StringKindMarkdown, + }, + }, + }, + }, + "test_quux": { + Version: 1, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "abc", + Type: tftypes.Number, + Computed: true, + Description: "some other field that's set by the provider", + DescriptionKind: tfprotov5.StringKindMarkdown, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), testCase.servers...) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + resp, err := muxServer.ProviderServer().GetProviderSchema(context.Background(), &tfprotov5.GetProviderSchemaRequest{}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := cmp.Diff(resp.DataSourceSchemas, testCase.expectedDataSourceSchemas); diff != "" { + t.Errorf("data source schemas didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.Provider, testCase.expectedProviderSchema); diff != "" { + t.Errorf("provider schema didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.ProviderMeta, testCase.expectedProviderMetaSchema); diff != "" { + t.Errorf("provider_meta schema didn't match expectations: %s", diff) + } + + if diff := cmp.Diff(resp.ResourceSchemas, testCase.expectedResourceSchemas); diff != "" { + t.Errorf("resource schemas didn't match expectations: %s", diff) + } + }) + } +} diff --git a/tf5muxserver/mux_server_ImportResourceState.go b/tf5muxserver/mux_server_ImportResourceState.go new file mode 100644 index 0000000..cff90d9 --- /dev/null +++ b/tf5muxserver/mux_server_ImportResourceState.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ImportResourceState calls the ImportResourceState method, passing `req`, on +// the provider that returned the resource specified by req.TypeName in its +// schema. +func (s muxServer) ImportResourceState(ctx context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { + rpc := "ImportResourceState" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ImportResourceState(ctx, req) +} diff --git a/tf5muxserver/mux_server_ImportResourceState_test.go b/tf5muxserver/mux_server_ImportResourceState_test.go new file mode 100644 index 0000000..d10a64c --- /dev/null +++ b/tf5muxserver/mux_server_ImportResourceState_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerImportResourceState(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ImportResourceState(ctx, &tfprotov5.ImportResourceStateRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ImportResourceStateCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 ImportResourceState to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ImportResourceStateCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 ImportResourceState called on server2") + } + + _, err = muxServer.ProviderServer().ImportResourceState(ctx, &tfprotov5.ImportResourceStateRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ImportResourceStateCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 ImportResourceState called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ImportResourceStateCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 ImportResourceState to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_PlanResourceChange.go b/tf5muxserver/mux_server_PlanResourceChange.go new file mode 100644 index 0000000..1ce933e --- /dev/null +++ b/tf5muxserver/mux_server_PlanResourceChange.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// PlanResourceChange calls the PlanResourceChange method, passing `req`, on +// the provider that returned the resource specified by req.TypeName in its +// schema. +func (s muxServer) PlanResourceChange(ctx context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { + rpc := "PlanResourceChange" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.PlanResourceChange(ctx, req) +} diff --git a/tf5muxserver/mux_server_PlanResourceChange_test.go b/tf5muxserver/mux_server_PlanResourceChange_test.go new file mode 100644 index 0000000..ca880d3 --- /dev/null +++ b/tf5muxserver/mux_server_PlanResourceChange_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerPlanResourceChange(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().PlanResourceChange(ctx, &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).PlanResourceChangeCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 PlanResourceChange to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).PlanResourceChangeCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 PlanResourceChange called on server2") + } + + _, err = muxServer.ProviderServer().PlanResourceChange(ctx, &tfprotov5.PlanResourceChangeRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).PlanResourceChangeCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 PlanResourceChange called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).PlanResourceChangeCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 PlanResourceChange to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_PrepareProviderConfig.go b/tf5muxserver/mux_server_PrepareProviderConfig.go new file mode 100644 index 0000000..68e6db1 --- /dev/null +++ b/tf5muxserver/mux_server_PrepareProviderConfig.go @@ -0,0 +1,58 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// PrepareProviderConfig calls the PrepareProviderConfig method on each server +// in order, passing `req`. Only one may respond with a non-nil PreparedConfig +// or a non-empty Diagnostics. +func (s muxServer) PrepareProviderConfig(ctx context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { + rpc := "PrepareProviderConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + var resp *tfprotov5.PrepareProviderConfigResponse + + for _, server := range s.servers { + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + res, err := server.PrepareProviderConfig(ctx, req) + + if err != nil { + return resp, fmt.Errorf("error from %T validating provider config: %w", server, err) + } + + if res == nil { + continue + } + + if resp == nil { + resp = res + continue + } + + if len(res.Diagnostics) > 0 { + // This could implement Diagnostic deduplication if/when + // implemented upstream. + resp.Diagnostics = append(resp.Diagnostics, res.Diagnostics...) + } + + if res.PreparedConfig != nil { + // This could check equality to bypass the error, however + // DynamicValue does not implement Equals() and previous mux server + // implementations have not requested the enhancement. + if resp.PreparedConfig != nil { + return nil, fmt.Errorf("got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use") + } + + resp.PreparedConfig = res.PreparedConfig + } + } + + return resp, nil +} diff --git a/tf5muxserver/mux_server_PrepareProviderConfig_test.go b/tf5muxserver/mux_server_PrepareProviderConfig_test.go new file mode 100644 index 0000000..e21ba86 --- /dev/null +++ b/tf5muxserver/mux_server_PrepareProviderConfig_test.go @@ -0,0 +1,357 @@ +package tf5muxserver_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerPrepareProviderConfig(t *testing.T) { + t.Parallel() + + config, err := tfprotov5.NewDynamicValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "hello": tftypes.String, + }, + }, map[string]tftypes.Value{ + "hello": tftypes.NewValue(tftypes.String, "world"), + })) + + if err != nil { + t.Fatalf("error constructing config: %s", err) + } + + testCases := map[string]struct { + servers []func() tfprotov5.ProviderServer + expectedError error + expectedResponse *tfprotov5.PrepareProviderConfigResponse + }{ + "error-once": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "error-multiple": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "warning-once": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }, + "warning-multiple": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }, + "warning-then-error": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }, + "no-response": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + }, + "PreparedConfig-once": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }, + "PreparedConfig-once-and-error": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + }, + PreparedConfig: &config, + }, + }, + "PreparedConfig-once-and-warning": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + }, + }).ProviderServer, + }, + expectedResponse: &tfprotov5.PrepareProviderConfigResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + }, + PreparedConfig: &config, + }, + }, + "PreparedConfig-multiple": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + PrepareProviderConfigResponse: &tfprotov5.PrepareProviderConfigResponse{ + PreparedConfig: &config, + }, + }).ProviderServer, + }, + expectedError: fmt.Errorf("got a PrepareProviderConfig PreparedConfig response from multiple servers, not sure which to use"), + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), testCase.servers...) + + if err != nil { + t.Fatalf("error setting up muxer: %s", err) + } + + got, err := muxServer.ProviderServer().PrepareProviderConfig(context.Background(), &tfprotov5.PrepareProviderConfigRequest{ + Config: &config, + }) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("wanted no error, got error: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("wanted error %q, got error: %s", testCase.expectedError.Error(), err.Error()) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, wanted err: %s", testCase.expectedError) + } + + if !cmp.Equal(got, testCase.expectedResponse) { + t.Errorf("unexpected response: %s", cmp.Diff(got, testCase.expectedResponse)) + } + + for num, server := range testCase.servers { + if !server().(*tf5testserver.TestServer).PrepareProviderConfigCalled { + t.Errorf("PrepareProviderConfig not called on server%d", num+1) + } + } + }) + } +} diff --git a/tf5muxserver/mux_server_ReadDataSource.go b/tf5muxserver/mux_server_ReadDataSource.go new file mode 100644 index 0000000..cdd8834 --- /dev/null +++ b/tf5muxserver/mux_server_ReadDataSource.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ReadDataSource calls the ReadDataSource method, passing `req`, on the +// provider that returned the data source specified by req.TypeName in its +// schema. +func (s muxServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { + rpc := "ReadDataSource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.dataSources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ReadDataSource(ctx, req) +} diff --git a/tf5muxserver/mux_server_ReadDataSource_test.go b/tf5muxserver/mux_server_ReadDataSource_test.go new file mode 100644 index 0000000..21734ae --- /dev/null +++ b/tf5muxserver/mux_server_ReadDataSource_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerReadDataSource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ReadDataSource(ctx, &tfprotov5.ReadDataSourceRequest{ + TypeName: "test_data_source_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ReadDataSourceCalled["test_data_source_server1"] { + t.Errorf("expected test_data_source_server1 ReadDataSource to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ReadDataSourceCalled["test_data_source_server1"] { + t.Errorf("unexpected test_data_source_server1 ReadDataSource called on server2") + } + + _, err = muxServer.ProviderServer().ReadDataSource(ctx, &tfprotov5.ReadDataSourceRequest{ + TypeName: "test_data_source_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ReadDataSourceCalled["test_data_source_server2"] { + t.Errorf("unexpected test_data_source_server2 ReadDataSource called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ReadDataSourceCalled["test_data_source_server2"] { + t.Errorf("expected test_data_source_server2 ReadDataSource to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_ReadResource.go b/tf5muxserver/mux_server_ReadResource.go new file mode 100644 index 0000000..e9837a0 --- /dev/null +++ b/tf5muxserver/mux_server_ReadResource.go @@ -0,0 +1,27 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ReadResource calls the ReadResource method, passing `req`, on the provider +// that returned the resource specified by req.TypeName in its schema. +func (s muxServer) ReadResource(ctx context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { + rpc := "ReadResource" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ReadResource(ctx, req) +} diff --git a/tf5muxserver/mux_server_ReadResource_test.go b/tf5muxserver/mux_server_ReadResource_test.go new file mode 100644 index 0000000..fd9bb45 --- /dev/null +++ b/tf5muxserver/mux_server_ReadResource_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerReadResource(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ReadResource(ctx, &tfprotov5.ReadResourceRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ReadResourceCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 ReadResource to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ReadResourceCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 ReadResource called on server2") + } + + _, err = muxServer.ProviderServer().ReadResource(ctx, &tfprotov5.ReadResourceRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ReadResourceCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 ReadResource called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ReadResourceCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 ReadResource to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_StopProvider.go b/tf5muxserver/mux_server_StopProvider.go new file mode 100644 index 0000000..8550211 --- /dev/null +++ b/tf5muxserver/mux_server_StopProvider.go @@ -0,0 +1,40 @@ +package tf5muxserver + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// StopProvider calls the StopProvider function for each provider associated +// with the muxServer, one at a time. All Error fields will be joined +// together and returned, but will not prevent the rest of the providers' +// StopProvider methods from being called. +func (s muxServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { + rpc := "StopProvider" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + var errs []string + + for _, server := range s.servers { + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + resp, err := server.StopProvider(ctx, req) + + if err != nil { + return resp, fmt.Errorf("error stopping %T: %w", server, err) + } + + if resp.Error != "" { + errs = append(errs, resp.Error) + } + } + + return &tfprotov5.StopProviderResponse{ + Error: strings.Join(errs, "\n"), + }, nil +} diff --git a/tf5muxserver/mux_server_StopProvider_test.go b/tf5muxserver/mux_server_StopProvider_test.go new file mode 100644 index 0000000..d6809a6 --- /dev/null +++ b/tf5muxserver/mux_server_StopProvider_test.go @@ -0,0 +1,44 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerStopProvider(t *testing.T) { + t.Parallel() + + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + StopProviderError: "error in server2", + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + (&tf5testserver.TestServer{ + StopProviderError: "error in server4", + }).ProviderServer, + (&tf5testserver.TestServer{}).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(context.Background(), servers...) + + if err != nil { + t.Fatalf("error setting up muxer: %s", err) + } + + _, err = muxServer.ProviderServer().StopProvider(context.Background(), &tfprotov5.StopProviderRequest{}) + + if err != nil { + t.Fatalf("error calling StopProvider: %s", err) + } + + for num, server := range servers { + if !server().(*tf5testserver.TestServer).StopProviderCalled { + t.Errorf("StopProvider not called on server%d", num+1) + } + } +} diff --git a/tf5muxserver/mux_server_UpgradeResourceState.go b/tf5muxserver/mux_server_UpgradeResourceState.go new file mode 100644 index 0000000..f788f78 --- /dev/null +++ b/tf5muxserver/mux_server_UpgradeResourceState.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// UpgradeResourceState calls the UpgradeResourceState method, passing `req`, +// on the provider that returned the resource specified by req.TypeName in its +// schema. +func (s muxServer) UpgradeResourceState(ctx context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { + rpc := "UpgradeResourceState" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.UpgradeResourceState(ctx, req) +} diff --git a/tf5muxserver/mux_server_UpgradeResourceState_test.go b/tf5muxserver/mux_server_UpgradeResourceState_test.go new file mode 100644 index 0000000..450d461 --- /dev/null +++ b/tf5muxserver/mux_server_UpgradeResourceState_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerUpgradeResourceState(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().UpgradeResourceState(ctx, &tfprotov5.UpgradeResourceStateRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).UpgradeResourceStateCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 UpgradeResourceState to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).UpgradeResourceStateCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 UpgradeResourceState called on server2") + } + + _, err = muxServer.ProviderServer().UpgradeResourceState(ctx, &tfprotov5.UpgradeResourceStateRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).UpgradeResourceStateCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 UpgradeResourceState called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).UpgradeResourceStateCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 UpgradeResourceState to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_ValidateDataSourceConfig.go b/tf5muxserver/mux_server_ValidateDataSourceConfig.go new file mode 100644 index 0000000..ac8f534 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateDataSourceConfig.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ValidateDataSourceConfig calls the ValidateDataSourceConfig method, passing +// `req`, on the provider that returned the data source specified by +// req.TypeName in its schema. +func (s muxServer) ValidateDataSourceConfig(ctx context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { + rpc := "ValidateDataSourceConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.dataSources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ValidateDataSourceConfig(ctx, req) +} diff --git a/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go b/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go new file mode 100644 index 0000000..3e4b643 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateDataSourceConfig_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerValidateDataSourceConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_data_source_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ValidateDataSourceConfig(ctx, &tfprotov5.ValidateDataSourceConfigRequest{ + TypeName: "test_data_source_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ValidateDataSourceConfigCalled["test_data_source_server1"] { + t.Errorf("expected test_data_source_server1 ValidateDataSourceConfig to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ValidateDataSourceConfigCalled["test_data_source_server1"] { + t.Errorf("unexpected test_data_source_server1 ValidateDataSourceConfig called on server2") + } + + _, err = muxServer.ProviderServer().ValidateDataSourceConfig(ctx, &tfprotov5.ValidateDataSourceConfigRequest{ + TypeName: "test_data_source_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ValidateDataSourceConfigCalled["test_data_source_server2"] { + t.Errorf("unexpected test_data_source_server2 ValidateDataSourceConfig called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ValidateDataSourceConfigCalled["test_data_source_server2"] { + t.Errorf("expected test_data_source_server2 ValidateDataSourceConfig to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_ValidateResourceTypeConfig.go b/tf5muxserver/mux_server_ValidateResourceTypeConfig.go new file mode 100644 index 0000000..16a7411 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateResourceTypeConfig.go @@ -0,0 +1,28 @@ +package tf5muxserver + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/logging" +) + +// ValidateResourceTypeConfig calls the ValidateResourceTypeConfig method, +// passing `req`, on the provider that returned the resource specified by +// req.TypeName in its schema. +func (s muxServer) ValidateResourceTypeConfig(ctx context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { + rpc := "ValidateResourceTypeConfig" + ctx = logging.InitContext(ctx) + ctx = logging.RpcContext(ctx, rpc) + server, ok := s.resources[req.TypeName] + + if !ok { + return nil, fmt.Errorf("%q isn't supported by any servers", req.TypeName) + } + + ctx = logging.Tfprotov5ProviderServerContext(ctx, server) + logging.MuxTrace(ctx, "calling downstream server") + + return server.ValidateResourceTypeConfig(ctx, req) +} diff --git a/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go b/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go new file mode 100644 index 0000000..dba84f5 --- /dev/null +++ b/tf5muxserver/mux_server_ValidateResourceTypeConfig_test.go @@ -0,0 +1,66 @@ +package tf5muxserver_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" +) + +func TestMuxServerValidateResourceTypeConfig(t *testing.T) { + t.Parallel() + + ctx := context.Background() + servers := []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server1": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_resource_server2": {}, + }, + }).ProviderServer, + } + + muxServer, err := tf5muxserver.NewMuxServer(ctx, servers...) + + if err != nil { + t.Fatalf("unexpected error setting up factory: %s", err) + } + + _, err = muxServer.ProviderServer().ValidateResourceTypeConfig(ctx, &tfprotov5.ValidateResourceTypeConfigRequest{ + TypeName: "test_resource_server1", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !servers[0]().(*tf5testserver.TestServer).ValidateResourceTypeConfigCalled["test_resource_server1"] { + t.Errorf("expected test_resource_server1 ValidateResourceTypeConfig to be called on server1") + } + + if servers[1]().(*tf5testserver.TestServer).ValidateResourceTypeConfigCalled["test_resource_server1"] { + t.Errorf("unexpected test_resource_server1 ValidateResourceTypeConfig called on server2") + } + + _, err = muxServer.ProviderServer().ValidateResourceTypeConfig(ctx, &tfprotov5.ValidateResourceTypeConfigRequest{ + TypeName: "test_resource_server2", + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if servers[0]().(*tf5testserver.TestServer).ValidateResourceTypeConfigCalled["test_resource_server2"] { + t.Errorf("unexpected test_resource_server2 ValidateResourceTypeConfig called on server1") + } + + if !servers[1]().(*tf5testserver.TestServer).ValidateResourceTypeConfigCalled["test_resource_server2"] { + t.Errorf("expected test_resource_server2 ValidateResourceTypeConfig to be called on server2") + } +} diff --git a/tf5muxserver/mux_server_example_test.go b/tf5muxserver/mux_server_example_test.go index af75a0c..5edf014 100644 --- a/tf5muxserver/mux_server_example_test.go +++ b/tf5muxserver/mux_server_example_test.go @@ -1,38 +1,39 @@ -package tfmux +package tf5muxserver_test import ( "context" "log" - "os" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" ) -func ExampleNewSchemaServerFactory_v2protocol() { +func ExampleNewMuxServer() { ctx := context.Background() - - // the ProviderServer from SDKv2 - // usually this is the Provider function - var sdkv2 func() tfprotov5.ProviderServer - - // the ProviderServer from the new protocol package - var protocolServer func() tfprotov5.ProviderServer + providers := []func() tfprotov5.ProviderServer{ + // Example terraform-plugin-sdk ProviderServer function + // sdkprovider.Provider().ProviderServer, + // + // Example terraform-plugin-go ProviderServer function + // goprovider.Provider(), + } // requests will be routed to whichever server advertises support for // them in the GetSchema response. Only one server may advertise // support for any given resource, data source, or the provider or // provider_meta schemas. An error will be returned if more than one // server claims support. - _, err := NewSchemaServerFactory(ctx, sdkv2, protocolServer) + muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + if err != nil { - log.Println(err.Error()) - os.Exit(1) + log.Fatalln(err.Error()) } - // use the result when instantiating the terraform-plugin-sdk.plugin.Serve - /* - plugin.Serve(&plugin.ServeOpts{ - GRPCProviderFunc: plugin.GRPCProviderFunc(factory), - }) - */ + // Use the result to start a muxed provider + err = tf5server.Serve("registry.terraform.io/namespace/example", muxServer.ProviderServer) + + if err != nil { + log.Fatalln(err.Error()) + } } diff --git a/tf5muxserver/mux_server_test.go b/tf5muxserver/mux_server_test.go index 5940a0d..b568464 100644 --- a/tf5muxserver/mux_server_test.go +++ b/tf5muxserver/mux_server_test.go @@ -1,1563 +1,575 @@ -package tfmux +package tf5muxserver_test import ( "context" + "fmt" "strings" "testing" - "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-go/tfprotov5" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-mux/internal/tf5testserver" + "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" ) -var _ tfprotov5.ProviderServer = &testServer{} - -func testFactory(s *testServer) func() tfprotov5.ProviderServer { - return func() tfprotov5.ProviderServer { - return s - } -} - -type testServer struct { - providerSchema *tfprotov5.Schema - providerMetaSchema *tfprotov5.Schema - resourceSchemas map[string]*tfprotov5.Schema - dataSourceSchemas map[string]*tfprotov5.Schema - - resourcesCalled map[string]bool - dataSourcesCalled map[string]bool - configureCalled bool - stopCalled bool - stopError string - - respondToPrepareProviderConfig bool - errorOnPrepareProviderConfig bool - warnOnPrepareProviderConfig bool -} - -func (s *testServer) GetProviderSchema(_ context.Context, _ *tfprotov5.GetProviderSchemaRequest) (*tfprotov5.GetProviderSchemaResponse, error) { - resources := s.resourceSchemas - if resources == nil { - resources = map[string]*tfprotov5.Schema{} - } - dataSources := s.dataSourceSchemas - if dataSources == nil { - dataSources = map[string]*tfprotov5.Schema{} - } - return &tfprotov5.GetProviderSchemaResponse{ - Provider: s.providerSchema, - ProviderMeta: s.providerMetaSchema, - ResourceSchemas: resources, - DataSourceSchemas: dataSources, - }, nil -} - -func (s *testServer) PrepareProviderConfig(_ context.Context, req *tfprotov5.PrepareProviderConfigRequest) (*tfprotov5.PrepareProviderConfigResponse, error) { - if s.respondToPrepareProviderConfig { - return &tfprotov5.PrepareProviderConfigResponse{ - PreparedConfig: req.Config, - }, nil - } - if s.errorOnPrepareProviderConfig { - return &tfprotov5.PrepareProviderConfigResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityError, - Summary: "hardcoded error for testing", - Detail: "testing that we only get errors from one thing", - }, - }, - }, nil - } - if s.warnOnPrepareProviderConfig { - return &tfprotov5.PrepareProviderConfigResponse{ - Diagnostics: []*tfprotov5.Diagnostic{ - { - Severity: tfprotov5.DiagnosticSeverityWarning, - Summary: "hardcoded warning for testing", - Detail: "testing that we only get warnings from one thing", - }, - }, - }, nil - } - return nil, nil -} - -func (s *testServer) StopProvider(_ context.Context, _ *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) { - s.stopCalled = true - if s.stopError != "" { - return &tfprotov5.StopProviderResponse{ - Error: s.stopError, - }, nil - } - return &tfprotov5.StopProviderResponse{}, nil -} - -func (s *testServer) ConfigureProvider(_ context.Context, _ *tfprotov5.ConfigureProviderRequest) (*tfprotov5.ConfigureProviderResponse, error) { - s.configureCalled = true - return &tfprotov5.ConfigureProviderResponse{}, nil -} - -func (s *testServer) ValidateResourceTypeConfig(_ context.Context, req *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) UpgradeResourceState(_ context.Context, req *tfprotov5.UpgradeResourceStateRequest) (*tfprotov5.UpgradeResourceStateResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) ReadResource(_ context.Context, req *tfprotov5.ReadResourceRequest) (*tfprotov5.ReadResourceResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) PlanResourceChange(_ context.Context, req *tfprotov5.PlanResourceChangeRequest) (*tfprotov5.PlanResourceChangeResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) ApplyResourceChange(_ context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) ImportResourceState(_ context.Context, req *tfprotov5.ImportResourceStateRequest) (*tfprotov5.ImportResourceStateResponse, error) { - s.resourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) ValidateDataSourceConfig(_ context.Context, req *tfprotov5.ValidateDataSourceConfigRequest) (*tfprotov5.ValidateDataSourceConfigResponse, error) { - s.dataSourcesCalled[req.TypeName] = true - return nil, nil -} - -func (s *testServer) ReadDataSource(_ context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) { - s.dataSourcesCalled[req.TypeName] = true - return nil, nil -} - -func TestSchemaServerGetProviderSchema_combined(t *testing.T) { - server1 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "enabled", - Type: tftypes.Bool, - Optional: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - }, - }, - }, - providerMetaSchema: &tfprotov5.Schema{ - Version: 4, - Block: &tfprotov5.SchemaBlock{ - Version: 4, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "module_id", - Type: tftypes.String, - Optional: true, - Description: "a unique identifier for the module", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, +func TestNewMuxServer(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + servers []func() tfprotov5.ProviderServer + expectedError error + }{ + "duplicate-data-source": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, + }, + }).ProviderServer, + (&tf5testserver.TestServer{ + DataSourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, + }, + }).ProviderServer, }, + expectedError: fmt.Errorf("data source \"test_foo\" is implemented by multiple servers; only one implementation allowed"), }, - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "airspeed_velocity", - Type: tftypes.Number, - Required: true, - Description: "the airspeed velocity of a swallow", - DescriptionKind: tfprotov5.StringKindPlain, - }, + "duplicate-resource": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, }, - }, - }, - "test_bar": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Optional: true, - Description: "your name", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "color", - Type: tftypes.String, - Optional: true, - Description: "your favorite color", - DescriptionKind: tfprotov5.StringKindPlain, - }, + }).ProviderServer, + (&tf5testserver.TestServer{ + ResourceSchemas: map[string]*tfprotov5.Schema{ + "test_foo": {}, }, - }, + }).ProviderServer, }, + expectedError: fmt.Errorf("resource \"test_foo\" is implemented by multiple servers; only one implementation allowed"), }, - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "current_time", - Type: tftypes.String, - Computed: true, - Description: "the current time in RFC 3339 format", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - }, - }) - server2 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + "provider-mismatch": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "enabled", - Type: tftypes.Bool, - Optional: true, - Description: "whether the feature is enabled", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, }, - }, - }, - }, - }, - }, - providerMetaSchema: &tfprotov5.Schema{ - Version: 4, - Block: &tfprotov5.SchemaBlock{ - Version: 4, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "module_id", - Type: tftypes.String, - Optional: true, - Description: "a unique identifier for the module", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_quux": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "a", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "b", - Type: tftypes.String, - Required: true, - }, - }, - }, - }, - }, - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_bar": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "a", - Type: tftypes.Number, - Computed: true, - Description: "some field that's set by the provider", - DescriptionKind: tfprotov5.StringKindMarkdown, - }, - }, - }, - }, - "test_quux": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "abc", - Type: tftypes.Number, - Computed: true, - Description: "some other field that's set by the provider", - DescriptionKind: tfprotov5.StringKindMarkdown, - }, - }, - }, - }, - }, - }) - - expectedProviderSchema := &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "enabled", - Type: tftypes.Bool, - Optional: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - }, - }, - } - expectedProviderMetaSchema := &tfprotov5.Schema{ - Version: 4, - Block: &tfprotov5.SchemaBlock{ - Version: 4, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "module_id", - Type: tftypes.String, - Optional: true, - Description: "a unique identifier for the module", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - } - expectedResourceSchemas := map[string]*tfprotov5.Schema{ - "test_foo": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "airspeed_velocity", - Type: tftypes.Number, - Required: true, - Description: "the airspeed velocity of a swallow", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - "test_bar": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "name", - Type: tftypes.String, - Optional: true, - Description: "your name", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "color", - Type: tftypes.String, - Optional: true, - Description: "your favorite color", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - "test_quux": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "a", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "b", - Type: tftypes.String, - Required: true, - }, - }, - }, - }, - } - expectedDataSourceSchemas := map[string]*tfprotov5.Schema{ - "test_foo": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "current_time", - Type: tftypes.String, - Computed: true, - Description: "the current time in RFC 3339 format", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - }, - }, - "test_bar": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "a", - Type: tftypes.Number, - Computed: true, - Description: "some field that's set by the provider", - DescriptionKind: tfprotov5.StringKindMarkdown, - }, - }, - }, - }, - "test_quux": { - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "abc", - Type: tftypes.Number, - Computed: true, - Description: "some other field that's set by the provider", - DescriptionKind: tfprotov5.StringKindMarkdown, - }, - }, - }, - }, - } - - factory, err := NewSchemaServerFactory(context.Background(), server1, server2) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - resp, err := factory.Server().GetProviderSchema(context.Background(), &tfprotov5.GetProviderSchemaRequest{}) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - if diff := cmp.Diff(resp.Provider, expectedProviderSchema); diff != "" { - t.Errorf("provider schema didn't match expectations: %s", diff) - } - - if diff := cmp.Diff(resp.ProviderMeta, expectedProviderMetaSchema); diff != "" { - t.Errorf("provider_meta schema didn't match expectations: %s", diff) - } - - if diff := cmp.Diff(resp.ResourceSchemas, expectedResourceSchemas); diff != "" { - t.Errorf("resource schemas didn't match expectations: %s", diff) - } - - if diff := cmp.Diff(resp.DataSourceSchemas, expectedDataSourceSchemas); diff != "" { - t.Errorf("data source schemas didn't match expectations: %s", diff) - } -} - -func TestSchemaServerGetProviderSchema_errorDuplicateResource(t *testing.T) { - server1 := testFactory(&testServer{ - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - server2 := testFactory(&testServer{ - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if !strings.Contains(err.Error(), "resource \"test_foo\" supported by multiple server implementations") { - t.Errorf("expected error about duplicated resources, got %q", err) - } -} - -func TestSchemaServerGetProviderSchema_errorDuplicateDataSource(t *testing.T) { - server1 := testFactory(&testServer{ - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - server2 := testFactory(&testServer{ - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if !strings.Contains(err.Error(), "data source \"test_foo\" supported by multiple server implementations") { - t.Errorf("expected error about duplicated data sources, got %q", err) - } -} - -func TestSchemaServerGetProviderSchema_providerOutOfOrder(t *testing.T) { - server1 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "secret", - Type: tftypes.String, - Required: true, - Description: "the secret to authenticate with", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "other_feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "enabled", - Type: tftypes.Bool, - Required: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }).ProviderServer, + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "enabled", - Type: tftypes.Bool, - Required: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Optional: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, + }).ProviderServer, }, + expectedError: fmt.Errorf("got a different provider schema across servers. Provider schemas must be identical across providers"), }, - }) - server2 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "secret", - Type: tftypes.String, - Required: true, - Description: "the secret to authenticate with", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + "provider-ordering": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "enabled", - Type: tftypes.Bool, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "whether the feature is enabled", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, { - Name: "feature_id", - Type: tftypes.Number, + Name: "secret", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the secret to authenticate with", DescriptionKind: tfprotov5.StringKindPlain, }, }, - }, - }, - { - TypeName: "other_feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "enabled", - Type: tftypes.Bool, - Required: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "other_feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, - }, - }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if err != nil { - t.Error(err) - } -} - -func TestSchemaServerGetProviderSchema_providerMetaOutOfOrder(t *testing.T) { - server1 := testFactory(&testServer{ - providerMetaSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "secret", - Type: tftypes.String, - Required: true, - Description: "the secret to authenticate with", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }).ProviderServer, + (&tf5testserver.TestServer{ + ProviderSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "secret", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the secret to authenticate with", DescriptionKind: tfprotov5.StringKindPlain, }, { - Name: "enabled", - Type: tftypes.Bool, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "whether the feature is enabled", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, }, - }, - }, - { - TypeName: "other_feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "enabled", - Type: tftypes.Bool, - Required: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "other_feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, + }).ProviderServer, }, }, - }) - server2 := testFactory(&testServer{ - providerMetaSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "secret", - Type: tftypes.String, - Required: true, - Description: "the secret to authenticate with", - DescriptionKind: tfprotov5.StringKindPlain, - }, - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "other_feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + "provider-meta-mismatch": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "enabled", - Type: tftypes.Bool, - Required: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }).ProviderServer, + (&tf5testserver.TestServer{ + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "enabled", - Type: tftypes.Bool, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "whether the feature is enabled", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, + }, + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Optional: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, + }).ProviderServer, }, + expectedError: fmt.Errorf("got a different provider meta schema across servers. Provider metadata schemas must be identical across providers"), }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if err != nil { - t.Error(err) - } -} - -func TestSchemaServerGetProviderSchema_errorProviderMismatch(t *testing.T) { - server1 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + "provider-meta-ordering": { + servers: []func() tfprotov5.ProviderServer{ + (&tf5testserver.TestServer{ + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, { - Name: "enabled", - Type: tftypes.Bool, + Name: "secret", + Type: tftypes.String, Required: true, - Description: "whether the feature is enabled", + Description: "the secret to authenticate with", DescriptionKind: tfprotov5.StringKindPlain, }, }, - }, - }, - }, - }, - }, - }) - server2 := testFactory(&testServer{ - providerSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, { - Name: "enabled", - Type: tftypes.Bool, - Optional: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "other_feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, - }, - }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if !strings.Contains(err.Error(), "got a different provider schema from two servers") { - t.Errorf("expected error about mismatched provider schemas, got %q", err) - } -} - -func TestSchemaServerGetProviderSchema_errorProviderMetaMismatch(t *testing.T) { - server1 := testFactory(&testServer{ - providerMetaSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + }).ProviderServer, + (&tf5testserver.TestServer{ + ProviderMetaSchema: &tfprotov5.Schema{ + Version: 1, Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, + Version: 1, Attributes: []*tfprotov5.SchemaAttribute{ { - Name: "feature_id", - Type: tftypes.Number, + Name: "secret", + Type: tftypes.String, Required: true, - Description: "The ID of the feature", + Description: "the secret to authenticate with", DescriptionKind: tfprotov5.StringKindPlain, }, { - Name: "enabled", - Type: tftypes.Bool, + Name: "account_id", + Type: tftypes.String, Required: true, - Description: "whether the feature is enabled", + Description: "the account ID to make requests for", DescriptionKind: tfprotov5.StringKindPlain, }, }, - }, - }, - }, - }, - }, - }) - server2 := testFactory(&testServer{ - providerMetaSchema: &tfprotov5.Schema{ - Version: 1, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Attributes: []*tfprotov5.SchemaAttribute{ - { - Name: "account_id", - Type: tftypes.String, - Required: true, - Description: "the account ID to make requests for", - DescriptionKind: tfprotov5.StringKindPlain, - }, - }, - BlockTypes: []*tfprotov5.SchemaNestedBlock{ - { - TypeName: "feature", - Nesting: tfprotov5.SchemaNestedBlockNestingModeList, - Block: &tfprotov5.SchemaBlock{ - Version: 1, - Description: "features to enable on the provider", - DescriptionKind: tfprotov5.StringKindPlain, - Attributes: []*tfprotov5.SchemaAttribute{ + BlockTypes: []*tfprotov5.SchemaNestedBlock{ { - Name: "feature_id", - Type: tftypes.Number, - Required: true, - Description: "The ID of the feature", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "other_feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, { - Name: "enabled", - Type: tftypes.Bool, - Optional: true, - Description: "whether the feature is enabled", - DescriptionKind: tfprotov5.StringKindPlain, + TypeName: "feature", + Nesting: tfprotov5.SchemaNestedBlockNestingModeList, + Block: &tfprotov5.SchemaBlock{ + Version: 1, + Description: "features to enable on the provider", + DescriptionKind: tfprotov5.StringKindPlain, + Attributes: []*tfprotov5.SchemaAttribute{ + { + Name: "enabled", + Type: tftypes.Bool, + Required: true, + Description: "whether the feature is enabled", + DescriptionKind: tfprotov5.StringKindPlain, + }, + { + Name: "feature_id", + Type: tftypes.Number, + Required: true, + Description: "The ID of the feature", + DescriptionKind: tfprotov5.StringKindPlain, + }, + }, + }, }, }, }, }, - }, + }).ProviderServer, }, }, - }) - - _, err := NewSchemaServerFactory(context.Background(), server1, server2) - if !strings.Contains(err.Error(), "got a different provider_meta schema from two servers") { - t.Errorf("expected error about mismatched provider_meta schemas, got %q", err) - } -} - -func TestSchemaServerPrepareProviderConfig_errorMultipleResponses(t *testing.T) { - config, err := tfprotov5.NewDynamicValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "hello": tftypes.String, - }, - }, tftypes.NewValue(tftypes.Object{ - AttributeTypes: map[string]tftypes.Type{ - "hello": tftypes.String, - }, - }, map[string]tftypes.Value{ - "hello": tftypes.NewValue(tftypes.String, "world"), - })) - if err != nil { - t.Fatalf("error constructing config: %s", err) - } - - server1 := testFactory(&testServer{ - respondToPrepareProviderConfig: true, - }) - server2 := testFactory(&testServer{}) - server3 := testFactory(&testServer{}) - factory, err := NewSchemaServerFactory(context.Background(), server1, server2, server3) - if err != nil { - t.Fatalf("error setting up muxer: %s", err) - } - _, err = factory.Server().PrepareProviderConfig(context.Background(), &tfprotov5.PrepareProviderConfigRequest{ - Config: &config, - }) - if err != nil { - t.Errorf("unexpected error when only one server replied to PrepareProviderConfig: %s", err) - } - - server1 = testFactory(&testServer{}) - server2 = testFactory(&testServer{}) - server3 = testFactory(&testServer{}) - factory, err = NewSchemaServerFactory(context.Background(), server1, server2, server3) - if err != nil { - t.Fatalf("error setting up muxer: %s", err) - } - _, err = factory.Server().PrepareProviderConfig(context.Background(), &tfprotov5.PrepareProviderConfigRequest{ - Config: &config, - }) - if err != nil { - t.Errorf("unexpected error when no servers replied to PrepareProviderConfig: %s", err) - } - - for _, fs := range [][]func() tfprotov5.ProviderServer{ - { - testFactory(&testServer{ - respondToPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - respondToPrepareProviderConfig: true, - }), - }, - { - testFactory(&testServer{ - respondToPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - errorOnPrepareProviderConfig: true, - }), - }, - { - testFactory(&testServer{ - respondToPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - warnOnPrepareProviderConfig: true, - }), - }, - { - testFactory(&testServer{ - errorOnPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - warnOnPrepareProviderConfig: true, - }), - }, - { - testFactory(&testServer{ - errorOnPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - errorOnPrepareProviderConfig: true, - }), - }, - { - testFactory(&testServer{ - warnOnPrepareProviderConfig: true, - }), - testFactory(&testServer{}), - testFactory(&testServer{ - warnOnPrepareProviderConfig: true, - }), - }, - } { - factory, err = NewSchemaServerFactory(context.Background(), fs...) - if err != nil { - t.Fatalf("error setting up muxer: %s", err) - } - _, err = factory.Server().PrepareProviderConfig(context.Background(), &tfprotov5.PrepareProviderConfigRequest{ - Config: &config, - }) - if !strings.Contains(err.Error(), "got a PrepareProviderConfig response from multiple servers") { - t.Errorf("expected error about multiple servers returning PrepareProviderConfigResponses, got %q", err) - } - } -} - -func TestSchemaServerConfigureProvider_configuredEveryone(t *testing.T) { - server1 := testFactory(&testServer{}) - server2 := testFactory(&testServer{}) - server3 := testFactory(&testServer{}) - server4 := testFactory(&testServer{}) - server5 := testFactory(&testServer{}) - factory, err := NewSchemaServerFactory(context.Background(), server1, server2, server3, server4, server5) - if err != nil { - t.Fatalf("error setting up muxer: %s", err) - } - _, err = factory.Server().ConfigureProvider(context.Background(), &tfprotov5.ConfigureProviderRequest{}) - if err != nil { - t.Fatalf("error calling ConfigureProvider: %s", err) - } - for num, f := range []func() tfprotov5.ProviderServer{ - server1, server2, server3, server4, server5, - } { - if !f().(*testServer).configureCalled { - t.Errorf("configure not called on server%d", num+1) - } - } -} - -func TestSchemaServerStopProvider_stoppedEveryone(t *testing.T) { - server1 := testFactory(&testServer{}) - server2 := testFactory(&testServer{ - stopError: "error in server2", - }) - server3 := testFactory(&testServer{}) - server4 := testFactory(&testServer{ - stopError: "error in server4", - }) - server5 := testFactory(&testServer{}) - factory, err := NewSchemaServerFactory(context.Background(), server1, server2, server3, server4, server5) - if err != nil { - t.Fatalf("error setting up muxer: %s", err) - } - _, err = factory.Server().StopProvider(context.Background(), &tfprotov5.StopProviderRequest{}) - if err != nil { - t.Fatalf("error calling StopProvider: %s", err) - } - for num, f := range []func() tfprotov5.ProviderServer{ - server1, server2, server3, server4, server5, - } { - if !f().(*testServer).stopCalled { - t.Errorf("stop not called on server%d", num+1) - } - } -} - -func TestSchemaServer_resourceRouting(t *testing.T) { - server1 := testFactory(&testServer{ - resourcesCalled: map[string]bool{}, - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - server2 := testFactory(&testServer{ - resourcesCalled: map[string]bool{}, - resourceSchemas: map[string]*tfprotov5.Schema{ - "test_bar": {}, - }, - }) - - factory, err := NewSchemaServerFactory(context.Background(), server1, server2) - if err != nil { - t.Fatalf("unexpected error setting up factory: %s", err) - } - - _, err = factory.Server().ValidateResourceTypeConfig(context.Background(), &tfprotov5.ValidateResourceTypeConfigRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") } - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} + for name, testCase := range testCases { + name, testCase := name, testCase - _, err = factory.Server().ValidateResourceTypeConfig(context.Background(), &tfprotov5.ValidateResourceTypeConfigRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } + t.Run(name, func(t *testing.T) { + t.Parallel() - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} + _, err := tf5muxserver.NewMuxServer(context.Background(), testCase.servers...) - _, err = factory.Server().UpgradeResourceState(context.Background(), &tfprotov5.UpgradeResourceStateRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().UpgradeResourceState(context.Background(), &tfprotov5.UpgradeResourceStateRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("unexpected error: %s", err) + } - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } - _, err = factory.Server().ReadResource(context.Background(), &tfprotov5.ReadResourceRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().ReadResource(context.Background(), &tfprotov5.ReadResourceRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().PlanResourceChange(context.Background(), &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().PlanResourceChange(context.Background(), &tfprotov5.PlanResourceChangeRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().ApplyResourceChange(context.Background(), &tfprotov5.ApplyResourceChangeRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().ApplyResourceChange(context.Background(), &tfprotov5.ApplyResourceChangeRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().ImportResourceState(context.Background(), &tfprotov5.ImportResourceStateRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).resourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).resourcesCalled = map[string]bool{} - server2().(*testServer).resourcesCalled = map[string]bool{} - - _, err = factory.Server().ImportResourceState(context.Background(), &tfprotov5.ImportResourceStateRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).resourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } -} - -func TestSchemaServer_dataSourceRouting(t *testing.T) { - server1 := testFactory(&testServer{ - dataSourcesCalled: map[string]bool{}, - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_foo": {}, - }, - }) - server2 := testFactory(&testServer{ - dataSourcesCalled: map[string]bool{}, - dataSourceSchemas: map[string]*tfprotov5.Schema{ - "test_bar": {}, - }, - }) - - factory, err := NewSchemaServerFactory(context.Background(), server1, server2) - if err != nil { - t.Fatalf("unexpected error setting up factory: %s", err) - } - - _, err = factory.Server().ValidateDataSourceConfig(context.Background(), &tfprotov5.ValidateDataSourceConfigRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).dataSourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).dataSourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).dataSourcesCalled = map[string]bool{} - server2().(*testServer).dataSourcesCalled = map[string]bool{} - - _, err = factory.Server().ReadDataSource(context.Background(), &tfprotov5.ReadDataSourceRequest{ - TypeName: "test_foo", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server1().(*testServer).dataSourcesCalled["test_foo"] { - t.Errorf("expected test_foo to be called on server1, was not") - } - if server2().(*testServer).dataSourcesCalled["test_foo"] { - t.Errorf("expected test_foo not to be called on server2, was") - } - - server1().(*testServer).dataSourcesCalled = map[string]bool{} - server2().(*testServer).dataSourcesCalled = map[string]bool{} - - _, err = factory.Server().ValidateDataSourceConfig(context.Background(), &tfprotov5.ValidateDataSourceConfigRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).dataSourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).dataSourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") - } - - server1().(*testServer).dataSourcesCalled = map[string]bool{} - server2().(*testServer).dataSourcesCalled = map[string]bool{} - - _, err = factory.Server().ReadDataSource(context.Background(), &tfprotov5.ReadDataSourceRequest{ - TypeName: "test_bar", - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - if !server2().(*testServer).dataSourcesCalled["test_bar"] { - t.Errorf("expected test_bar to be called on server2, was not") - } - if server1().(*testServer).dataSourcesCalled["test_bar"] { - t.Errorf("expected test_bar not to be called on server1, was") + if err == nil && testCase.expectedError != nil { + t.Fatalf("expected error: %s", testCase.expectedError) + } + }) } } diff --git a/tf5muxserver/schema_equality.go b/tf5muxserver/schema_equality.go new file mode 100644 index 0000000..acaae7d --- /dev/null +++ b/tf5muxserver/schema_equality.go @@ -0,0 +1,31 @@ +package tf5muxserver + +import ( + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// schemaCmpOptions ensures comparisons of SchemaAttribute and +// SchemaNestedBlock slices are considered equal despite ordering differences. +var schemaCmpOptions = []cmp.Option{ + cmpopts.SortSlices(func(i, j *tfprotov5.SchemaAttribute) bool { + return i.Name < j.Name + }), + cmpopts.SortSlices(func(i, j *tfprotov5.SchemaNestedBlock) bool { + return i.TypeName < j.TypeName + }), +} + +// schemaDiff outputs the difference between schemas while accounting for +// inconsequential ordering differences in SchemaAttribute and +// SchemaNestedBlock slices. +func schemaDiff(i, j *tfprotov5.Schema) string { + return cmp.Diff(i, j, schemaCmpOptions...) +} + +// schemaEquals asserts equality between schemas by normalizing inconsequential +// ordering differences in SchemaAttribute and SchemaNestedBlock slices. +func schemaEquals(i, j *tfprotov5.Schema) bool { + return cmp.Equal(i, j, schemaCmpOptions...) +}