From 80fc1f032b29d227d1fbd836cc4c0f1ac4ba8292 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 19 Jun 2015 15:06:06 -0700 Subject: [PATCH] provisioner/shell-local: a first stab --- provisioner/shell-local/communicator.go | 81 +++++++++++++++ provisioner/shell-local/provisioner.go | 109 ++++++++++++++++++++ provisioner/shell-local/provisioner_test.go | 67 ++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 provisioner/shell-local/communicator.go create mode 100644 provisioner/shell-local/provisioner.go create mode 100644 provisioner/shell-local/provisioner_test.go diff --git a/provisioner/shell-local/communicator.go b/provisioner/shell-local/communicator.go new file mode 100644 index 000000000..5cf3cd980 --- /dev/null +++ b/provisioner/shell-local/communicator.go @@ -0,0 +1,81 @@ +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 +} diff --git a/provisioner/shell-local/provisioner.go b/provisioner/shell-local/provisioner.go new file mode 100644 index 000000000..499be7f1d --- /dev/null +++ b/provisioner/shell-local/provisioner.go @@ -0,0 +1,109 @@ +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 +} diff --git a/provisioner/shell-local/provisioner_test.go b/provisioner/shell-local/provisioner_test.go new file mode 100644 index 000000000..ad8f3065d --- /dev/null +++ b/provisioner/shell-local/provisioner_test.go @@ -0,0 +1,67 @@ +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) + } +}