diff --git a/config.go b/config.go index c9b2a7f75438..1d473d4ad4e4 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 8a3b03fc0a25..000000000000 --- 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 68b995c13972..000000000000 --- 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 85894670044f..00fa7b2967bd 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 d395837c3b97..ddef40ab217b 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 3fe6927de8b2..712e79c8631b 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 41cbb8191289..944041d3e584 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) +} + +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("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) - _, err := c.Client() + // 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("should not have error: %s", err) + 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 715704d024ea..9823095803c1 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 e0920b4afbce..073c8d2b7ed3 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) +} + +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("err: %s", err) + } + provisioner := raw.(terraform.ResourceProvisioner) - _, err := c.Client() + // 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("should not have error: %s", err) + 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 000000000000..ba20e373455a --- /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 3daa8a3dee23..000000000000 --- 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 6c95806c56ab..493efc0a91b1 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 6de494831de2..a13dc0ee181a 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 a997b943b25a..c222b00cde61 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 0113a09038db..50eadaa02248 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 0c80385eef55..000000000000 --- 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 f8c286fe8e9c..000000000000 --- 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 c3ab7b1a4cca..000000000000 --- 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 8ca8b60ebf8a..000000000000 --- 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 639902a82548..000000000000 --- 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 3efdbce25f03..000000000000 --- 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 6fabdb6d419c..000000000000 --- 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 f11a482f34ab..000000000000 --- 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 f23d9332aff4..000000000000 --- 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 dd1e9b7b082b..000000000000 --- 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) -}