Merge pull request #2283 from mitchellh/f-local-shell
Local shell provisioner
This commit is contained in:
commit
0e8036a023
|
@ -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()
|
||||
}
|
|
@ -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,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())
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue