Merge pull request #432 from mwhooker/chroot_cmd
build/amazon/chroot: command_wrapper to support sudo-less
This commit is contained in:
commit
707fe57edd
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
|
@ -29,14 +30,17 @@ type Config struct {
|
|||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
MountCommand string `mapstructure:"mount_command"`
|
||||
CommandWrapper string `mapstructure:"command_wrapper"`
|
||||
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 +82,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)...)
|
||||
|
@ -128,9 +128,7 @@ 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,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
|
@ -167,12 +165,24 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
|||
|
||||
ec2conn := ec2.New(auth, region)
|
||||
|
||||
wrappedCommand := func(command string) *exec.Cmd {
|
||||
wrapped, err := b.config.tpl.Process(
|
||||
b.config.CommandWrapper, &wrappedCommandTemplate{
|
||||
Command: command,
|
||||
})
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
}
|
||||
return ShellCommand(wrapped)
|
||||
}
|
||||
|
||||
// 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", Command(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package chroot
|
||||
|
||||
// pf := func () { somefunc("a str", 1) }
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -10,19 +14,18 @@ import (
|
|||
"syscall"
|
||||
)
|
||||
|
||||
type Command func(string) *exec.Cmd
|
||||
|
||||
// Communicator is a special communicator that works by executing
|
||||
// commands locally but within a chroot.
|
||||
type Communicator struct {
|
||||
Chroot string
|
||||
ChrootCmd Command
|
||||
WrappedCommand Command
|
||||
}
|
||||
|
||||
func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
||||
chrootCmdPath, err := exec.LookPath("chroot")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localCmd := exec.Command(chrootCmdPath, c.Chroot, "/bin/sh", "-c", cmd.Command)
|
||||
localCmd := c.ChrootCmd(cmd.Command)
|
||||
localCmd.Stdin = cmd.Stdin
|
||||
localCmd.Stdout = cmd.Stdout
|
||||
localCmd.Stderr = cmd.Stderr
|
||||
|
@ -46,7 +49,7 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
|
|||
}
|
||||
|
||||
log.Printf(
|
||||
"Chroot executation ended with '%d': '%s'",
|
||||
"Chroot executation exited with '%d': '%s'",
|
||||
exitStatus, cmd.Command)
|
||||
cmd.SetExited(exitStatus)
|
||||
}()
|
||||
|
@ -57,20 +60,18 @@ 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 err
|
||||
return fmt.Errorf("Error preparing shell script: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
defer os.Remove(tf.Name())
|
||||
io.Copy(tf, r)
|
||||
cpCmd := fmt.Sprintf("cp %s %s", tf.Name(), dst)
|
||||
return (c.WrappedCommand(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
|
||||
|
@ -88,18 +89,18 @@ func (c *Communicator) UploadDir(dst string, src string, exclude []string) error
|
|||
}
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dst, path)
|
||||
f, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
chrootDest := filepath.Join(c.Chroot, dst, path)
|
||||
log.Printf("Uploading dir %s to chroot dir: %s", src, dst)
|
||||
cpCmd := fmt.Sprintf("cp %s %s", fullPath, chrootDest)
|
||||
return c.WrappedCommand(cpCmd).Run()
|
||||
}
|
||||
defer f.Close()
|
||||
*/
|
||||
|
||||
return c.Upload(dstPath, f)
|
||||
}
|
||||
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, dst)
|
||||
return filepath.Walk(src, walkFn)
|
||||
// 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 := fmt.Sprintf("cp -R %s* %s", src, chrootDest)
|
||||
return c.WrappedCommand(cpCmd).Run()
|
||||
}
|
||||
|
||||
func (c *Communicator) Download(src string, w io.Writer) error {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package chroot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func ChrootCommand(chroot string, command string) *exec.Cmd {
|
||||
cmd := fmt.Sprintf("sudo chroot %s", chroot)
|
||||
return ShellCommand(cmd, command)
|
||||
}
|
||||
|
||||
func ShellCommand(commands ...string) *exec.Cmd {
|
||||
cmds := append([]string{"-c"}, commands...)
|
||||
cmd := exec.Command("/bin/sh", cmds...)
|
||||
log.Printf("ShellCommand: %s %v", cmd.Path, cmd.Args[1:])
|
||||
return cmd
|
||||
}
|
|
@ -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.")
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// StepChrootProvision provisions the instance within a chroot.
|
||||
|
@ -15,10 +16,16 @@ 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").(Command)
|
||||
chrootCmd := func(command string) *exec.Cmd {
|
||||
return ChrootCommand(mountPath, command)
|
||||
}
|
||||
|
||||
// Create our communicator
|
||||
comm := &Communicator{
|
||||
Chroot: mountPath,
|
||||
ChrootCmd: chrootCmd,
|
||||
WrappedCommand: 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").(Command)
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
s.files = make([]string, 0, len(config.CopyFiles))
|
||||
if len(config.CopyFiles) > 0 {
|
||||
|
@ -32,8 +33,12 @@ 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)
|
||||
cmd := wrappedCommand(fmt.Sprintf("cp %s %s", path, chrootPath))
|
||||
stderr.Reset()
|
||||
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 +59,13 @@ 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").(Command)
|
||||
if s.files != nil {
|
||||
for _, file := range s.files {
|
||||
log.Printf("Removing: %s", file)
|
||||
if err := os.Remove(file); err != nil {
|
||||
localCmd := wrappedCommand(fmt.Sprintf("rm -f %s", file))
|
||||
if err := localCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -67,41 +74,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
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
|
@ -59,8 +58,9 @@ 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 := fmt.Sprintf("mount %s %s", device, mountPath)
|
||||
wrappedCommand := state.Get("wrappedCommand").(Command)
|
||||
cmd := wrappedCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
|
@ -90,12 +90,12 @@ 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...")
|
||||
|
||||
unmountCommand := fmt.Sprintf("%s %s", config.UnmountCommand, s.mountPath)
|
||||
cmd := exec.Command("/bin/sh", "-c", unmountCommand)
|
||||
unmountCommand := fmt.Sprintf("umount %s", s.mountPath)
|
||||
wrappedCommand := state.Get("wrappedCommand").(Command)
|
||||
cmd := wrappedCommand(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").(Command)
|
||||
|
||||
s.mounts = make([]string, 0, len(config.ChrootMounts))
|
||||
|
||||
|
@ -43,12 +43,11 @@ 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,
|
||||
"mount %s %s %s",
|
||||
flags,
|
||||
mountInfo[1],
|
||||
innerPath)
|
||||
cmd := exec.Command("/bin/sh", "-c", mountCommand)
|
||||
cmd := wrappedCommand(mountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
err := fmt.Errorf(
|
||||
|
@ -79,15 +78,15 @@ func (s *StepMountExtra) CleanupFunc(state multistep.StateBag) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
config := state.Get("config").(*Config)
|
||||
wrappedCommand := state.Get("wrappedCommand").(Command)
|
||||
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 := fmt.Sprintf("umount %s", path)
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
cmd := exec.Command("/bin/sh", "-c", unmountCommand)
|
||||
cmd := wrappedCommand(unmountCommand)
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf(
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue