start work on helper/resource test fixtures

The helper resource tests won't pass for now, as they use a
terraform.MockProvider which can't be used in the schema.Provider shims.
This commit is contained in:
James Bardin 2018-10-15 21:15:08 -04:00 committed by Martin Atkins
parent f8b1a3c7a4
commit 0d4d572c39
11 changed files with 1359 additions and 324 deletions

View File

@ -0,0 +1,43 @@
package resource
import (
"context"
"net"
"time"
"google.golang.org/grpc/test/bufconn"
"github.com/hashicorp/terraform/helper/plugin"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/plugin/proto"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/terraform"
"google.golang.org/grpc"
)
// GRPCTestProvider takes a legacy ResourceProvider, wraps it in the new GRPC
// shim and starts it in a grpc server using an inmem connection. It returns a
// GRPCClient for this new server to test the shimmed resource provider.
func GRPCTestProvider(rp terraform.ResourceProvider) providers.Interface {
listener := bufconn.Listen(256 * 1024)
grpcServer := grpc.NewServer()
p := plugin.NewGRPCProviderServerShim(rp)
proto.RegisterProviderServer(grpcServer, p)
go grpcServer.Serve(listener)
conn, err := grpc.Dial("", grpc.WithDialer(func(string, time.Duration) (net.Conn, error) {
return listener.Dial()
}), grpc.WithInsecure())
if err != nil {
panic(err)
}
var pp tfplugin.GRPCProviderPlugin
client, _ := pp.GRPCClient(context.Background(), nil, conn)
grpcClient := client.(*tfplugin.GRPCProvider)
grpcClient.TestListener = listener
return grpcClient
}

View File

@ -0,0 +1,137 @@
package resource
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/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) {
state := terraform.NewState()
for _, newMod := range newState.Modules {
mod := state.AddModule(newMod.Addr)
for name, out := range newMod.OutputValues {
outputType := ""
val := hcl2shim.ConfigValueFromHCL2(out.Value)
ty := out.Value.Type()
switch {
case ty == cty.String:
outputType = "string"
case ty.IsTupleType() || ty.IsListType():
outputType = "list"
case ty.IsMapType():
outputType = "map"
}
mod.Outputs[name] = &terraform.OutputState{
Type: outputType,
Value: val,
Sensitive: out.Sensitive,
}
}
for _, res := range newMod.Resources {
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("mising resource schema for %q in %q", resType, providerType)
}
for key, i := range res.Instances {
flatmap, err := shimmedAttributes(i.Current, resSchema.ImpliedType())
if err != nil {
return nil, fmt.Errorf("error decoding state for %q: %s", resType, err)
}
resState := &terraform.ResourceState{
Type: resType,
Primary: &terraform.InstanceState{
ID: flatmap["id"],
Attributes: flatmap,
Tainted: i.Current.Status == states.ObjectTainted,
},
}
for _, dep := range i.Current.Dependencies {
resState.Dependencies = append(resState.Dependencies, dep.String())
}
// convert the indexes to the old style flapmap indexes
idx := ""
switch key.(type) {
case addrs.IntKey:
idx = fmt.Sprintf(".%d", key)
case addrs.StringKey:
idx = "." + key.String()
}
mod.Resources[res.Addr.String()+idx] = resState
// ad any deposed instances
for _, dep := range i.Deposed {
flatmap, err := shimmedAttributes(dep, resSchema.ImpliedType())
if err != nil {
return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err)
}
deposed := &terraform.InstanceState{
ID: flatmap["id"],
Attributes: flatmap,
Tainted: dep.Status == states.ObjectTainted,
}
resState.Deposed = append(resState.Deposed, deposed)
}
}
}
}
return state, nil
}
func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, ty cty.Type) (map[string]string, error) {
flatmap := instance.AttrsFlat
// if we have json attrs, they need to be decoded
if flatmap == nil {
rio, err := instance.Decode(ty)
if err != nil {
return nil, err
}
flatmap = hcl2shim.FlatmapValueFromHCL2(rio.Value)
}
return flatmap, nil
}

View File

@ -0,0 +1,314 @@
package resource
import (
"testing"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
)
// TestStateShim is meant to be a fairly comprehensive test, checking for dependencies, root outputs,
func TestStateShim(t *testing.T) {
state := states.NewState()
rootModule := state.RootModule()
if rootModule == nil {
t.Errorf("root module is nil; want valid object")
}
rootModule.SetOutputValue("bar", cty.ListVal([]cty.Value{cty.StringVal("bar"), cty.StringVal("value")}), false)
rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true)
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsFlat: map[string]string{"id": "foo", "bazzle": "dazzle"},
//AttrsJSON: []byte(`{"id": "foo", "bazzle":"dazzle"}`),
Dependencies: []addrs.Referenceable{
addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: 'M',
Type: "test_thing",
Name: "baz",
},
},
},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(addrs.RootModuleInstance),
)
rootModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "baz",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
//AttrsJSON: []byte(`{"id": "baz", "bazzle":"dazzle"}`),
AttrsFlat: map[string]string{"id": "baz", "bazzle": "dazzle"},
Dependencies: []addrs.Referenceable{},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(addrs.RootModuleInstance),
)
childInstance := addrs.RootModuleInstance.Child("child", addrs.NoKey)
childModule := state.EnsureModule(childInstance)
childModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.DataResourceMode,
Type: "test_data_thing",
Name: "foo",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
//AttrsFlat: map[string]string{"id": "bar", "fuzzle": "wuzzle"},
AttrsJSON: []byte(`{"id": "bar", "fuzzle":"wuzzle"}`),
Dependencies: []addrs.Referenceable{},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(childInstance),
)
childModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "baz",
}.Instance(addrs.NoKey),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
//AttrsFlat: map[string]string{"id": "bar", "fizzle": "wizzle"},
AttrsJSON: []byte(`{"id": "bar", "fizzle":"wizzle"}`),
Dependencies: []addrs.Referenceable{
addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: 'D',
Type: "test_data_thing",
Name: "foo",
},
},
},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(childInstance),
)
childModule.SetResourceInstanceDeposed(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "baz",
}.Instance(addrs.NoKey),
"00000001",
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsFlat: map[string]string{"id": "old", "fizzle": "wizzle"},
//AttrsJSON: []byte(`{"id": "old", "fizzle":"wizzle"}`),
Dependencies: []addrs.Referenceable{
addrs.ResourceInstance{
Resource: addrs.Resource{
Mode: 'D',
Type: "test_data_thing",
Name: "foo",
},
},
},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(childInstance),
)
childModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "lots",
}.Instance(addrs.IntKey(0)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsFlat: map[string]string{"id": "0", "bazzle": "dazzle"},
//AttrsJSON: []byte(`{"id": "0", "bazzle":"dazzle"}`),
Dependencies: []addrs.Referenceable{},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(childInstance),
)
childModule.SetResourceInstanceCurrent(
addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "test_thing",
Name: "lots",
}.Instance(addrs.IntKey(1)),
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsFlat: map[string]string{"id": "1", "bazzle": "dazzle"},
//AttrsJSON: []byte(`{"id": "1", "bazzle":"dazzle"}`),
Dependencies: []addrs.Referenceable{},
},
addrs.ProviderConfig{
Type: "test",
}.Absolute(childInstance),
)
expected := &terraform.State{
Version: 3,
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]*terraform.OutputState{
"bar": {
Type: "list",
Value: []interface{}{"bar", "value"},
},
"secret": {
Sensitive: true,
Type: "string",
Value: "secret value",
},
},
Resources: map[string]*terraform.ResourceState{
"test_thing.baz": &terraform.ResourceState{
Type: "test_thing",
Primary: &terraform.InstanceState{
ID: "baz",
Attributes: map[string]string{
"id": "baz",
"bazzle": "dazzle",
},
},
},
"test_thing.foo": &terraform.ResourceState{
Type: "test_thing",
Primary: &terraform.InstanceState{
ID: "foo",
Attributes: map[string]string{
"id": "foo",
"bazzle": "dazzle",
},
},
Dependencies: []string{"test_thing.baz"},
},
},
},
&terraform.ModuleState{
Path: []string{"root", "child"},
Resources: map[string]*terraform.ResourceState{
"test_thing.baz": &terraform.ResourceState{
Type: "test_thing",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"id": "bar",
"fizzle": "wizzle",
},
},
Deposed: []*terraform.InstanceState{
{
ID: "old",
Attributes: map[string]string{
"id": "old",
"fizzle": "wizzle",
},
},
},
Dependencies: []string{"data.test_data_thing.foo"},
},
"data.test_data_thing.foo": &terraform.ResourceState{
Type: "test_data_thing",
Primary: &terraform.InstanceState{
ID: "bar",
Attributes: map[string]string{
"id": "bar",
"fuzzle": "wuzzle",
},
},
},
"test_thing.lots.0": &terraform.ResourceState{
Type: "test_thing",
Primary: &terraform.InstanceState{
ID: "0",
Attributes: map[string]string{
"id": "0",
"bazzle": "dazzle",
},
},
},
"test_thing.lots.1": &terraform.ResourceState{
Type: "test_thing",
Primary: &terraform.InstanceState{
ID: "1",
Attributes: map[string]string{
"id": "1",
"bazzle": "dazzle",
},
Tainted: true,
},
},
},
},
},
}
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,
},
},
},
},
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,
},
},
},
},
},
},
}
shimmed, err := shimNewState(state, schemas)
if err != nil {
t.Fatal(err)
}
if !expected.Equal(shimmed) {
t.Fatalf("\nexpected state:\n%s\ngot state:\n%s", expected, shimmed)
}
}

View File

@ -14,6 +14,10 @@ import (
"syscall" "syscall"
"testing" "testing"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
@ -444,176 +448,209 @@ func ParallelTest(t TestT, c TestCase) {
// long, we require the verbose flag so users are able to see progress // long, we require the verbose flag so users are able to see progress
// output. // output.
func Test(t TestT, c TestCase) { func Test(t TestT, c TestCase) {
t.Fatal("resource.Test is not yet updated for the new provider API") // We only run acceptance tests if an env var is set because they're
return // slow and generally require some outside configuration. You can opt out
/* // of this with OverrideEnvVar on individual TestCases.
// We only run acceptance tests if an env var is set because they're if os.Getenv(TestEnvVar) == "" && !c.IsUnitTest {
// slow and generally require some outside configuration. You can opt out t.Skip(fmt.Sprintf(
// of this with OverrideEnvVar on individual TestCases. "Acceptance tests skipped unless env '%s' set",
if os.Getenv(TestEnvVar) == "" && !c.IsUnitTest { TestEnvVar))
t.Skip(fmt.Sprintf( return
"Acceptance tests skipped unless env '%s' set", }
TestEnvVar))
return
}
logWriter, err := LogOutput(t) logWriter, err := LogOutput(t)
if err != nil { if err != nil {
t.Error(fmt.Errorf("error setting up logging: %s", err)) t.Error(fmt.Errorf("error setting up logging: %s", err))
} }
log.SetOutput(logWriter) log.SetOutput(logWriter)
// We require verbose mode so that the user knows what is going on. // We require verbose mode so that the user knows what is going on.
if !testTesting && !testing.Verbose() && !c.IsUnitTest { if !testTesting && !testing.Verbose() && !c.IsUnitTest {
t.Fatal("Acceptance tests must be run with the -v flag on tests") t.Fatal("Acceptance tests must be run with the -v flag on tests")
return return
} }
// Run the PreCheck if we have it // Run the PreCheck if we have it
if c.PreCheck != nil { if c.PreCheck != nil {
c.PreCheck() c.PreCheck()
} }
providerResolver, err := testProviderResolver(c) providerResolver, err := testProviderResolver(c)
if err != nil {
t.Fatal(err)
}
// collect the provider schemas
schemas := &terraform.Schemas{
Providers: make(map[string]*terraform.ProviderSchema),
}
factories, err := testProviderFactories(c)
if err != nil {
t.Fatal(err)
}
for providerName, f := range factories {
p, err := f()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
opts := terraform.ContextOpts{ProviderResolver: providerResolver}
// A single state variable to track the lifecycle, starting with no state resp := p.GetSchema()
var state *terraform.State if resp.Diagnostics.HasErrors() {
t.Fatal(fmt.Sprintf("error fetching schema for %q: %v", providerName, resp.Diagnostics.Err()))
}
// Go through each step and run it providerSchema := &terraform.ProviderSchema{
var idRefreshCheck *terraform.ResourceState Provider: resp.Provider.Block,
idRefresh := c.IDRefreshName != "" ResourceTypes: make(map[string]*configschema.Block),
errored := false DataSources: make(map[string]*configschema.Block),
for i, step := range c.Steps { }
var err error
log.Printf("[DEBUG] Test: Executing step %d", i)
if step.SkipFunc != nil { for r, s := range resp.ResourceTypes {
skip, err := step.SkipFunc() providerSchema.ResourceTypes[r] = s.Block
if err != nil { }
t.Fatal(err)
}
if skip {
log.Printf("[WARN] Skipping step %d", i)
continue
}
}
if step.Config == "" && !step.ImportState { for d, s := range resp.DataSources {
err = fmt.Errorf( providerSchema.DataSources[d] = s.Block
"unknown test mode for step. Please see TestStep docs\n\n%#v", }
step)
} else {
if step.ImportState {
if step.Config == "" {
step.Config = testProviderConfig(c)
}
// Can optionally set step.Config in addition to schemas.Providers[providerName] = providerSchema
// step.ImportState, to provide config for the import. }
state, err = testStepImportState(opts, state, step)
} else {
state, err = testStepConfig(opts, state, step)
}
}
// If we expected an error, but did not get one, fail opts := terraform.ContextOpts{ProviderResolver: providerResolver}
if err == nil && step.ExpectError != nil {
errored = true
t.Error(fmt.Sprintf(
"Step %d, no error received, but expected a match to:\n\n%s\n\n",
i, step.ExpectError))
break
}
// If there was an error, exit // A single state variable to track the lifecycle, starting with no state
var state *terraform.State
// Go through each step and run it
var idRefreshCheck *terraform.ResourceState
idRefresh := c.IDRefreshName != ""
errored := false
for i, step := range c.Steps {
var err error
log.Printf("[DEBUG] Test: Executing step %d", i)
if step.SkipFunc != nil {
skip, err := step.SkipFunc()
if err != nil { if err != nil {
// Perhaps we expected an error? Check if it matches t.Fatal(err)
if step.ExpectError != nil { }
if !step.ExpectError.MatchString(err.Error()) { if skip {
errored = true log.Printf("[WARN] Skipping step %d", i)
t.Error(fmt.Sprintf( continue
"Step %d, expected error:\n\n%s\n\nTo match:\n\n%s\n\n", }
i, err, step.ExpectError)) }
break
} if step.Config == "" && !step.ImportState {
} else { err = fmt.Errorf(
"unknown test mode for step. Please see TestStep docs\n\n%#v",
step)
} else {
if step.ImportState {
if step.Config == "" {
step.Config = testProviderConfig(c)
}
// Can optionally set step.Config in addition to
// step.ImportState, to provide config for the import.
state, err = testStepImportState(opts, state, step, schemas)
} else {
state, err = testStepConfig(opts, state, step, schemas)
}
}
// If we expected an error, but did not get one, fail
if err == nil && step.ExpectError != nil {
errored = true
t.Error(fmt.Sprintf(
"Step %d, no error received, but expected a match to:\n\n%s\n\n",
i, step.ExpectError))
break
}
// If there was an error, exit
if err != nil {
// Perhaps we expected an error? Check if it matches
if step.ExpectError != nil {
if !step.ExpectError.MatchString(err.Error()) {
errored = true errored = true
t.Error(fmt.Sprintf( t.Error(fmt.Sprintf(
"Step %d error: %s", i, err)) "Step %d, expected error:\n\n%s\n\nTo match:\n\n%s\n\n",
i, err, step.ExpectError))
break
}
} else {
errored = true
t.Error(fmt.Sprintf(
"Step %d error: %s", i, err))
break
}
}
// If we've never checked an id-only refresh and our state isn't
// empty, find the first resource and test it.
if idRefresh && idRefreshCheck == nil && !state.Empty() {
// Find the first non-nil resource in the state
for _, m := range state.Modules {
if len(m.Resources) > 0 {
if v, ok := m.Resources[c.IDRefreshName]; ok {
idRefreshCheck = v
}
break break
} }
} }
// If we've never checked an id-only refresh and our state isn't // If we have an instance to check for refreshes, do it
// empty, find the first resource and test it. // immediately. We do it in the middle of another test
if idRefresh && idRefreshCheck == nil && !state.Empty() { // because it shouldn't affect the overall state (refresh
// Find the first non-nil resource in the state // is read-only semantically) and we want to fail early if
for _, m := range state.Modules { // this fails. If refresh isn't read-only, then this will have
if len(m.Resources) > 0 { // caught a different bug.
if v, ok := m.Resources[c.IDRefreshName]; ok { if idRefreshCheck != nil {
idRefreshCheck = v log.Printf(
} "[WARN] Test: Running ID-only refresh check on %s",
idRefreshCheck.Primary.ID)
break if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil {
} log.Printf("[ERROR] Test: ID-only test failed: %s", err)
} t.Error(fmt.Sprintf(
"[ERROR] Test: ID-only test failed: %s", err))
// If we have an instance to check for refreshes, do it break
// immediately. We do it in the middle of another test
// because it shouldn't affect the overall state (refresh
// is read-only semantically) and we want to fail early if
// this fails. If refresh isn't read-only, then this will have
// caught a different bug.
if idRefreshCheck != nil {
log.Printf(
"[WARN] Test: Running ID-only refresh check on %s",
idRefreshCheck.Primary.ID)
if err := testIDOnlyRefresh(c, opts, step, idRefreshCheck); err != nil {
log.Printf("[ERROR] Test: ID-only test failed: %s", err)
t.Error(fmt.Sprintf(
"[ERROR] Test: ID-only test failed: %s", err))
break
}
} }
} }
} }
}
// If we never checked an id-only refresh, it is a failure. // If we never checked an id-only refresh, it is a failure.
if idRefresh { if idRefresh {
if !errored && len(c.Steps) > 0 && idRefreshCheck == nil { if !errored && len(c.Steps) > 0 && idRefreshCheck == nil {
t.Error("ID-only refresh check never ran.") t.Error("ID-only refresh check never ran.")
} }
}
// If we have a state, then run the destroy
if state != nil {
lastStep := c.Steps[len(c.Steps)-1]
destroyStep := TestStep{
Config: lastStep.Config,
Check: c.CheckDestroy,
Destroy: true,
PreventDiskCleanup: lastStep.PreventDiskCleanup,
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
} }
// If we have a state, then run the destroy log.Printf("[WARN] Test: Executing destroy step")
if state != nil { state, err := testStep(opts, state, destroyStep, schemas)
lastStep := c.Steps[len(c.Steps)-1] if err != nil {
destroyStep := TestStep{ t.Error(fmt.Sprintf(
Config: lastStep.Config, "Error destroying resource! WARNING: Dangling resources\n"+
Check: c.CheckDestroy, "may exist. The full state and error is shown below.\n\n"+
Destroy: true, "Error: %s\n\nState: %s",
PreventDiskCleanup: lastStep.PreventDiskCleanup, err,
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh, state))
}
log.Printf("[WARN] Test: Executing destroy step")
state, err := testStep(opts, state, destroyStep)
if err != nil {
t.Error(fmt.Sprintf(
"Error destroying resource! WARNING: Dangling resources\n"+
"may exist. The full state and error is shown below.\n\n"+
"Error: %s\n\nState: %s",
err,
state))
}
} else {
log.Printf("[WARN] Skipping destroy test since there is no state.")
} }
*/ } else {
log.Printf("[WARN] Skipping destroy test since there is no state.")
}
} }
// testProviderConfig takes the list of Providers in a TestCase and returns a // testProviderConfig takes the list of Providers in a TestCase and returns a
@ -633,7 +670,7 @@ func testProviderConfig(c TestCase) string {
// test, while only calling the factory function once. // test, while only calling the factory function once.
// Any errors are stored so that they can be returned by the factory in // Any errors are stored so that they can be returned by the factory in
// terraform to match non-test behavior. // terraform to match non-test behavior.
func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error) { func testProviderResolver(c TestCase) (providers.Resolver, error) {
ctxProviders := c.ProviderFactories ctxProviders := c.ProviderFactories
if ctxProviders == nil { if ctxProviders == nil {
ctxProviders = make(map[string]terraform.ResourceProviderFactory) ctxProviders = make(map[string]terraform.ResourceProviderFactory)
@ -644,6 +681,10 @@ func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p) ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
} }
// wrap the old provider factories in the test grpc server so they can be
// called from terraform.
newProviders := make(map[string]providers.Factory)
// reset the providers if needed // reset the providers if needed
for k, pf := range ctxProviders { for k, pf := range ctxProviders {
// we can ignore any errors here, if we don't have a provider to reset // we can ignore any errors here, if we don't have a provider to reset
@ -652,15 +693,48 @@ func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
// FIXME: verify if this is still needed with the new plugins being
// closed after every walk.
if p, ok := p.(TestProvider); ok { if p, ok := p.(TestProvider); ok {
err := p.TestReset() err := p.TestReset()
if err != nil { if err != nil {
return nil, fmt.Errorf("[ERROR] failed to reset provider %q: %s", k, err) return nil, fmt.Errorf("[ERROR] failed to reset provider %q: %s", k, err)
} }
} }
// The provider is wrapped in a GRPCTestProvider so that it can be
// passed back to terraform core as a providers.Interface, rather
// than the legacy ResourceProvider.
newProviders[k] = providers.FactoryFixed(GRPCTestProvider(p))
} }
return terraform.ResourceProviderResolverFixed(ctxProviders), 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)
}
// now that the provider are all loaded in factories, fix each of them into
// a providers.Factory
newFactories := make(map[string]providers.Factory)
for k, pf := range factories {
p, err := pf()
if err != nil {
return nil, err
}
newFactories[k] = providers.FactoryFixed(GRPCTestProvider(p))
}
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

View File

@ -2,162 +2,355 @@ package resource
import ( import (
"errors" "errors"
"bufio"
"bytes"
"fmt" "fmt"
"log"
"sort"
"strings"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/tfdiags"
) )
// testStepConfig runs a config-mode test step // testStepConfig runs a config-mode test step
func testStepConfig( func testStepConfig(
opts terraform.ContextOpts, opts terraform.ContextOpts,
state *terraform.State, state *terraform.State,
step TestStep) (*terraform.State, error) { step TestStep,
return testStep(opts, state, step) schemas *terraform.Schemas) (*terraform.State, error) {
return testStep(opts, state, step, schemas)
} }
func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep) (*terraform.State, error) { func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep, schemas *terraform.Schemas) (*terraform.State, error) {
return nil, fmt.Errorf("testStep not yet updated for new state type") if !step.Destroy {
/* if err := testStepTaint(state, step); err != nil {
// Pre-taint any resources that have been defined in Taint, as long as this
// is not a destroy step.
if !step.Destroy {
if err := testStepTaint(state, step); err != nil {
return state, err
}
}
cfg, err := testConfig(opts, step)
if err != nil {
return state, err return state, err
} }
}
var stepDiags tfdiags.Diagnostics cfg, err := testConfig(opts, step)
if err != nil {
return state, err
}
// Build the context var stepDiags tfdiags.Diagnostics
opts.Config = cfg
opts.State = state // Build the context
opts.Destroy = step.Destroy opts.Config = cfg
ctx, stepDiags := terraform.NewContext(&opts) opts.State = terraform.MustShimLegacyState(state)
opts.Destroy = step.Destroy
ctx, stepDiags := terraform.NewContext(&opts)
if stepDiags.HasErrors() {
return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err())
}
if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
if stepDiags.HasErrors() { if stepDiags.HasErrors() {
return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err()) return nil, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
}
if stepDiags := ctx.Validate(); len(stepDiags) > 0 {
if stepDiags.HasErrors() {
return nil, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err())
}
log.Printf("[WARN] Config warnings:\n%s", stepDiags)
} }
// Refresh! log.Printf("[WARN] Config warnings:\n%s", stepDiags)
state, stepDiags = ctx.Refresh() }
// Refresh!
newState, stepDiags := ctx.Refresh()
state = mustShimNewState(newState, schemas)
if stepDiags.HasErrors() {
return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err())
}
// If this step is a PlanOnly step, skip over this first Plan and subsequent
// Apply, and use the follow up Plan that checks for perpetual diffs
if !step.PlanOnly {
// Plan!
if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
return state, fmt.Errorf("Error planning: %s", stepDiags.Err())
} else {
log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
}
// We need to keep a copy of the state prior to destroying
// such that destroy steps can verify their behaviour in the check
// function
stateBeforeApplication := state.DeepCopy()
// Apply the diff, creating real resources.
newState, stepDiags = ctx.Apply()
state = mustShimNewState(newState, schemas)
if stepDiags.HasErrors() { if stepDiags.HasErrors() {
return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err()) return state, fmt.Errorf("Error applying: %s", stepDiags.Err())
} }
// If this step is a PlanOnly step, skip over this first Plan and subsequent // Run any configured checks
// Apply, and use the follow up Plan that checks for perpetual diffs if step.Check != nil {
if !step.PlanOnly { if step.Destroy {
// Plan! if err := step.Check(stateBeforeApplication); err != nil {
if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() { return state, fmt.Errorf("Check failed: %s", err)
return state, fmt.Errorf("Error planning: %s", stepDiags.Err()) }
} else { } else {
log.Printf("[WARN] Test: Step plan: %s", p) if err := step.Check(state); err != nil {
} return state, fmt.Errorf("Check failed: %s", err)
// We need to keep a copy of the state prior to destroying
// such that destroy steps can verify their behaviour in the check
// function
stateBeforeApplication := state.DeepCopy()
// Apply the diff, creating real resources.
state, stepDiags = ctx.Apply()
if stepDiags.HasErrors() {
return state, fmt.Errorf("Error applying: %s", stepDiags.Err())
}
// Run any configured checks
if step.Check != nil {
if step.Destroy {
if err := step.Check(stateBeforeApplication); err != nil {
return state, fmt.Errorf("Check failed: %s", err)
}
} else {
if err := step.Check(state); err != nil {
return state, fmt.Errorf("Check failed: %s", err)
}
} }
} }
} }
}
// Now, verify that Plan is now empty and we don't have a perpetual diff issue // Now, verify that Plan is now empty and we don't have a perpetual diff issue
// We do this with TWO plans. One without a refresh. // We do this with TWO plans. One without a refresh.
var p *terraform.Plan var p *plans.Plan
if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err()) return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err())
}
if !p.Changes.Empty() {
if step.ExpectNonEmptyPlan {
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
} else {
return state, fmt.Errorf(
"After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
} }
if p.Diff != nil && !p.Diff.Empty() { }
if step.ExpectNonEmptyPlan {
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) // And another after a Refresh.
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
newState, stepDiags = ctx.Refresh()
if stepDiags.HasErrors() {
return state, fmt.Errorf("Error on follow-up refresh: %s", stepDiags.Err())
}
state = mustShimNewState(newState, schemas)
}
if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
return state, fmt.Errorf("Error on second follow-up plan: %s", stepDiags.Err())
}
empty := p.Changes.Empty()
// Data resources are tricky because they legitimately get instantiated
// during refresh so that they will be already populated during the
// plan walk. Because of this, if we have any data resources in the
// config we'll end up wanting to destroy them again here. This is
// acceptable and expected, and we'll treat it as "empty" for the
// sake of this testing.
if step.Destroy && !empty {
empty = true
for _, change := range p.Changes.Resources {
if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
empty = false
break
}
}
}
if !empty {
if step.ExpectNonEmptyPlan {
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
} else {
return state, fmt.Errorf(
"After applying this step and refreshing, "+
"the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
}
}
// Made it here, but expected a non-empty plan, fail!
if step.ExpectNonEmptyPlan && empty {
return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
}
// Made it here? Good job test step!
return state, nil
}
// legacyPlanComparisonString produces a string representation of the changes
// from a plan and a given state togther, as was formerly produced by the
// String method of terraform.Plan.
//
// 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
// data structures.
func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
return fmt.Sprintf(
"DIFF:\n\n%s\n\nSTATE:\n\n%s",
legacyDiffComparisonString(changes),
state.String(),
)
}
// legacyDiffComparisonString produces a string representation of the changes
// from a planned changes object, as was formerly produced by the String method
// of terraform.Diff.
//
// This is here only for compatibility with existing tests that predate our
// new plan 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 data structures.
func legacyDiffComparisonString(changes *plans.Changes) string {
// The old string representation of a plan was grouped by module, but
// our new plan structure is not grouped in that way and so we'll need
// to preprocess it in order to produce that grouping.
type ResourceChanges struct {
Current *plans.ResourceInstanceChangeSrc
Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc
}
byModule := map[string]map[string]*ResourceChanges{}
resourceKeys := map[string][]string{}
var moduleKeys []string
for _, rc := range changes.Resources {
if rc.Action == plans.NoOp {
// We won't mention no-op changes here at all, since the old plan
// model we are emulating here didn't have such a concept.
continue
}
moduleKey := rc.Addr.Module.String()
if _, exists := byModule[moduleKey]; !exists {
moduleKeys = append(moduleKeys, moduleKey)
byModule[moduleKey] = make(map[string]*ResourceChanges)
}
resourceKey := rc.Addr.Resource.String()
if _, exists := byModule[moduleKey][resourceKey]; !exists {
resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey)
byModule[moduleKey][resourceKey] = &ResourceChanges{
Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc),
}
}
if rc.DeposedKey == states.NotDeposed {
byModule[moduleKey][resourceKey].Current = rc
} else {
byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc
}
}
sort.Strings(moduleKeys)
for _, ks := range resourceKeys {
sort.Strings(ks)
}
var buf bytes.Buffer
for _, moduleKey := range moduleKeys {
rcs := byModule[moduleKey]
var mBuf bytes.Buffer
for _, resourceKey := range resourceKeys[moduleKey] {
rc := rcs[resourceKey]
crud := "UPDATE"
if rc.Current != nil {
switch rc.Current.Action {
case plans.DeleteThenCreate:
crud = "DESTROY/CREATE"
case plans.CreateThenDelete:
crud = "CREATE/DESTROY"
case plans.Delete:
crud = "DESTROY"
case plans.Create:
crud = "CREATE"
}
} else { } else {
return state, fmt.Errorf( // We must be working on a deposed object then, in which
"After applying this step, the plan was not empty:\n\n%s", p) // case destroying is the only possible action.
crud = "DESTROY"
} }
}
// And another after a Refresh. extra := ""
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { if rc.Current == nil && len(rc.Deposed) > 0 {
state, stepDiags = ctx.Refresh() extra = " (deposed only)"
if stepDiags.HasErrors() {
return state, fmt.Errorf("Error on follow-up refresh: %s", stepDiags.Err())
} }
}
if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
return state, fmt.Errorf("Error on second follow-up plan: %s", stepDiags.Err())
}
empty := p.Diff == nil || p.Diff.Empty()
// Data resources are tricky because they legitimately get instantiated fmt.Fprintf(
// during refresh so that they will be already populated during the &mBuf, "%s: %s%s\n",
// plan walk. Because of this, if we have any data resources in the crud, resourceKey, extra,
// config we'll end up wanting to destroy them again here. This is )
// acceptable and expected, and we'll treat it as "empty" for the
// sake of this testing.
if step.Destroy {
empty = true
for _, moduleDiff := range p.Diff.Modules { attrNames := map[string]bool{}
for k, instanceDiff := range moduleDiff.Resources { var oldAttrs map[string]string
if !strings.HasPrefix(k, "data.") { var newAttrs map[string]string
empty = false if rc.Current != nil {
break if before := rc.Current.Before; before != nil {
ty, err := before.ImpliedType()
if err == nil {
val, err := before.Decode(ty)
if err == nil {
oldAttrs = hcl2shim.FlatmapValueFromHCL2(val)
for k := range oldAttrs {
attrNames[k] = true
}
}
} }
}
if !instanceDiff.Destroy { if after := rc.Current.After; after != nil {
empty = false ty, err := after.ImpliedType()
if err == nil {
val, err := after.Decode(ty)
if err == nil {
newAttrs = hcl2shim.FlatmapValueFromHCL2(val)
for k := range newAttrs {
attrNames[k] = true
}
}
} }
} }
} }
} if oldAttrs == nil {
oldAttrs = make(map[string]string)
}
if newAttrs == nil {
newAttrs = make(map[string]string)
}
if !empty { attrNamesOrder := make([]string, 0, len(attrNames))
if step.ExpectNonEmptyPlan { keyLen := 0
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) for n := range attrNames {
} else { attrNamesOrder = append(attrNamesOrder, n)
return state, fmt.Errorf( if len(n) > keyLen {
"After applying this step and refreshing, "+ keyLen = len(n)
"the plan was not empty:\n\n%s", p) }
}
sort.Strings(attrNamesOrder)
for _, attrK := range attrNamesOrder {
v := newAttrs[attrK]
u := oldAttrs[attrK]
if v == config.UnknownVariableValue {
v = "<computed>"
}
// NOTE: we don't support <sensitive> here because we would
// need schema to do that. Excluding sensitive values
// is now done at the UI layer, and so should not be tested
// at the core layer.
updateMsg := ""
// TODO: Mark " (forces new resource)" in updateMsg when appropriate.
fmt.Fprintf(
&mBuf, " %s:%s %#v => %#v%s\n",
attrK,
strings.Repeat(" ", keyLen-len(attrK)),
u, v,
updateMsg,
)
} }
} }
// Made it here, but expected a non-empty plan, fail! if moduleKey == "" { // root module
if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) { buf.Write(mBuf.Bytes())
return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!") buf.WriteByte('\n')
continue
} }
// Made it here? Good job test step! fmt.Fprintf(&buf, "%s:\n", moduleKey)
return state, nil s := bufio.NewScanner(&mBuf)
*/ for s.Scan() {
buf.WriteString(fmt.Sprintf(" %s\n", s.Text()))
}
}
return buf.String()
} }
func testStepTaint(state *terraform.State, step TestStep) error { func testStepTaint(state *terraform.State, step TestStep) error {

View File

@ -3,7 +3,10 @@ package resource
import ( import (
"fmt" "fmt"
"log" "log"
"reflect"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/hcl2/hcl/hclsyntax"
@ -16,7 +19,8 @@ import (
func testStepImportState( func testStepImportState(
opts terraform.ContextOpts, opts terraform.ContextOpts,
state *terraform.State, state *terraform.State,
step TestStep) (*terraform.State, error) { step TestStep,
schemas *terraform.Schemas) (*terraform.State, error) {
// Determine the ID to import // Determine the ID to import
var importId string var importId string
switch { switch {
@ -49,7 +53,10 @@ func testStepImportState(
} }
opts.Config = cfg opts.Config = cfg
// import tests start with empty state
opts.State = states.NewState() opts.State = states.NewState()
ctx, stepDiags := terraform.NewContext(&opts) ctx, stepDiags := terraform.NewContext(&opts)
if stepDiags.HasErrors() { if stepDiags.HasErrors() {
return state, stepDiags.Err() return state, stepDiags.Err()
@ -68,7 +75,7 @@ func testStepImportState(
} }
// Do the import // Do the import
newState, stepDiags := ctx.Import(&terraform.ImportOpts{ importedState, stepDiags := ctx.Import(&terraform.ImportOpts{
// Set the module so that any provider config is loaded // Set the module so that any provider config is loaded
Config: cfg, Config: cfg,
@ -84,14 +91,14 @@ func testStepImportState(
return state, stepDiags.Err() return state, stepDiags.Err()
} }
newState := mustShimNewState(importedState, schemas)
// Go through the new state and verify // Go through the new state and verify
if step.ImportStateCheck != nil { if step.ImportStateCheck != nil {
var states []*states.ResourceInstanceObjectSrc var states []*terraform.InstanceState
for _, r := range newState.RootModule().Resources { for _, r := range newState.RootModule().Resources {
for _, i := range r.Instances { if r.Primary != nil {
if i.Current != nil { states = append(states, r.Primary)
states = append(states, i.Current)
}
} }
} }
// TODO: update for new state types // TODO: update for new state types
@ -103,67 +110,64 @@ func testStepImportState(
// Verify that all the states match // Verify that all the states match
if step.ImportStateVerify { if step.ImportStateVerify {
return nil, fmt.Errorf("testStepImportStep ImportStateVerify not yet updated for new state types") new := newState.RootModule().Resources
/* old := state.RootModule().Resources
new := newState.RootModule().Resources for _, r := range new {
old := state.RootModule().Resources // Find the existing resource
for _, r := range new { var oldR *terraform.ResourceState
// Find the existing resource for _, r2 := range old {
var oldR *terraform.ResourceState if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type {
for _, r2 := range old { oldR = r2
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type { break
oldR = r2
break
}
}
if oldR == nil {
return state, fmt.Errorf(
"Failed state verification, resource with ID %s not found",
r.Primary.ID)
}
// Compare their attributes
actual := make(map[string]string)
for k, v := range r.Primary.Attributes {
actual[k] = v
}
expected := make(map[string]string)
for k, v := range oldR.Primary.Attributes {
expected[k] = v
}
// Remove fields we're ignoring
for _, v := range step.ImportStateVerifyIgnore {
for k, _ := range actual {
if strings.HasPrefix(k, v) {
delete(actual, k)
}
}
for k, _ := range expected {
if strings.HasPrefix(k, v) {
delete(expected, k)
}
}
}
if !reflect.DeepEqual(actual, expected) {
// Determine only the different attributes
for k, v := range expected {
if av, ok := actual[k]; ok && v == av {
delete(expected, k)
delete(actual, k)
}
}
spewConf := spew.NewDefaultConfig()
spewConf.SortKeys = true
return state, fmt.Errorf(
"ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
"\n\n%s\n\n%s",
spewConf.Sdump(actual), spewConf.Sdump(expected))
} }
} }
*/ if oldR == nil {
return state, fmt.Errorf(
"Failed state verification, resource with ID %s not found",
r.Primary.ID)
}
// Compare their attributes
actual := make(map[string]string)
for k, v := range r.Primary.Attributes {
actual[k] = v
}
expected := make(map[string]string)
for k, v := range oldR.Primary.Attributes {
expected[k] = v
}
// Remove fields we're ignoring
for _, v := range step.ImportStateVerifyIgnore {
for k, _ := range actual {
if strings.HasPrefix(k, v) {
delete(actual, k)
}
}
for k, _ := range expected {
if strings.HasPrefix(k, v) {
delete(expected, k)
}
}
}
if !reflect.DeepEqual(actual, expected) {
// Determine only the different attributes
for k, v := range expected {
if av, ok := actual[k]; ok && v == av {
delete(expected, k)
delete(actual, k)
}
}
spewConf := spew.NewDefaultConfig()
spewConf.SortKeys = true
return state, fmt.Errorf(
"ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
"\n\n%s\n\n%s",
spewConf.Sdump(actual), spewConf.Sdump(expected))
}
}
} }
// Return the old state (non-imported) so we don't change anything. // Return the old state (non-imported) so we don't change anything.

View File

@ -20,6 +20,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/copystructure" "github.com/mitchellh/copystructure"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
@ -1289,6 +1290,13 @@ func (m schemaMap) validateList(
raw interface{}, raw interface{},
schema *Schema, schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) { c *terraform.ResourceConfig) ([]string, []error) {
// first check if the list is wholly unknown
if s, ok := raw.(string); ok {
if s == config.UnknownVariableValue {
return nil, nil
}
}
// We use reflection to verify the slice because you can't // We use reflection to verify the slice because you can't
// case to []interface{} unless the slice is exactly that type. // case to []interface{} unless the slice is exactly that type.
rawV := reflect.ValueOf(raw) rawV := reflect.ValueOf(raw)
@ -1360,6 +1368,13 @@ func (m schemaMap) validateMap(
raw interface{}, raw interface{},
schema *Schema, schema *Schema,
c *terraform.ResourceConfig) ([]string, []error) { c *terraform.ResourceConfig) ([]string, []error) {
// first check if the list is wholly unknown
if s, ok := raw.(string); ok {
if s == config.UnknownVariableValue {
return nil, nil
}
}
// We use reflection to verify the slice because you can't // We use reflection to verify the slice because you can't
// case to []interface{} unless the slice is exactly that type. // case to []interface{} unless the slice is exactly that type.
rawV := reflect.ValueOf(raw) rawV := reflect.ValueOf(raw)

View File

@ -29,7 +29,12 @@ func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffF
cfg := terraform.NewResourceConfigShimmed(planned, configSchema) cfg := terraform.NewResourceConfigShimmed(planned, configSchema)
return schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil) diff, err := schemaMap(res.Schema).Diff(instanceState, cfg, cust, nil)
if err != nil {
return nil, err
}
return diff, err
} }
// ApplyDiff takes a cty.Value state and applies a terraform.InstanceDiff to // ApplyDiff takes a cty.Value state and applies a terraform.InstanceDiff to

View File

@ -3,6 +3,7 @@ package plugin
import ( import (
"context" "context"
"errors" "errors"
"io"
"log" "log"
"sync" "sync"
@ -44,6 +45,10 @@ type GRPCProvider struct {
// This allows the GRPCProvider a way to shutdown the plugin process. // This allows the GRPCProvider a way to shutdown the plugin process.
PluginClient *plugin.Client PluginClient *plugin.Client
// TestListener contains a net.Conn to close when the GRPCProvider is being
// used in an end to end test of a provider.
TestListener io.Closer
// Proto client use to make the grpc service calls. // Proto client use to make the grpc service calls.
client proto.ProviderClient client proto.ProviderClient

244
vendor/google.golang.org/grpc/test/bufconn/bufconn.go generated vendored Normal file
View File

@ -0,0 +1,244 @@
/*
*
* Copyright 2017 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package bufconn provides a net.Conn implemented by a buffer and related
// dialing and listening functionality.
package bufconn
import (
"fmt"
"io"
"net"
"sync"
"time"
)
// Listener implements a net.Listener that creates local, buffered net.Conns
// via its Accept and Dial method.
type Listener struct {
mu sync.Mutex
sz int
ch chan net.Conn
done chan struct{}
}
var errClosed = fmt.Errorf("Closed")
// Listen returns a Listener that can only be contacted by its own Dialers and
// creates buffered connections between the two.
func Listen(sz int) *Listener {
return &Listener{sz: sz, ch: make(chan net.Conn), done: make(chan struct{})}
}
// Accept blocks until Dial is called, then returns a net.Conn for the server
// half of the connection.
func (l *Listener) Accept() (net.Conn, error) {
select {
case <-l.done:
return nil, errClosed
case c := <-l.ch:
return c, nil
}
}
// Close stops the listener.
func (l *Listener) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
select {
case <-l.done:
// Already closed.
break
default:
close(l.done)
}
return nil
}
// Addr reports the address of the listener.
func (l *Listener) Addr() net.Addr { return addr{} }
// Dial creates an in-memory full-duplex network connection, unblocks Accept by
// providing it the server half of the connection, and returns the client half
// of the connection.
func (l *Listener) Dial() (net.Conn, error) {
p1, p2 := newPipe(l.sz), newPipe(l.sz)
select {
case <-l.done:
return nil, errClosed
case l.ch <- &conn{p1, p2}:
return &conn{p2, p1}, nil
}
}
type pipe struct {
mu sync.Mutex
// buf contains the data in the pipe. It is a ring buffer of fixed capacity,
// with r and w pointing to the offset to read and write, respsectively.
//
// Data is read between [r, w) and written to [w, r), wrapping around the end
// of the slice if necessary.
//
// The buffer is empty if r == len(buf), otherwise if r == w, it is full.
//
// w and r are always in the range [0, cap(buf)) and [0, len(buf)].
buf []byte
w, r int
wwait sync.Cond
rwait sync.Cond
closed bool
writeClosed bool
}
func newPipe(sz int) *pipe {
p := &pipe{buf: make([]byte, 0, sz)}
p.wwait.L = &p.mu
p.rwait.L = &p.mu
return p
}
func (p *pipe) empty() bool {
return p.r == len(p.buf)
}
func (p *pipe) full() bool {
return p.r < len(p.buf) && p.r == p.w
}
func (p *pipe) Read(b []byte) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()
// Block until p has data.
for {
if p.closed {
return 0, io.ErrClosedPipe
}
if !p.empty() {
break
}
if p.writeClosed {
return 0, io.EOF
}
p.rwait.Wait()
}
wasFull := p.full()
n = copy(b, p.buf[p.r:len(p.buf)])
p.r += n
if p.r == cap(p.buf) {
p.r = 0
p.buf = p.buf[:p.w]
}
// Signal a blocked writer, if any
if wasFull {
p.wwait.Signal()
}
return n, nil
}
func (p *pipe) Write(b []byte) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return 0, io.ErrClosedPipe
}
for len(b) > 0 {
// Block until p is not full.
for {
if p.closed || p.writeClosed {
return 0, io.ErrClosedPipe
}
if !p.full() {
break
}
p.wwait.Wait()
}
wasEmpty := p.empty()
end := cap(p.buf)
if p.w < p.r {
end = p.r
}
x := copy(p.buf[p.w:end], b)
b = b[x:]
n += x
p.w += x
if p.w > len(p.buf) {
p.buf = p.buf[:p.w]
}
if p.w == cap(p.buf) {
p.w = 0
}
// Signal a blocked reader, if any.
if wasEmpty {
p.rwait.Signal()
}
}
return n, nil
}
func (p *pipe) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
p.closed = true
// Signal all blocked readers and writers to return an error.
p.rwait.Broadcast()
p.wwait.Broadcast()
return nil
}
func (p *pipe) closeWrite() error {
p.mu.Lock()
defer p.mu.Unlock()
p.writeClosed = true
// Signal all blocked readers and writers to return an error.
p.rwait.Broadcast()
p.wwait.Broadcast()
return nil
}
type conn struct {
io.Reader
io.Writer
}
func (c *conn) Close() error {
err1 := c.Reader.(*pipe).Close()
err2 := c.Writer.(*pipe).closeWrite()
if err1 != nil {
return err1
}
return err2
}
func (*conn) LocalAddr() net.Addr { return addr{} }
func (*conn) RemoteAddr() net.Addr { return addr{} }
func (c *conn) SetDeadline(t time.Time) error { return fmt.Errorf("unsupported") }
func (c *conn) SetReadDeadline(t time.Time) error { return fmt.Errorf("unsupported") }
func (c *conn) SetWriteDeadline(t time.Time) error { return fmt.Errorf("unsupported") }
type addr struct{}
func (addr) Network() string { return "bufconn" }
func (addr) String() string { return "bufconn" }

1
vendor/modules.txt vendored
View File

@ -558,6 +558,7 @@ google.golang.org/genproto/googleapis/api/annotations
google.golang.org/genproto/googleapis/rpc/status google.golang.org/genproto/googleapis/rpc/status
# google.golang.org/grpc v1.14.0 # google.golang.org/grpc v1.14.0
google.golang.org/grpc google.golang.org/grpc
google.golang.org/grpc/test/bufconn
google.golang.org/grpc/metadata google.golang.org/grpc/metadata
google.golang.org/grpc/credentials google.golang.org/grpc/credentials
google.golang.org/grpc/health google.golang.org/grpc/health