packer/plugin: Refactor to get interfaces from Client

This commit is contained in:
Mitchell Hashimoto 2013-06-11 11:00:06 -07:00
parent 250cb0106b
commit 7fe98e50fe
10 changed files with 127 additions and 249 deletions

View File

@ -2,10 +2,7 @@ package plugin
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"log" "log"
"net/rpc"
"os/exec"
) )
type cmdBuilder struct { type cmdBuilder struct {
@ -47,43 +44,3 @@ func (c *cmdBuilder) checkExit(p interface{}, cb func()) {
log.Panic(p) log.Panic(p)
} }
} }
// Returns a valid packer.Builder where the builder is executed via RPC
// to a plugin that is within a subprocess.
//
// This method will start the given exec.Cmd, which should point to
// the plugin binary to execute. Some configuration will be done to
// the command, such as overriding Stdout and some environmental variables.
//
// This function guarantees the subprocess will end in a timely manner.
func Builder(cmd *exec.Cmd) (result packer.Builder, err error) {
config := &ClientConfig{
Cmd: cmd,
Managed: true,
}
cmdClient := NewClient(config)
address, err := cmdClient.Start()
if err != nil {
return
}
defer func() {
// Make sure the command is properly killed in the case of an error
if err != nil {
cmdClient.Kill()
}
}()
client, err := rpc.Dial("tcp", address)
if err != nil {
return
}
result = &cmdBuilder{
packrpc.Builder(client),
cmdClient,
}
return
}

View File

@ -1,7 +1,6 @@
package plugin package plugin
import ( import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
@ -20,15 +19,21 @@ func (helperBuilder) Run(packer.Ui, packer.Hook, packer.Cache) packer.Artifact {
func (helperBuilder) Cancel() {} func (helperBuilder) Cancel() {}
func TestBuilder_NoExist(t *testing.T) { func TestBuilder_NoExist(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill()
_, err := Builder(exec.Command("i-should-never-ever-ever-exist")) _, err := c.Builder()
assert.NotNil(err, "should have an error") if err == nil {
t.Fatal("should have error")
}
} }
func TestBuilder_Good(t *testing.T) { func TestBuilder_Good(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: helperProcess("builder")})
defer c.Kill()
_, err := Builder(helperProcess("builder")) _, err := c.Builder()
assert.Nil(err, "should start builder properly") if err != nil {
t.Fatalf("should not have error: %s", err)
}
} }

View File

@ -4,8 +4,11 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"io" "io"
"log" "log"
"net/rpc"
"os" "os"
"os/exec" "os/exec"
"strings" "strings"
@ -20,9 +23,9 @@ var managedClients = make([]*client, 0, 5)
type client struct { type client struct {
config *ClientConfig config *ClientConfig
exited bool exited bool
started bool
startL sync.Mutex
doneLogging bool doneLogging bool
l sync.Mutex
address string
} }
// ClientConfig is the configuration used to initialize a new // ClientConfig is the configuration used to initialize a new
@ -101,6 +104,50 @@ func (c *client) Exited() bool {
return c.exited return c.exited
} }
// Returns a builder implementation that is communicating over this
// client. If the client hasn't been started, this will start it.
func (c *client) Builder() (packer.Builder, error) {
client, err := c.rpcClient()
if err != nil {
return nil, err
}
return &cmdBuilder{packrpc.Builder(client), c}, nil
}
// Returns a command implementation that is communicating over this
// client. If the client hasn't been started, this will start it.
func (c *client) Command() (packer.Command, error) {
client, err := c.rpcClient()
if err != nil {
return nil, err
}
return &cmdCommand{packrpc.Command(client), c}, nil
}
// Returns a hook implementation that is communicating over this
// client. If the client hasn't been started, this will start it.
func (c *client) Hook() (packer.Hook, error) {
client, err := c.rpcClient()
if err != nil {
return nil, err
}
return &cmdHook{packrpc.Hook(client), c}, nil
}
// Returns a provisioner implementation that is communicating over this
// client. If the client hasn't been started, this will start it.
func (c *client) Provisioner() (packer.Provisioner, error) {
client, err := c.rpcClient()
if err != nil {
return nil, err
}
return &cmdProvisioner{packrpc.Provisioner(client), c}, nil
}
// End the executing subprocess (if it is running) and perform any cleanup // End the executing subprocess (if it is running) and perform any cleanup
// tasks necessary such as capturing any remaining logs and so on. // tasks necessary such as capturing any remaining logs and so on.
// //
@ -136,13 +183,12 @@ func (c *client) Kill() {
// Once a client has been started once, it cannot be started again, even if // Once a client has been started once, it cannot be started again, even if
// it was killed. // it was killed.
func (c *client) Start() (address string, err error) { func (c *client) Start() (address string, err error) {
c.startL.Lock() c.l.Lock()
defer c.startL.Unlock() defer c.l.Unlock()
if c.started { if c.address != "" {
panic("plugin client already started once") return c.address, nil
} }
c.started = true
env := []string{ env := []string{
fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort), fmt.Sprintf("PACKER_PLUGIN_MIN_PORT=%d", c.config.MinPort),
@ -205,7 +251,8 @@ func (c *client) Start() (address string, err error) {
if line, lerr := stdout.ReadBytes('\n'); lerr == nil { if line, lerr := stdout.ReadBytes('\n'); lerr == nil {
// Trim the address and reset the err since we were able // Trim the address and reset the err since we were able
// to read some sort of address. // to read some sort of address.
address = strings.TrimSpace(string(line)) c.address = strings.TrimSpace(string(line))
address = c.address
err = nil err = nil
break break
} }
@ -243,3 +290,17 @@ func (c *client) logStderr(buf *bytes.Buffer) {
// Flag that we've completed logging for others // Flag that we've completed logging for others
c.doneLogging = true c.doneLogging = true
} }
func (c *client) rpcClient() (*rpc.Client, error) {
address, err := c.Start()
if err != nil {
return nil, err
}
client, err := rpc.Dial("tcp", address)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -33,22 +33,6 @@ func TestClient(t *testing.T) {
} }
} }
func TestClient_Start_Once(t *testing.T) {
process := helperProcess("mock")
c := NewClient(&ClientConfig{Cmd: process})
defer c.Kill()
defer func() {
p := recover()
if p == nil {
t.Fatal("should've paniced")
}
}()
c.Start()
c.Start()
}
func TestClient_Start_Timeout(t *testing.T) { func TestClient_Start_Timeout(t *testing.T) {
config := &ClientConfig{ config := &ClientConfig{
Cmd: helperProcess("start-timeout"), Cmd: helperProcess("start-timeout"),

View File

@ -2,10 +2,7 @@ package plugin
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"log" "log"
"net/rpc"
"os/exec"
) )
type cmdCommand struct { type cmdCommand struct {
@ -52,43 +49,3 @@ func (c *cmdCommand) checkExit(p interface{}, cb func()) {
log.Panic(p) log.Panic(p)
} }
} }
// Returns a valid packer.Command where the command is executed via RPC
// to a plugin that is within a subprocess.
//
// This method will start the given exec.Cmd, which should point to
// the plugin binary to execute. Some configuration will be done to
// the command, such as overriding Stdout and some environmental variables.
//
// This function guarantees the subprocess will end in a timely manner.
func Command(cmd *exec.Cmd) (result packer.Command, err error) {
config := &ClientConfig{
Cmd: cmd,
Managed: true,
}
cmdClient := NewClient(config)
address, err := cmdClient.Start()
if err != nil {
return
}
defer func() {
// Make sure the command is properly killed in the case of an error
if err != nil {
cmdClient.Kill()
}
}()
client, err := rpc.Dial("tcp", address)
if err != nil {
return
}
result = &cmdCommand{
packrpc.Command(client),
cmdClient,
}
return
}

View File

@ -1,7 +1,6 @@
package plugin package plugin
import ( import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
@ -22,40 +21,31 @@ func (helperCommand) Synopsis() string {
} }
func TestCommand_NoExist(t *testing.T) { func TestCommand_NoExist(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill()
_, err := Command(exec.Command("i-should-never-ever-ever-exist")) _, err := c.Command()
assert.NotNil(err, "should have an error") if err == nil {
} t.Fatal("should have error")
func TestCommand_Good(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true)
command, err := Command(helperProcess("command"))
assert.Nil(err, "should start command properly")
assert.NotNil(command, "should have a command")
if command != nil {
result := command.Synopsis()
assert.Equal(result, "1", "should return result")
result = command.Help()
assert.Equal(result, "2", "should return result")
} }
} }
func TestCommand_CommandExited(t *testing.T) { func TestCommand_Good(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: helperProcess("command")})
defer c.Kill()
_, err := Command(helperProcess("im-a-command-that-doesnt-work")) command, err := c.Command()
assert.NotNil(err, "should have an error") if err != nil {
assert.Equal(err.Error(), "plugin exited before we could connect", "be correct error") t.Fatalf("should not have error: %s", err)
} }
func TestCommand_BadRPC(t *testing.T) { result := command.Synopsis()
assert := asserts.NewTestingAsserts(t, true) if result != "1" {
t.Errorf("synopsis not correct: %s", result)
_, err := Command(helperProcess("invalid-rpc-address")) }
assert.NotNil(err, "should have an error")
assert.Equal(err.Error(), "missing port in address lolinvalid", "be correct error") result = command.Help()
if result != "2" {
t.Errorf("help not correct: %s", result)
}
} }

View File

@ -2,10 +2,7 @@ package plugin
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"log" "log"
"net/rpc"
"os/exec"
) )
type cmdHook struct { type cmdHook struct {
@ -29,43 +26,3 @@ func (c *cmdHook) checkExit(p interface{}, cb func()) {
log.Panic(p) log.Panic(p)
} }
} }
// Returns a valid packer.Hook where the hook is executed via RPC
// to a plugin that is within a subprocess.
//
// This method will start the given exec.Cmd, which should point to
// the plugin binary to execute. Some configuration will be done to
// the command, such as overriding Stdout and some environmental variables.
//
// This function guarantees the subprocess will end in a timely manner.
func Hook(cmd *exec.Cmd) (result packer.Hook, err error) {
config := &ClientConfig{
Cmd: cmd,
Managed: true,
}
cmdClient := NewClient(config)
address, err := cmdClient.Start()
if err != nil {
return
}
defer func() {
// Make sure the command is properly killed in the case of an error
if err != nil {
cmdClient.Kill()
}
}()
client, err := rpc.Dial("tcp", address)
if err != nil {
return
}
result = &cmdHook{
packrpc.Hook(client),
cmdClient,
}
return
}

View File

@ -1,7 +1,6 @@
package plugin package plugin
import ( import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
@ -12,15 +11,21 @@ type helperHook byte
func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) {} func (helperHook) Run(string, packer.Ui, packer.Communicator, interface{}) {}
func TestHook_NoExist(t *testing.T) { func TestHook_NoExist(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill()
_, err := Hook(exec.Command("i-should-never-ever-ever-exist")) _, err := c.Hook()
assert.NotNil(err, "should have an error") if err == nil {
t.Fatal("should have error")
}
} }
func TestHook_Good(t *testing.T) { func TestHook_Good(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: helperProcess("hook")})
defer c.Kill()
_, err := Hook(helperProcess("hook")) _, err := c.Hook()
assert.Nil(err, "should start hook properly") if err != nil {
t.Fatalf("should not have error: %s", err)
}
} }

View File

@ -2,10 +2,7 @@ package plugin
import ( import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
packrpc "github.com/mitchellh/packer/packer/rpc"
"log" "log"
"net/rpc"
"os/exec"
) )
type cmdProvisioner struct { type cmdProvisioner struct {
@ -38,43 +35,3 @@ func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
log.Panic(p) log.Panic(p)
} }
} }
// Returns a valid packer.Provisioner where the hook is executed via RPC
// to a plugin that is within a subprocess.
//
// This method will start the given exec.Cmd, which should point to
// the plugin binary to execute. Some configuration will be done to
// the command, such as overriding Stdout and some environmental variables.
//
// This function guarantees the subprocess will end in a timely manner.
func Provisioner(cmd *exec.Cmd) (result packer.Provisioner, err error) {
config := &ClientConfig{
Cmd: cmd,
Managed: true,
}
cmdClient := NewClient(config)
address, err := cmdClient.Start()
if err != nil {
return
}
defer func() {
// Make sure the command is properly killed in the case of an error
if err != nil {
cmdClient.Kill()
}
}()
client, err := rpc.Dial("tcp", address)
if err != nil {
return
}
result = &cmdProvisioner{
packrpc.Provisioner(client),
cmdClient,
}
return
}

View File

@ -1,7 +1,6 @@
package plugin package plugin
import ( import (
"cgl.tideland.biz/asserts"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"os/exec" "os/exec"
"testing" "testing"
@ -16,15 +15,21 @@ func (helperProvisioner) Prepare(...interface{}) error {
func (helperProvisioner) Provision(packer.Ui, packer.Communicator) {} func (helperProvisioner) Provision(packer.Ui, packer.Communicator) {}
func TestProvisioner_NoExist(t *testing.T) { func TestProvisioner_NoExist(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: exec.Command("i-should-not-exist")})
defer c.Kill()
_, err := Provisioner(exec.Command("i-should-never-ever-ever-exist")) _, err := c.Provisioner()
assert.NotNil(err, "should have an error") if err == nil {
t.Fatal("should have error")
}
} }
func TestProvisioner_Good(t *testing.T) { func TestProvisioner_Good(t *testing.T) {
assert := asserts.NewTestingAsserts(t, true) c := NewClient(&ClientConfig{Cmd: helperProcess("provisioner")})
defer c.Kill()
_, err := Provisioner(helperProcess("provisioner")) _, err := c.Provisioner()
assert.Nil(err, "should start provisioner properly") if err != nil {
t.Fatalf("should not have error: %s", err)
}
} }