Commit 80fc1f03 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

provisioner/shell-local: a first stab

parent f41429b6
package shell
import (
"fmt"
"io"
"os"
"os/exec"
"syscall"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
type Communicator struct {
ExecuteCommand []string
Ctx interpolate.Context
}
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
// Render the template so that we know how to execute the command
c.Ctx.Data = &ExecuteCommandTemplate{
Command: cmd.Command,
}
for i, field := range c.ExecuteCommand {
command, err := interpolate.Render(field, &c.Ctx)
if err != nil {
return fmt.Errorf("Error processing command: %s", err)
}
c.ExecuteCommand[i] = command
}
// Build the local command to execute
localCmd := exec.Command(c.ExecuteCommand[0], c.ExecuteCommand[1:]...)
localCmd.Stdin = cmd.Stdin
localCmd.Stdout = cmd.Stdout
localCmd.Stderr = cmd.Stderr
// Start it. If it doesn't work, then error right away.
if err := localCmd.Start(); err != nil {
return err
}
// We've started successfully. Start a goroutine to wait for
// it to complete and track exit status.
go func() {
var exitStatus int
err := localCmd.Wait()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
exitStatus = 1
// There is no process-independent way to get the REAL
// exit status so we just try to go deeper.
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitStatus = status.ExitStatus()
}
}
}
cmd.SetExited(exitStatus)
}()
return nil
}
func (c *Communicator) Upload(string, io.Reader, *os.FileInfo) error {
return fmt.Errorf("upload not supported")
}
func (c *Communicator) UploadDir(string, string, []string) error {
return fmt.Errorf("uploadDir not supported")
}
func (c *Communicator) Download(string, io.Writer) error {
return fmt.Errorf("download not supported")
}
type ExecuteCommandTemplate struct {
Command string
}
package shell
import (
"errors"
"fmt"
"runtime"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/helper/config"
"github.com/mitchellh/packer/packer"
"github.com/mitchellh/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
// Command is the command to execute
Command string
// ExecuteCommand is the command used to execute the command.
ExecuteCommand []string `mapstructure:"execute_command"`
ctx interpolate.Context
}
type Provisioner struct {
config Config
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
Interpolate: true,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"execute_command",
},
},
}, raws...)
if err != nil {
return err
}
if len(p.config.ExecuteCommand) == 0 {
if runtime.GOOS == "windows" {
p.config.ExecuteCommand = []string{
"cmd",
"/C",
"{{.Command}}",
}
} else {
p.config.ExecuteCommand = []string{
"/bin/sh",
"-c",
"{{.Command}}",
}
}
}
var errs *packer.MultiError
if p.config.Command == "" {
errs = packer.MultiErrorAppend(errs,
errors.New("command must be specified"))
}
if len(p.config.ExecuteCommand) == 0 {
errs = packer.MultiErrorAppend(errs,
errors.New("execute_command must not be empty"))
}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
}
func (p *Provisioner) Provision(ui packer.Ui, _ packer.Communicator) error {
// Make another communicator for local
comm := &Communicator{
Ctx: p.config.ctx,
ExecuteCommand: p.config.ExecuteCommand,
}
// Build the remote command
cmd := &packer.RemoteCmd{Command: p.config.Command}
ui.Say(fmt.Sprintf(
"Executing local command: %s",
p.config.Command))
if err := cmd.StartWithUi(comm, ui); err != nil {
return fmt.Errorf(
"Error executing command: %s\n\n"+
"Please see output above for more information.",
p.config.Command)
}
if cmd.ExitStatus != 0 {
return fmt.Errorf(
"Erroneous exit code %s while executing command: %s\n\n"+
"Please see output above for more information.",
cmd.ExitStatus,
p.config.Command)
}
return nil
}
func (p *Provisioner) Cancel() {
// Just do nothing. When the process ends, so will our provisioner
}
package shell
import (
"testing"
"github.com/mitchellh/packer/packer"
)
func TestProvisioner_impl(t *testing.T) {
var _ packer.Provisioner = new(Provisioner)
}
func TestConfigPrepare(t *testing.T) {
cases := []struct {
Key string
Value interface{}
Err bool
}{
{
"unknown_key",
"bad",
true,
},
{
"command",
nil,
true,
},
}
for _, tc := range cases {
raw := testConfig(t)
if tc.Value == nil {
delete(raw, tc.Key)
} else {
raw[tc.Key] = tc.Value
}
var p Provisioner
err := p.Prepare(raw)
if tc.Err {
testConfigErr(t, err, tc.Key)
} else {
testConfigOk(t, err)
}
}
}
func testConfig(t *testing.T) map[string]interface{} {
return map[string]interface{}{
"command": "echo foo",
}
}
func testConfigErr(t *testing.T, err error, extra string) {
if err == nil {
t.Fatalf("should error: %s", extra)
}
}
func testConfigOk(t *testing.T, err error) {
if err != nil {
t.Fatalf("bad: %s", err)
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment