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:
parent
f8b1a3c7a4
commit
0d4d572c39
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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" }
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue