opentofu/command/e2etest/unmanaged_test.go

159 lines
4.4 KiB
Go
Raw Normal View History

command: Unmanaged providers This adds supports for "unmanaged" providers, or providers with process lifecycles not controlled by Terraform. These providers are assumed to be started before Terraform is launched, and are assumed to shut themselves down after Terraform has finished running. To do this, we must update the go-plugin dependency to v1.3.0, which added support for the "test mode" plugin serving that powers all this. As a side-effect of not needing to manage the process lifecycle anymore, Terraform also no longer needs to worry about the provider's binary, as it won't be used for anything anymore. Because of this, we can disable the init behavior that concerns itself with downloading that provider's binary, checking its version, and otherwise managing the binary. This is all managed on a per-provider basis, so managed providers that Terraform downloads, starts, and stops can be used in the same commands as unmanaged providers. The TF_REATTACH_PROVIDERS environment variable is added, and is a JSON encoding of the provider's address to the information we need to connect to it. This change enables two benefits: first, delve and other debuggers can now be attached to provider server processes, and Terraform can connect. This allows for attaching debuggers to provider processes, which before was difficult to impossible. Second, it allows the SDK test framework to host the provider in the same process as the test driver, while running a production Terraform binary against the provider. This allows for Go's built-in race detector and test coverage tooling to work as expected in provider tests. Unmanaged providers are expected to work in the exact same way as managed providers, with one caveat: Terraform kills provider processes and restarts them once per graph walk, meaning multiple times during most Terraform CLI commands. As unmanaged providers can't be killed by Terraform, and have no visibility into graph walks, unmanaged providers are likely to have differences in how their global mutable state behaves when compared to managed providers. Namely, unmanaged providers are likely to retain global state when managed providers would have reset it. Developers relying on global state should be aware of this.
2020-05-26 19:48:57 -05:00
package e2etest
import (
"context"
"encoding/json"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/builtin/providers/test"
"github.com/hashicorp/terraform/e2e"
grpcplugin "github.com/hashicorp/terraform/helper/plugin"
proto "github.com/hashicorp/terraform/internal/tfplugin5"
tfplugin "github.com/hashicorp/terraform/plugin"
)
// The tests in this file are for the "unmanaged provider workflow", which
// includes variants of the following sequence, with different details:
// terraform init
// terraform plan
// terraform apply
//
// These tests are run against an in-process server, and checked to make sure
// they're not trying to control the lifecycle of the binary. They are not
// checked for correctness of the operations themselves.
type reattachConfig struct {
Protocol string
Pid int
Test bool
Addr reattachConfigAddr
}
type reattachConfigAddr struct {
Network string
String string
}
type providerServer struct {
*grpcplugin.GRPCProviderServer
planResourceChangeCalled bool
applyResourceChangeCalled bool
}
func (p *providerServer) PlanResourceChange(ctx context.Context, req *proto.PlanResourceChange_Request) (*proto.PlanResourceChange_Response, error) {
p.planResourceChangeCalled = true
return p.GRPCProviderServer.PlanResourceChange(ctx, req)
}
func (p *providerServer) ApplyResourceChange(ctx context.Context, req *proto.ApplyResourceChange_Request) (*proto.ApplyResourceChange_Response, error) {
p.applyResourceChangeCalled = true
return p.GRPCProviderServer.ApplyResourceChange(ctx, req)
}
func TestUnmanagedSeparatePlan(t *testing.T) {
t.Parallel()
fixturePath := filepath.Join("testdata", "test-provider")
tf := e2e.NewBinary(terraformBin, fixturePath)
defer tf.Close()
reattachCh := make(chan *plugin.ReattachConfig)
closeCh := make(chan struct{})
provider := &providerServer{
GRPCProviderServer: grpcplugin.NewGRPCProviderServerShim(test.Provider()),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go plugin.Serve(&plugin.ServeConfig{
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugintest",
Level: hclog.Trace,
Output: ioutil.Discard,
}),
Test: &plugin.ServeTestConfig{
Context: ctx,
ReattachConfigCh: reattachCh,
CloseCh: closeCh,
},
GRPCServer: plugin.DefaultGRPCServer,
VersionedPlugins: map[int]plugin.PluginSet{
5: plugin.PluginSet{
"provider": &tfplugin.GRPCProviderPlugin{
GRPCProvider: func() proto.ProviderServer {
return provider
},
},
},
},
})
config := <-reattachCh
if config == nil {
t.Fatalf("no reattach config received")
}
reattachStr, err := json.Marshal(map[string]reattachConfig{
"hashicorp/test": reattachConfig{
Protocol: string(config.Protocol),
Pid: config.Pid,
Test: true,
Addr: reattachConfigAddr{
Network: config.Addr.Network(),
String: config.Addr.String(),
},
},
})
tf.AddEnv("TF_REATTACH_PROVIDERS=" + string(reattachStr))
tf.AddEnv("PLUGIN_PROTOCOL_VERSION=5")
//// INIT
stdout, stderr, err := tf.Run("init")
if err != nil {
t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr)
}
// Make sure we didn't download the binary
if strings.Contains(stdout, "Installing hashicorp/test v") {
t.Errorf("test provider download message is present in init output:\n%s", stdout)
}
if tf.FileExists(filepath.Join(".terraform", "plugins", "registry.terraform.io", "hashicorp", "test")) {
t.Errorf("test provider binary found in .terraform dir")
}
//// PLAN
_, stderr, err = tf.Run("plan", "-out=tfplan")
if err != nil {
t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr)
}
if !provider.planResourceChangeCalled {
t.Error("PlanResourceChange not called on in-process provider")
}
//// APPLY
_, stderr, err = tf.Run("apply", "tfplan")
if err != nil {
t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr)
}
if !provider.applyResourceChangeCalled {
t.Error("ApplyResourceChange not called on in-process provider")
}
provider.applyResourceChangeCalled = false
//// DESTROY
_, stderr, err = tf.Run("destroy", "-auto-approve")
if err != nil {
t.Fatalf("unexpected destroy error: %s\nstderr:\n%s", err, stderr)
}
if !provider.applyResourceChangeCalled {
t.Error("ApplyResourceChange (destroy) not called on in-process provider")
}
cancel()
<-closeCh
}