Use hashicorp/go-plugin for plugin system

This replaces this plugin system with the extracted hashicorp/go-plugin
library.

This doesn't introduce any new features such as binary flattening but
opens us up to that a lot more easily and removes a lot of code from TF
in favor of the upstream lib.

This will introduce a protocol change that will cause all existing
plugins to have to be recompiled to work properly. There is no actual
API changes so they just have to recompile, but it is technically
backwards incompatible.
This commit is contained in:
Mitchell Hashimoto 2016-01-24 18:10:52 -08:00 committed by James Nugent
parent feb9cbe175
commit 84214437b3
25 changed files with 966 additions and 2149 deletions

View File

@ -9,8 +9,9 @@ import (
"path/filepath"
"strings"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/hcl"
"github.com/hashicorp/terraform/plugin"
tfplugin "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
"github.com/kardianos/osext"
)
@ -202,7 +203,9 @@ func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.Cmd = pluginCmd(path)
config.HandshakeConfig = tfplugin.Handshake
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvider, error) {
@ -213,7 +216,12 @@ func (c *Config) providerFactory(path string) terraform.ResourceProviderFactory
return nil, err
}
return rpcClient.ResourceProvider()
raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvider), nil
}
}
@ -232,8 +240,10 @@ func (c *Config) ProvisionerFactories() map[string]terraform.ResourceProvisioner
func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFactory {
// Build the plugin client configuration and init the plugin
var config plugin.ClientConfig
config.HandshakeConfig = tfplugin.Handshake
config.Cmd = pluginCmd(path)
config.Managed = true
config.Plugins = tfplugin.PluginMap
client := plugin.NewClient(&config)
return func() (terraform.ResourceProvisioner, error) {
@ -242,7 +252,12 @@ func (c *Config) provisionerFactory(path string) terraform.ResourceProvisionerFa
return nil, err
}
return rpcClient.ResourceProvisioner()
raw, err := rpcClient.Dispense(tfplugin.ProvisionerPluginName)
if err != nil {
return nil, err
}
return raw.(terraform.ResourceProvisioner), nil
}
}

View File

@ -1,339 +0,0 @@
package plugin
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"unicode"
tfrpc "github.com/hashicorp/terraform/rpc"
)
// If this is true, then the "unexpected EOF" panic will not be
// raised throughout the clients.
var Killed = false
// This is a slice of the "managed" clients which are cleaned up when
// calling Cleanup
var managedClients = make([]*Client, 0, 5)
// Client handles the lifecycle of a plugin application, determining its
// RPC address, and returning various types of Terraform interface implementations
// across the multi-process communication layer.
type Client struct {
config *ClientConfig
exited bool
doneLogging chan struct{}
l sync.Mutex
address net.Addr
client *tfrpc.Client
}
// ClientConfig is the configuration used to initialize a new
// plugin client. After being used to initialize a plugin client,
// that configuration must not be modified again.
type ClientConfig struct {
// The unstarted subprocess for starting the plugin.
Cmd *exec.Cmd
// Managed represents if the client should be managed by the
// plugin package or not. If true, then by calling CleanupClients,
// it will automatically be cleaned up. Otherwise, the client
// user is fully responsible for making sure to Kill all plugin
// clients. By default the client is _not_ managed.
Managed bool
// The minimum and maximum port to use for communicating with
// the subprocess. If not set, this defaults to 10,000 and 25,000
// respectively.
MinPort, MaxPort uint
// StartTimeout is the timeout to wait for the plugin to say it
// has started successfully.
StartTimeout time.Duration
// If non-nil, then the stderr of the client will be written to here
// (as well as the log).
Stderr io.Writer
}
// This makes sure all the managed subprocesses are killed and properly
// logged. This should be called before the parent process running the
// plugins exits.
//
// This must only be called _once_.
func CleanupClients() {
// Set the killed to true so that we don't get unexpected panics
Killed = true
// Kill all the managed clients in parallel and use a WaitGroup
// to wait for them all to finish up.
var wg sync.WaitGroup
for _, client := range managedClients {
wg.Add(1)
go func(client *Client) {
client.Kill()
wg.Done()
}(client)
}
log.Println("[DEBUG] waiting for all plugin processes to complete...")
wg.Wait()
}
// Creates a new plugin client which manages the lifecycle of an external
// plugin and gets the address for the RPC connection.
//
// The client must be cleaned up at some point by calling Kill(). If
// the client is a managed client (created with NewManagedClient) you
// can just call CleanupClients at the end of your program and they will
// be properly cleaned.
func NewClient(config *ClientConfig) (c *Client) {
if config.MinPort == 0 && config.MaxPort == 0 {
config.MinPort = 10000
config.MaxPort = 25000
}
if config.StartTimeout == 0 {
config.StartTimeout = 1 * time.Minute
}
if config.Stderr == nil {
config.Stderr = ioutil.Discard
}
c = &Client{config: config}
if config.Managed {
managedClients = append(managedClients, c)
}
return
}
// Client returns an RPC client for the plugin.
//
// Subsequent calls to this will return the same RPC client.
func (c *Client) Client() (*tfrpc.Client, error) {
addr, err := c.Start()
if err != nil {
return nil, err
}
c.l.Lock()
defer c.l.Unlock()
if c.client != nil {
return c.client, nil
}
c.client, err = tfrpc.Dial(addr.Network(), addr.String())
if err != nil {
return nil, err
}
return c.client, nil
}
// Tells whether or not the underlying process has exited.
func (c *Client) Exited() bool {
c.l.Lock()
defer c.l.Unlock()
return c.exited
}
// End the executing subprocess (if it is running) and perform any cleanup
// tasks necessary such as capturing any remaining logs and so on.
//
// This method blocks until the process successfully exits.
//
// This method can safely be called multiple times.
func (c *Client) Kill() {
cmd := c.config.Cmd
if cmd.Process == nil {
return
}
cmd.Process.Kill()
// Wait for the client to finish logging so we have a complete log
<-c.doneLogging
}
// Starts the underlying subprocess, communicating with it to negotiate
// a port for RPC connections, and returning the address to connect via RPC.
//
// This method is safe to call multiple times. Subsequent calls have no effect.
// Once a client has been started once, it cannot be started again, even if
// it was killed.
func (c *Client) Start() (addr net.Addr, err error) {
c.l.Lock()
defer c.l.Unlock()
if c.address != nil {
return c.address, nil
}
c.doneLogging = make(chan struct{})
env := []string{
fmt.Sprintf("%s=%s", MagicCookieKey, MagicCookieValue),
fmt.Sprintf("TF_PLUGIN_MIN_PORT=%d", c.config.MinPort),
fmt.Sprintf("TF_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
}
stdout_r, stdout_w := io.Pipe()
stderr_r, stderr_w := io.Pipe()
cmd := c.config.Cmd
cmd.Env = append(cmd.Env, os.Environ()...)
cmd.Env = append(cmd.Env, env...)
cmd.Stdin = os.Stdin
cmd.Stderr = stderr_w
cmd.Stdout = stdout_w
log.Printf("[DEBUG] Starting plugin: %s %#v", cmd.Path, cmd.Args)
err = cmd.Start()
if err != nil {
return
}
// Make sure the command is properly cleaned up if there is an error
defer func() {
r := recover()
if err != nil || r != nil {
cmd.Process.Kill()
}
if r != nil {
panic(r)
}
}()
// Start goroutine to wait for process to exit
exitCh := make(chan struct{})
go func() {
// Make sure we close the write end of our stderr/stdout so
// that the readers send EOF properly.
defer stderr_w.Close()
defer stdout_w.Close()
// Wait for the command to end.
cmd.Wait()
// Log and make sure to flush the logs write away
log.Printf("[DEBUG] %s: plugin process exited\n", cmd.Path)
os.Stderr.Sync()
// Mark that we exited
close(exitCh)
// Set that we exited, which takes a lock
c.l.Lock()
defer c.l.Unlock()
c.exited = true
}()
// Start goroutine that logs the stderr
go c.logStderr(stderr_r)
// Start a goroutine that is going to be reading the lines
// out of stdout
linesCh := make(chan []byte)
go func() {
defer close(linesCh)
buf := bufio.NewReader(stdout_r)
for {
line, err := buf.ReadBytes('\n')
if line != nil {
linesCh <- line
}
if err == io.EOF {
return
}
}
}()
// Make sure after we exit we read the lines from stdout forever
// so they don't block since it is an io.Pipe
defer func() {
go func() {
for _ = range linesCh {
}
}()
}()
// Some channels for the next step
timeout := time.After(c.config.StartTimeout)
// Start looking for the address
log.Printf("[DEBUG] Waiting for RPC address for: %s", cmd.Path)
select {
case <-timeout:
err = errors.New("timeout while waiting for plugin to start")
case <-exitCh:
err = errors.New("plugin exited before we could connect")
case lineBytes := <-linesCh:
// Trim the line and split by "|" in order to get the parts of
// the output.
line := strings.TrimSpace(string(lineBytes))
parts := strings.SplitN(line, "|", 3)
if len(parts) < 3 {
err = fmt.Errorf("Unrecognized remote plugin message: %s", line)
return
}
// Test the API version
if parts[0] != APIVersion {
err = fmt.Errorf("Incompatible API version with plugin. "+
"Plugin version: %s, Ours: %s", parts[0], APIVersion)
return
}
switch parts[1] {
case "tcp":
addr, err = net.ResolveTCPAddr("tcp", parts[2])
case "unix":
addr, err = net.ResolveUnixAddr("unix", parts[2])
default:
err = fmt.Errorf("Unknown address type: %s", parts[1])
}
}
c.address = addr
return
}
func (c *Client) logStderr(r io.Reader) {
bufR := bufio.NewReader(r)
for {
line, err := bufR.ReadString('\n')
if line != "" {
c.config.Stderr.Write([]byte(line))
line = strings.TrimRightFunc(line, unicode.IsSpace)
log.Printf("[DEBUG] %s: %s", filepath.Base(c.config.Cmd.Path), line)
}
if err == io.EOF {
break
}
}
// Flag that we've completed logging for others
close(c.doneLogging)
}

View File

@ -1,145 +0,0 @@
package plugin
import (
"bytes"
"io/ioutil"
"os"
"strings"
"testing"
"time"
)
func TestClient(t *testing.T) {
process := helperProcess("mock")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
// Test that it parses the proper address
addr, err := c.Start()
if err != nil {
t.Fatalf("err should be nil, got %s", err)
}
if addr.Network() != "tcp" {
t.Fatalf("bad: %#v", addr)
}
if addr.String() != ":1234" {
t.Fatalf("bad: %#v", addr)
}
// Test that it exits properly if killed
c.Kill()
if process.ProcessState == nil {
t.Fatal("should have process state")
}
// Test that it knows it is exited
if !c.Exited() {
t.Fatal("should say client has exited")
}
}
func TestClientStart_badVersion(t *testing.T) {
config := &ClientConfig{
Cmd: helperProcess("bad-version"),
StartTimeout: 50 * time.Millisecond,
}
c := NewClient(config)
defer c.Kill()
_, err := c.Start()
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestClient_Start_Timeout(t *testing.T) {
config := &ClientConfig{
Cmd: helperProcess("start-timeout"),
StartTimeout: 50 * time.Millisecond,
}
c := NewClient(config)
defer c.Kill()
_, err := c.Start()
if err == nil {
t.Fatal("err should not be nil")
}
}
func TestClient_Stderr(t *testing.T) {
stderr := new(bytes.Buffer)
process := helperProcess("stderr")
c := NewClient(&ClientConfig{
Cmd: process,
Stderr: stderr,
})
defer c.Kill()
if _, err := c.Start(); err != nil {
t.Fatalf("err: %s", err)
}
for !c.Exited() {
time.Sleep(10 * time.Millisecond)
}
if !strings.Contains(stderr.String(), "HELLO\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
if !strings.Contains(stderr.String(), "WORLD\n") {
t.Fatalf("bad log data: '%s'", stderr.String())
}
}
func TestClient_Stdin(t *testing.T) {
// Overwrite stdin for this test with a temporary file
tf, err := ioutil.TempFile("", "terraform")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err = tf.WriteString("hello"); err != nil {
t.Fatalf("error: %s", err)
}
if err = tf.Sync(); err != nil {
t.Fatalf("error: %s", err)
}
if _, err = tf.Seek(0, 0); err != nil {
t.Fatalf("error: %s", err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }()
os.Stdin = tf
process := helperProcess("stdin")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
_, err = c.Start()
if err != nil {
t.Fatalf("error: %s", err)
}
for {
if c.Exited() {
break
}
time.Sleep(50 * time.Millisecond)
}
if !process.ProcessState.Success() {
t.Fatal("process didn't exit cleanly")
}
}

View File

@ -1,10 +1,13 @@
// The plugin package exposes functions and helpers for communicating to
// Terraform plugins which are implemented as standalone binary applications.
//
// plugin.Client fully manages the lifecycle of executing the application,
// connecting to it, and returning the RPC client and service names for
// connecting to it using the terraform/rpc package.
//
// plugin.Serve fully manages listeners to expose an RPC server from a binary
// that plugin.Client can connect to.
package plugin
import (
"github.com/hashicorp/go-plugin"
)
// See serve.go for serving plugins
// PluginMap should be used by clients for the map of plugins.
var PluginMap = map[string]plugin.Plugin{
"provider": &ResourceProviderPlugin{},
"provisioner": &ResourceProvisionerPlugin{},
}

View File

@ -1,107 +1,16 @@
package plugin
import (
"fmt"
"log"
"os"
"os/exec"
"testing"
"time"
tfrpc "github.com/hashicorp/terraform/rpc"
"github.com/hashicorp/terraform/terraform"
)
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
env := []string{
"GO_WANT_HELPER_PROCESS=1",
"TF_PLUGIN_MIN_PORT=10000",
"TF_PLUGIN_MAX_PORT=25000",
}
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = append(env, os.Environ()...)
return cmd
}
// This is not a real test. This is just a helper process kicked off by
// tests.
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
defer os.Exit(0)
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "bad-version":
fmt.Printf("%s1|tcp|:1234\n", APIVersion)
<-make(chan int)
case "resource-provider":
Serve(&ServeOpts{
ProviderFunc: testProviderFixed(new(terraform.MockResourceProvider)),
})
case "resource-provisioner":
Serve(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(
new(terraform.MockResourceProvisioner)),
})
case "invalid-rpc-address":
fmt.Println("lolinvalid")
case "mock":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
<-make(chan int)
case "start-timeout":
time.Sleep(1 * time.Minute)
os.Exit(1)
case "stderr":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
log.Println("HELLO")
log.Println("WORLD")
case "stdin":
fmt.Printf("%s|tcp|:1234\n", APIVersion)
data := make([]byte, 5)
if _, err := os.Stdin.Read(data); err != nil {
log.Printf("stdin read error: %s", err)
os.Exit(100)
}
if string(data) == "hello" {
os.Exit(0)
}
os.Exit(1)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
os.Exit(2)
}
}
func testProviderFixed(p terraform.ResourceProvider) tfrpc.ProviderFunc {
func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider {
return p
}
}
func testProvisionerFixed(p terraform.ResourceProvisioner) tfrpc.ProvisionerFunc {
func testProvisionerFixed(p terraform.ResourceProvisioner) ProvisionerFunc {
return func() terraform.ResourceProvisioner {
return p
}

View File

@ -1,24 +1,38 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// ResourceProviderPlugin is the plugin.Plugin implementation.
type ResourceProviderPlugin struct {
F func() terraform.ResourceProvider
}
func (p *ResourceProviderPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
return &ResourceProviderServer{Broker: b, Provider: p.F()}, nil
}
func (p *ResourceProviderPlugin) Client(
b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ResourceProvider{Broker: b, Client: c}, nil
}
// ResourceProvider is an implementation of terraform.ResourceProvider
// that communicates over RPC.
type ResourceProvider struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Client *rpc.Client
Name string
}
func (p *ResourceProvider) Input(
input terraform.UIInput,
c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
id := p.Broker.NextId()
go acceptAndServe(p.Broker, id, "UIInput", &UIInputServer{
go p.Broker.AcceptAndServe(id, &UIInputServer{
UIInput: input,
})
@ -28,7 +42,7 @@ func (p *ResourceProvider) Input(
Config: c,
}
err := p.Client.Call(p.Name+".Input", &args, &resp)
err := p.Client.Call("Plugin.Input", &args, &resp)
if err != nil {
return nil, err
}
@ -46,7 +60,7 @@ func (p *ResourceProvider) Validate(c *terraform.ResourceConfig) ([]string, []er
Config: c,
}
err := p.Client.Call(p.Name+".Validate", &args, &resp)
err := p.Client.Call("Plugin.Validate", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -70,7 +84,7 @@ func (p *ResourceProvider) ValidateResource(
Type: t,
}
err := p.Client.Call(p.Name+".ValidateResource", &args, &resp)
err := p.Client.Call("Plugin.ValidateResource", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -88,7 +102,7 @@ func (p *ResourceProvider) ValidateResource(
func (p *ResourceProvider) Configure(c *terraform.ResourceConfig) error {
var resp ResourceProviderConfigureResponse
err := p.Client.Call(p.Name+".Configure", c, &resp)
err := p.Client.Call("Plugin.Configure", c, &resp)
if err != nil {
return err
}
@ -110,7 +124,7 @@ func (p *ResourceProvider) Apply(
Diff: d,
}
err := p.Client.Call(p.Name+".Apply", args, &resp)
err := p.Client.Call("Plugin.Apply", args, &resp)
if err != nil {
return nil, err
}
@ -131,7 +145,7 @@ func (p *ResourceProvider) Diff(
State: s,
Config: c,
}
err := p.Client.Call(p.Name+".Diff", args, &resp)
err := p.Client.Call("Plugin.Diff", args, &resp)
if err != nil {
return nil, err
}
@ -151,7 +165,7 @@ func (p *ResourceProvider) Refresh(
State: s,
}
err := p.Client.Call(p.Name+".Refresh", args, &resp)
err := p.Client.Call("Plugin.Refresh", args, &resp)
if err != nil {
return nil, err
}
@ -165,7 +179,7 @@ func (p *ResourceProvider) Refresh(
func (p *ResourceProvider) Resources() []terraform.ResourceType {
var result []terraform.ResourceType
err := p.Client.Call(p.Name+".Resources", new(interface{}), &result)
err := p.Client.Call("Plugin.Resources", new(interface{}), &result)
if err != nil {
// TODO: panic, log, what?
return nil
@ -181,12 +195,12 @@ func (p *ResourceProvider) Close() error {
// ResourceProviderServer is a net/rpc compatible structure for serving
// a ResourceProvider. This should not be used directly.
type ResourceProviderServer struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Provider terraform.ResourceProvider
}
type ResourceProviderConfigureResponse struct {
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderInputArgs struct {
@ -196,7 +210,7 @@ type ResourceProviderInputArgs struct {
type ResourceProviderInputResponse struct {
Config *terraform.ResourceConfig
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderApplyArgs struct {
@ -207,7 +221,7 @@ type ResourceProviderApplyArgs struct {
type ResourceProviderApplyResponse struct {
State *terraform.InstanceState
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderDiffArgs struct {
@ -218,7 +232,7 @@ type ResourceProviderDiffArgs struct {
type ResourceProviderDiffResponse struct {
Diff *terraform.InstanceDiff
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderRefreshArgs struct {
@ -228,7 +242,7 @@ type ResourceProviderRefreshArgs struct {
type ResourceProviderRefreshResponse struct {
State *terraform.InstanceState
Error *BasicError
Error *plugin.BasicError
}
type ResourceProviderValidateArgs struct {
@ -237,7 +251,7 @@ type ResourceProviderValidateArgs struct {
type ResourceProviderValidateResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
type ResourceProviderValidateResourceArgs struct {
@ -247,7 +261,7 @@ type ResourceProviderValidateResourceArgs struct {
type ResourceProviderValidateResourceResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
func (s *ResourceProviderServer) Input(
@ -256,22 +270,19 @@ func (s *ResourceProviderServer) Input(
conn, err := s.Broker.Dial(args.InputId)
if err != nil {
*reply = ResourceProviderInputResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
client := rpc.NewClient(conn)
defer client.Close()
input := &UIInput{
Client: client,
Name: "UIInput",
}
input := &UIInput{Client: client}
config, err := s.Provider.Input(input, args.Config)
*reply = ResourceProviderInputResponse{
Config: config,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
@ -281,9 +292,9 @@ func (s *ResourceProviderServer) Validate(
args *ResourceProviderValidateArgs,
reply *ResourceProviderValidateResponse) error {
warns, errs := s.Provider.Validate(args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProviderValidateResponse{
Warnings: warns,
@ -296,9 +307,9 @@ func (s *ResourceProviderServer) ValidateResource(
args *ResourceProviderValidateResourceArgs,
reply *ResourceProviderValidateResourceResponse) error {
warns, errs := s.Provider.ValidateResource(args.Type, args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProviderValidateResourceResponse{
Warnings: warns,
@ -312,7 +323,7 @@ func (s *ResourceProviderServer) Configure(
reply *ResourceProviderConfigureResponse) error {
err := s.Provider.Configure(config)
*reply = ResourceProviderConfigureResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -323,7 +334,7 @@ func (s *ResourceProviderServer) Apply(
state, err := s.Provider.Apply(args.Info, args.State, args.Diff)
*result = ResourceProviderApplyResponse{
State: state,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -334,7 +345,7 @@ func (s *ResourceProviderServer) Diff(
diff, err := s.Provider.Diff(args.Info, args.State, args.Config)
*result = ResourceProviderDiffResponse{
Diff: diff,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -345,7 +356,7 @@ func (s *ResourceProviderServer) Refresh(
newState, err := s.Provider.Refresh(args.Info, args.State)
*result = ResourceProviderRefreshResponse{
State: newState,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}

View File

@ -1,15 +1,624 @@
package plugin
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: helperProcess("resource-provider")})
defer c.Kill()
func TestResourceProvider_impl(t *testing.T) {
var _ plugin.Plugin = new(ResourceProviderPlugin)
var _ terraform.ResourceProvider = new(ResourceProvider)
}
_, err := c.Client()
func TestResourceProvider_input(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvider)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("should not have error: %s", err)
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
input := new(terraform.MockUIInput)
expected := &terraform.ResourceConfig{
Raw: map[string]interface{}{"bar": "baz"},
}
p.InputReturnConfig = expected
// Input
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
actual, err := provider.Input(input, config)
if !p.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(p.InputConfig, config) {
t.Fatalf("bad: %#v", p.InputConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceProvider_configure(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvider)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_configure_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ConfigureReturnError = errors.New("foo")
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e == nil {
t.Fatal("should have error")
}
if e.Error() != "foo" {
t.Fatalf("bad: %s", e)
}
}
func TestResourceProvider_configure_warnings(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_apply(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ApplyReturn = &terraform.InstanceState{
ID: "bob",
}
// Apply
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
diff := &terraform.InstanceDiff{}
newState, err := provider.Apply(info, state, diff)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyDiff, diff) {
t.Fatalf("bad: %#v", p.ApplyDiff)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.ApplyReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_diff(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.DiffReturn = &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
}
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.DiffReturn, diff) {
t.Fatalf("bad: %#v", diff)
}
}
func TestResourceProvider_diff_error(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.DiffReturnError = errors.New("foo")
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err == nil {
t.Fatal("should have error")
}
if diff != nil {
t.Fatal("should not have diff")
}
}
func TestResourceProvider_refresh(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.RefreshReturn = &terraform.InstanceState{
ID: "bob",
}
// Refresh
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
newState, err := provider.Refresh(info, state)
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
if !reflect.DeepEqual(p.RefreshState, state) {
t.Fatalf("bad: %#v", p.RefreshState)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.RefreshReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_resources(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
expected := []terraform.ResourceType{
{"foo"},
{"bar"},
}
p.ResourcesReturn = expected
// Resources
result := provider.Resources()
if !p.ResourcesCalled {
t.Fatal("resources should be called")
}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
}
func TestResourceProvider_validate(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_validateResource(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateResourceReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
p.ValidateResourceReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_close(t *testing.T) {
p := new(terraform.MockResourceProvider)
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProviderFunc: testProviderFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProviderPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvider)
var iface interface{} = provider
pCloser, ok := iface.(terraform.ResourceProviderCloser)
if !ok {
t.Fatal("should be a ResourceProviderCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provider: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
err = provider.Configure(&terraform.ResourceConfig{})
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,17 +1,31 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// ResourceProvisionerPlugin is the plugin.Plugin implementation.
type ResourceProvisionerPlugin struct {
F func() terraform.ResourceProvisioner
}
func (p *ResourceProvisionerPlugin) Server(b *plugin.MuxBroker) (interface{}, error) {
return &ResourceProvisionerServer{Broker: b, Provisioner: p.F()}, nil
}
func (p *ResourceProvisionerPlugin) Client(
b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ResourceProvisioner{Broker: b, Client: c}, nil
}
// ResourceProvisioner is an implementation of terraform.ResourceProvisioner
// that communicates over RPC.
type ResourceProvisioner struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Client *rpc.Client
Name string
}
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {
@ -20,7 +34,7 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, [
Config: c,
}
err := p.Client.Call(p.Name+".Validate", &args, &resp)
err := p.Client.Call("Plugin.Validate", &args, &resp)
if err != nil {
return nil, []error{err}
}
@ -41,7 +55,7 @@ func (p *ResourceProvisioner) Apply(
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
id := p.Broker.NextId()
go acceptAndServe(p.Broker, id, "UIOutput", &UIOutputServer{
go p.Broker.AcceptAndServe(id, &UIOutputServer{
UIOutput: output,
})
@ -52,7 +66,7 @@ func (p *ResourceProvisioner) Apply(
Config: c,
}
err := p.Client.Call(p.Name+".Apply", args, &resp)
err := p.Client.Call("Plugin.Apply", args, &resp)
if err != nil {
return err
}
@ -73,7 +87,7 @@ type ResourceProvisionerValidateArgs struct {
type ResourceProvisionerValidateResponse struct {
Warnings []string
Errors []*BasicError
Errors []*plugin.BasicError
}
type ResourceProvisionerApplyArgs struct {
@ -83,13 +97,13 @@ type ResourceProvisionerApplyArgs struct {
}
type ResourceProvisionerApplyResponse struct {
Error *BasicError
Error *plugin.BasicError
}
// ResourceProvisionerServer is a net/rpc compatible structure for serving
// a ResourceProvisioner. This should not be used directly.
type ResourceProvisionerServer struct {
Broker *muxBroker
Broker *plugin.MuxBroker
Provisioner terraform.ResourceProvisioner
}
@ -99,21 +113,18 @@ func (s *ResourceProvisionerServer) Apply(
conn, err := s.Broker.Dial(args.OutputId)
if err != nil {
*result = ResourceProvisionerApplyResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
client := rpc.NewClient(conn)
defer client.Close()
output := &UIOutput{
Client: client,
Name: "UIOutput",
}
output := &UIOutput{Client: client}
err = s.Provisioner.Apply(output, args.State, args.Config)
*result = ResourceProvisionerApplyResponse{
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil
}
@ -122,9 +133,9 @@ func (s *ResourceProvisionerServer) Validate(
args *ResourceProvisionerValidateArgs,
reply *ResourceProvisionerValidateResponse) error {
warns, errs := s.Provisioner.Validate(args.Config)
berrs := make([]*BasicError, len(errs))
berrs := make([]*plugin.BasicError, len(errs))
for i, err := range errs {
berrs[i] = NewBasicError(err)
berrs[i] = plugin.NewBasicError(err)
}
*reply = ResourceProvisionerValidateResponse{
Warnings: warns,

View File

@ -1,15 +1,193 @@
package plugin
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvisioner(t *testing.T) {
c := NewClient(&ClientConfig{Cmd: helperProcess("resource-provisioner")})
defer c.Kill()
func TestResourceProvisioner_impl(t *testing.T) {
var _ plugin.Plugin = new(ResourceProvisionerPlugin)
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
_, err := c.Client()
func TestResourceProvisioner_apply(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("should not have error: %s", err)
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}
func TestResourceProvisioner_validate(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_errors(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
p.ValidateReturnErrors = []error{errors.New("foo")}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_warns(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
p.ValidateReturnWarns = []string{"foo"}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvisioner_close(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := raw.(terraform.ResourceProvisioner)
pCloser, ok := raw.(terraform.ResourceProvisionerCloser)
if !ok {
t.Fatal("should be a ResourceProvisionerCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provisioner: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
o := &terraform.MockUIOutput{}
s := &terraform.InstanceState{}
c := &terraform.ResourceConfig{}
err = provisioner.Apply(o, s, c)
if err == nil {
t.Fatal("should have error")
}
}

47
plugin/serve.go Normal file
View File

@ -0,0 +1,47 @@
package plugin
import (
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
// The constants below are the names of the plugins that can be dispensed
// from the plugin server.
const (
ProviderPluginName = "provider"
ProvisionerPluginName = "provisioner"
)
// Handshake is the HandshakeConfig used to configure clients and servers.
var Handshake = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
MagicCookieValue: "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2",
}
type ProviderFunc func() terraform.ResourceProvider
type ProvisionerFunc func() terraform.ResourceProvisioner
// ServeOpts are the configurations to serve a plugin.
type ServeOpts struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
}
// Serve serves a plugin. This function never returns and should be the final
// function called in the main function of the plugin.
func Serve(opts *ServeOpts) {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: Handshake,
Plugins: pluginMap(opts),
})
}
// pluginMap returns the map[string]plugin.Plugin to use for configuring a plugin
// server or client.
func pluginMap(opts *ServeOpts) map[string]plugin.Plugin {
return map[string]plugin.Plugin{
"provider": &ResourceProviderPlugin{F: opts.ProviderFunc},
"provisioner": &ResourceProvisionerPlugin{F: opts.ProvisionerFunc},
}
}

View File

@ -1,138 +0,0 @@
package plugin
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"sync/atomic"
tfrpc "github.com/hashicorp/terraform/rpc"
)
// The APIVersion is outputted along with the RPC address. The plugin
// client validates this API version and will show an error if it doesn't
// know how to speak it.
const APIVersion = "2"
// The "magic cookie" is used to verify that the user intended to
// actually run this binary. If this cookie isn't present as an
// environmental variable, then we bail out early with an error.
const MagicCookieKey = "TF_PLUGIN_MAGIC_COOKIE"
const MagicCookieValue = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2"
// ServeOpts configures what sorts of plugins are served.
type ServeOpts struct {
ProviderFunc tfrpc.ProviderFunc
ProvisionerFunc tfrpc.ProvisionerFunc
}
// Serve serves the plugins given by ServeOpts.
//
// Serve doesn't return until the plugin is done being executed. Any
// errors will be outputted to the log.
func Serve(opts *ServeOpts) {
// First check the cookie
if os.Getenv(MagicCookieKey) != MagicCookieValue {
fmt.Fprintf(os.Stderr,
"This binary is a Terraform plugin. These are not meant to be\n"+
"executed directly. Please execute `terraform`, which will load\n"+
"any plugins automatically.\n")
os.Exit(1)
}
// Register a listener so we can accept a connection
listener, err := serverListener()
if err != nil {
log.Printf("[ERR] plugin init: %s", err)
return
}
defer listener.Close()
// Create the RPC server to dispense
server := &tfrpc.Server{
ProviderFunc: opts.ProviderFunc,
ProvisionerFunc: opts.ProvisionerFunc,
}
// Output the address and service name to stdout so that Terraform
// core can bring it up.
log.Printf("Plugin address: %s %s\n",
listener.Addr().Network(), listener.Addr().String())
fmt.Printf("%s|%s|%s\n",
APIVersion,
listener.Addr().Network(),
listener.Addr().String())
os.Stdout.Sync()
// Eat the interrupts
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
go func() {
var count int32 = 0
for {
<-ch
newCount := atomic.AddInt32(&count, 1)
log.Printf(
"Received interrupt signal (count: %d). Ignoring.",
newCount)
}
}()
// Serve
server.Accept(listener)
}
func serverListener() (net.Listener, error) {
if runtime.GOOS == "windows" {
return serverListener_tcp()
}
return serverListener_unix()
}
func serverListener_tcp() (net.Listener, error) {
minPort, err := strconv.ParseInt(os.Getenv("TF_PLUGIN_MIN_PORT"), 10, 32)
if err != nil {
return nil, err
}
maxPort, err := strconv.ParseInt(os.Getenv("TF_PLUGIN_MAX_PORT"), 10, 32)
if err != nil {
return nil, err
}
for port := minPort; port <= maxPort; port++ {
address := fmt.Sprintf("127.0.0.1:%d", port)
listener, err := net.Listen("tcp", address)
if err == nil {
return listener, nil
}
}
return nil, errors.New("Couldn't bind plugin TCP listener")
}
func serverListener_unix() (net.Listener, error) {
tf, err := ioutil.TempFile("", "tf-plugin")
if err != nil {
return nil, err
}
path := tf.Name()
// Close the file and remove it because it has to not exist for
// the domain socket.
if err := tf.Close(); err != nil {
return nil, err
}
if err := os.Remove(path); err != nil {
return nil, err
}
return net.Listen("unix", path)
}

View File

@ -1,8 +1,9 @@
package rpc
package plugin
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -10,12 +11,11 @@ import (
// over RPC.
type UIInput struct {
Client *rpc.Client
Name string
}
func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
var resp UIInputInputResponse
err := i.Client.Call(i.Name+".Input", opts, &resp)
err := i.Client.Call("Plugin.Input", opts, &resp)
if err != nil {
return "", err
}
@ -29,7 +29,7 @@ func (i *UIInput) Input(opts *terraform.InputOpts) (string, error) {
type UIInputInputResponse struct {
Value string
Error *BasicError
Error *plugin.BasicError
}
// UIInputServer is a net/rpc compatible structure for serving
@ -44,7 +44,7 @@ func (s *UIInputServer) Input(
value, err := s.UIInput.Input(opts)
*reply = UIInputInputResponse{
Value: value,
Error: NewBasicError(err),
Error: plugin.NewBasicError(err),
}
return nil

View File

@ -1,9 +1,10 @@
package rpc
package plugin
import (
"reflect"
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -12,20 +13,20 @@ func TestUIInput_impl(t *testing.T) {
}
func TestUIInput_input(t *testing.T) {
client, server := testClientServer(t)
client, server := plugin.TestRPCConn(t)
defer client.Close()
i := new(terraform.MockUIInput)
i.InputReturnString = "foo"
err := server.RegisterName("UIInput", &UIInputServer{
err := server.RegisterName("Plugin", &UIInputServer{
UIInput: i,
})
if err != nil {
t.Fatalf("err: %s", err)
}
input := &UIInput{Client: client, Name: "UIInput"}
input := &UIInput{Client: client}
opts := &terraform.InputOpts{
Id: "foo",

View File

@ -1,4 +1,4 @@
package rpc
package plugin
import (
"net/rpc"
@ -10,11 +10,10 @@ import (
// over RPC.
type UIOutput struct {
Client *rpc.Client
Name string
}
func (o *UIOutput) Output(v string) {
o.Client.Call(o.Name+".Output", v, new(interface{}))
o.Client.Call("Plugin.Output", v, new(interface{}))
}
// UIOutputServer is the RPC server for serving UIOutput.

View File

@ -1,8 +1,9 @@
package rpc
package plugin
import (
"testing"
"github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform/terraform"
)
@ -11,19 +12,19 @@ func TestUIOutput_impl(t *testing.T) {
}
func TestUIOutput_input(t *testing.T) {
client, server := testClientServer(t)
client, server := plugin.TestRPCConn(t)
defer client.Close()
o := new(terraform.MockUIOutput)
err := server.RegisterName("UIOutput", &UIOutputServer{
err := server.RegisterName("Plugin", &UIOutputServer{
UIOutput: o,
})
if err != nil {
t.Fatalf("err: %s", err)
}
output := &UIOutput{Client: client, Name: "UIOutput"}
output := &UIOutput{Client: client}
output.Output("foo")
if !o.OutputCalled {
t.Fatal("output should be called")

View File

@ -1,108 +0,0 @@
package rpc
import (
"io"
"net"
"net/rpc"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/yamux"
)
// Client connects to a Server in order to request plugin implementations
// for Terraform.
type Client struct {
broker *muxBroker
control *rpc.Client
}
// Dial opens a connection to a Terraform RPC server and returns a client.
func Dial(network, address string) (*Client, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
if tcpConn, ok := conn.(*net.TCPConn); ok {
// Make sure to set keep alive so that the connection doesn't die
tcpConn.SetKeepAlive(true)
}
return NewClient(conn)
}
// NewClient creates a client from an already-open connection-like value.
// Dial is typically used instead.
func NewClient(conn io.ReadWriteCloser) (*Client, error) {
// Create the yamux client so we can multiplex
mux, err := yamux.Client(conn, nil)
if err != nil {
conn.Close()
return nil, err
}
// Connect to the control stream.
control, err := mux.Open()
if err != nil {
mux.Close()
return nil, err
}
// Create the broker and start it up
broker := newMuxBroker(mux)
go broker.Run()
// Build the client using our broker and control channel.
return &Client{
broker: broker,
control: rpc.NewClient(control),
}, nil
}
// Close closes the connection. The client is no longer usable after this
// is called.
func (c *Client) Close() error {
if err := c.control.Close(); err != nil {
return err
}
return c.broker.Close()
}
func (c *Client) ResourceProvider() (terraform.ResourceProvider, error) {
var id uint32
if err := c.control.Call(
"Dispenser.ResourceProvider", new(interface{}), &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
return &ResourceProvider{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvider",
}, nil
}
func (c *Client) ResourceProvisioner() (terraform.ResourceProvisioner, error) {
var id uint32
if err := c.control.Call(
"Dispenser.ResourceProvisioner", new(interface{}), &id); err != nil {
return nil, err
}
conn, err := c.broker.Dial(id)
if err != nil {
return nil, err
}
return &ResourceProvisioner{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvisioner",
}, nil
}

View File

@ -1,76 +0,0 @@
package rpc
import (
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestClient_ResourceProvider(t *testing.T) {
clientConn, serverConn := testConn(t)
p := new(terraform.MockResourceProvider)
server := &Server{ProviderFunc: testProviderFixed(p)}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
defer client.Close()
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestClient_ResourceProvisioner(t *testing.T) {
clientConn, serverConn := testConn(t)
p := new(terraform.MockResourceProvisioner)
server := &Server{ProvisionerFunc: testProvisionerFixed(p)}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
defer client.Close()
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}

View File

@ -1,21 +0,0 @@
package rpc
// This is a type that wraps error types so that they can be messaged
// across RPC channels. Since "error" is an interface, we can't always
// gob-encode the underlying structure. This is a valid error interface
// implementer that we will push across.
type BasicError struct {
Message string
}
func NewBasicError(err error) *BasicError {
if err == nil {
return nil
}
return &BasicError{err.Error()}
}
func (e *BasicError) Error() string {
return e.Message
}

View File

@ -1,26 +0,0 @@
package rpc
import (
"errors"
"testing"
)
func TestBasicError_ImplementsError(t *testing.T) {
var _ error = new(BasicError)
}
func TestBasicError_MatchesMessage(t *testing.T) {
err := errors.New("foo")
wrapped := NewBasicError(err)
if wrapped.Error() != err.Error() {
t.Fatalf("bad: %#v", wrapped.Error())
}
}
func TestNewBasicError_nil(t *testing.T) {
r := NewBasicError(nil)
if r != nil {
t.Fatalf("bad: %#v", r)
}
}

View File

@ -1,172 +0,0 @@
package rpc
import (
"encoding/binary"
"fmt"
"net"
"sync"
"sync/atomic"
"time"
"github.com/hashicorp/yamux"
)
// muxBroker is responsible for brokering multiplexed connections by unique ID.
//
// This allows a plugin to request a channel with a specific ID to connect to
// or accept a connection from, and the broker handles the details of
// holding these channels open while they're being negotiated.
type muxBroker struct {
nextId uint32
session *yamux.Session
streams map[uint32]*muxBrokerPending
sync.Mutex
}
type muxBrokerPending struct {
ch chan net.Conn
doneCh chan struct{}
}
func newMuxBroker(s *yamux.Session) *muxBroker {
return &muxBroker{
session: s,
streams: make(map[uint32]*muxBrokerPending),
}
}
// Accept accepts a connection by ID.
//
// This should not be called multiple times with the same ID at one time.
func (m *muxBroker) Accept(id uint32) (net.Conn, error) {
var c net.Conn
p := m.getStream(id)
select {
case c = <-p.ch:
close(p.doneCh)
case <-time.After(5 * time.Second):
m.Lock()
defer m.Unlock()
delete(m.streams, id)
return nil, fmt.Errorf("timeout waiting for accept")
}
// Ack our connection
if err := binary.Write(c, binary.LittleEndian, id); err != nil {
c.Close()
return nil, err
}
return c, nil
}
// Close closes the connection and all sub-connections.
func (m *muxBroker) Close() error {
return m.session.Close()
}
// Dial opens a connection by ID.
func (m *muxBroker) Dial(id uint32) (net.Conn, error) {
// Open the stream
stream, err := m.session.OpenStream()
if err != nil {
return nil, err
}
// Write the stream ID onto the wire.
if err := binary.Write(stream, binary.LittleEndian, id); err != nil {
stream.Close()
return nil, err
}
// Read the ack that we connected. Then we're off!
var ack uint32
if err := binary.Read(stream, binary.LittleEndian, &ack); err != nil {
stream.Close()
return nil, err
}
if ack != id {
stream.Close()
return nil, fmt.Errorf("bad ack: %d (expected %d)", ack, id)
}
return stream, nil
}
// NextId returns a unique ID to use next.
func (m *muxBroker) NextId() uint32 {
return atomic.AddUint32(&m.nextId, 1)
}
// Run starts the brokering and should be executed in a goroutine, since it
// blocks forever, or until the session closes.
func (m *muxBroker) Run() {
for {
stream, err := m.session.AcceptStream()
if err != nil {
// Once we receive an error, just exit
break
}
// Read the stream ID from the stream
var id uint32
if err := binary.Read(stream, binary.LittleEndian, &id); err != nil {
stream.Close()
continue
}
// Initialize the waiter
p := m.getStream(id)
select {
case p.ch <- stream:
default:
}
// Wait for a timeout
go m.timeoutWait(id, p)
}
}
func (m *muxBroker) getStream(id uint32) *muxBrokerPending {
m.Lock()
defer m.Unlock()
p, ok := m.streams[id]
if ok {
return p
}
m.streams[id] = &muxBrokerPending{
ch: make(chan net.Conn, 1),
doneCh: make(chan struct{}),
}
return m.streams[id]
}
func (m *muxBroker) timeoutWait(id uint32, p *muxBrokerPending) {
// Wait for the stream to either be picked up and connected, or
// for a timeout.
timeout := false
select {
case <-p.doneCh:
case <-time.After(5 * time.Second):
timeout = true
}
m.Lock()
defer m.Unlock()
// Delete the stream so no one else can grab it
delete(m.streams, id)
// If we timed out, then check if we have a channel in the buffer,
// and if so, close it.
if timeout {
select {
case s := <-p.ch:
s.Close()
}
}
}

View File

@ -1,518 +0,0 @@
package rpc
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider_impl(t *testing.T) {
var _ terraform.ResourceProvider = new(ResourceProvider)
}
func TestResourceProvider_input(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
input := new(terraform.MockUIInput)
expected := &terraform.ResourceConfig{
Raw: map[string]interface{}{"bar": "baz"},
}
p.InputReturnConfig = expected
// Input
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
actual, err := provider.Input(input, config)
if !p.InputCalled {
t.Fatal("input should be called")
}
if !reflect.DeepEqual(p.InputConfig, config) {
t.Fatalf("bad: %#v", p.InputConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("bad: %#v", actual)
}
}
func TestResourceProvider_configure(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProviderFunc().(*terraform.MockResourceProvider)
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_configure_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.ConfigureReturnError = errors.New("foo")
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e == nil {
t.Fatal("should have error")
}
if e.Error() != "foo" {
t.Fatalf("bad: %s", e)
}
}
func TestResourceProvider_configure_warnings(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
e := provider.Configure(config)
if !p.ConfigureCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ConfigureConfig, config) {
t.Fatalf("bad: %#v", p.ConfigureConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_apply(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.ApplyReturn = &terraform.InstanceState{
ID: "bob",
}
// Apply
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
diff := &terraform.InstanceDiff{}
newState, err := provider.Apply(info, state, diff)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyDiff, diff) {
t.Fatalf("bad: %#v", p.ApplyDiff)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.ApplyReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_diff(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.DiffReturn = &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"foo": &terraform.ResourceAttrDiff{
Old: "",
New: "bar",
},
},
}
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.DiffReturn, diff) {
t.Fatalf("bad: %#v", diff)
}
}
func TestResourceProvider_diff_error(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.DiffReturnError = errors.New("foo")
// Diff
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
diff, err := provider.Diff(info, state, config)
if !p.DiffCalled {
t.Fatal("diff should be called")
}
if !reflect.DeepEqual(p.DiffDesired, config) {
t.Fatalf("bad: %#v", p.DiffDesired)
}
if err == nil {
t.Fatal("should have error")
}
if diff != nil {
t.Fatal("should not have diff")
}
}
func TestResourceProvider_refresh(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
p.RefreshReturn = &terraform.InstanceState{
ID: "bob",
}
// Refresh
info := &terraform.InstanceInfo{}
state := &terraform.InstanceState{}
newState, err := provider.Refresh(info, state)
if !p.RefreshCalled {
t.Fatal("refresh should be called")
}
if !reflect.DeepEqual(p.RefreshState, state) {
t.Fatalf("bad: %#v", p.RefreshState)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
if !reflect.DeepEqual(p.RefreshReturn, newState) {
t.Fatalf("bad: %#v", newState)
}
}
func TestResourceProvider_resources(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
expected := []terraform.ResourceType{
{"foo"},
{"bar"},
}
p.ResourcesReturn = expected
// Resources
result := provider.Resources()
if !p.ResourcesCalled {
t.Fatal("resources should be called")
}
if !reflect.DeepEqual(result, expected) {
t.Fatalf("bad: %#v", result)
}
}
func TestResourceProvider_validate(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_validateResource(t *testing.T) {
p := new(terraform.MockResourceProvider)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_errors(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateResourceReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvider_validateResource_warns(t *testing.T) {
p := new(terraform.MockResourceProvider)
p.ValidateResourceReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := &ResourceProvider{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provider.ValidateResource("foo", config)
if !p.ValidateResourceCalled {
t.Fatal("configure should be called")
}
if p.ValidateResourceType != "foo" {
t.Fatalf("bad: %#v", p.ValidateResourceType)
}
if !reflect.DeepEqual(p.ValidateResourceConfig, config) {
t.Fatalf("bad: %#v", p.ValidateResourceConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvider_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()
provider, err := client.ResourceProvider()
if err != nil {
t.Fatalf("err: %s", err)
}
var p interface{}
p = provider
pCloser, ok := p.(terraform.ResourceProviderCloser)
if !ok {
t.Fatal("should be a ResourceProviderCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provider: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
err = provider.Configure(&terraform.ResourceConfig{})
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,165 +0,0 @@
package rpc
import (
"errors"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
func TestResourceProvisioner_apply(t *testing.T) {
client, server := testNewClientServer(t)
defer client.Close()
p := server.ProvisionerFunc().(*terraform.MockResourceProvisioner)
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
// Apply
output := &terraform.MockUIOutput{}
state := &terraform.InstanceState{}
conf := &terraform.ResourceConfig{}
err = provisioner.Apply(output, state, conf)
if !p.ApplyCalled {
t.Fatal("apply should be called")
}
if !reflect.DeepEqual(p.ApplyConfig, conf) {
t.Fatalf("bad: %#v", p.ApplyConfig)
}
if err != nil {
t.Fatalf("bad: %#v", err)
}
}
func TestResourceProvisioner_validate(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_errors(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
p.ValidateReturnErrors = []error{errors.New("foo")}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if w != nil {
t.Fatalf("bad: %#v", w)
}
if len(e) != 1 {
t.Fatalf("bad: %#v", e)
}
if e[0].Error() != "foo" {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_validate_warns(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
p.ValidateReturnWarns = []string{"foo"}
client, server := testClientServer(t)
name, err := Register(server, p)
if err != nil {
t.Fatalf("err: %s", err)
}
provisioner := &ResourceProvisioner{Client: client, Name: name}
// Configure
config := &terraform.ResourceConfig{
Raw: map[string]interface{}{"foo": "bar"},
}
w, e := provisioner.Validate(config)
if !p.ValidateCalled {
t.Fatal("configure should be called")
}
if !reflect.DeepEqual(p.ValidateConfig, config) {
t.Fatalf("bad: %#v", p.ValidateConfig)
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
expected := []string{"foo"}
if !reflect.DeepEqual(w, expected) {
t.Fatalf("bad: %#v", w)
}
}
func TestResourceProvisioner_close(t *testing.T) {
client, _ := testNewClientServer(t)
defer client.Close()
provisioner, err := client.ResourceProvisioner()
if err != nil {
t.Fatalf("err: %s", err)
}
var p interface{}
p = provisioner
pCloser, ok := p.(terraform.ResourceProvisionerCloser)
if !ok {
t.Fatal("should be a ResourceProvisionerCloser")
}
if err := pCloser.Close(); err != nil {
t.Fatalf("failed to close provisioner: %s", err)
}
// The connection should be closed now, so if we to make a
// new call we should get an error.
o := &terraform.MockUIOutput{}
s := &terraform.InstanceState{}
c := &terraform.ResourceConfig{}
err = provisioner.Apply(o, s, c)
if err == nil {
t.Fatal("should have error")
}
}

View File

@ -1,35 +0,0 @@
package rpc
import (
"errors"
"fmt"
"net/rpc"
"sync"
"github.com/hashicorp/terraform/terraform"
)
// nextId is the next ID to use for names registered.
var nextId uint32 = 0
var nextLock sync.Mutex
// Register registers a Terraform thing with the RPC server and returns
// the name it is registered under.
func Register(server *rpc.Server, thing interface{}) (name string, err error) {
nextLock.Lock()
defer nextLock.Unlock()
switch t := thing.(type) {
case terraform.ResourceProvider:
name = fmt.Sprintf("Terraform%d", nextId)
err = server.RegisterName(name, &ResourceProviderServer{Provider: t})
case terraform.ResourceProvisioner:
name = fmt.Sprintf("Terraform%d", nextId)
err = server.RegisterName(name, &ResourceProvisionerServer{Provisioner: t})
default:
return "", errors.New("Unknown type to register for RPC server.")
}
nextId += 1
return
}

View File

@ -1,77 +0,0 @@
package rpc
import (
"net"
"net/rpc"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func testConn(t *testing.T) (net.Conn, net.Conn) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %s", err)
}
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)
}
}()
clientConn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("err: %s", err)
}
<-doneCh
return clientConn, serverConn
}
func testClientServer(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
}
func testNewClientServer(t *testing.T) (*Client, *Server) {
clientConn, serverConn := testConn(t)
server := &Server{
ProviderFunc: testProviderFixed(new(terraform.MockResourceProvider)),
ProvisionerFunc: testProvisionerFixed(
new(terraform.MockResourceProvisioner)),
}
go server.ServeConn(serverConn)
client, err := NewClient(clientConn)
if err != nil {
t.Fatalf("err: %s", err)
}
return client, server
}
func testProviderFixed(p terraform.ResourceProvider) ProviderFunc {
return func() terraform.ResourceProvider {
return p
}
}
func testProvisionerFixed(p terraform.ResourceProvisioner) ProvisionerFunc {
return func() terraform.ResourceProvisioner {
return p
}
}

View File

@ -1,147 +0,0 @@
package rpc
import (
"io"
"log"
"net"
"net/rpc"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/yamux"
)
// Server listens for network connections and then dispenses interface
// implementations for Terraform over net/rpc.
type Server struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
}
// ProviderFunc creates terraform.ResourceProviders when they're requested
// from the server.
type ProviderFunc func() terraform.ResourceProvider
// ProvisionerFunc creates terraform.ResourceProvisioners when they're requested
// from the server.
type ProvisionerFunc func() terraform.ResourceProvisioner
// Accept accepts connections on a listener and serves requests for
// each incoming connection. Accept blocks; the caller typically invokes
// it in a go statement.
func (s *Server) Accept(lis net.Listener) {
for {
conn, err := lis.Accept()
if err != nil {
log.Printf("[ERR] plugin server: %s", err)
return
}
go s.ServeConn(conn)
}
}
// ServeConn runs a single connection.
//
// ServeConn blocks, serving the connection until the client hangs up.
func (s *Server) ServeConn(conn io.ReadWriteCloser) {
// First create the yamux server to wrap this connection
mux, err := yamux.Server(conn, nil)
if err != nil {
conn.Close()
log.Printf("[ERR] plugin: %s", err)
return
}
// Accept the control connection
control, err := mux.Accept()
if err != nil {
mux.Close()
log.Printf("[ERR] plugin: %s", err)
return
}
// Create the broker and start it up
broker := newMuxBroker(mux)
go broker.Run()
// Use the control connection to build the dispenser and serve the
// connection.
server := rpc.NewServer()
server.RegisterName("Dispenser", &dispenseServer{
ProviderFunc: s.ProviderFunc,
ProvisionerFunc: s.ProvisionerFunc,
broker: broker,
})
server.ServeConn(control)
}
// dispenseServer dispenses variousinterface implementations for Terraform.
type dispenseServer struct {
ProviderFunc ProviderFunc
ProvisionerFunc ProvisionerFunc
broker *muxBroker
}
func (d *dispenseServer) ResourceProvider(
args interface{}, response *uint32) error {
id := d.broker.NextId()
*response = id
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
serve(conn, "ResourceProvider", &ResourceProviderServer{
Broker: d.broker,
Provider: d.ProviderFunc(),
})
}()
return nil
}
func (d *dispenseServer) ResourceProvisioner(
args interface{}, response *uint32) error {
id := d.broker.NextId()
*response = id
go func() {
conn, err := d.broker.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
Broker: d.broker,
Provisioner: d.ProvisionerFunc(),
})
}()
return nil
}
func acceptAndServe(mux *muxBroker, id uint32, n string, v interface{}) {
conn, err := mux.Accept(id)
if err != nil {
log.Printf("[ERR] Plugin acceptAndServe: %s", err)
return
}
serve(conn, n, v)
}
func serve(conn io.ReadWriteCloser, name string, v interface{}) {
server := rpc.NewServer()
if err := server.RegisterName(name, v); err != nil {
log.Printf("[ERR] Plugin dispense: %s", err)
return
}
server.ServeConn(conn)
}