provisioner/shell-local: a first stab
This commit is contained in:
parent
f41429b6b4
commit
80fc1f032b
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue