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:
parent
35365e8ccf
commit
a7b399cb4c
|
@ -4,25 +4,17 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/config/hcl2shim"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
|
||||
"github.com/hashicorp/terraform/states"
|
||||
"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
|
||||
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()
|
||||
|
||||
// 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
|
||||
providerType := res.ProviderConfig.ProviderConfig.Type
|
||||
|
||||
providerSchema := schemas.Providers[providerType]
|
||||
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)
|
||||
}
|
||||
resource := getResource(providers, providerType, resType)
|
||||
|
||||
for key, i := range res.Instances {
|
||||
flatmap, err := shimmedAttributes(i.Current, resSchema.ImpliedType())
|
||||
flatmap, err := shimmedAttributes(i.Current, resource)
|
||||
if err != nil {
|
||||
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
|
||||
for _, dep := range i.Deposed {
|
||||
flatmap, err := shimmedAttributes(dep, resSchema.ImpliedType())
|
||||
flatmap, err := shimmedAttributes(dep, resource)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if flatmap != nil {
|
||||
return flatmap, nil
|
||||
}
|
||||
|
||||
// if we have json attrs, they need to be decoded
|
||||
if flatmap == nil {
|
||||
rio, err := instance.Decode(ty)
|
||||
rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flatmap = hcl2shim.FlatmapValueFromHCL2(rio.Value)
|
||||
instanceState, err := res.ShimInstanceStateFromValue(rio.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return flatmap, nil
|
||||
|
||||
return instanceState.Attributes, nil
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"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/terraform"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
@ -286,46 +286,29 @@ func TestStateShim(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
schemas := &terraform.Schemas{
|
||||
Providers: map[string]*terraform.ProviderSchema{
|
||||
"test": {
|
||||
ResourceTypes: map[string]*configschema.Block{
|
||||
"test_thing": &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"fizzle": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"bazzle": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
providers := map[string]terraform.ResourceProvider{
|
||||
"test": &schema.Provider{
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"test_thing": &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"id": {Type: schema.TypeString, Computed: true},
|
||||
"fizzle": {Type: schema.TypeString, Optional: true},
|
||||
"bazzle": {Type: schema.TypeString, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
DataSources: map[string]*configschema.Block{
|
||||
"test_data_thing": &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"fuzzle": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"test_data_thing": &schema.Resource{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"id": {Type: schema.TypeString, Computed: true},
|
||||
"fuzzle": {Type: schema.TypeString, Optional: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
shimmed, err := shimNewState(state, schemas)
|
||||
shimmed, err := shimNewState(state, providers)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -382,6 +382,10 @@ type TestStep struct {
|
|||
// be refreshed and don't matter.
|
||||
ImportStateVerify bool
|
||||
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
|
||||
|
@ -476,6 +480,17 @@ func Test(t TestT, c TestCase) {
|
|||
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)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -491,6 +506,10 @@ func Test(t TestT, c TestCase) {
|
|||
idRefresh := c.IDRefreshName != ""
|
||||
errored := false
|
||||
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
|
||||
log.Printf("[DEBUG] Test: Executing step %d", i)
|
||||
|
||||
|
@ -600,6 +619,7 @@ func Test(t TestT, c TestCase) {
|
|||
Destroy: true,
|
||||
PreventDiskCleanup: lastStep.PreventDiskCleanup,
|
||||
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
|
||||
providers: providers,
|
||||
}
|
||||
|
||||
log.Printf("[WARN] Test: Executing destroy step")
|
||||
|
@ -629,12 +649,10 @@ func testProviderConfig(c TestCase) string {
|
|||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// testProviderFactories combines the fixed Providers and
|
||||
// ResourceProviderFactory functions into a single map of
|
||||
// ResourceProviderFactory functions.
|
||||
func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory {
|
||||
ctxProviders := make(map[string]terraform.ResourceProviderFactory)
|
||||
for k, pf := range c.ProviderFactories {
|
||||
ctxProviders[k] = pf
|
||||
|
@ -644,6 +662,16 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
|
|||
for k, p := range c.Providers {
|
||||
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
|
||||
// called from terraform.
|
||||
|
@ -667,32 +695,6 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
|
|||
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
|
||||
// normal unit test suite. This should only be used for resource that don't
|
||||
// have any external dependencies.
|
||||
|
|
|
@ -62,14 +62,11 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
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!
|
||||
newState, stepDiags := ctx.Refresh()
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -95,7 +92,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
// Apply the diff, creating real resources.
|
||||
newState, stepDiags = ctx.Apply()
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -139,7 +136,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
return state, newOperationError("follow-up refresh", stepDiags)
|
||||
}
|
||||
|
||||
state, err = shimNewState(newState, schemas)
|
||||
state, err = shimNewState(newState, step.providers)
|
||||
if err != nil {
|
||||
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
|
||||
// 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.
|
||||
func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
|
||||
return fmt.Sprintf(
|
||||
|
|
|
@ -62,10 +62,6 @@ func testStepImportState(
|
|||
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
|
||||
// to parse it to get an addrs.AbsResourceAddress to pass in to the
|
||||
// import method.
|
||||
|
@ -95,7 +91,7 @@ func testStepImportState(
|
|||
return state, stepDiags.Err()
|
||||
}
|
||||
|
||||
newState, err := shimNewState(importedState, schemas)
|
||||
newState, err := shimNewState(importedState, step.providers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue