Merge pull request #5956 from hashicorp/dedupe_shell_local
Deduplicate the code for the shell-local post-processor and shell-local provisioner
This commit is contained in:
commit
62e1323577
|
@ -3,8 +3,8 @@ package chroot
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
sl "github.com/hashicorp/packer/common/shell-local"
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
"github.com/hashicorp/packer/post-processor/shell-local"
|
|
||||||
"github.com/hashicorp/packer/template/interpolate"
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,7 +21,9 @@ func RunLocalCommands(commands []string, wrappedCommand CommandWrapper, ctx inte
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Executing command: %s", command))
|
ui.Say(fmt.Sprintf("Executing command: %s", command))
|
||||||
comm := &shell_local.Communicator{}
|
comm := &sl.Communicator{
|
||||||
|
ExecuteCommand: []string{command},
|
||||||
|
}
|
||||||
cmd := &packer.RemoteCmd{Command: command}
|
cmd := &packer.RemoteCmd{Command: command}
|
||||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||||
return fmt.Errorf("Error executing command: %s", err)
|
return fmt.Errorf("Error executing command: %s", err)
|
||||||
|
|
|
@ -1,36 +1,27 @@
|
||||||
package shell
|
package shell_local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
"github.com/hashicorp/packer/template/interpolate"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Communicator struct {
|
type Communicator struct {
|
||||||
ExecuteCommand []string
|
ExecuteCommand []string
|
||||||
Ctx interpolate.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||||
// Render the template so that we know how to execute the command
|
if len(c.ExecuteCommand) == 0 {
|
||||||
c.Ctx.Data = &ExecuteCommandTemplate{
|
return fmt.Errorf("Error launching command via shell-local communicator: No ExecuteCommand provided")
|
||||||
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
|
// Build the local command to execute
|
||||||
|
log.Printf("[INFO] (shell-local communicator): Executing local shell command %s", c.ExecuteCommand)
|
||||||
localCmd := exec.Command(c.ExecuteCommand[0], c.ExecuteCommand[1:]...)
|
localCmd := exec.Command(c.ExecuteCommand[0], c.ExecuteCommand[1:]...)
|
||||||
localCmd.Stdin = cmd.Stdin
|
localCmd.Stdin = cmd.Stdin
|
||||||
localCmd.Stdout = cmd.Stdout
|
localCmd.Stdout = cmd.Stdout
|
||||||
|
@ -79,7 +70,3 @@ func (c *Communicator) Download(string, io.Writer) error {
|
||||||
func (c *Communicator) DownloadDir(string, string, []string) error {
|
func (c *Communicator) DownloadDir(string, string, []string) error {
|
||||||
return fmt.Errorf("downloadDir not supported")
|
return fmt.Errorf("downloadDir not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecuteCommandTemplate struct {
|
|
||||||
Command string
|
|
||||||
}
|
|
|
@ -19,12 +19,13 @@ func TestCommunicator(t *testing.T) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &Communicator{}
|
c := &Communicator{
|
||||||
|
ExecuteCommand: []string{"/bin/sh", "-c", "echo foo"},
|
||||||
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
cmd := &packer.RemoteCmd{
|
cmd := &packer.RemoteCmd{
|
||||||
Command: "/bin/echo foo",
|
Stdout: &buf,
|
||||||
Stdout: &buf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Start(cmd); err != nil {
|
if err := c.Start(cmd); err != nil {
|
|
@ -0,0 +1,215 @@
|
||||||
|
package shell_local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/common"
|
||||||
|
configHelper "github.com/hashicorp/packer/helper/config"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
|
||||||
|
// ** DEPRECATED: USE INLINE INSTEAD **
|
||||||
|
// ** Only Present for backwards compatibiltiy **
|
||||||
|
// Command is the command to execute
|
||||||
|
Command string
|
||||||
|
|
||||||
|
// An inline script to execute. Multiple strings are all executed
|
||||||
|
// in the context of a single shell.
|
||||||
|
Inline []string
|
||||||
|
|
||||||
|
// The shebang value used when running inline scripts.
|
||||||
|
InlineShebang string `mapstructure:"inline_shebang"`
|
||||||
|
|
||||||
|
// The file extension to use for the file generated from the inline commands
|
||||||
|
TempfileExtension string `mapstructure:"tempfile_extension"`
|
||||||
|
|
||||||
|
// The local path of the shell script to upload and execute.
|
||||||
|
Script string
|
||||||
|
|
||||||
|
// An array of multiple scripts to run.
|
||||||
|
Scripts []string
|
||||||
|
|
||||||
|
// An array of environment variables that will be injected before
|
||||||
|
// your command(s) are executed.
|
||||||
|
Vars []string `mapstructure:"environment_vars"`
|
||||||
|
|
||||||
|
EnvVarFormat string `mapstructure:"env_var_format"`
|
||||||
|
// End dedupe with postprocessor
|
||||||
|
|
||||||
|
// The command used to execute the script. The '{{ .Path }}' variable
|
||||||
|
// should be used to specify where the script goes, {{ .Vars }}
|
||||||
|
// can be used to inject the environment_vars into the environment.
|
||||||
|
ExecuteCommand []string `mapstructure:"execute_command"`
|
||||||
|
|
||||||
|
UseLinuxPathing bool `mapstructure:"use_linux_pathing"`
|
||||||
|
|
||||||
|
Ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(config *Config, raws ...interface{}) error {
|
||||||
|
err := configHelper.Decode(&config, &configHelper.DecodeOpts{
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateContext: &config.Ctx,
|
||||||
|
InterpolateFilter: &interpolate.RenderFilter{
|
||||||
|
Exclude: []string{
|
||||||
|
"execute_command",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error decoding config: %s, config is %#v, and raws is %#v", err, config, raws)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(config *Config) error {
|
||||||
|
var errs *packer.MultiError
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
if len(config.ExecuteCommand) == 0 {
|
||||||
|
config.ExecuteCommand = []string{
|
||||||
|
"cmd",
|
||||||
|
"/V",
|
||||||
|
"/C",
|
||||||
|
"{{.Vars}}",
|
||||||
|
"call",
|
||||||
|
"{{.Script}}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if config.InlineShebang == "" {
|
||||||
|
config.InlineShebang = "/bin/sh -e"
|
||||||
|
}
|
||||||
|
if len(config.ExecuteCommand) == 0 {
|
||||||
|
config.ExecuteCommand = []string{
|
||||||
|
"/bin/sh",
|
||||||
|
"-c",
|
||||||
|
"{{.Vars}} {{.Script}}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up input
|
||||||
|
if config.Inline != nil && len(config.Inline) == 0 {
|
||||||
|
config.Inline = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Scripts == nil {
|
||||||
|
config.Scripts = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Vars == nil {
|
||||||
|
config.Vars = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the user has given us a command to run
|
||||||
|
if config.Command == "" && len(config.Inline) == 0 &&
|
||||||
|
len(config.Scripts) == 0 && config.Script == "" {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
errors.New("Command, Inline, Script and Scripts options cannot all be empty."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that user hasn't given us too many commands to run
|
||||||
|
tooManyOptionsErr := errors.New("You may only specify one of the " +
|
||||||
|
"following options: Command, Inline, Script or Scripts. Please" +
|
||||||
|
" consolidate these options in your config.")
|
||||||
|
|
||||||
|
if config.Command != "" {
|
||||||
|
if len(config.Inline) != 0 || len(config.Scripts) != 0 || config.Script != "" {
|
||||||
|
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
|
||||||
|
} else {
|
||||||
|
config.Inline = []string{config.Command}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Script != "" {
|
||||||
|
if len(config.Scripts) > 0 || len(config.Inline) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
|
||||||
|
} else {
|
||||||
|
config.Scripts = []string{config.Script}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Scripts) > 0 && config.Inline != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs, tooManyOptionsErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all scripts we need to run exist locally
|
||||||
|
for _, path := range config.Scripts {
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Bad script '%s': %s", path, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.UseLinuxPathing {
|
||||||
|
for index, script := range config.Scripts {
|
||||||
|
scriptAbsPath, err := filepath.Abs(script)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error converting %s to absolute path: %s", script, err.Error())
|
||||||
|
}
|
||||||
|
converted, err := ConvertToLinuxPath(scriptAbsPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config.Scripts[index] = converted
|
||||||
|
}
|
||||||
|
// Interoperability issues with WSL makes creating and running tempfiles
|
||||||
|
// via golang's os package basically impossible.
|
||||||
|
if len(config.Inline) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Packer is unable to use the Command and Inline "+
|
||||||
|
"features with the Windows Linux Subsystem. Please use "+
|
||||||
|
"the Script or Scripts options instead"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This is currently undocumented and not a feature users are expected to
|
||||||
|
// interact with.
|
||||||
|
if config.EnvVarFormat == "" {
|
||||||
|
if (runtime.GOOS == "windows") && !config.UseLinuxPathing {
|
||||||
|
config.EnvVarFormat = "set %s=%s && "
|
||||||
|
} else {
|
||||||
|
config.EnvVarFormat = "%s='%s' "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop unnecessary "." in extension; we add this later.
|
||||||
|
if config.TempfileExtension != "" {
|
||||||
|
if strings.HasPrefix(config.TempfileExtension, ".") {
|
||||||
|
config.TempfileExtension = config.TempfileExtension[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a check for bad environment variables, such as '=foo', 'foobar'
|
||||||
|
for _, kv := range config.Vars {
|
||||||
|
vs := strings.SplitN(kv, "=", 2)
|
||||||
|
if len(vs) != 2 || vs[0] == "" {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// C:/path/to/your/file becomes /mnt/c/path/to/your/file
|
||||||
|
func ConvertToLinuxPath(winAbsPath string) (string, error) {
|
||||||
|
// get absolute path of script, and morph it into the bash path
|
||||||
|
winAbsPath = strings.Replace(winAbsPath, "\\", "/", -1)
|
||||||
|
splitPath := strings.SplitN(winAbsPath, ":/", 2)
|
||||||
|
winBashPath := fmt.Sprintf("/mnt/%s/%s", strings.ToLower(splitPath[0]), splitPath[1])
|
||||||
|
return winBashPath, nil
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package shell_local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/hashicorp/packer/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecuteCommandTemplate struct {
|
||||||
|
Vars string
|
||||||
|
Script string
|
||||||
|
Command string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(ui packer.Ui, config *Config) (bool, error) {
|
||||||
|
scripts := make([]string, len(config.Scripts))
|
||||||
|
if len(config.Scripts) > 0 {
|
||||||
|
copy(scripts, config.Scripts)
|
||||||
|
} else if config.Inline != nil {
|
||||||
|
// If we have an inline script, then turn that into a temporary
|
||||||
|
// shell script and use that.
|
||||||
|
tempScriptFileName, err := createInlineScriptFile(config)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
scripts = append(scripts, tempScriptFileName)
|
||||||
|
|
||||||
|
// figure out what extension the file should have, and rename it.
|
||||||
|
if config.TempfileExtension != "" {
|
||||||
|
os.Rename(tempScriptFileName, fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension))
|
||||||
|
tempScriptFileName = fmt.Sprintf("%s.%s", tempScriptFileName, config.TempfileExtension)
|
||||||
|
}
|
||||||
|
defer os.Remove(tempScriptFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create environment variables to set before executing the command
|
||||||
|
flattenedEnvVars, err := createFlattenedEnvVars(config)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, script := range scripts {
|
||||||
|
interpolatedCmds, err := createInterpolatedCommands(config, script, flattenedEnvVars)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
ui.Say(fmt.Sprintf("Running local shell script: %s", script))
|
||||||
|
|
||||||
|
comm := &Communicator{
|
||||||
|
ExecuteCommand: interpolatedCmds,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The remoteCmd generated here isn't actually run, but it allows us to
|
||||||
|
// use the same interafce for the shell-local communicator as we use for
|
||||||
|
// the other communicators; ultimately, this command is just used for
|
||||||
|
// buffers and for reading the final exit status.
|
||||||
|
flattenedCmd := strings.Join(interpolatedCmds, " ")
|
||||||
|
cmd := &packer.RemoteCmd{Command: flattenedCmd}
|
||||||
|
log.Printf("[INFO] (shell-local): starting local command: %s", flattenedCmd)
|
||||||
|
|
||||||
|
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"Error executing script: %s\n\n"+
|
||||||
|
"Please see output above for more information.",
|
||||||
|
script)
|
||||||
|
}
|
||||||
|
if cmd.ExitStatus != 0 {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"Erroneous exit code %d while executing script: %s\n\n"+
|
||||||
|
"Please see output above for more information.",
|
||||||
|
cmd.ExitStatus,
|
||||||
|
script)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createInlineScriptFile(config *Config) (string, error) {
|
||||||
|
tf, err := ioutil.TempFile("", "packer-shell")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error preparing shell script: %s", err)
|
||||||
|
}
|
||||||
|
defer tf.Close()
|
||||||
|
// Write our contents to it
|
||||||
|
writer := bufio.NewWriter(tf)
|
||||||
|
if config.InlineShebang != "" {
|
||||||
|
shebang := fmt.Sprintf("#!%s\n", config.InlineShebang)
|
||||||
|
log.Printf("[INFO] (shell-local): Prepending inline script with %s", shebang)
|
||||||
|
writer.WriteString(shebang)
|
||||||
|
}
|
||||||
|
for _, command := range config.Inline {
|
||||||
|
if _, err := writer.WriteString(command + "\n"); err != nil {
|
||||||
|
return "", fmt.Errorf("Error preparing shell script: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Flush(); err != nil {
|
||||||
|
return "", fmt.Errorf("Error preparing shell script: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(tf.Name(), 0700)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] (shell-local): error modifying permissions of temp script file: %s", err.Error())
|
||||||
|
}
|
||||||
|
return tf.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the final command to send to the communicator, using either the
|
||||||
|
// user-provided ExecuteCommand or defaulting to something that makes sense for
|
||||||
|
// the host OS
|
||||||
|
func createInterpolatedCommands(config *Config, script string, flattenedEnvVars string) ([]string, error) {
|
||||||
|
config.Ctx.Data = &ExecuteCommandTemplate{
|
||||||
|
Vars: flattenedEnvVars,
|
||||||
|
Script: script,
|
||||||
|
Command: script,
|
||||||
|
}
|
||||||
|
|
||||||
|
interpolatedCmds := make([]string, len(config.ExecuteCommand))
|
||||||
|
for i, cmd := range config.ExecuteCommand {
|
||||||
|
interpolatedCmd, err := interpolate.Render(cmd, &config.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error processing command: %s", err)
|
||||||
|
}
|
||||||
|
interpolatedCmds[i] = interpolatedCmd
|
||||||
|
}
|
||||||
|
return interpolatedCmds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFlattenedEnvVars(config *Config) (string, error) {
|
||||||
|
flattened := ""
|
||||||
|
envVars := make(map[string]string)
|
||||||
|
|
||||||
|
// Always available Packer provided env vars
|
||||||
|
envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", config.PackerBuildName)
|
||||||
|
envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", config.PackerBuilderType)
|
||||||
|
|
||||||
|
// Split vars into key/value components
|
||||||
|
for _, envVar := range config.Vars {
|
||||||
|
keyValue := strings.SplitN(envVar, "=", 2)
|
||||||
|
// Store pair, replacing any single quotes in value so they parse
|
||||||
|
// correctly with required environment variable format
|
||||||
|
envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a list of env var keys in sorted order
|
||||||
|
var keys []string
|
||||||
|
for k := range envVars {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
flattened += fmt.Sprintf(config.EnvVarFormat, key, envVars[key])
|
||||||
|
}
|
||||||
|
return flattened, nil
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
package shell_local
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/packer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Communicator struct{}
|
|
||||||
|
|
||||||
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
|
||||||
localCmd := exec.Command("sh", "-c", cmd.Command)
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
|
|
||||||
return fmt.Errorf("downloadDir not supported")
|
|
||||||
}
|
|
|
@ -1,51 +1,12 @@
|
||||||
package shell_local
|
package shell_local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
sl "github.com/hashicorp/packer/common/shell-local"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/common"
|
|
||||||
"github.com/hashicorp/packer/helper/config"
|
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
"github.com/hashicorp/packer/template/interpolate"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
common.PackerConfig `mapstructure:",squash"`
|
|
||||||
|
|
||||||
// An inline script to execute. Multiple strings are all executed
|
|
||||||
// in the context of a single shell.
|
|
||||||
Inline []string
|
|
||||||
|
|
||||||
// The shebang value used when running inline scripts.
|
|
||||||
InlineShebang string `mapstructure:"inline_shebang"`
|
|
||||||
|
|
||||||
// The local path of the shell script to upload and execute.
|
|
||||||
Script string
|
|
||||||
|
|
||||||
// An array of multiple scripts to run.
|
|
||||||
Scripts []string
|
|
||||||
|
|
||||||
// An array of environment variables that will be injected before
|
|
||||||
// your command(s) are executed.
|
|
||||||
Vars []string `mapstructure:"environment_vars"`
|
|
||||||
|
|
||||||
// The command used to execute the script. The '{{ .Path }}' variable
|
|
||||||
// should be used to specify where the script goes, {{ .Vars }}
|
|
||||||
// can be used to inject the environment_vars into the environment.
|
|
||||||
ExecuteCommand string `mapstructure:"execute_command"`
|
|
||||||
|
|
||||||
ctx interpolate.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostProcessor struct {
|
type PostProcessor struct {
|
||||||
config Config
|
config sl.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExecuteCommandTemplate struct {
|
type ExecuteCommandTemplate struct {
|
||||||
|
@ -54,179 +15,34 @@ type ExecuteCommandTemplate struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
err := sl.Decode(&p.config, raws...)
|
||||||
Interpolate: true,
|
|
||||||
InterpolateContext: &p.config.ctx,
|
|
||||||
InterpolateFilter: &interpolate.RenderFilter{
|
|
||||||
Exclude: []string{
|
|
||||||
"execute_command",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, raws...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(p.config.ExecuteCommand) == 1 {
|
||||||
if p.config.ExecuteCommand == "" {
|
// Backwards compatibility -- before we merged the shell-local
|
||||||
p.config.ExecuteCommand = `chmod +x "{{.Script}}"; {{.Vars}} "{{.Script}}"`
|
// post-processor and provisioners, the post-processor accepted
|
||||||
|
// execute_command as a string rather than a slice of strings. It didn't
|
||||||
|
// have a configurable call to shell program, automatically prepending
|
||||||
|
// the user-supplied execute_command string with "sh -c". If users are
|
||||||
|
// still using the old way of defining ExecuteCommand (by supplying a
|
||||||
|
// single string rather than a slice of strings) then we need to
|
||||||
|
// prepend this command with the call that the post-processor defaulted
|
||||||
|
// to before.
|
||||||
|
p.config.ExecuteCommand = append([]string{"sh", "-c"}, p.config.ExecuteCommand...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.config.Inline != nil && len(p.config.Inline) == 0 {
|
return sl.Validate(&p.config)
|
||||||
p.config.Inline = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.config.InlineShebang == "" {
|
|
||||||
p.config.InlineShebang = "/bin/sh -e"
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.config.Scripts == nil {
|
|
||||||
p.config.Scripts = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.config.Vars == nil {
|
|
||||||
p.config.Vars = make([]string, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs *packer.MultiError
|
|
||||||
if p.config.Script != "" && len(p.config.Scripts) > 0 {
|
|
||||||
errs = packer.MultiErrorAppend(errs,
|
|
||||||
errors.New("Only one of script or scripts can be specified."))
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.config.Script != "" {
|
|
||||||
p.config.Scripts = []string{p.config.Script}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.config.Scripts) == 0 && p.config.Inline == nil {
|
|
||||||
errs = packer.MultiErrorAppend(errs,
|
|
||||||
errors.New("Either a script file or inline script must be specified."))
|
|
||||||
} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
|
|
||||||
errs = packer.MultiErrorAppend(errs,
|
|
||||||
errors.New("Only a script file or an inline script can be specified, not both."))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range p.config.Scripts {
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
errs = packer.MultiErrorAppend(errs,
|
|
||||||
fmt.Errorf("Bad script '%s': %s", path, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a check for bad environment variables, such as '=foo', 'foobar'
|
|
||||||
for _, kv := range p.config.Vars {
|
|
||||||
vs := strings.SplitN(kv, "=", 2)
|
|
||||||
if len(vs) != 2 || vs[0] == "" {
|
|
||||||
errs = packer.MultiErrorAppend(errs,
|
|
||||||
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if errs != nil && len(errs.Errors) > 0 {
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
||||||
|
// this particular post-processor doesn't do anything with the artifact
|
||||||
|
// except to return it.
|
||||||
|
|
||||||
scripts := make([]string, len(p.config.Scripts))
|
retBool, retErr := sl.Run(ui, &p.config)
|
||||||
copy(scripts, p.config.Scripts)
|
if !retBool {
|
||||||
|
return nil, retBool, retErr
|
||||||
// If we have an inline script, then turn that into a temporary
|
|
||||||
// shell script and use that.
|
|
||||||
if p.config.Inline != nil {
|
|
||||||
tf, err := ioutil.TempFile("", "packer-shell")
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(tf.Name())
|
|
||||||
|
|
||||||
// Set the path to the temporary file
|
|
||||||
scripts = append(scripts, tf.Name())
|
|
||||||
|
|
||||||
// Write our contents to it
|
|
||||||
writer := bufio.NewWriter(tf)
|
|
||||||
writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
|
|
||||||
for _, command := range p.config.Inline {
|
|
||||||
if _, err := writer.WriteString(command + "\n"); err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writer.Flush(); err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error preparing shell script: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tf.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create environment variables to set before executing the command
|
return artifact, retBool, retErr
|
||||||
flattenedEnvVars := p.createFlattenedEnvVars()
|
|
||||||
|
|
||||||
for _, script := range scripts {
|
|
||||||
|
|
||||||
p.config.ctx.Data = &ExecuteCommandTemplate{
|
|
||||||
Vars: flattenedEnvVars,
|
|
||||||
Script: script,
|
|
||||||
}
|
|
||||||
|
|
||||||
command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, fmt.Errorf("Error processing command: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Post processing with local shell script: %s", script))
|
|
||||||
|
|
||||||
comm := &Communicator{}
|
|
||||||
|
|
||||||
cmd := &packer.RemoteCmd{Command: command}
|
|
||||||
|
|
||||||
log.Printf("starting local command: %s", command)
|
|
||||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
|
||||||
return nil, false, fmt.Errorf(
|
|
||||||
"Error executing script: %s\n\n"+
|
|
||||||
"Please see output above for more information.",
|
|
||||||
script)
|
|
||||||
}
|
|
||||||
if cmd.ExitStatus != 0 {
|
|
||||||
return nil, false, fmt.Errorf(
|
|
||||||
"Erroneous exit code %d while executing script: %s\n\n"+
|
|
||||||
"Please see output above for more information.",
|
|
||||||
cmd.ExitStatus,
|
|
||||||
script)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artifact, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PostProcessor) createFlattenedEnvVars() (flattened string) {
|
|
||||||
flattened = ""
|
|
||||||
envVars := make(map[string]string)
|
|
||||||
|
|
||||||
// Always available Packer provided env vars
|
|
||||||
envVars["PACKER_BUILD_NAME"] = fmt.Sprintf("%s", p.config.PackerBuildName)
|
|
||||||
envVars["PACKER_BUILDER_TYPE"] = fmt.Sprintf("%s", p.config.PackerBuilderType)
|
|
||||||
|
|
||||||
// Split vars into key/value components
|
|
||||||
for _, envVar := range p.config.Vars {
|
|
||||||
keyValue := strings.SplitN(envVar, "=", 2)
|
|
||||||
// Store pair, replacing any single quotes in value so they parse
|
|
||||||
// correctly with required environment variable format
|
|
||||||
envVars[keyValue[0]] = strings.Replace(keyValue[1], "'", `'"'"'`, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a list of env var keys in sorted order
|
|
||||||
var keys []string
|
|
||||||
for k := range envVars {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
// Re-assemble vars surrounding value with single quotes and flatten
|
|
||||||
for _, key := range keys {
|
|
||||||
flattened += fmt.Sprintf("%s='%s' ", key, envVars[key])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,11 @@ package shell_local
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
|
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
|
||||||
|
@ -28,32 +30,35 @@ func TestPostProcessor_Impl(t *testing.T) {
|
||||||
|
|
||||||
func TestPostProcessorPrepare_Defaults(t *testing.T) {
|
func TestPostProcessorPrepare_Defaults(t *testing.T) {
|
||||||
var p PostProcessor
|
var p PostProcessor
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcessorPrepare_InlineShebang(t *testing.T) {
|
func TestPostProcessorPrepare_InlineShebang(t *testing.T) {
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
delete(config, "inline_shebang")
|
delete(raws, "inline_shebang")
|
||||||
p := new(PostProcessor)
|
p := new(PostProcessor)
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
|
expected := ""
|
||||||
if p.config.InlineShebang != "/bin/sh -e" {
|
if runtime.GOOS != "windows" {
|
||||||
|
expected = "/bin/sh -e"
|
||||||
|
}
|
||||||
|
if p.config.InlineShebang != expected {
|
||||||
t.Fatalf("bad value: %s", p.config.InlineShebang)
|
t.Fatalf("bad value: %s", p.config.InlineShebang)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with a good one
|
// Test with a good one
|
||||||
config["inline_shebang"] = "foo"
|
raws["inline_shebang"] = "foo"
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -65,23 +70,23 @@ func TestPostProcessorPrepare_InlineShebang(t *testing.T) {
|
||||||
|
|
||||||
func TestPostProcessorPrepare_InvalidKey(t *testing.T) {
|
func TestPostProcessorPrepare_InvalidKey(t *testing.T) {
|
||||||
var p PostProcessor
|
var p PostProcessor
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
// Add a random key
|
// Add a random key
|
||||||
config["i_should_not_be_valid"] = true
|
raws["i_should_not_be_valid"] = true
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcessorPrepare_Script(t *testing.T) {
|
func TestPostProcessorPrepare_Script(t *testing.T) {
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
delete(config, "inline")
|
delete(raws, "inline")
|
||||||
|
|
||||||
config["script"] = "/this/should/not/exist"
|
raws["script"] = "/this/should/not/exist"
|
||||||
p := new(PostProcessor)
|
p := new(PostProcessor)
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
|
@ -93,23 +98,65 @@ func TestPostProcessorPrepare_Script(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
config["script"] = tf.Name()
|
raws["script"] = tf.Name()
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPostProcessorPrepare_ExecuteCommand(t *testing.T) {
|
||||||
|
// Check that passing a string will work (Backwards Compatibility)
|
||||||
|
p := new(PostProcessor)
|
||||||
|
raws := testConfig()
|
||||||
|
raws["execute_command"] = "foo bar"
|
||||||
|
err := p.Configure(raws)
|
||||||
|
expected := []string{"sh", "-c", "foo bar"}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should handle backwards compatibility: %s", err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, p.config.ExecuteCommand, expected,
|
||||||
|
"Did not get expected execute_command: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
|
||||||
|
|
||||||
|
// Check that passing a list will work
|
||||||
|
p = new(PostProcessor)
|
||||||
|
raws = testConfig()
|
||||||
|
raws["execute_command"] = []string{"foo", "bar"}
|
||||||
|
err = p.Configure(raws)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should handle backwards compatibility: %s", err)
|
||||||
|
}
|
||||||
|
expected = []string{"foo", "bar"}
|
||||||
|
assert.Equal(t, p.config.ExecuteCommand, expected,
|
||||||
|
"Did not get expected execute_command: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
|
||||||
|
|
||||||
|
// Check that default is as expected
|
||||||
|
raws = testConfig()
|
||||||
|
delete(raws, "execute_command")
|
||||||
|
p = new(PostProcessor)
|
||||||
|
p.Configure(raws)
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
expected = []string{"/bin/sh", "-c", "{{.Vars}} {{.Script}}"}
|
||||||
|
} else {
|
||||||
|
expected = []string{"cmd", "/V", "/C", "{{.Vars}}", "call", "{{.Script}}"}
|
||||||
|
}
|
||||||
|
assert.Equal(t, p.config.ExecuteCommand, expected,
|
||||||
|
"Did not get expected default: expected: %#v; received %#v", expected, p.config.ExecuteCommand)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
|
func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
|
||||||
var p PostProcessor
|
var p PostProcessor
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
delete(config, "inline")
|
// Error if no scripts/inline commands provided
|
||||||
delete(config, "script")
|
delete(raws, "inline")
|
||||||
err := p.Configure(config)
|
delete(raws, "script")
|
||||||
|
delete(raws, "command")
|
||||||
|
delete(raws, "scripts")
|
||||||
|
err := p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatalf("should error when no scripts/inline commands are provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with both
|
// Test with both
|
||||||
|
@ -119,9 +166,9 @@ func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
config["inline"] = []interface{}{"foo"}
|
raws["inline"] = []interface{}{"foo"}
|
||||||
config["script"] = tf.Name()
|
raws["script"] = tf.Name()
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
|
@ -129,7 +176,7 @@ func TestPostProcessorPrepare_ScriptAndInline(t *testing.T) {
|
||||||
|
|
||||||
func TestPostProcessorPrepare_ScriptAndScripts(t *testing.T) {
|
func TestPostProcessorPrepare_ScriptAndScripts(t *testing.T) {
|
||||||
var p PostProcessor
|
var p PostProcessor
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
// Test with both
|
// Test with both
|
||||||
tf, err := ioutil.TempFile("", "packer")
|
tf, err := ioutil.TempFile("", "packer")
|
||||||
|
@ -138,21 +185,21 @@ func TestPostProcessorPrepare_ScriptAndScripts(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
config["inline"] = []interface{}{"foo"}
|
raws["inline"] = []interface{}{"foo"}
|
||||||
config["scripts"] = []string{tf.Name()}
|
raws["scripts"] = []string{tf.Name()}
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcessorPrepare_Scripts(t *testing.T) {
|
func TestPostProcessorPrepare_Scripts(t *testing.T) {
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
delete(config, "inline")
|
delete(raws, "inline")
|
||||||
|
|
||||||
config["scripts"] = []string{}
|
raws["scripts"] = []string{}
|
||||||
p := new(PostProcessor)
|
p := new(PostProcessor)
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
|
@ -164,92 +211,55 @@ func TestPostProcessorPrepare_Scripts(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.Remove(tf.Name())
|
defer os.Remove(tf.Name())
|
||||||
|
|
||||||
config["scripts"] = []string{tf.Name()}
|
raws["scripts"] = []string{tf.Name()}
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcessorPrepare_EnvironmentVars(t *testing.T) {
|
func TestPostProcessorPrepare_EnvironmentVars(t *testing.T) {
|
||||||
config := testConfig()
|
raws := testConfig()
|
||||||
|
|
||||||
// Test with a bad case
|
// Test with a bad case
|
||||||
config["environment_vars"] = []string{"badvar", "good=var"}
|
raws["environment_vars"] = []string{"badvar", "good=var"}
|
||||||
p := new(PostProcessor)
|
p := new(PostProcessor)
|
||||||
err := p.Configure(config)
|
err := p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with a trickier case
|
// Test with a trickier case
|
||||||
config["environment_vars"] = []string{"=bad"}
|
raws["environment_vars"] = []string{"=bad"}
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("should have error")
|
t.Fatal("should have error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with a good case
|
// Test with a good case
|
||||||
// Note: baz= is a real env variable, just empty
|
// Note: baz= is a real env variable, just empty
|
||||||
config["environment_vars"] = []string{"FOO=bar", "baz="}
|
raws["environment_vars"] = []string{"FOO=bar", "baz="}
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test when the env variable value contains an equals sign
|
// Test when the env variable value contains an equals sign
|
||||||
config["environment_vars"] = []string{"good=withequals=true"}
|
raws["environment_vars"] = []string{"good=withequals=true"}
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test when the env variable value starts with an equals sign
|
// Test when the env variable value starts with an equals sign
|
||||||
config["environment_vars"] = []string{"good==true"}
|
raws["environment_vars"] = []string{"good==true"}
|
||||||
p = new(PostProcessor)
|
p = new(PostProcessor)
|
||||||
err = p.Configure(config)
|
err = p.Configure(raws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("should not have error: %s", err)
|
t.Fatalf("should not have error: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostProcessor_createFlattenedEnvVars(t *testing.T) {
|
|
||||||
var flattenedEnvVars string
|
|
||||||
config := testConfig()
|
|
||||||
|
|
||||||
userEnvVarTests := [][]string{
|
|
||||||
{}, // No user env var
|
|
||||||
{"FOO=bar"}, // Single user env var
|
|
||||||
{"FOO=bar's"}, // User env var with single quote in value
|
|
||||||
{"FOO=bar", "BAZ=qux"}, // Multiple user env vars
|
|
||||||
{"FOO=bar=baz"}, // User env var with value containing equals
|
|
||||||
{"FOO==bar"}, // User env var with value starting with equals
|
|
||||||
}
|
|
||||||
expected := []string{
|
|
||||||
`PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
`FOO='bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
`FOO='bar'"'"'s' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
`BAZ='qux' FOO='bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
`FOO='bar=baz' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
`FOO='=bar' PACKER_BUILDER_TYPE='iso' PACKER_BUILD_NAME='vmware' `,
|
|
||||||
}
|
|
||||||
|
|
||||||
p := new(PostProcessor)
|
|
||||||
p.Configure(config)
|
|
||||||
|
|
||||||
// Defaults provided by Packer
|
|
||||||
p.config.PackerBuildName = "vmware"
|
|
||||||
p.config.PackerBuilderType = "iso"
|
|
||||||
|
|
||||||
for i, expectedValue := range expected {
|
|
||||||
p.config.Vars = userEnvVarTests[i]
|
|
||||||
flattenedEnvVars = p.createFlattenedEnvVars()
|
|
||||||
if flattenedEnvVars != expectedValue {
|
|
||||||
t.Fatalf("expected flattened env vars to be: %s, got %s.", expectedValue, flattenedEnvVars)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
package shell
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,105 +1,32 @@
|
||||||
package shell
|
package shell
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
sl "github.com/hashicorp/packer/common/shell-local"
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/hashicorp/packer/common"
|
|
||||||
"github.com/hashicorp/packer/helper/config"
|
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
"github.com/hashicorp/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 {
|
type Provisioner struct {
|
||||||
config Config
|
config sl.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||||
err := config.Decode(&p.config, &config.DecodeOpts{
|
err := sl.Decode(&p.config, raws...)
|
||||||
Interpolate: true,
|
|
||||||
InterpolateContext: &p.config.ctx,
|
|
||||||
InterpolateFilter: &interpolate.RenderFilter{
|
|
||||||
Exclude: []string{
|
|
||||||
"execute_command",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, raws...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(p.config.ExecuteCommand) == 0 {
|
err = sl.Validate(&p.config)
|
||||||
if runtime.GOOS == "windows" {
|
if err != nil {
|
||||||
p.config.ExecuteCommand = []string{
|
return err
|
||||||
"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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provisioner) Provision(ui packer.Ui, _ packer.Communicator) error {
|
func (p *Provisioner) Provision(ui packer.Ui, _ packer.Communicator) error {
|
||||||
// Make another communicator for local
|
_, retErr := sl.Run(ui, &p.config)
|
||||||
comm := &Communicator{
|
if retErr != nil {
|
||||||
Ctx: p.config.ctx,
|
return retErr
|
||||||
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 %d while executing command: %s\n\n"+
|
|
||||||
"Please see output above for more information.",
|
|
||||||
cmd.ExitStatus,
|
|
||||||
p.config.Command)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -13,7 +13,7 @@ Type: `shell-local`
|
||||||
|
|
||||||
The local shell post processor executes scripts locally during the post
|
The local shell post processor executes scripts locally during the post
|
||||||
processing stage. Shell local provides a convenient way to automate executing
|
processing stage. Shell local provides a convenient way to automate executing
|
||||||
some task with the packer outputs.
|
some task with packer outputs and variables.
|
||||||
|
|
||||||
## Basic example
|
## Basic example
|
||||||
|
|
||||||
|
@ -33,6 +33,9 @@ required element is either "inline" or "script". Every other option is optional.
|
||||||
|
|
||||||
Exactly *one* of the following is required:
|
Exactly *one* of the following is required:
|
||||||
|
|
||||||
|
- `command` (string) - This is a single command to execute. It will be written
|
||||||
|
to a temporary file and run using the `execute_command` call below.
|
||||||
|
|
||||||
- `inline` (array of strings) - This is an array of commands to execute. The
|
- `inline` (array of strings) - This is an array of commands to execute. The
|
||||||
commands are concatenated by newlines and turned into a single file, so they
|
commands are concatenated by newlines and turned into a single file, so they
|
||||||
are all executed within the same context. This allows you to change
|
are all executed within the same context. This allows you to change
|
||||||
|
@ -52,15 +55,34 @@ Exactly *one* of the following is required:
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
|
|
||||||
- `environment_vars` (array of strings) - An array of key/value pairs to
|
- `environment_vars` (array of strings) - An array of key/value pairs to
|
||||||
inject prior to the execute\_command. The format should be `key=value`.
|
inject prior to the `execute_command`. The format should be `key=value`.
|
||||||
Packer injects some environmental variables by default into the environment,
|
Packer injects some environmental variables by default into the environment,
|
||||||
as well, which are covered in the section below.
|
as well, which are covered in the section below.
|
||||||
|
|
||||||
- `execute_command` (string) - The command to use to execute the script. By
|
- `execute_command` (array of strings) - The command used to execute the script. By
|
||||||
default this is `chmod +x "{{.Script}}"; {{.Vars}} "{{.Script}}"`.
|
default this is `["/bin/sh", "-c", "{{.Vars}}, "{{.Script}}"]`
|
||||||
The value of this is treated as [template engine](/docs/templates/engine.html).
|
on unix and `["cmd", "/c", "{{.Vars}}", "{{.Script}}"]` on windows.
|
||||||
|
This is treated as a [template engine](/docs/templates/engine.html).
|
||||||
There are two available variables: `Script`, which is the path to the script
|
There are two available variables: `Script`, which is the path to the script
|
||||||
to run, `Vars`, which is the list of `environment_vars`, if configured.
|
to run, and `Vars`, which is the list of `environment_vars`, if configured.
|
||||||
|
If you choose to set this option, make sure that the first element in the
|
||||||
|
array is the shell program you want to use (for example, "sh" or
|
||||||
|
"/usr/local/bin/zsh" or even "powershell.exe" although anything other than
|
||||||
|
a flavor of the shell command language is not explicitly supported and may
|
||||||
|
be broken by assumptions made within Packer). It's worth noting that if you
|
||||||
|
choose to try to use shell-local for Powershell or other Windows commands,
|
||||||
|
the environment variables will not be set properly for your environment.
|
||||||
|
|
||||||
|
For backwards compatibility, `execute_command` will accept a string instead
|
||||||
|
of an array of strings. If a single string or an array of strings with only
|
||||||
|
one element is provided, Packer will replicate past behavior by appending
|
||||||
|
your `execute_command` to the array of strings `["sh", "-c"]`. For example,
|
||||||
|
if you set `"execute_command": "foo bar"`, the final `execute_command` that
|
||||||
|
Packer runs will be ["sh", "-c", "foo bar"]. If you set `"execute_command": ["foo", "bar"]`,
|
||||||
|
the final execute_command will remain `["foo", "bar"]`.
|
||||||
|
|
||||||
|
Again, the above is only provided as a backwards compatibility fix; we
|
||||||
|
strongly recommend that you set execute_command as an array of strings.
|
||||||
|
|
||||||
- `inline_shebang` (string) - The
|
- `inline_shebang` (string) - The
|
||||||
[shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when
|
[shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when
|
||||||
|
@ -69,13 +91,72 @@ Optional parameters:
|
||||||
**Important:** If you customize this, be sure to include something like the
|
**Important:** If you customize this, be sure to include something like the
|
||||||
`-e` flag, otherwise individual steps failing won't fail the provisioner.
|
`-e` flag, otherwise individual steps failing won't fail the provisioner.
|
||||||
|
|
||||||
## Execute Command Example
|
- `use_linux_pathing` (bool) - This is only relevant to windows hosts. If you
|
||||||
|
are running Packer in a Windows environment with the Windows Subsystem for
|
||||||
|
Linux feature enabled, and would like to invoke a bash script rather than
|
||||||
|
invoking a Cmd script, you'll need to set this flag to true; it tells Packer
|
||||||
|
to use the linux subsystem path for your script rather than the Windows path.
|
||||||
|
(e.g. /mnt/c/path/to/your/file instead of C:/path/to/your/file). Please see
|
||||||
|
the example below for more guidance on how to use this feature. If you are
|
||||||
|
not on a Windows host, or you do not intend to use the shell-local
|
||||||
|
post-processor to run a bash script, please ignore this option.
|
||||||
|
If you set this flag to true, you still need to provide the standard windows
|
||||||
|
path to the script when providing a `script`. This is a beta feature.
|
||||||
|
|
||||||
|
## Execute Command
|
||||||
|
|
||||||
To many new users, the `execute_command` is puzzling. However, it provides an
|
To many new users, the `execute_command` is puzzling. However, it provides an
|
||||||
important function: customization of how the command is executed. The most
|
important function: customization of how the command is executed. The most
|
||||||
common use case for this is dealing with **sudo password prompts**. You may also
|
common use case for this is dealing with **sudo password prompts**. You may also
|
||||||
need to customize this if you use a non-POSIX shell, such as `tcsh` on FreeBSD.
|
need to customize this if you use a non-POSIX shell, such as `tcsh` on FreeBSD.
|
||||||
|
|
||||||
|
### The Windows Linux Subsystem
|
||||||
|
|
||||||
|
The shell-local post-processor was designed with the idea of allowing you to run
|
||||||
|
commands in your local operating system's native shell. For Windows, we've
|
||||||
|
assumed in our defaults that this is Cmd. However, it is possible to run a
|
||||||
|
bash script as part of the Windows Linux Subsystem from the shell-local
|
||||||
|
post-processor, by modifying the `execute_command` and the `use_linux_pathing`
|
||||||
|
options in the post-processor config.
|
||||||
|
|
||||||
|
The example below is a fully functional test config.
|
||||||
|
|
||||||
|
One limitation of this offering is that "inline" and "command" options are not
|
||||||
|
available to you; please limit yourself to using the "script" or "scripts"
|
||||||
|
options instead.
|
||||||
|
|
||||||
|
Please note that this feature is still in beta, as the underlying WSL is also
|
||||||
|
still in beta. There will be some limitations as a result. For example, it will
|
||||||
|
likely not work unless both Packer and the scripts you want to run are both on
|
||||||
|
the C drive.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{
|
||||||
|
"type": "null",
|
||||||
|
"communicator": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provisioners": [
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest1"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"scripts": ["C:/Users/me/scripts/example_bash.sh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest2"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"script": "C:/Users/me/scripts/example_bash.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Default Environmental Variables
|
## Default Environmental Variables
|
||||||
|
|
||||||
In addition to being able to specify custom environmental variables using the
|
In addition to being able to specify custom environmental variables using the
|
||||||
|
@ -150,3 +231,106 @@ are cleaned up.
|
||||||
|
|
||||||
For a shell script, that means the script **must** exit with a zero code. You
|
For a shell script, that means the script **must** exit with a zero code. You
|
||||||
*must* be extra careful to `exit 0` when necessary.
|
*must* be extra careful to `exit 0` when necessary.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples:
|
||||||
|
|
||||||
|
Example of running a .cmd file on windows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest1"],
|
||||||
|
"scripts": ["./scripts/test_cmd.cmd"]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Contents of "test_cmd.cmd":
|
||||||
|
|
||||||
|
```
|
||||||
|
echo %SHELLLOCALTEST%
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running an inline command on windows:
|
||||||
|
Required customization: tempfile_extension
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest2"],
|
||||||
|
"tempfile_extension": ".cmd",
|
||||||
|
"inline": ["echo %SHELLLOCALTEST%"]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a bash command on windows using WSL:
|
||||||
|
Required customizations: use_linux_pathing and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest3"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"script": "./scripts/example_bash.sh"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Contents of "example_bash.sh":
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
echo $SHELLLOCALTEST
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a powershell script on windows:
|
||||||
|
Required customizations: env_var_format and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest4"],
|
||||||
|
"execute_command": ["powershell.exe", "{{.Vars}} {{.Script}}"],
|
||||||
|
"env_var_format": "$env:%s=\"%s\"; ",
|
||||||
|
"script": "./scripts/example_ps.ps1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a powershell script on windows as "inline":
|
||||||
|
Required customizations: env_var_format, tempfile_extension, and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"tempfile_extension": ".ps1",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest5"],
|
||||||
|
"execute_command": ["powershell.exe", "{{.Vars}} {{.Script}}"],
|
||||||
|
"env_var_format": "$env:%s=\"%s\"; ",
|
||||||
|
"inline": ["write-output $env:SHELLLOCALTEST"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Example of running a bash script on linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest1"],
|
||||||
|
"scripts": ["./scripts/example_bash.sh"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a bash "inline" on linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest2"],
|
||||||
|
"inline": ["echo hello",
|
||||||
|
"echo $PROVISIONERTEST"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,26 @@ The example below is fully functional.
|
||||||
The reference of available configuration options is listed below. The only
|
The reference of available configuration options is listed below. The only
|
||||||
required element is "command".
|
required element is "command".
|
||||||
|
|
||||||
Required:
|
Exactly *one* of the following is required:
|
||||||
|
|
||||||
- `command` (string) - The command to execute. This will be executed within
|
- `command` (string) - This is a single command to execute. It will be written
|
||||||
the context of a shell as specified by `execute_command`.
|
to a temporary file and run using the `execute_command` call below.
|
||||||
|
|
||||||
|
- `inline` (array of strings) - This is an array of commands to execute. The
|
||||||
|
commands are concatenated by newlines and turned into a single file, so they
|
||||||
|
are all executed within the same context. This allows you to change
|
||||||
|
directories in one command and use something in the directory in the next
|
||||||
|
and so on. Inline scripts are the easiest way to pull off simple tasks
|
||||||
|
within the machine.
|
||||||
|
|
||||||
|
- `script` (string) - The path to a script to execute. This path can be
|
||||||
|
absolute or relative. If it is relative, it is relative to the working
|
||||||
|
directory when Packer is executed.
|
||||||
|
|
||||||
|
- `scripts` (array of strings) - An array of scripts to execute. The scripts
|
||||||
|
will be executed in the order specified. Each script is executed in
|
||||||
|
isolation, so state such as variables from one script won't carry on to the
|
||||||
|
next.
|
||||||
|
|
||||||
Optional parameters:
|
Optional parameters:
|
||||||
|
|
||||||
|
@ -50,3 +66,235 @@ Optional parameters:
|
||||||
treated as [configuration
|
treated as [configuration
|
||||||
template](/docs/templates/engine.html). The only available
|
template](/docs/templates/engine.html). The only available
|
||||||
variable is `Command` which is the command to execute.
|
variable is `Command` which is the command to execute.
|
||||||
|
|
||||||
|
- `environment_vars` (array of strings) - An array of key/value pairs to
|
||||||
|
inject prior to the `execute_command`. The format should be `key=value`.
|
||||||
|
Packer injects some environmental variables by default into the environment,
|
||||||
|
as well, which are covered in the section below.
|
||||||
|
|
||||||
|
- `execute_command` (array of strings) - The command used to execute the script.
|
||||||
|
By default this is `["/bin/sh", "-c", "{{.Vars}}, "{{.Script}}"]`
|
||||||
|
on unix and `["cmd", "/c", "{{.Vars}}", "{{.Script}}"]` on windows.
|
||||||
|
This is treated as a [template engine](/docs/templates/engine.html).
|
||||||
|
There are two available variables: `Script`, which is the path to the script
|
||||||
|
to run, and `Vars`, which is the list of `environment_vars`, if configured
|
||||||
|
|
||||||
|
If you choose to set this option, make sure that the first element in the
|
||||||
|
array is the shell program you want to use (for example, "sh"), and a later
|
||||||
|
element in the array must be `{{.Script}}`.
|
||||||
|
|
||||||
|
This option provides you a great deal of flexibility. You may choose to
|
||||||
|
provide your own shell program, for example "/usr/local/bin/zsh" or even
|
||||||
|
"powershell.exe". However, with great power comes great responsibility -
|
||||||
|
these commands are not officially supported and things like environment
|
||||||
|
variables may not work if you use a different shell than the default.
|
||||||
|
|
||||||
|
For backwards compatibility, you may also use {{.Command}}, but it is
|
||||||
|
decoded the same way as {{.Script}}. We recommend using {{.Script}} for the
|
||||||
|
sake of clarity, as even when you set only a single `command` to run,
|
||||||
|
Packer writes it to a temporary file and then runs it as a script.
|
||||||
|
|
||||||
|
- `inline_shebang` (string) - The
|
||||||
|
[shebang](http://en.wikipedia.org/wiki/Shebang_%28Unix%29) value to use when
|
||||||
|
running commands specified by `inline`. By default, this is `/bin/sh -e`. If
|
||||||
|
you're not using `inline`, then this configuration has no effect.
|
||||||
|
**Important:** If you customize this, be sure to include something like the
|
||||||
|
`-e` flag, otherwise individual steps failing won't fail the provisioner.
|
||||||
|
|
||||||
|
- `use_linux_pathing` (bool) - This is only relevant to windows hosts. If you
|
||||||
|
are running Packer in a Windows environment with the Windows Subsystem for
|
||||||
|
Linux feature enabled, and would like to invoke a bash script rather than
|
||||||
|
invoking a Cmd script, you'll need to set this flag to true; it tells Packer
|
||||||
|
to use the linux subsystem path for your script rather than the Windows path.
|
||||||
|
(e.g. /mnt/c/path/to/your/file instead of C:/path/to/your/file). Please see
|
||||||
|
the example below for more guidance on how to use this feature. If you are
|
||||||
|
not on a Windows host, or you do not intend to use the shell-local
|
||||||
|
provisioner to run a bash script, please ignore this option.
|
||||||
|
|
||||||
|
## Execute Command
|
||||||
|
|
||||||
|
To many new users, the `execute_command` is puzzling. However, it provides an
|
||||||
|
important function: customization of how the command is executed. The most
|
||||||
|
common use case for this is dealing with **sudo password prompts**. You may also
|
||||||
|
need to customize this if you use a non-POSIX shell, such as `tcsh` on FreeBSD.
|
||||||
|
|
||||||
|
### The Windows Linux Subsystem
|
||||||
|
|
||||||
|
The shell-local provisioner was designed with the idea of allowing you to run
|
||||||
|
commands in your local operating system's native shell. For Windows, we've
|
||||||
|
assumed in our defaults that this is Cmd. However, it is possible to run a
|
||||||
|
bash script as part of the Windows Linux Subsystem from the shell-local
|
||||||
|
provisioner, by modifying the `execute_command` and the `use_linux_pathing`
|
||||||
|
options in the provisioner config.
|
||||||
|
|
||||||
|
The example below is a fully functional test config.
|
||||||
|
|
||||||
|
One limitation of this offering is that "inline" and "command" options are not
|
||||||
|
available to you; please limit yourself to using the "script" or "scripts"
|
||||||
|
options instead.
|
||||||
|
|
||||||
|
Please note that the WSL is a beta feature, and this tool is not guaranteed to
|
||||||
|
work as you expect it to.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{
|
||||||
|
"type": "null",
|
||||||
|
"communicator": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"provisioners": [
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest1"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"scripts": ["C:/Users/me/scripts/example_bash.sh"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest2"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"script": "C:/Users/me/scripts/example_bash.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Default Environmental Variables
|
||||||
|
|
||||||
|
In addition to being able to specify custom environmental variables using the
|
||||||
|
`environment_vars` configuration, the provisioner automatically defines certain
|
||||||
|
commonly useful environmental variables:
|
||||||
|
|
||||||
|
- `PACKER_BUILD_NAME` is set to the name of the build that Packer is running.
|
||||||
|
This is most useful when Packer is making multiple builds and you want to
|
||||||
|
distinguish them slightly from a common provisioning script.
|
||||||
|
|
||||||
|
- `PACKER_BUILDER_TYPE` is the type of the builder that was used to create the
|
||||||
|
machine that the script is running on. This is useful if you want to run
|
||||||
|
only certain parts of the script on systems built with certain builders.
|
||||||
|
|
||||||
|
## Safely Writing A Script
|
||||||
|
|
||||||
|
Whether you use the `inline` option, or pass it a direct `script` or `scripts`,
|
||||||
|
it is important to understand a few things about how the shell-local
|
||||||
|
provisioner works to run it safely and easily. This understanding will save
|
||||||
|
you much time in the process.
|
||||||
|
|
||||||
|
### Once Per Builder
|
||||||
|
|
||||||
|
The `shell-local` script(s) you pass are run once per builder. That means that
|
||||||
|
if you have an `amazon-ebs` builder and a `docker` builder, your script will be
|
||||||
|
run twice. If you have 3 builders, it will run 3 times, once for each builder.
|
||||||
|
|
||||||
|
### Always Exit Intentionally
|
||||||
|
|
||||||
|
If any provisioner fails, the `packer build` stops and all interim artifacts
|
||||||
|
are cleaned up.
|
||||||
|
|
||||||
|
For a shell script, that means the script **must** exit with a zero code. You
|
||||||
|
*must* be extra careful to `exit 0` when necessary.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage Examples:
|
||||||
|
|
||||||
|
Example of running a .cmd file on windows:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest1"],
|
||||||
|
"scripts": ["./scripts/test_cmd.cmd"]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Contents of "test_cmd.cmd":
|
||||||
|
|
||||||
|
```
|
||||||
|
echo %SHELLLOCALTEST%
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running an inline command on windows:
|
||||||
|
Required customization: tempfile_extension
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest2"],
|
||||||
|
"tempfile_extension": ".cmd",
|
||||||
|
"inline": ["echo %SHELLLOCALTEST%"]
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a bash command on windows using WSL:
|
||||||
|
Required customizations: use_linux_pathing and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest3"],
|
||||||
|
"execute_command": ["bash", "-c", "{{.Vars}} {{.Script}}"],
|
||||||
|
"use_linux_pathing": true,
|
||||||
|
"script": "./scripts/example_bash.sh"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Contents of "example_bash.sh":
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/bin/bash
|
||||||
|
echo $SHELLLOCALTEST
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a powershell script on windows:
|
||||||
|
Required customizations: env_var_format and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest4"],
|
||||||
|
"execute_command": ["powershell.exe", "{{.Vars}} {{.Script}}"],
|
||||||
|
"env_var_format": "$env:%s=\"%s\"; ",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a powershell script on windows as "inline":
|
||||||
|
Required customizations: env_var_format, tempfile_extension, and execute_command
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"tempfile_extension": ".ps1",
|
||||||
|
"environment_vars": ["SHELLLOCALTEST=ShellTest5"],
|
||||||
|
"execute_command": ["powershell.exe", "{{.Vars}} {{.Script}}"],
|
||||||
|
"env_var_format": "$env:%s=\"%s\"; ",
|
||||||
|
"inline": ["write-output $env:SHELLLOCALTEST"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Example of running a bash script on linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest1"],
|
||||||
|
"scripts": ["./scripts/dummy_bash.sh"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example of running a bash "inline" on linux:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"type": "shell-local",
|
||||||
|
"environment_vars": ["PROVISIONERTEST=ProvisionerTest2"],
|
||||||
|
"inline": ["echo hello",
|
||||||
|
"echo $PROVISIONERTEST"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue