mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-04-29 06:01:57 -04:00
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.
181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
package plugin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"net/rpc"
|
|
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-plugin/internal/plugin"
|
|
"github.com/mitchellh/go-testing-interface"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// TestOptions allows specifying options that can affect the behavior of the
|
|
// test functions
|
|
type TestOptions struct {
|
|
//ServerStdout causes the given value to be used in place of a blank buffer
|
|
//for RPCServer's Stdout
|
|
ServerStdout io.ReadCloser
|
|
|
|
//ServerStderr causes the given value to be used in place of a blank buffer
|
|
//for RPCServer's Stderr
|
|
ServerStderr io.ReadCloser
|
|
}
|
|
|
|
// The testing file contains test helpers that you can use outside of
|
|
// this package for making it easier to test plugins themselves.
|
|
|
|
// TestConn is a helper function for returning a client and server
|
|
// net.Conn connected to each other.
|
|
func TestConn(t testing.T) (net.Conn, net.Conn) {
|
|
// Listen to any local port. This listener will be closed
|
|
// after a single connection is established.
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Start a goroutine to accept our client connection
|
|
var serverConn net.Conn
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
defer close(doneCh)
|
|
defer l.Close()
|
|
var err error
|
|
serverConn, err = l.Accept()
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}()
|
|
|
|
// Connect to the server
|
|
clientConn, err := net.Dial("tcp", l.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Wait for the server side to acknowledge it has connected
|
|
<-doneCh
|
|
|
|
return clientConn, serverConn
|
|
}
|
|
|
|
// TestRPCConn returns a rpc client and server connected to each other.
|
|
func TestRPCConn(t testing.T) (*rpc.Client, *rpc.Server) {
|
|
clientConn, serverConn := TestConn(t)
|
|
|
|
server := rpc.NewServer()
|
|
go server.ServeConn(serverConn)
|
|
|
|
client := rpc.NewClient(clientConn)
|
|
return client, server
|
|
}
|
|
|
|
// TestPluginRPCConn returns a plugin RPC client and server that are connected
|
|
// together and configured.
|
|
func TestPluginRPCConn(t testing.T, ps map[string]Plugin, opts *TestOptions) (*RPCClient, *RPCServer) {
|
|
// Create two net.Conns we can use to shuttle our control connection
|
|
clientConn, serverConn := TestConn(t)
|
|
|
|
// Start up the server
|
|
server := &RPCServer{Plugins: ps, Stdout: new(bytes.Buffer), Stderr: new(bytes.Buffer)}
|
|
if opts != nil {
|
|
if opts.ServerStdout != nil {
|
|
server.Stdout = opts.ServerStdout
|
|
}
|
|
if opts.ServerStderr != nil {
|
|
server.Stderr = opts.ServerStderr
|
|
}
|
|
}
|
|
go server.ServeConn(serverConn)
|
|
|
|
// Connect the client to the server
|
|
client, err := NewRPCClient(clientConn, ps)
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
return client, server
|
|
}
|
|
|
|
// TestGRPCConn returns a gRPC client conn and grpc server that are connected
|
|
// together and configured. The register function is used to register services
|
|
// prior to the Serve call. This is used to test gRPC connections.
|
|
func TestGRPCConn(t testing.T, register func(*grpc.Server)) (*grpc.ClientConn, *grpc.Server) {
|
|
// Create a listener
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
server := grpc.NewServer()
|
|
register(server)
|
|
go server.Serve(l)
|
|
|
|
// Connect to the server
|
|
conn, err := grpc.Dial(
|
|
l.Addr().String(),
|
|
grpc.WithBlock(),
|
|
grpc.WithInsecure())
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Connection successful, close the listener
|
|
l.Close()
|
|
|
|
return conn, server
|
|
}
|
|
|
|
// TestPluginGRPCConn returns a plugin gRPC client and server that are connected
|
|
// together and configured. This is used to test gRPC connections.
|
|
func TestPluginGRPCConn(t testing.T, ps map[string]Plugin) (*GRPCClient, *GRPCServer) {
|
|
// Create a listener
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
// Start up the server
|
|
server := &GRPCServer{
|
|
Plugins: ps,
|
|
DoneCh: make(chan struct{}),
|
|
Server: DefaultGRPCServer,
|
|
Stdout: new(bytes.Buffer),
|
|
Stderr: new(bytes.Buffer),
|
|
logger: hclog.Default(),
|
|
}
|
|
if err := server.Init(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
go server.Serve(l)
|
|
|
|
// Connect to the server
|
|
conn, err := grpc.Dial(
|
|
l.Addr().String(),
|
|
grpc.WithBlock(),
|
|
grpc.WithInsecure())
|
|
if err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
brokerGRPCClient := newGRPCBrokerClient(conn)
|
|
broker := newGRPCBroker(brokerGRPCClient, nil)
|
|
go broker.Run()
|
|
go brokerGRPCClient.StartStream()
|
|
|
|
// Create the client
|
|
client := &GRPCClient{
|
|
Conn: conn,
|
|
Plugins: ps,
|
|
broker: broker,
|
|
doneCtx: context.Background(),
|
|
controller: plugin.NewGRPCControllerClient(conn),
|
|
}
|
|
|
|
return client, server
|
|
}
|