Merge pull request #2283 from mitchellh/f-local-shell

Local shell provisioner
This commit is contained in:
Mitchell Hashimoto 2015-06-22 12:18:48 -07:00
commit 0e8036a023
7 changed files with 364 additions and 1 deletions

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/mitchellh/packer/provisioner/shell-local"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterProvisioner(new(shell.Provisioner))
server.Serve()
}

View File

@ -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
}

View File

@ -0,0 +1,45 @@
package shell
import (
"bytes"
"runtime"
"strings"
"testing"
"github.com/mitchellh/packer/packer"
)
func TestCommunicator_impl(t *testing.T) {
var _ packer.Communicator = new(Communicator)
}
func TestCommunicator(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("windows not supported for this test")
return
}
c := &Communicator{
ExecuteCommand: []string{"/bin/sh", "-c", "{{.Command}}"},
}
var buf bytes.Buffer
cmd := &packer.RemoteCmd{
Command: "echo foo",
Stdout: &buf,
}
if err := c.Start(cmd); err != nil {
t.Fatalf("err: %s", err)
}
cmd.Wait()
if cmd.ExitStatus != 0 {
t.Fatalf("err bad exit status: %d", cmd.ExitStatus)
}
if strings.TrimSpace(buf.String()) != "foo" {
t.Fatalf("bad: %s", buf.String())
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,45 @@
---
layout: "docs"
page_title: "Local Shell Provisioner"
description: |-
The shell Packer provisioner provisions machines built by Packer using shell scripts. Shell provisioning is the easiest way to get software installed and configured on a machine.
---
# Local Shell Provisioner
Type: `shell-local`
The local shell provisioner executes a local shell script on the machine
running Packer. The [remote shell](/docs/provisioners/shell.html)
provisioner executes shell scripts on a remote machine.
## Basic Example
The example below is fully functional.
```javascript
{
"type": "shell-local",
"command": "echo foo"
}
```
## Configuration Reference
The reference of available configuration options is listed below. The only
required element is "command".
Required:
* `command` (string) - The command to execute. This will be executed
within the context of a shell as specified by `execute_command`.
Optional parameters:
* `execute_command` (array of strings) - The command to use to execute the script.
By default this is `["/bin/sh", "-c", "{{.Command}"]`. The value is an array
of arguments executed directly by the OS.
The value of this is
treated as [configuration template](/docs/templates/configuration-templates.html).
The only available variable is `Command` which is the command to execute.

View File

@ -47,7 +47,8 @@
<ul>
<li><h4>Provisioners</h4></li>
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
<li><a href="/docs/provisioners/shell.html">Remote Shell</a></li>
<li><a href="/docs/provisioners/shell-local.html">Local Shell</a></li>
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
<li><a href="/docs/provisioners/powershell.html">PowerShell</a></li>
<li><a href="/docs/provisioners/windows-shell.html">Windows Shell</a></li>