From 84214437b34c5d2d84211bc3595a6b61254f10a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 24 Jan 2016 18:10:52 -0800 Subject: [PATCH] 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. --- config.go | 21 +- plugin/client.go | 339 ------------- plugin/client_test.go | 145 ------ plugin/plugin.go | 21 +- plugin/plugin_test.go | 95 +--- {rpc => plugin}/resource_provider.go | 79 +-- plugin/resource_provider_test.go | 619 +++++++++++++++++++++++- {rpc => plugin}/resource_provisioner.go | 45 +- plugin/resource_provisioner_test.go | 188 ++++++- plugin/serve.go | 47 ++ plugin/server.go | 138 ------ {rpc => plugin}/ui_input.go | 10 +- {rpc => plugin}/ui_input_test.go | 9 +- {rpc => plugin}/ui_output.go | 5 +- {rpc => plugin}/ui_output_test.go | 9 +- rpc/client.go | 108 ----- rpc/client_test.go | 76 --- rpc/error.go | 21 - rpc/error_test.go | 26 - rpc/mux_broker.go | 172 ------- rpc/resource_provider_test.go | 518 -------------------- rpc/resource_provisioner_test.go | 165 ------- rpc/rpc.go | 35 -- rpc/rpc_test.go | 77 --- rpc/server.go | 147 ------ 25 files changed, 966 insertions(+), 2149 deletions(-) delete mode 100644 plugin/client.go delete mode 100644 plugin/client_test.go rename {rpc => plugin}/resource_provider.go (80%) rename {rpc => plugin}/resource_provisioner.go (71%) create mode 100644 plugin/serve.go delete mode 100644 plugin/server.go rename {rpc => plugin}/ui_input.go (84%) rename {rpc => plugin}/ui_input_test.go (78%) rename {rpc => plugin}/ui_output.go (85%) rename {rpc => plugin}/ui_output_test.go (72%) delete mode 100644 rpc/client.go delete mode 100644 rpc/client_test.go delete mode 100644 rpc/error.go delete mode 100644 rpc/error_test.go delete mode 100644 rpc/mux_broker.go delete mode 100644 rpc/resource_provider_test.go delete mode 100644 rpc/resource_provisioner_test.go delete mode 100644 rpc/rpc.go delete mode 100644 rpc/rpc_test.go delete mode 100644 rpc/server.go diff --git a/config.go b/config.go index c9b2a7f75..1d473d4ad 100644 --- a/config.go +++ b/config.go @@ -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 } } diff --git a/plugin/client.go b/plugin/client.go deleted file mode 100644 index 8a3b03fc0..000000000 --- a/plugin/client.go +++ /dev/null @@ -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) -} diff --git a/plugin/client_test.go b/plugin/client_test.go deleted file mode 100644 index 68b995c13..000000000 --- a/plugin/client_test.go +++ /dev/null @@ -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") - } -} diff --git a/plugin/plugin.go b/plugin/plugin.go index 858946700..00fa7b296 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -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{}, +} diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index d395837c3..ddef40ab2 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -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 } diff --git a/rpc/resource_provider.go b/plugin/resource_provider.go similarity index 80% rename from rpc/resource_provider.go rename to plugin/resource_provider.go index 3fe6927de..712e79c86 100644 --- a/rpc/resource_provider.go +++ b/plugin/resource_provider.go @@ -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 } diff --git a/plugin/resource_provider_test.go b/plugin/resource_provider_test.go index 41cbb8191..944041d3e 100644 --- a/plugin/resource_provider_test.go +++ b/plugin/resource_provider_test.go @@ -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") } } diff --git a/rpc/resource_provisioner.go b/plugin/resource_provisioner.go similarity index 71% rename from rpc/resource_provisioner.go rename to plugin/resource_provisioner.go index 715704d02..982309580 100644 --- a/rpc/resource_provisioner.go +++ b/plugin/resource_provisioner.go @@ -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, diff --git a/plugin/resource_provisioner_test.go b/plugin/resource_provisioner_test.go index e0920b4af..073c8d2b7 100644 --- a/plugin/resource_provisioner_test.go +++ b/plugin/resource_provisioner_test.go @@ -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") } } diff --git a/plugin/serve.go b/plugin/serve.go new file mode 100644 index 000000000..ba20e3734 --- /dev/null +++ b/plugin/serve.go @@ -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}, + } +} diff --git a/plugin/server.go b/plugin/server.go deleted file mode 100644 index 3daa8a3de..000000000 --- a/plugin/server.go +++ /dev/null @@ -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) -} diff --git a/rpc/ui_input.go b/plugin/ui_input.go similarity index 84% rename from rpc/ui_input.go rename to plugin/ui_input.go index 6c95806c5..493efc0a9 100644 --- a/rpc/ui_input.go +++ b/plugin/ui_input.go @@ -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 diff --git a/rpc/ui_input_test.go b/plugin/ui_input_test.go similarity index 78% rename from rpc/ui_input_test.go rename to plugin/ui_input_test.go index 6de494831..a13dc0ee1 100644 --- a/rpc/ui_input_test.go +++ b/plugin/ui_input_test.go @@ -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", diff --git a/rpc/ui_output.go b/plugin/ui_output.go similarity index 85% rename from rpc/ui_output.go rename to plugin/ui_output.go index a997b943b..c222b00cd 100644 --- a/rpc/ui_output.go +++ b/plugin/ui_output.go @@ -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. diff --git a/rpc/ui_output_test.go b/plugin/ui_output_test.go similarity index 72% rename from rpc/ui_output_test.go rename to plugin/ui_output_test.go index 0113a0903..50eadaa02 100644 --- a/rpc/ui_output_test.go +++ b/plugin/ui_output_test.go @@ -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") diff --git a/rpc/client.go b/rpc/client.go deleted file mode 100644 index 0c80385ee..000000000 --- a/rpc/client.go +++ /dev/null @@ -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 -} diff --git a/rpc/client_test.go b/rpc/client_test.go deleted file mode 100644 index f8c286fe8..000000000 --- a/rpc/client_test.go +++ /dev/null @@ -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) - } -} diff --git a/rpc/error.go b/rpc/error.go deleted file mode 100644 index c3ab7b1a4..000000000 --- a/rpc/error.go +++ /dev/null @@ -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 -} diff --git a/rpc/error_test.go b/rpc/error_test.go deleted file mode 100644 index 8ca8b60eb..000000000 --- a/rpc/error_test.go +++ /dev/null @@ -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) - } -} diff --git a/rpc/mux_broker.go b/rpc/mux_broker.go deleted file mode 100644 index 639902a82..000000000 --- a/rpc/mux_broker.go +++ /dev/null @@ -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() - } - } -} diff --git a/rpc/resource_provider_test.go b/rpc/resource_provider_test.go deleted file mode 100644 index 3efdbce25..000000000 --- a/rpc/resource_provider_test.go +++ /dev/null @@ -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") - } -} diff --git a/rpc/resource_provisioner_test.go b/rpc/resource_provisioner_test.go deleted file mode 100644 index 6fabdb6d4..000000000 --- a/rpc/resource_provisioner_test.go +++ /dev/null @@ -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") - } -} diff --git a/rpc/rpc.go b/rpc/rpc.go deleted file mode 100644 index f11a482f3..000000000 --- a/rpc/rpc.go +++ /dev/null @@ -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 -} diff --git a/rpc/rpc_test.go b/rpc/rpc_test.go deleted file mode 100644 index f23d9332a..000000000 --- a/rpc/rpc_test.go +++ /dev/null @@ -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 - } -} diff --git a/rpc/server.go b/rpc/server.go deleted file mode 100644 index dd1e9b7b0..000000000 --- a/rpc/server.go +++ /dev/null @@ -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) -}