f555e7a9f2
* I had to contextualise Communicator.Start and RemoteCmd.StartWithUi NOTE: Communicator.Start starts a RemoteCmd but RemoteCmd.StartWithUi will run the cmd and wait for a return, so I renamed StartWithUi to RunWithUi so that the intent is clearer. Ideally in the future RunWithUi will be named back to StartWithUi and the exit status or wait funcs of the command will allow to wait for a return. If you do so please read carrefully https://golang.org/pkg/os/exec/#Cmd.Stdout to avoid a deadlock * cmd.ExitStatus to cmd.ExitStatus() is now blocking to avoid race conditions * also had to simplify StartWithUi
233 lines
6.7 KiB
Go
233 lines
6.7 KiB
Go
package shell_local
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/packer/common"
|
|
commonhelper "github.com/hashicorp/packer/helper/common"
|
|
"github.com/hashicorp/packer/packer"
|
|
"github.com/hashicorp/packer/packer/tmp"
|
|
"github.com/hashicorp/packer/template/interpolate"
|
|
)
|
|
|
|
type ExecuteCommandTemplate struct {
|
|
Vars string
|
|
Script string
|
|
Command string
|
|
WinRMPassword string
|
|
}
|
|
|
|
type EnvVarsTemplate struct {
|
|
WinRMPassword string
|
|
}
|
|
|
|
func Run(ctx context.Context, ui packer.Ui, config *Config) (bool, error) {
|
|
// Check if shell-local can even execute against this runtime OS
|
|
if len(config.OnlyOn) > 0 {
|
|
runCommand := false
|
|
for _, os := range config.OnlyOn {
|
|
if os == runtime.GOOS {
|
|
runCommand = true
|
|
break
|
|
}
|
|
}
|
|
if !runCommand {
|
|
ui.Say(fmt.Sprintf("Skipping shell-local due to runtime OS"))
|
|
log.Printf("[INFO] (shell-local): skipping shell-local due to missing runtime OS")
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
scripts = append(scripts, tempScriptFileName)
|
|
|
|
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.RunWithUi(ctx, 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 := tmp.File("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)
|
|
}
|
|
|
|
// generate context so you can interpolate the command
|
|
config.Ctx.Data = &EnvVarsTemplate{
|
|
WinRMPassword: getWinRMPassword(config.PackerBuildName),
|
|
}
|
|
|
|
for _, command := range config.Inline {
|
|
// interpolate command to check for template variables.
|
|
command, err := interpolate.Render(command, &config.Ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
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,
|
|
WinRMPassword: getWinRMPassword(config.PackerBuildName),
|
|
}
|
|
|
|
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)
|
|
|
|
// expose ip address variables
|
|
httpAddr := common.GetHTTPAddr()
|
|
if httpAddr != "" {
|
|
envVars["PACKER_HTTP_ADDR"] = httpAddr
|
|
}
|
|
httpIP := common.GetHTTPIP()
|
|
if httpIP != "" {
|
|
envVars["PACKER_HTTP_IP"] = httpIP
|
|
}
|
|
httpPort := common.GetHTTPPort()
|
|
if httpPort != "" {
|
|
envVars["PACKER_HTTP_PORT"] = httpPort
|
|
}
|
|
|
|
// interpolate environment variables
|
|
config.Ctx.Data = &EnvVarsTemplate{
|
|
WinRMPassword: getWinRMPassword(config.PackerBuildName),
|
|
}
|
|
// Split vars into key/value components
|
|
for _, envVar := range config.Vars {
|
|
envVar, err := interpolate.Render(envVar, &config.Ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Split vars into key/value components
|
|
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
|
|
}
|
|
|
|
func getWinRMPassword(buildName string) string {
|
|
winRMPass, _ := commonhelper.RetrieveSharedState("winrm_password", buildName)
|
|
packer.LogSecretFilter.Set(winRMPass)
|
|
return winRMPass
|
|
}
|