Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8597b5c5a3 | |||
| 5b8c6582b6 | |||
| fbd5a3c534 | |||
| a4309ff33a | |||
| 96a0ae50b7 | |||
| bb99b79733 | |||
| 0d5cd549a4 | |||
| 8ba2346fe3 | |||
| cf5abfc0f1 | |||
| 2af19060ce | |||
| 43e76cc7d0 | |||
| b230453b0a | |||
| b2a00e5772 | |||
| 7bad5923d3 | |||
| ff163e4459 | |||
| a793e03fb6 | |||
| c84dd13539 | |||
| 5b7c745c3c | |||
| d916604c24 | |||
| 68fb788c97 | |||
| c92b73899c | |||
| d10a2596b8 | |||
| 604fe9ffaa | |||
| 22a3cdb6bc | |||
| 6f7aad23f0 | |||
| 581a33ab7d | |||
| 2fa53edb2d | |||
| dc5fd1cc17 | |||
| a84c74318c | |||
| 837e641802 | |||
| 8857358830 | |||
| 7761c9329d | |||
| 40e15c84ef | |||
| 299ee6efd2 | |||
| ace8109867 | |||
| 2da011f46c | |||
| 93c2de0af3 | |||
| ccd9b56f9c | |||
| 6d158eaa29 | |||
| 34cafbe68e | |||
| 308b148bb4 | |||
| 6d4b834f61 | |||
| 75d2d769a9 | |||
| 7e2b9de778 | |||
| 74adaa534e | |||
| da467b7d67 | |||
| 0659ccdea0 | |||
| 95f5cea285 | |||
| 03994f052d | |||
| a03807f8e8 | |||
| 73c5aec24d | |||
| cd0cae207c | |||
| 67d8803223 | |||
| cf89e8b783 | |||
| a3a654732c | |||
| 2e095aa292 | |||
| 71d3f89b00 | |||
| db84c9f547 | |||
| 633d6d8d45 | |||
| 1e4d207149 | |||
| 6067167713 | |||
| e1cb7a5ed4 | |||
| f3ef6fc1da | |||
| 59e7c94bbc | |||
| 6509e899d0 | |||
| 277d54fe69 | |||
| bac3d8ffed |
@@ -1,3 +1,23 @@
|
||||
## 0.3.9 (October 2, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* The Amazon chroot builder is now able to run without any `sudo` privileges
|
||||
by using the "command_wrapper" configuration. [GH-430]
|
||||
* Chef provisioner supports environments. [GH-483]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: default user variable values don't need to be strings. [GH-456]
|
||||
* builder/amazon-chroot: Fix errors with waitin for state change. [GH-459]
|
||||
* builder/digitalocean: Use proper error message JSON key (DO API change).
|
||||
* communicator/ssh: SCP uploads now work properly when directories
|
||||
contain symlinks. [GH-449]
|
||||
* provisioner/chef-solo: Data bags and roles path are now properly
|
||||
populated when set. [GH-470]
|
||||
* provisioner/shell: Windows line endings are actually properly changed
|
||||
to Unix line endings. [GH-477]
|
||||
|
||||
## 0.3.8 (September 22, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
@@ -7,7 +7,7 @@ DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
|
||||
all: deps
|
||||
@mkdir -p bin/
|
||||
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
|
||||
@./scripts/build.sh
|
||||
@bash --norc -i ./scripts/build.sh
|
||||
|
||||
deps:
|
||||
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
|
||||
@@ -27,16 +27,19 @@ type Config struct {
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CommandWrapper string `mapstructure:"command_wrapper"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
MountCommand string `mapstructure:"mount_command"`
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
UnmountCommand string `mapstructure:"unmount_command"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type wrappedCommandTemplate struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config Config
|
||||
runner multistep.Runner
|
||||
@@ -78,18 +81,14 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.CopyFiles = []string{"/etc/resolv.conf"}
|
||||
}
|
||||
|
||||
if b.config.MountCommand == "" {
|
||||
b.config.MountCommand = "mount"
|
||||
if b.config.CommandWrapper == "" {
|
||||
b.config.CommandWrapper = "{{.Command}}"
|
||||
}
|
||||
|
||||
if b.config.MountPath == "" {
|
||||
b.config.MountPath = "packer-amazon-chroot-volumes/{{.Device}}"
|
||||
}
|
||||
|
||||
if b.config.UnmountCommand == "" {
|
||||
b.config.UnmountCommand = "umount"
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
@@ -127,10 +126,8 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"device_path": &b.config.DevicePath,
|
||||
"mount_command": &b.config.MountCommand,
|
||||
"source_ami": &b.config.SourceAmi,
|
||||
"unmount_command": &b.config.UnmountCommand,
|
||||
"device_path": &b.config.DevicePath,
|
||||
"source_ami": &b.config.SourceAmi,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
@@ -167,12 +164,20 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
|
||||
ec2conn := ec2.New(auth, region)
|
||||
|
||||
wrappedCommand := func(command string) (string, error) {
|
||||
return b.config.tpl.Process(
|
||||
b.config.CommandWrapper, &wrappedCommandTemplate{
|
||||
Command: command,
|
||||
})
|
||||
}
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("config", &b.config)
|
||||
state.Put("ec2", ec2conn)
|
||||
state.Put("hook", hook)
|
||||
state.Put("ui", ui)
|
||||
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
|
||||
@@ -82,3 +82,14 @@ func TestBuilderPrepare_SourceAmi(t *testing.T) {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_CommandWrapper(t *testing.T) {
|
||||
b := &Builder{}
|
||||
config := testConfig()
|
||||
|
||||
config["command_wrapper"] = "echo hi; {{.Command}}"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// CommandWrapper is a type that given a command, will possibly modify that
|
||||
// command in-flight. This might return an error.
|
||||
type CommandWrapper func(string) (string, error)
|
||||
|
||||
// ShellCommand takes a command string and returns an *exec.Cmd to execute
|
||||
// it within the context of a shell (/bin/sh).
|
||||
func ShellCommand(command string) *exec.Cmd {
|
||||
return exec.Command("/bin/sh", "-c", command)
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -13,16 +15,18 @@ import (
|
||||
// Communicator is a special communicator that works by executing
|
||||
// commands locally but within a chroot.
|
||||
type Communicator struct {
|
||||
Chroot string
|
||||
Chroot string
|
||||
CmdWrapper CommandWrapper
|
||||
}
|
||||
|
||||
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||
chrootCmdPath, err := exec.LookPath("chroot")
|
||||
command, err := c.CmdWrapper(
|
||||
fmt.Sprintf("chroot %s %s", c.Chroot, cmd.Command))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.Command)
|
||||
localCmd := ShellCommand(command)
|
||||
localCmd.Stdin = cmd.Stdin
|
||||
localCmd.Stdout = cmd.Stdout
|
||||
localCmd.Stderr = cmd.Stderr
|
||||
@@ -46,7 +50,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"Chroot executation ended with '%d': '%s'",
|
||||
"Chroot execution exited with '%d': '%s'",
|
||||
exitStatus, cmd.Command)
|
||||
cmd.SetExited(exitStatus)
|
||||
}()
|
||||
@@ -57,49 +61,31 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||
func (c *Communicator) Upload(dst string, r io.Reader) error {
|
||||
dst = filepath.Join(c.Chroot, dst)
|
||||
log.Printf("Uploading to chroot dir: %s", dst)
|
||||
f, err := os.Create(dst)
|
||||
tf, err := ioutil.TempFile("", "packer-amazon-chroot")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error preparing shell script: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
io.Copy(tf, r)
|
||||
|
||||
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp %s %s", tf.Name(), dst))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return ShellCommand(cpCmd).Run()
|
||||
}
|
||||
|
||||
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
walkFn := func(fullPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := filepath.Rel(src, fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range exclude {
|
||||
if e == path {
|
||||
log.Printf("Skipping excluded file: %s", path)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dst, path)
|
||||
f, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return c.Upload(dstPath, f)
|
||||
// TODO: remove any file copied if it appears in `exclude`
|
||||
chrootDest := filepath.Join(c.Chroot, dst)
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, chrootDest)
|
||||
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R %s* %s", src, chrootDest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, dst)
|
||||
return filepath.Walk(src, walkFn)
|
||||
return ShellCommand(cpCmd).Run()
|
||||
}
|
||||
|
||||
func (c *Communicator) Download(src string, w io.Writer) error {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package chroot
|
||||
@@ -0,0 +1,44 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
first, err := ioutil.TempFile("", "copy_files_test")
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't create temp file.")
|
||||
}
|
||||
defer os.Remove(first.Name())
|
||||
newName := first.Name() + "-new"
|
||||
|
||||
payload := "copy_files_test.go payload"
|
||||
if _, err = first.WriteString(payload); err != nil {
|
||||
t.Fatalf("Couldn't write payload to first file.")
|
||||
}
|
||||
first.Sync()
|
||||
|
||||
cmd := ShellCommand(fmt.Sprintf("cp %s %s", first.Name(), newName))
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("Couldn't copy file")
|
||||
}
|
||||
defer os.Remove(newName)
|
||||
|
||||
second, err := os.Open(newName)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't open copied file.")
|
||||
}
|
||||
defer second.Close()
|
||||
|
||||
var copiedPayload = make([]byte, len(payload))
|
||||
if _, err := second.Read(copiedPayload); err != nil {
|
||||
t.Fatalf("Couldn't open copied file for reading.")
|
||||
}
|
||||
|
||||
if string(copiedPayload) != payload {
|
||||
t.Fatalf("payload not copied.")
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,8 @@ func (s *StepAttachVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return nil, "", errors.New("No attachments on volume.")
|
||||
}
|
||||
|
||||
return nil, resp.Volumes[0].Attachments[0].Status, nil
|
||||
a := resp.Volumes[0].Attachments[0]
|
||||
return a, a.Status, nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -111,12 +112,12 @@ func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
state := "detached"
|
||||
if len(resp.Volumes[0].Attachments) > 0 {
|
||||
state = resp.Volumes[0].Attachments[0].Status
|
||||
v := resp.Volumes[0]
|
||||
if len(v.Attachments) > 0 {
|
||||
return v, v.Attachments[0].Status, nil
|
||||
} else {
|
||||
return v, "detached", nil
|
||||
}
|
||||
|
||||
return nil, state, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,12 @@ func (s *StepChrootProvision) Run(state multistep.StateBag) multistep.StepAction
|
||||
hook := state.Get("hook").(packer.Hook)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
// Create our communicator
|
||||
comm := &Communicator{
|
||||
Chroot: mountPath,
|
||||
Chroot: mountPath,
|
||||
CmdWrapper: wrappedCommand,
|
||||
}
|
||||
|
||||
// Provision
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
@@ -23,6 +22,8 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
s.files = make([]string, 0, len(config.CopyFiles))
|
||||
if len(config.CopyFiles) > 0 {
|
||||
@@ -32,8 +33,20 @@ func (s *StepCopyFiles) Run(state multistep.StateBag) multistep.StepAction {
|
||||
chrootPath := filepath.Join(mountPath, path)
|
||||
log.Printf("Copying '%s' to '%s'", path, chrootPath)
|
||||
|
||||
if err := s.copySingle(chrootPath, path); err != nil {
|
||||
err := fmt.Errorf("Error copying file: %s", err)
|
||||
cmdText, err := wrappedCommand(fmt.Sprintf("cp %s %s", path, chrootPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error building copy command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
stderr.Reset()
|
||||
cmd := ShellCommand(cmdText)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
"Error copying file: %s\nnStderr: %s", err, stderr.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
@@ -54,11 +67,18 @@ func (s *StepCopyFiles) Cleanup(state multistep.StateBag) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error {
|
||||
func (s *StepCopyFiles) CleanupFunc(state multistep.StateBag) error {
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
if s.files != nil {
|
||||
for _, file := range s.files {
|
||||
log.Printf("Removing: %s", file)
|
||||
if err := os.Remove(file); err != nil {
|
||||
localCmdText, err := wrappedCommand(fmt.Sprintf("rm -f %s", file))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localCmd := ShellCommand(localCmdText)
|
||||
if err := localCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -67,41 +87,3 @@ func (s *StepCopyFiles) CleanupFunc(multistep.StateBag) error {
|
||||
s.files = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StepCopyFiles) copySingle(dst, src string) error {
|
||||
// Stat the src file so we can copy the mode later
|
||||
srcInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove any existing destination file
|
||||
if err := os.Remove(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy the files
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcF.Close()
|
||||
|
||||
dstF, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstF.Close()
|
||||
|
||||
if _, err := io.Copy(dstF, srcF); err != nil {
|
||||
return err
|
||||
}
|
||||
dstF.Close()
|
||||
|
||||
// Match the mode
|
||||
if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ func (s *StepCreateVolume) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return nil, resp.Volumes[0].Status, nil
|
||||
v := resp.Volumes[0]
|
||||
return v, v.Status, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
@@ -28,6 +27,7 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
device := state.Get("device").(string)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
mountPath, err := config.tpl.Process(config.MountPath, &mountPathData{
|
||||
Device: filepath.Base(device),
|
||||
@@ -59,8 +59,16 @@ func (s *StepMountDevice) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
||||
ui.Say("Mounting the root device...")
|
||||
stderr := new(bytes.Buffer)
|
||||
mountCommand := fmt.Sprintf("%s %s %s", config.MountCommand, device, mountPath)
|
||||
cmd := exec.Command("/bin/sh", "-c", mountCommand)
|
||||
mountCommand, err := wrappedCommand(
|
||||
fmt.Sprintf("mount %s %s", device, mountPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating mount command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
cmd := ShellCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
@@ -90,12 +98,16 @@ func (s *StepMountDevice) CleanupFunc(state multistep.StateBag) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
ui.Say("Unmounting the root device...")
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath)
|
||||
cmd := exec.Command("/bin/sh", "-c", unmountCommand)
|
||||
ui.Say("Unmounting the root device...")
|
||||
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", s.mountPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating unmount command: %s", err)
|
||||
}
|
||||
|
||||
cmd := ShellCommand(unmountCommand)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("Error unmounting root device: %s", err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// StepMountExtra mounts the attached device.
|
||||
@@ -21,6 +20,7 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
|
||||
config := state.Get("config").(*Config)
|
||||
mountPath := state.Get("mount_path").(string)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
|
||||
s.mounts = make([]string, 0, len(config.ChrootMounts))
|
||||
|
||||
@@ -42,13 +42,19 @@ func (s *StepMountExtra) Run(state multistep.StateBag) multistep.StepAction {
|
||||
|
||||
ui.Message(fmt.Sprintf("Mounting: %s", mountInfo[2]))
|
||||
stderr := new(bytes.Buffer)
|
||||
mountCommand := fmt.Sprintf(
|
||||
"%s %s %s %s",
|
||||
config.MountCommand,
|
||||
mountCommand, err := wrappedCommand(fmt.Sprintf(
|
||||
"mount %s %s %s",
|
||||
flags,
|
||||
mountInfo[1],
|
||||
innerPath)
|
||||
cmd := exec.Command("/bin/sh", "-c", mountCommand)
|
||||
innerPath))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating mount command: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
cmd := ShellCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
@@ -79,15 +85,18 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||
for len(s.mounts) > 0 {
|
||||
var path string
|
||||
lastIndex := len(s.mounts) - 1
|
||||
path, s.mounts = s.mounts[lastIndex], s.mounts[:lastIndex]
|
||||
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, path)
|
||||
unmountCommand, err := wrappedCommand(fmt.Sprintf("umount %s", path))
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating unmount command: %s", err)
|
||||
}
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd := exec.Command("/bin/sh", "-c", unmountCommand)
|
||||
cmd := ShellCommand(unmountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf(
|
||||
|
||||
@@ -51,7 +51,8 @@ func (s *StepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||
return nil, "", errors.New("No snapshots found.")
|
||||
}
|
||||
|
||||
return nil, resp.Snapshots[0].Status, nil
|
||||
s := resp.Snapshots[0]
|
||||
return s, s.Status, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
|
||||
}
|
||||
|
||||
if status == "ERROR" {
|
||||
status = decodedResponse["error_message"].(string)
|
||||
status = decodedResponse["message"].(string)
|
||||
}
|
||||
|
||||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
||||
|
||||
@@ -408,8 +408,27 @@ func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) e
|
||||
for _, fi := range fs {
|
||||
realPath := filepath.Join(root, fi.Name())
|
||||
|
||||
if !fi.IsDir() {
|
||||
// It is a regular file, just upload it
|
||||
// Track if this is actually a symlink to a directory. If it is
|
||||
// a symlink to a file we don't do any special behavior because uploading
|
||||
// a file just works. If it is a directory, we need to know so we
|
||||
// treat it as such.
|
||||
isSymlinkToDir := false
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
symPath, err := filepath.EvalSymlinks(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
symFi, err := os.Lstat(symPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isSymlinkToDir = symFi.IsDir()
|
||||
}
|
||||
|
||||
if !fi.IsDir() && !isSymlinkToDir {
|
||||
// It is a regular file (or symlink to a file), just upload it
|
||||
f, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+16
-9
@@ -124,18 +124,25 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// Gather all the variables
|
||||
for k, v := range rawTpl.Variables {
|
||||
var variable RawVariable
|
||||
variable.Default = ""
|
||||
variable.Required = v == nil
|
||||
|
||||
if v != nil {
|
||||
def, ok := v.(string)
|
||||
if !ok {
|
||||
errors = append(errors,
|
||||
fmt.Errorf("variable '%s': default value must be string or null", k))
|
||||
continue
|
||||
}
|
||||
// Create a new mapstructure decoder in order to decode the default
|
||||
// value since this is the only value in the regular template that
|
||||
// can be weakly typed.
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Result: &variable.Default,
|
||||
WeaklyTypedInput: true,
|
||||
})
|
||||
if err != nil {
|
||||
// This should never happen.
|
||||
panic(err)
|
||||
}
|
||||
|
||||
variable.Default = def
|
||||
err = decoder.Decode(v)
|
||||
if err != nil {
|
||||
errors = append(errors,
|
||||
fmt.Errorf("Error decoding default value for user var '%s': %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
t.Variables[k] = variable
|
||||
|
||||
+11
-2
@@ -391,7 +391,8 @@ func TestParseTemplate_Variables(t *testing.T) {
|
||||
{
|
||||
"variables": {
|
||||
"foo": "bar",
|
||||
"bar": null
|
||||
"bar": null,
|
||||
"baz": 27
|
||||
},
|
||||
|
||||
"builders": [{"type": "something"}]
|
||||
@@ -403,7 +404,7 @@ func TestParseTemplate_Variables(t *testing.T) {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if result.Variables == nil || len(result.Variables) != 2 {
|
||||
if result.Variables == nil || len(result.Variables) != 3 {
|
||||
t.Fatalf("bad vars: %#v", result.Variables)
|
||||
}
|
||||
|
||||
@@ -422,6 +423,14 @@ func TestParseTemplate_Variables(t *testing.T) {
|
||||
if !result.Variables["bar"].Required {
|
||||
t.Fatal("bar should be required")
|
||||
}
|
||||
|
||||
if result.Variables["baz"].Default != "27" {
|
||||
t.Fatal("default should be empty")
|
||||
}
|
||||
|
||||
if result.Variables["baz"].Required {
|
||||
t.Fatal("baz should not be required")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplate_variablesBadDefault(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
var GitCommit string
|
||||
|
||||
// The version of packer.
|
||||
const Version = "0.3.8"
|
||||
const Version = "0.3.9"
|
||||
|
||||
// Any pre-release marker for the version. If this is "" (empty string),
|
||||
// then it means that it is a final release. Otherwise, this is the
|
||||
|
||||
@@ -18,10 +18,12 @@ import (
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
|
||||
ChefEnvironment string `mapstructure:"chef_environment"`
|
||||
ConfigTemplate string `mapstructure:"config_template"`
|
||||
CookbookPaths []string `mapstructure:"cookbook_paths"`
|
||||
RolesPath string `mapstructure:"roles_path"`
|
||||
DataBagsPath string `mapstructure:"data_bags_path"`
|
||||
EnvironmentsPath string `mapstructure:"environments_path"`
|
||||
ExecuteCommand string `mapstructure:"execute_command"`
|
||||
InstallCommand string `mapstructure:"install_command"`
|
||||
RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"`
|
||||
@@ -39,15 +41,18 @@ type Provisioner struct {
|
||||
}
|
||||
|
||||
type ConfigTemplate struct {
|
||||
CookbookPaths string
|
||||
DataBagsPath string
|
||||
RolesPath string
|
||||
CookbookPaths string
|
||||
DataBagsPath string
|
||||
RolesPath string
|
||||
EnvironmentsPath string
|
||||
ChefEnvironment string
|
||||
|
||||
// Templates don't support boolean statements until Go 1.2. In the
|
||||
// mean time, we do this.
|
||||
// TODO(mitchellh): Remove when Go 1.2 is released
|
||||
HasDataBagsPath bool
|
||||
HasRolesPath bool
|
||||
HasDataBagsPath bool
|
||||
HasRolesPath bool
|
||||
HasEnvironmentsPath bool
|
||||
}
|
||||
|
||||
type ExecuteTemplate struct {
|
||||
@@ -92,10 +97,12 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
|
||||
templates := map[string]*string{
|
||||
"config_template": &p.config.ConfigTemplate,
|
||||
"data_bags_path": &p.config.DataBagsPath,
|
||||
"roles_path": &p.config.RolesPath,
|
||||
"staging_dir": &p.config.StagingDir,
|
||||
"config_template": &p.config.ConfigTemplate,
|
||||
"data_bags_path": &p.config.DataBagsPath,
|
||||
"roles_path": &p.config.RolesPath,
|
||||
"staging_dir": &p.config.StagingDir,
|
||||
"environments_path": &p.config.EnvironmentsPath,
|
||||
"chef_environment": &p.config.ChefEnvironment,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
@@ -174,6 +181,15 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if p.config.EnvironmentsPath != "" {
|
||||
pFileInfo, err := os.Stat(p.config.EnvironmentsPath)
|
||||
|
||||
if err != nil || !pFileInfo.IsDir() {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Bad environments path '%s': %s", p.config.EnvironmentsPath, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Process the user variables within the JSON and set the JSON.
|
||||
// Do this early so that we can validate and show errors.
|
||||
p.config.Json, err = p.processJsonUserVars()
|
||||
@@ -212,7 +228,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
rolesPath := ""
|
||||
if p.config.RolesPath != "" {
|
||||
rolesPath := fmt.Sprintf("%s/roles", p.config.StagingDir)
|
||||
rolesPath = fmt.Sprintf("%s/roles", p.config.StagingDir)
|
||||
if err := p.uploadDirectory(ui, comm, rolesPath, p.config.RolesPath); err != nil {
|
||||
return fmt.Errorf("Error uploading roles: %s", err)
|
||||
}
|
||||
@@ -220,13 +236,21 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
dataBagsPath := ""
|
||||
if p.config.DataBagsPath != "" {
|
||||
dataBagsPath := fmt.Sprintf("%s/data_bags", p.config.StagingDir)
|
||||
dataBagsPath = fmt.Sprintf("%s/data_bags", p.config.StagingDir)
|
||||
if err := p.uploadDirectory(ui, comm, dataBagsPath, p.config.DataBagsPath); err != nil {
|
||||
return fmt.Errorf("Error uploading data bags: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath)
|
||||
environmentsPath := ""
|
||||
if p.config.EnvironmentsPath != "" {
|
||||
environmentsPath = fmt.Sprintf("%s/environments", p.config.StagingDir)
|
||||
if err := p.uploadDirectory(ui, comm, environmentsPath, p.config.EnvironmentsPath); err != nil {
|
||||
return fmt.Errorf("Error uploading environments: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
configPath, err := p.createConfig(ui, comm, cookbookPaths, rolesPath, dataBagsPath, environmentsPath, p.config.ChefEnvironment)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating Chef config file: %s", err)
|
||||
}
|
||||
@@ -263,7 +287,7 @@ func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, ds
|
||||
return comm.UploadDir(dst, src, nil)
|
||||
}
|
||||
|
||||
func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string) (string, error) {
|
||||
func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string, environmentsPath string, chefEnvironment string) (string, error) {
|
||||
ui.Message("Creating configuration file 'solo.rb'")
|
||||
|
||||
cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks))
|
||||
@@ -294,11 +318,14 @@ func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, local
|
||||
}
|
||||
|
||||
configString, err := p.config.tpl.Process(tpl, &ConfigTemplate{
|
||||
CookbookPaths: strings.Join(cookbook_paths, ","),
|
||||
RolesPath: rolesPath,
|
||||
DataBagsPath: dataBagsPath,
|
||||
HasRolesPath: rolesPath != "",
|
||||
HasDataBagsPath: dataBagsPath != "",
|
||||
CookbookPaths: strings.Join(cookbook_paths, ","),
|
||||
RolesPath: rolesPath,
|
||||
DataBagsPath: dataBagsPath,
|
||||
EnvironmentsPath: environmentsPath,
|
||||
HasRolesPath: rolesPath != "",
|
||||
HasDataBagsPath: dataBagsPath != "",
|
||||
HasEnvironmentsPath: environmentsPath != "",
|
||||
ChefEnvironment: chefEnvironment,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -456,4 +483,8 @@ role_path "{{.RolesPath}}"
|
||||
{{if .HasDataBagsPath}}
|
||||
data_bag_path "{{.DataBagsPath}}"
|
||||
{{end}}
|
||||
{{if .HasEnvironmentsPath}}
|
||||
environments_path "{{.EnvironmentsPath}}"
|
||||
chef_environment "{{.ChefEnvironment}}"
|
||||
{{end}}
|
||||
`
|
||||
|
||||
@@ -19,6 +19,22 @@ func TestProvisioner_Impl(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_chefEnvironment(t *testing.T) {
|
||||
var p Provisioner
|
||||
|
||||
config := testConfig()
|
||||
config["chef_environment"] = "some-env"
|
||||
|
||||
err := p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.config.ChefEnvironment != "some-env" {
|
||||
t.Fatalf("unexpected: %#v", p.config.ChefEnvironment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_configTemplate(t *testing.T) {
|
||||
var err error
|
||||
var p Provisioner
|
||||
@@ -139,6 +155,28 @@ func TestProvisionerPrepare_dataBagsPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_environmentsPath(t *testing.T) {
|
||||
var p Provisioner
|
||||
|
||||
environmentsPath, err := ioutil.TempDir("", "environments")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.Remove(environmentsPath)
|
||||
|
||||
config := testConfig()
|
||||
config["environments_path"] = environmentsPath
|
||||
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if p.config.EnvironmentsPath != environmentsPath {
|
||||
t.Fatalf("unexpected: %#v", p.config.EnvironmentsPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_rolesPath(t *testing.T) {
|
||||
var p Provisioner
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
r = &UnixReader{Reader: r}
|
||||
}
|
||||
|
||||
if err := comm.Upload(p.config.RemotePath, f); err != nil {
|
||||
if err := comm.Upload(p.config.RemotePath, r); err != nil {
|
||||
return fmt.Errorf("Error uploading script: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
@@ -54,35 +53,6 @@ func (r *UnixReader) Read(p []byte) (n int, err error) {
|
||||
// only returns unix-style lines. So even if the line is "one\r\n", the
|
||||
// token returned will be "one\n".
|
||||
func scanUnixLine(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
// We have a new-line terminated line. Return the line with the newline
|
||||
return i + 1, dropCR(data[0 : i+1]), nil
|
||||
}
|
||||
|
||||
if atEOF {
|
||||
// We have a final, non-terminated line
|
||||
return len(data), dropCR(data), nil
|
||||
}
|
||||
|
||||
if data[len(data)-1] != '\r' {
|
||||
// We have a normal line, just let it tokenize
|
||||
return len(data), data, nil
|
||||
}
|
||||
|
||||
// We need more data
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
func dropCR(data []byte) []byte {
|
||||
if len(data) > 0 && data[len(data)-2] == '\r' {
|
||||
// Trim off the last byte and replace it with a '\n'
|
||||
data = data[0 : len(data)-1]
|
||||
data[len(data)-1] = '\n'
|
||||
}
|
||||
|
||||
return data
|
||||
advance, token, err = bufio.ScanLines(data, atEOF)
|
||||
return advance, append(token, "\n"...), err
|
||||
}
|
||||
|
||||
@@ -31,3 +31,21 @@ func TestUnixReader(t *testing.T) {
|
||||
t.Fatalf("bad: %#v", result.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnixReader_unixOnly(t *testing.T) {
|
||||
input := "one\n\ntwo\nthree\n"
|
||||
expected := "one\n\ntwo\nthree\n"
|
||||
|
||||
r := &UnixReader{
|
||||
Reader: bytes.NewReader([]byte(input)),
|
||||
}
|
||||
|
||||
result := new(bytes.Buffer)
|
||||
if _, err := io.Copy(result, r); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if result.String() != expected {
|
||||
t.Fatalf("bad: %#v", result.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,3 +92,6 @@ for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do
|
||||
done
|
||||
|
||||
waitAll
|
||||
|
||||
# Reset signal trapping to avoid "Terminated: 15" at the end
|
||||
trap - SIGINT SIGTERM EXIT
|
||||
|
||||
@@ -111,9 +111,11 @@ Optional:
|
||||
of the source AMI will be attached. This defaults to "" (empty string),
|
||||
which forces Packer to find an open device automatically.
|
||||
|
||||
* `mount_command` (string) - The command to use to mount devices. This
|
||||
defaults to "mount". This may be useful to set if you want to set
|
||||
environmental variables or perhaps run it with `sudo` or so on.
|
||||
* `command_wrapper` (string) - How to run shell commands. This
|
||||
defaults to "{{.Command}}". This may be useful to set if you want to set
|
||||
environmental variables or perhaps run it with `sudo` or so on. This is a
|
||||
configuration template where the `.Command` variable is replaced with the
|
||||
command to be run..
|
||||
|
||||
* `mount_path` (string) - The path where the volume will be mounted. This is
|
||||
where the chroot environment will be. This defaults to
|
||||
@@ -123,9 +125,6 @@ Optional:
|
||||
|
||||
* `tags` (object of key/value strings) - Tags applied to the AMI.
|
||||
|
||||
* `unmount_command` (string) - Just like `mount_command`, except this is
|
||||
the command to unmount devices.
|
||||
|
||||
## Basic Example
|
||||
|
||||
Here is a basic example. It is completely valid except for the access keys:
|
||||
@@ -184,3 +183,37 @@ out of your AMI builds.
|
||||
|
||||
Packer properly obtains a process lock for the parallelism-sensitive parts
|
||||
of its internals such as finding an available device.
|
||||
|
||||
## Using an IAM Instance Profile
|
||||
|
||||
If AWS keys are not specified in the template or through environment variables
|
||||
Packer will use credentials provided by the instance's IAM profile, if it has one.
|
||||
|
||||
The following policy document provides the minimal set permissions necessary for Packer to work:
|
||||
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Action" : [
|
||||
"ec2:AttachVolume",
|
||||
"ec2:CreateVolume",
|
||||
"ec2:DeleteVolume",
|
||||
"ec2:DescribeVolumes",
|
||||
"ec2:DetachVolume",
|
||||
|
||||
"ec2:DescribeInstances",
|
||||
|
||||
"ec2:CreateSnapshot",
|
||||
"ec2:DeleteSnapshot",
|
||||
"ec2:DescribeSnapshots",
|
||||
|
||||
"ec2:DescribeImages",
|
||||
"ec2:RegisterImage",
|
||||
|
||||
"ec2:CreateTags"
|
||||
],
|
||||
"Resource" : "*"
|
||||
}]
|
||||
}
|
||||
</pre>
|
||||
|
||||
@@ -35,11 +35,6 @@ Required:
|
||||
|
||||
Optional:
|
||||
|
||||
* `event_delay` (string) - The delay, as a duration string, before checking
|
||||
the status of an event. DigitalOcean's current API has consistency issues
|
||||
where events take time to appear after being created. This defaults to "5s"
|
||||
and generally shouldn't have to be changed.
|
||||
|
||||
* `image_id` (int) - The ID of the base image to use. This is the image that
|
||||
will be used to launch a new droplet and provision it. Defaults to "284203",
|
||||
which happens to be "Ubuntu 12.04 x64 Server."
|
||||
|
||||
@@ -42,6 +42,8 @@ usage: packer [--version] [--help] <command> [<args>]
|
||||
|
||||
Available commands are:
|
||||
build build image(s) from template
|
||||
fix fixes templates from old versions of packer
|
||||
inspect see components of a template
|
||||
validate check that a template is valid
|
||||
```
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ root privileges without worrying about password prompts.
|
||||
## Default Environmental Variables
|
||||
|
||||
In addition to being able to specify custom environmental variables using
|
||||
the `environmental_vars` configuration, the provisioner automatically
|
||||
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.
|
||||
|
||||
@@ -45,6 +45,8 @@ usage: packer [--version] [--help] <command> [<args>]
|
||||
|
||||
Available commands are:
|
||||
build build image(s) from template
|
||||
fix fixes templates from old versions of packer
|
||||
inspect see components of a template
|
||||
validate check that a template is valid
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user