diff --git a/schema_server.go b/schema_server.go index 3915b14..2aba7a3 100644 --- a/schema_server.go +++ b/schema_server.go @@ -3,21 +3,23 @@ package tfmux import ( "context" "fmt" - "sort" "strings" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform-plugin-go/tfprotov5" ) var _ tfprotov5.ProviderServer = SchemaServer{} -var sortCmpTransformer = cmp.Transformer("Sort", func(in []*tfprotov5.SchemaAttribute) []*tfprotov5.SchemaAttribute { - copied := make([]*tfprotov5.SchemaAttribute, len(in)) - copy(copied, in) - sort.Slice(copied, func(i, j int) bool { return copied[i].Name < copied[j].Name }) - return in -}) +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 + }), +} // SchemaServerFactory is a generator for SchemaServers, which are Terraform // gRPC servers that route requests to different gRPC provider implementations @@ -87,14 +89,14 @@ func NewSchemaServerFactory(ctx context.Context, servers ...func() tfprotov5.Pro } 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, sortCmpTransformer) { - return factory, fmt.Errorf("got a different provider schema from two servers (%T, %T). Provider schemas must be identical across providers.", factory.servers[factory.providerSchemaFrom](), s) + 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, sortCmpTransformer) { - return factory, fmt.Errorf("got a different provider_meta schema from two servers (%T, %T). Provider metadata schemas must be identical across providers.", factory.servers[factory.providerMetaSchemaFrom](), s) + 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 diff --git a/schema_server_test.go b/schema_server_test.go index dc99814..dfb64b7 100644 --- a/schema_server_test.go +++ b/schema_server_test.go @@ -595,6 +595,326 @@ func TestSchemaServerGetProviderSchema_errorDuplicateDataSource(t *testing.T) { } } +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{ + { + 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, + }, + }, + }, + }, + }, + }, + }, + }) + 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, + 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, + }, + }, + }, + }, + { + 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 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, + 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: "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, + }, + }, + }, + }, + }, + }, + }, + }) + 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, + 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, + 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 err != nil { + t.Error(err) + } +} + func TestSchemaServerGetProviderSchema_errorProviderMismatch(t *testing.T) { server1 := testFactory(&testServer{ providerSchema: &tfprotov5.Schema{