Skip to content

Commit

Permalink
Another pass at more lenient schema checks.
Browse files Browse the repository at this point in the history
Make sure that schemas are compared semantically, by not caring about
the order of blocks or attributes in the results for Provider and
ProviderMeta.

Add tests to verify that this is the case.

When Provider or ProviderMeta schemas don't match, include the diff in
the error for easier debugging.
  • Loading branch information
paddycarver committed Feb 2, 2021
1 parent 44fd9b8 commit 6c12ef6
Show file tree
Hide file tree
Showing 2 changed files with 333 additions and 11 deletions.
24 changes: 13 additions & 11 deletions schema_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
320 changes: 320 additions & 0 deletions schema_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 6c12ef6

Please sign in to comment.