Skip to content

Commit

Permalink
Merge pull request hashicorp#2283 from mitchellh/f-local-shell
Browse files Browse the repository at this point in the history
Local shell provisioner
  • Loading branch information
mitchellh committed Jun 22, 2015
2 parents 9bacbeb + a0b6928 commit 0e8036a
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 1 deletion.
15 changes: 15 additions & 0 deletions plugin/provisioner-shell-local/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
81 changes: 81 additions & 0 deletions provisioner/shell-local/communicator.go
Original file line number Diff line number Diff line change
@@ -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
}
45 changes: 45 additions & 0 deletions provisioner/shell-local/communicator_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
109 changes: 109 additions & 0 deletions provisioner/shell-local/provisioner.go
Original file line number Diff line number Diff line change
@@ -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
}
67 changes: 67 additions & 0 deletions provisioner/shell-local/provisioner_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
45 changes: 45 additions & 0 deletions website/source/docs/provisioners/shell-local.html.md
Original file line number Diff line number Diff line change
@@ -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.

3 changes: 2 additions & 1 deletion website/source/layouts/docs.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit 0e8036a

Please sign in to comment.