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"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
|
||||
"github.com/hashicorp/terraform/providers"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/errwrap"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
@ -444,9 +448,6 @@ func ParallelTest(t TestT, c TestCase) {
|
|||
// long, we require the verbose flag so users are able to see progress
|
||||
// output.
|
||||
func Test(t TestT, c TestCase) {
|
||||
t.Fatal("resource.Test is not yet updated for the new provider API")
|
||||
return
|
||||
/*
|
||||
// We only run acceptance tests if an env var is set because they're
|
||||
// slow and generally require some outside configuration. You can opt out
|
||||
// of this with OverrideEnvVar on individual TestCases.
|
||||
|
@ -478,6 +479,43 @@ func Test(t TestT, c TestCase) {
|
|||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := p.GetSchema()
|
||||
if resp.Diagnostics.HasErrors() {
|
||||
t.Fatal(fmt.Sprintf("error fetching schema for %q: %v", providerName, resp.Diagnostics.Err()))
|
||||
}
|
||||
|
||||
providerSchema := &terraform.ProviderSchema{
|
||||
Provider: resp.Provider.Block,
|
||||
ResourceTypes: make(map[string]*configschema.Block),
|
||||
DataSources: make(map[string]*configschema.Block),
|
||||
}
|
||||
|
||||
for r, s := range resp.ResourceTypes {
|
||||
providerSchema.ResourceTypes[r] = s.Block
|
||||
}
|
||||
|
||||
for d, s := range resp.DataSources {
|
||||
providerSchema.DataSources[d] = s.Block
|
||||
}
|
||||
|
||||
schemas.Providers[providerName] = providerSchema
|
||||
}
|
||||
|
||||
opts := terraform.ContextOpts{ProviderResolver: providerResolver}
|
||||
|
||||
// A single state variable to track the lifecycle, starting with no state
|
||||
|
@ -514,9 +552,9 @@ func Test(t TestT, c TestCase) {
|
|||
|
||||
// Can optionally set step.Config in addition to
|
||||
// step.ImportState, to provide config for the import.
|
||||
state, err = testStepImportState(opts, state, step)
|
||||
state, err = testStepImportState(opts, state, step, schemas)
|
||||
} else {
|
||||
state, err = testStepConfig(opts, state, step)
|
||||
state, err = testStepConfig(opts, state, step, schemas)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -601,7 +639,7 @@ func Test(t TestT, c TestCase) {
|
|||
}
|
||||
|
||||
log.Printf("[WARN] Test: Executing destroy step")
|
||||
state, err := testStep(opts, state, destroyStep)
|
||||
state, err := testStep(opts, state, destroyStep, schemas)
|
||||
if err != nil {
|
||||
t.Error(fmt.Sprintf(
|
||||
"Error destroying resource! WARNING: Dangling resources\n"+
|
||||
|
@ -613,7 +651,6 @@ func Test(t TestT, c TestCase) {
|
|||
} else {
|
||||
log.Printf("[WARN] Skipping destroy test since there is no state.")
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Any errors are stored so that they can be returned by the factory in
|
||||
// terraform to match non-test behavior.
|
||||
func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error) {
|
||||
func testProviderResolver(c TestCase) (providers.Resolver, error) {
|
||||
ctxProviders := c.ProviderFactories
|
||||
if ctxProviders == nil {
|
||||
ctxProviders = make(map[string]terraform.ResourceProviderFactory)
|
||||
|
@ -644,6 +681,10 @@ func testProviderResolver(c TestCase) (terraform.ResourceProviderResolver, error
|
|||
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
|
||||
for k, pf := range ctxProviders {
|
||||
// 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 {
|
||||
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 {
|
||||
err := p.TestReset()
|
||||
if err != nil {
|
||||
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
|
||||
|
|
|
@ -2,24 +2,34 @@ package resource
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"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/tfdiags"
|
||||
)
|
||||
|
||||
// testStepConfig runs a config-mode test step
|
||||
func testStepConfig(
|
||||
opts terraform.ContextOpts,
|
||||
state *terraform.State,
|
||||
step TestStep) (*terraform.State, error) {
|
||||
return testStep(opts, state, step)
|
||||
step TestStep,
|
||||
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) {
|
||||
return nil, fmt.Errorf("testStep not yet updated for new state type")
|
||||
/*
|
||||
// Pre-taint any resources that have been defined in Taint, as long as this
|
||||
// is not a destroy step.
|
||||
func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep, schemas *terraform.Schemas) (*terraform.State, error) {
|
||||
if !step.Destroy {
|
||||
if err := testStepTaint(state, step); err != nil {
|
||||
return state, err
|
||||
|
@ -35,7 +45,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
|
||||
// Build the context
|
||||
opts.Config = cfg
|
||||
opts.State = state
|
||||
opts.State = terraform.MustShimLegacyState(state)
|
||||
opts.Destroy = step.Destroy
|
||||
ctx, stepDiags := terraform.NewContext(&opts)
|
||||
if stepDiags.HasErrors() {
|
||||
|
@ -50,7 +60,8 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
}
|
||||
|
||||
// Refresh!
|
||||
state, stepDiags = ctx.Refresh()
|
||||
newState, stepDiags := ctx.Refresh()
|
||||
state = mustShimNewState(newState, schemas)
|
||||
if stepDiags.HasErrors() {
|
||||
return state, fmt.Errorf("Error refreshing: %s", stepDiags.Err())
|
||||
}
|
||||
|
@ -62,7 +73,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() {
|
||||
return state, fmt.Errorf("Error planning: %s", stepDiags.Err())
|
||||
} else {
|
||||
log.Printf("[WARN] Test: Step plan: %s", p)
|
||||
log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes))
|
||||
}
|
||||
|
||||
// We need to keep a copy of the state prior to destroying
|
||||
|
@ -71,7 +82,8 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
stateBeforeApplication := state.DeepCopy()
|
||||
|
||||
// Apply the diff, creating real resources.
|
||||
state, stepDiags = ctx.Apply()
|
||||
newState, stepDiags = ctx.Apply()
|
||||
state = mustShimNewState(newState, schemas)
|
||||
if stepDiags.HasErrors() {
|
||||
return state, fmt.Errorf("Error applying: %s", stepDiags.Err())
|
||||
}
|
||||
|
@ -92,30 +104,31 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
|
||||
// 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.
|
||||
var p *terraform.Plan
|
||||
var p *plans.Plan
|
||||
if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() {
|
||||
return state, fmt.Errorf("Error on follow-up plan: %s", stepDiags.Err())
|
||||
}
|
||||
if p.Diff != nil && !p.Diff.Empty() {
|
||||
if !p.Changes.Empty() {
|
||||
if step.ExpectNonEmptyPlan {
|
||||
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
||||
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", p)
|
||||
"After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes))
|
||||
}
|
||||
}
|
||||
|
||||
// And another after a Refresh.
|
||||
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
|
||||
state, stepDiags = ctx.Refresh()
|
||||
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.Diff == nil || p.Diff.Empty()
|
||||
empty := p.Changes.Empty()
|
||||
|
||||
// Data resources are tricky because they legitimately get instantiated
|
||||
// during refresh so that they will be already populated during the
|
||||
|
@ -123,41 +136,221 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
|
|||
// 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 {
|
||||
if step.Destroy && !empty {
|
||||
empty = true
|
||||
|
||||
for _, moduleDiff := range p.Diff.Modules {
|
||||
for k, instanceDiff := range moduleDiff.Resources {
|
||||
if !strings.HasPrefix(k, "data.") {
|
||||
for _, change := range p.Changes.Resources {
|
||||
if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode {
|
||||
empty = false
|
||||
break
|
||||
}
|
||||
|
||||
if !instanceDiff.Destroy {
|
||||
empty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !empty {
|
||||
if step.ExpectNonEmptyPlan {
|
||||
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
||||
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", p)
|
||||
"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 && (p.Diff == nil || p.Diff.Empty()) {
|
||||
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 {
|
||||
// We must be working on a deposed object then, in which
|
||||
// case destroying is the only possible action.
|
||||
crud = "DESTROY"
|
||||
}
|
||||
|
||||
extra := ""
|
||||
if rc.Current == nil && len(rc.Deposed) > 0 {
|
||||
extra = " (deposed only)"
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
&mBuf, "%s: %s%s\n",
|
||||
crud, resourceKey, extra,
|
||||
)
|
||||
|
||||
attrNames := map[string]bool{}
|
||||
var oldAttrs map[string]string
|
||||
var newAttrs map[string]string
|
||||
if rc.Current != nil {
|
||||
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 after := rc.Current.After; after != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
attrNamesOrder := make([]string, 0, len(attrNames))
|
||||
keyLen := 0
|
||||
for n := range attrNames {
|
||||
attrNamesOrder = append(attrNamesOrder, n)
|
||||
if len(n) > keyLen {
|
||||
keyLen = len(n)
|
||||
}
|
||||
}
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if moduleKey == "" { // root module
|
||||
buf.Write(mBuf.Bytes())
|
||||
buf.WriteByte('\n')
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, "%s:\n", moduleKey)
|
||||
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 {
|
||||
|
|
|
@ -3,7 +3,10 @@ package resource
|
|||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
|
||||
|
@ -16,7 +19,8 @@ import (
|
|||
func testStepImportState(
|
||||
opts terraform.ContextOpts,
|
||||
state *terraform.State,
|
||||
step TestStep) (*terraform.State, error) {
|
||||
step TestStep,
|
||||
schemas *terraform.Schemas) (*terraform.State, error) {
|
||||
// Determine the ID to import
|
||||
var importId string
|
||||
switch {
|
||||
|
@ -49,7 +53,10 @@ func testStepImportState(
|
|||
}
|
||||
|
||||
opts.Config = cfg
|
||||
|
||||
// import tests start with empty state
|
||||
opts.State = states.NewState()
|
||||
|
||||
ctx, stepDiags := terraform.NewContext(&opts)
|
||||
if stepDiags.HasErrors() {
|
||||
return state, stepDiags.Err()
|
||||
|
@ -68,7 +75,7 @@ func testStepImportState(
|
|||
}
|
||||
|
||||
// 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
|
||||
Config: cfg,
|
||||
|
||||
|
@ -84,14 +91,14 @@ func testStepImportState(
|
|||
return state, stepDiags.Err()
|
||||
}
|
||||
|
||||
newState := mustShimNewState(importedState, schemas)
|
||||
|
||||
// Go through the new state and verify
|
||||
if step.ImportStateCheck != nil {
|
||||
var states []*states.ResourceInstanceObjectSrc
|
||||
var states []*terraform.InstanceState
|
||||
for _, r := range newState.RootModule().Resources {
|
||||
for _, i := range r.Instances {
|
||||
if i.Current != nil {
|
||||
states = append(states, i.Current)
|
||||
}
|
||||
if r.Primary != nil {
|
||||
states = append(states, r.Primary)
|
||||
}
|
||||
}
|
||||
// TODO: update for new state types
|
||||
|
@ -103,8 +110,6 @@ func testStepImportState(
|
|||
|
||||
// Verify that all the states match
|
||||
if step.ImportStateVerify {
|
||||
return nil, fmt.Errorf("testStepImportStep ImportStateVerify not yet updated for new state types")
|
||||
/*
|
||||
new := newState.RootModule().Resources
|
||||
old := state.RootModule().Resources
|
||||
for _, r := range new {
|
||||
|
@ -163,7 +168,6 @@ func testStepImportState(
|
|||
spewConf.Sdump(actual), spewConf.Sdump(expected))
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// Return the old state (non-imported) so we don't change anything.
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/copystructure"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
@ -1289,6 +1290,13 @@ func (m schemaMap) validateList(
|
|||
raw interface{},
|
||||
schema *Schema,
|
||||
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
|
||||
// case to []interface{} unless the slice is exactly that type.
|
||||
rawV := reflect.ValueOf(raw)
|
||||
|
@ -1360,6 +1368,13 @@ func (m schemaMap) validateMap(
|
|||
raw interface{},
|
||||
schema *Schema,
|
||||
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
|
||||
// case to []interface{} unless the slice is exactly that type.
|
||||
rawV := reflect.ValueOf(raw)
|
||||
|
|
|
@ -29,7 +29,12 @@ func diffFromValues(prior, planned cty.Value, res *Resource, cust CustomizeDiffF
|
|||
|
||||
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
|
||||
|
|
|
@ -3,6 +3,7 @@ package plugin
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
|
@ -44,6 +45,10 @@ type GRPCProvider struct {
|
|||
// This allows the GRPCProvider a way to shutdown the plugin process.
|
||||
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.
|
||||
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/grpc v1.14.0
|
||||
google.golang.org/grpc
|
||||
google.golang.org/grpc/test/bufconn
|
||||
google.golang.org/grpc/metadata
|
||||
google.golang.org/grpc/credentials
|
||||
google.golang.org/grpc/health
|
||||
|
|
Loading…
Reference in New Issue