use actual schema.Resources for state shims

Provider tests often rely on checking values contained within sets, by
directly accessing their flatmapped representation. In order to provider
the test harness with the expected set hashes, the sets must be
generated by the schema.Resource itself.

During the test we now build a fixed map of the providers, which should
only contain schema.Provider instances, and pass them into each
TestStep. The individual schema.Resource instances can then be pulled
from the providers, and used to recreate the state from the cty.Value
returned by the core operations.
This commit is contained in:
James Bardin 2019-01-10 12:20:03 -05:00
parent 35365e8ccf
commit a7b399cb4c
5 changed files with 100 additions and 116 deletions

View File

@ -4,25 +4,17 @@ import (
"fmt" "fmt"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/config/hcl2shim" "github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func mustShimNewState(newState *states.State, schemas *terraform.Schemas) *terraform.State {
s, err := shimNewState(newState, schemas)
if err != nil {
panic(err)
}
return s
}
// shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests // shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests
func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terraform.State, error) { func shimNewState(newState *states.State, providers map[string]terraform.ResourceProvider) (*terraform.State, error) {
state := terraform.NewState() state := terraform.NewState()
// in the odd case of a nil state, let the helper packages handle it // in the odd case of a nil state, let the helper packages handle it
@ -57,25 +49,10 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
resType := res.Addr.Type resType := res.Addr.Type
providerType := res.ProviderConfig.ProviderConfig.Type providerType := res.ProviderConfig.ProviderConfig.Type
providerSchema := schemas.Providers[providerType] resource := getResource(providers, providerType, resType)
if providerSchema == nil {
return nil, fmt.Errorf("missing schema for %q", providerType)
}
var resSchema *configschema.Block
switch res.Addr.Mode {
case addrs.ManagedResourceMode:
resSchema = providerSchema.ResourceTypes[resType]
case addrs.DataResourceMode:
resSchema = providerSchema.DataSources[resType]
}
if resSchema == nil {
return nil, fmt.Errorf("missing resource schema for %q in %q", resType, providerType)
}
for key, i := range res.Instances { for key, i := range res.Instances {
flatmap, err := shimmedAttributes(i.Current, resSchema.ImpliedType()) flatmap, err := shimmedAttributes(i.Current, resource)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding state for %q: %s", resType, err) return nil, fmt.Errorf("error decoding state for %q: %s", resType, err)
} }
@ -114,7 +91,7 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
// add any deposed instances // add any deposed instances
for _, dep := range i.Deposed { for _, dep := range i.Deposed {
flatmap, err := shimmedAttributes(dep, resSchema.ImpliedType()) flatmap, err := shimmedAttributes(dep, resource)
if err != nil { if err != nil {
return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err) return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err)
} }
@ -139,17 +116,46 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
return state, nil return state, nil
} }
func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, ty cty.Type) (map[string]string, error) { func getResource(providers map[string]terraform.ResourceProvider, providerName, resourceType string) *schema.Resource {
p := providers[providerName]
if p == nil {
panic(fmt.Sprintf("provider %q not found in test step", providerName))
}
// this is only for tests, so should only see schema.Providers
provider := p.(*schema.Provider)
resource := provider.ResourcesMap[resourceType]
if resource != nil {
return resource
}
resource = provider.DataSourcesMap[resourceType]
if resource != nil {
return resource
}
panic(fmt.Sprintf("resource %s not found in test step", resourceType))
}
func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.Resource) (map[string]string, error) {
flatmap := instance.AttrsFlat flatmap := instance.AttrsFlat
// if we have json attrs, they need to be decoded if flatmap != nil {
if flatmap == nil { return flatmap, nil
rio, err := instance.Decode(ty)
if err != nil {
return nil, err
}
flatmap = hcl2shim.FlatmapValueFromHCL2(rio.Value)
} }
return flatmap, nil
// if we have json attrs, they need to be decoded
rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType())
if err != nil {
return nil, err
}
instanceState, err := res.ShimInstanceStateFromValue(rio.Value)
if err != nil {
return nil, err
}
return instanceState.Attributes, nil
} }

View File

@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
@ -286,46 +286,29 @@ func TestStateShim(t *testing.T) {
}, },
} }
schemas := &terraform.Schemas{ providers := map[string]terraform.ResourceProvider{
Providers: map[string]*terraform.ProviderSchema{ "test": &schema.Provider{
"test": { ResourcesMap: map[string]*schema.Resource{
ResourceTypes: map[string]*configschema.Block{ "test_thing": &schema.Resource{
"test_thing": &configschema.Block{ Schema: map[string]*schema.Schema{
Attributes: map[string]*configschema.Attribute{ "id": {Type: schema.TypeString, Computed: true},
"id": { "fizzle": {Type: schema.TypeString, Optional: true},
Type: cty.String, "bazzle": {Type: schema.TypeString, Optional: true},
Computed: true,
},
"fizzle": {
Type: cty.String,
Optional: true,
},
"bazzle": {
Type: cty.String,
Optional: true,
},
},
}, },
}, },
DataSources: map[string]*configschema.Block{ },
"test_data_thing": &configschema.Block{ DataSourcesMap: map[string]*schema.Resource{
Attributes: map[string]*configschema.Attribute{ "test_data_thing": &schema.Resource{
"id": { Schema: map[string]*schema.Schema{
Type: cty.String, "id": {Type: schema.TypeString, Computed: true},
Computed: true, "fuzzle": {Type: schema.TypeString, Optional: true},
},
"fuzzle": {
Type: cty.String,
Optional: true,
},
},
}, },
}, },
}, },
}, },
} }
shimmed, err := shimNewState(state, schemas) shimmed, err := shimNewState(state, providers)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -382,6 +382,10 @@ type TestStep struct {
// be refreshed and don't matter. // be refreshed and don't matter.
ImportStateVerify bool ImportStateVerify bool
ImportStateVerifyIgnore []string ImportStateVerifyIgnore []string
// provider s is used internally to maintain a reference to the
// underlying providers during the tests
providers map[string]terraform.ResourceProvider
} }
// Set to a file mask in sprintf format where %s is test name // Set to a file mask in sprintf format where %s is test name
@ -476,6 +480,17 @@ func Test(t TestT, c TestCase) {
c.PreCheck() c.PreCheck()
} }
// get instances of all providers, so we can use the individual
// resources to shim the state during the tests.
providers := make(map[string]terraform.ResourceProvider)
for name, pf := range testProviderFactories(c) {
p, err := pf()
if err != nil {
t.Fatal(err)
}
providers[name] = p
}
providerResolver, err := testProviderResolver(c) providerResolver, err := testProviderResolver(c)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -491,6 +506,10 @@ func Test(t TestT, c TestCase) {
idRefresh := c.IDRefreshName != "" idRefresh := c.IDRefreshName != ""
errored := false errored := false
for i, step := range c.Steps { for i, step := range c.Steps {
// insert the providers into the step so we can get the resources for
// shimming the state
step.providers = providers
var err error var err error
log.Printf("[DEBUG] Test: Executing step %d", i) log.Printf("[DEBUG] Test: Executing step %d", i)
@ -600,6 +619,7 @@ func Test(t TestT, c TestCase) {
Destroy: true, Destroy: true,
PreventDiskCleanup: lastStep.PreventDiskCleanup, PreventDiskCleanup: lastStep.PreventDiskCleanup,
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh, PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
providers: providers,
} }
log.Printf("[WARN] Test: Executing destroy step") log.Printf("[WARN] Test: Executing destroy step")
@ -629,12 +649,10 @@ func testProviderConfig(c TestCase) string {
return strings.Join(lines, "") return strings.Join(lines, "")
} }
// testProviderResolver is a helper to build a ResourceProviderResolver // testProviderFactories combines the fixed Providers and
// with pre instantiated ResourceProviders, so that we can reset them for the // ResourceProviderFactory functions into a single map of
// test, while only calling the factory function once. // ResourceProviderFactory functions.
// Any errors are stored so that they can be returned by the factory in func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory {
// terraform to match non-test behavior.
func testProviderResolver(c TestCase) (providers.Resolver, error) {
ctxProviders := make(map[string]terraform.ResourceProviderFactory) ctxProviders := make(map[string]terraform.ResourceProviderFactory)
for k, pf := range c.ProviderFactories { for k, pf := range c.ProviderFactories {
ctxProviders[k] = pf ctxProviders[k] = pf
@ -644,6 +662,16 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
for k, p := range c.Providers { for k, p := range c.Providers {
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
} }
return ctxProviders
}
// testProviderResolver is a helper to build a ResourceProviderResolver
// with pre instantiated ResourceProviders, so that we can reset them for the
// test, while only calling the factory function once.
// Any errors are stored so that they can be returned by the factory in
// terraform to match non-test behavior.
func testProviderResolver(c TestCase) (providers.Resolver, error) {
ctxProviders := testProviderFactories(c)
// wrap the old provider factories in the test grpc server so they can be // wrap the old provider factories in the test grpc server so they can be
// called from terraform. // called from terraform.
@ -667,32 +695,6 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
return providers.ResolverFixed(newProviders), nil return providers.ResolverFixed(newProviders), nil
} }
// testProviderFactores returns a fixed and reset factories for creating a resolver
func testProviderFactories(c TestCase) (map[string]providers.Factory, error) {
factories := c.ProviderFactories
if factories == nil {
factories = make(map[string]terraform.ResourceProviderFactory)
}
// add any fixed providers
for k, p := range c.Providers {
factories[k] = terraform.ResourceProviderFactoryFixed(p)
}
// wrap the providers to be GRPC mocks rather than legacy terraform.ResourceProvider
newFactories := make(map[string]providers.Factory)
for k, pf := range factories {
newFactories[k] = func() (providers.Interface, error) {
p, err := pf()
if err != nil {
return nil, err
}
return GRPCTestProvider(p), nil
}
}
return newFactories, nil
}
// UnitTest is a helper to force the acceptance testing harness to run in the // UnitTest is a helper to force the acceptance testing harness to run in the
// normal unit test suite. This should only be used for resource that don't // normal unit test suite. This should only be used for resource that don't
// have any external dependencies. // have any external dependencies.

View File

@ -62,14 +62,11 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
log.Printf("[WARN] Config warnings:\n%s", stepDiags) log.Printf("[WARN] Config warnings:\n%s", stepDiags)
} }
// We will need access to the schemas in order to shim to the old-style
// testing API.
schemas := ctx.Schemas()
// Refresh! // Refresh!
newState, stepDiags := ctx.Refresh() newState, stepDiags := ctx.Refresh()
// shim the state first so the test can check the state on errors // shim the state first so the test can check the state on errors
state, err = shimNewState(newState, schemas)
state, err = shimNewState(newState, step.providers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -95,7 +92,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
// Apply the diff, creating real resources. // Apply the diff, creating real resources.
newState, stepDiags = ctx.Apply() newState, stepDiags = ctx.Apply()
// shim the state first so the test can check the state on errors // shim the state first so the test can check the state on errors
state, err = shimNewState(newState, schemas) state, err = shimNewState(newState, step.providers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -139,7 +136,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
return state, newOperationError("follow-up refresh", stepDiags) return state, newOperationError("follow-up refresh", stepDiags)
} }
state, err = shimNewState(newState, schemas) state, err = shimNewState(newState, step.providers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -190,7 +187,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
// //
// This is here only for compatibility with existing tests that predate our // This is here only for compatibility with existing tests that predate our
// new plan and state types, and should not be used in new tests. Instead, use // new plan and state types, and should not be used in new tests. Instead, use
// a library like "cmp" to do a deep equality check and diff on the two // a library like "cmp" to do a deep equality and diff on the two
// data structures. // data structures.
func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
return fmt.Sprintf( return fmt.Sprintf(

View File

@ -62,10 +62,6 @@ func testStepImportState(
return state, stepDiags.Err() return state, stepDiags.Err()
} }
// We will need access to the schemas in order to shim to the old-style
// testing API.
schemas := ctx.Schemas()
// The test step provides the resource address as a string, so we need // The test step provides the resource address as a string, so we need
// to parse it to get an addrs.AbsResourceAddress to pass in to the // to parse it to get an addrs.AbsResourceAddress to pass in to the
// import method. // import method.
@ -95,7 +91,7 @@ func testStepImportState(
return state, stepDiags.Err() return state, stepDiags.Err()
} }
newState, err := shimNewState(importedState, schemas) newState, err := shimNewState(importedState, step.providers)
if err != nil { if err != nil {
return nil, err return nil, err
} }