diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2ee3d54..562fe7b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,49 @@ -## 0.3.8 (unreleased) - -FEATURES: - -* provisioner/chef-solo: Ability to specify a custom Chef configuration - template. - -IMPROVEMENTS: - -* builder/amazon/*: Interrupts work while waiting for AMI to be ready. +## 0.3.9 (unreleased) BUG FIXES: -* builder/amazon/*: While waiting for AMI, will detect "failed" state. -* builder/amazon/*: Waiting for state will detect if the resource (AMI, +* core: default user variable values don't need to be strings. [GH-456] + +## 0.3.8 (September 22, 2013) + +FEATURES: + +* core: You can now specify `only` and `except` configurations on any + provisioner or post-processor to specify a list of builds that they + are valid for. [GH-438] +* builders/virtualbox: Guest additions can be attached rather than uploaded, + easier to handle for Windows guests. [GH-405] +* provisioner/chef-solo: Ability to specify a custom Chef configuration + template. +* provisioner/chef-solo: Roles and data bags support. [GH-348] + +IMPROVEMENTS: + +* core: User variables can now be used for integer, boolean, etc. + values. [GH-418] +* core: Plugins made with incompatible versions will no longer load. +* builder/amazon/all: Interrupts work while waiting for AMI to be ready. +* provisioner/shell: Script line-endings are automatically converted to + Unix-style line-endings. Can be disabled by setting "binary" to "true". + [GH-277] + +BUG FIXES: + +* core: Set TCP KeepAlives on internally created RPC connections so that + they don't die. [GH-416] +* builder/amazon/all: While waiting for AMI, will detect "failed" state. +* builder/amazon/all: Waiting for state will detect if the resource (AMI, instance, etc.) disappears from under it. +* builder/amazon/instance: Exclude only contents of /tmp, not /tmp + itself. [GH-437] +* builder/amazon/instance: Make AccessKey/SecretKey available to bundle + command even when they come from the environment. [GH-434] * builder/virtualbox: F1-F12 and delete scancodes now work. [GH-425] +* post-processor/vagrant: Override configurations properly work. [GH-426] * provisioner/puppet-masterless: Fix failure case when both facter vars are used and prevent_sudo. [GH-415] +* provisioner/puppet-masterless: User variables now work properly in + manifest file and hiera path. [GH-448] ## 0.3.7 (September 9, 2013) diff --git a/Makefile b/Makefile index 776d27dca..77e9b5a23 100644 --- a/Makefile +++ b/Makefile @@ -2,16 +2,22 @@ NO_COLOR=\033[0m OK_COLOR=\033[32;01m ERROR_COLOR=\033[31;01m WARN_COLOR=\033[33;01m +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)" @go get -d -v ./... - @go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d + @echo $(DEPS) | xargs -n1 go get -d + +updatedeps: + @echo "$(OK_COLOR)==> Updating all dependencies$(NO_COLOR)" + @go get -d -v -u ./... + @echo $(DEPS) | xargs -n1 go get -d -u clean: @rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build @@ -23,4 +29,4 @@ test: deps @echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)" go test ./... -.PHONY: all deps format test +.PHONY: all clean deps format test updatedeps diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go index 10ce2eba2..1690869d0 100644 --- a/builder/amazon/common/access_config.go +++ b/builder/amazon/common/access_config.go @@ -18,7 +18,14 @@ type AccessConfig struct { // Auth returns a valid aws.Auth object for access to AWS services, or // an error if the authentication couldn't be resolved. func (c *AccessConfig) Auth() (aws.Auth, error) { - return aws.GetAuth(c.AccessKey, c.SecretKey) + auth, err := aws.GetAuth(c.AccessKey, c.SecretKey) + if err == nil { + // Store the accesskey and secret that we got... + c.AccessKey = auth.AccessKey + c.SecretKey = auth.SecretKey + } + + return auth, err } // Region returns the aws.Region object for access to AWS services, requesting diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 071e0e24e..44691ecee 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -83,7 +83,7 @@ func (b *Builder) Prepare(raws ...interface{}) error { "-u {{.AccountId}} " + "-c {{.CertPath}} " + "-r {{.Architecture}} " + - "-e {{.PrivatePath}} " + + "-e {{.PrivatePath}}/* " + "-d {{.Destination}} " + "-p {{.Prefix}} " + "--batch" diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go index 6a9f0426c..04699ec66 100644 --- a/builder/digitalocean/step_create_ssh_key.go +++ b/builder/digitalocean/step_create_ssh_key.go @@ -38,7 +38,8 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk))) // Marshal the public key into SSH compatible format - pub := ssh.NewRSAPublicKey(&priv.PublicKey) + // TODO properly handle the public key error + pub, _ := ssh.NewPublicKey(&priv.PublicKey) pub_sshformat := string(ssh.MarshalAuthorizedKey(pub)) // The name of the public key on DO diff --git a/builder/virtualbox/builder.go b/builder/virtualbox/builder.go index 18b16867a..452a7de49 100644 --- a/builder/virtualbox/builder.go +++ b/builder/virtualbox/builder.go @@ -28,6 +28,7 @@ type config struct { DiskSize uint `mapstructure:"disk_size"` FloppyFiles []string `mapstructure:"floppy_files"` Format string `mapstructure:"format"` + GuestAdditionsAttach bool `mapstructure:"guest_additions_attach"` GuestAdditionsPath string `mapstructure:"guest_additions_path"` GuestAdditionsURL string `mapstructure:"guest_additions_url"` GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` @@ -361,6 +362,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe new(stepCreateVM), new(stepCreateDisk), new(stepAttachISO), + new(stepAttachGuestAdditions), new(stepAttachFloppy), new(stepForwardSSH), new(stepVBoxManage), diff --git a/builder/virtualbox/step_attach_guest_additions.go b/builder/virtualbox/step_attach_guest_additions.go new file mode 100644 index 000000000..9bbeb949a --- /dev/null +++ b/builder/virtualbox/step_attach_guest_additions.go @@ -0,0 +1,81 @@ +package virtualbox + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +// This step attaches the VirtualBox guest additions as a inserted CD onto +// the virtual machine. +// +// Uses: +// config *config +// driver Driver +// guest_additions_path string +// ui packer.Ui +// vmName string +// +// Produces: +type stepAttachGuestAdditions struct { + attachedPath string +} + +func (s *stepAttachGuestAdditions) Run(state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*config) + driver := state.Get("driver").(Driver) + guestAdditionsPath := state.Get("guest_additions_path").(string) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + // If we're not attaching the guest additions then just return + if !config.GuestAdditionsAttach { + log.Println("Not attaching guest additions since we're uploading.") + return multistep.ActionContinue + } + + // Attach the guest additions to the computer + log.Println("Attaching guest additions ISO onto IDE controller...") + command := []string{ + "storageattach", vmName, + "--storagectl", "IDE Controller", + "--port", "1", + "--device", "0", + "--type", "dvddrive", + "--medium", guestAdditionsPath, + } + if err := driver.VBoxManage(command...); err != nil { + err := fmt.Errorf("Error attaching guest additions: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Track the path so that we can unregister it from VirtualBox later + s.attachedPath = guestAdditionsPath + + return multistep.ActionContinue +} + +func (s *stepAttachGuestAdditions) Cleanup(state multistep.StateBag) { + if s.attachedPath == "" { + return + } + + driver := state.Get("driver").(Driver) + ui := state.Get("ui").(packer.Ui) + vmName := state.Get("vmName").(string) + + command := []string{ + "storageattach", vmName, + "--storagectl", "IDE Controller", + "--port", "1", + "--device", "0", + "--medium", "none", + } + + if err := driver.VBoxManage(command...); err != nil { + ui.Error(fmt.Sprintf("Error unregistering guest additions: %s", err)) + } +} diff --git a/builder/virtualbox/step_upload_guest_additions.go b/builder/virtualbox/step_upload_guest_additions.go index 86b83b3c6..ff2252c86 100644 --- a/builder/virtualbox/step_upload_guest_additions.go +++ b/builder/virtualbox/step_upload_guest_additions.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" "os" ) @@ -21,6 +22,12 @@ func (s *stepUploadGuestAdditions) Run(state multistep.StateBag) multistep.StepA guestAdditionsPath := state.Get("guest_additions_path").(string) ui := state.Get("ui").(packer.Ui) + // If we're attaching then don't do this, since we attached. + if config.GuestAdditionsAttach { + log.Println("Not uploading guest additions since we're attaching.") + return multistep.ActionContinue + } + version, err := driver.Version() if err != nil { state.Put("error", fmt.Errorf("Error reading version for guest additions upload: %s", err)) diff --git a/common/config.go b/common/config.go index 2a2c8b21f..b579562d5 100644 --- a/common/config.go +++ b/common/config.go @@ -41,8 +41,9 @@ func CheckUnusedConfig(md *mapstructure.Metadata) *packer.MultiError { func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metadata, error) { var md mapstructure.Metadata decoderConfig := &mapstructure.DecoderConfig{ - Metadata: &md, - Result: target, + Metadata: &md, + Result: target, + WeaklyTypedInput: true, } decoder, err := mapstructure.NewDecoder(decoderConfig) diff --git a/communicator/ssh/keychain.go b/communicator/ssh/keychain.go index d7934f8a8..f9965c0da 100644 --- a/communicator/ssh/keychain.go +++ b/communicator/ssh/keychain.go @@ -60,9 +60,9 @@ func (k *SimpleKeychain) Key(i int) (ssh.PublicKey, error) { } switch key := k.keys[i].(type) { case *rsa.PrivateKey: - return ssh.NewRSAPublicKey(&key.PublicKey), nil + return ssh.NewPublicKey(&key.PublicKey) case *dsa.PrivateKey: - return ssh.NewDSAPublicKey(&key.PublicKey), nil + return ssh.NewPublicKey(&key.PublicKey) } panic("unknown key type") } diff --git a/packer/plugin/client.go b/packer/plugin/client.go index d3a302977..792f9d99d 100644 --- a/packer/plugin/client.go +++ b/packer/plugin/client.go @@ -317,10 +317,24 @@ func (c *Client) Start() (address string, err error) { err = errors.New("timeout while waiting for plugin to start") case <-exitCh: err = errors.New("plugin exited before we could connect") - case line := <-linesCh: - // Trim the address and reset the err since we were able - // to read some sort of address. - c.address = strings.TrimSpace(string(line)) + case lineBytes := <-linesCh: + // Trim the line and split by "|" in order to get the parts of + // the output. + line := strings.TrimSpace(string(lineBytes)) + parts := strings.SplitN(line, "|", 2) + if len(parts) < 2 { + err = fmt.Errorf("Unrecognized remote plugin message: %s", line) + return + } + + // Test the API version + if parts[0] != APIVersion { + err = fmt.Errorf("Incompatible API version with plugin. "+ + "Plugin version: %s, Ours: %s", parts[0], APIVersion) + return + } + + c.address = parts[1] address = c.address } diff --git a/packer/plugin/client_test.go b/packer/plugin/client_test.go index ae71c3362..f9257034e 100644 --- a/packer/plugin/client_test.go +++ b/packer/plugin/client_test.go @@ -37,6 +37,21 @@ func TestClient(t *testing.T) { } } +func TestClientStart_badVersion(t *testing.T) { + config := &ClientConfig{ + Cmd: helperProcess("bad-version"), + StartTimeout: 50 * time.Millisecond, + } + + c := NewClient(config) + defer c.Kill() + + _, err := c.Start() + if err == nil { + t.Fatal("err should not be nil") + } +} + func TestClient_Start_Timeout(t *testing.T) { config := &ClientConfig{ Cmd: helperProcess("start-timeout"), diff --git a/packer/plugin/plugin.go b/packer/plugin/plugin.go index f9fca8cdd..a91fcc3ce 100644 --- a/packer/plugin/plugin.go +++ b/packer/plugin/plugin.go @@ -30,9 +30,16 @@ var Interrupts int32 = 0 const MagicCookieKey = "PACKER_PLUGIN_MAGIC_COOKIE" const MagicCookieValue = "d602bf8f470bc67ca7faa0386276bbdd4330efaf76d1a219cb4d6991ca9872b2" +// The APIVersion is outputted along with the RPC address. The plugin +// client validates this API version and will show an error if it doesn't +// know how to speak it. +const APIVersion = "1" + // This serves a single RPC connection on the given RPC server on // a random port. func serve(server *rpc.Server) (err error) { + log.Printf("Plugin build against Packer '%s'", packer.GitCommit) + if os.Getenv(MagicCookieKey) != MagicCookieValue { return errors.New("Please do not execute plugins directly. Packer will execute these for you.") } @@ -75,7 +82,7 @@ func serve(server *rpc.Server) (err error) { // Output the address to stdout log.Printf("Plugin address: %s\n", address) - fmt.Println(address) + fmt.Printf("%s|%s\n", APIVersion, address) os.Stdout.Sync() // Accept a connection diff --git a/packer/plugin/plugin_test.go b/packer/plugin/plugin_test.go index 17018f82d..10c3f9d5c 100644 --- a/packer/plugin/plugin_test.go +++ b/packer/plugin/plugin_test.go @@ -50,6 +50,9 @@ func TestHelperProcess(*testing.T) { cmd, args := args[0], args[1:] switch cmd { + case "bad-version": + fmt.Printf("%s1|:1234\n", APIVersion) + <-make(chan int) case "builder": ServeBuilder(new(helperBuilder)) case "command": @@ -59,7 +62,7 @@ func TestHelperProcess(*testing.T) { case "invalid-rpc-address": fmt.Println("lolinvalid") case "mock": - fmt.Println(":1234") + fmt.Printf("%s|:1234\n", APIVersion) <-make(chan int) case "post-processor": ServePostProcessor(new(helperPostProcessor)) @@ -69,11 +72,11 @@ func TestHelperProcess(*testing.T) { time.Sleep(1 * time.Minute) os.Exit(1) case "stderr": - fmt.Println(":1234") + fmt.Printf("%s|:1234\n", APIVersion) log.Println("HELLO") log.Println("WORLD") case "stdin": - fmt.Println(":1234") + fmt.Printf("%s|:1234\n", APIVersion) data := make([]byte, 5) if _, err := os.Stdin.Read(data); err != nil { log.Printf("stdin read error: %s", err) diff --git a/packer/rpc/build.go b/packer/rpc/build.go index 6d81ce870..8f21b4ce9 100644 --- a/packer/rpc/build.go +++ b/packer/rpc/build.go @@ -52,7 +52,7 @@ func (b *build) Run(ui packer.Ui, cache packer.Cache) ([]packer.Artifact, error) artifacts := make([]packer.Artifact, len(result)) for i, addr := range result { - client, err := rpc.Dial("tcp", addr) + client, err := rpcDial(addr) if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (b *BuildServer) Prepare(v map[string]string, reply *error) error { } func (b *BuildServer) Run(args *BuildRunArgs, reply *[]string) error { - client, err := rpc.Dial("tcp", args.UiRPCAddress) + client, err := rpcDial(args.UiRPCAddress) if err != nil { return err } diff --git a/packer/rpc/builder.go b/packer/rpc/builder.go index 8c56e4959..5e2c20d77 100644 --- a/packer/rpc/builder.go +++ b/packer/rpc/builder.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/mitchellh/packer/packer" "log" - "net" "net/rpc" ) @@ -95,7 +94,7 @@ func (b *builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, nil } - client, err := rpc.Dial("tcp", response.RPCAddress) + client, err := rpcDial(response.RPCAddress) if err != nil { return nil, err } @@ -119,12 +118,12 @@ func (b *BuilderServer) Prepare(args *BuilderPrepareArgs, reply *error) error { } func (b *BuilderServer) Run(args *BuilderRunArgs, reply *interface{}) error { - client, err := rpc.Dial("tcp", args.RPCAddress) + client, err := rpcDial(args.RPCAddress) if err != nil { return err } - responseC, err := net.Dial("tcp", args.ResponseAddress) + responseC, err := tcpDial(args.ResponseAddress) if err != nil { return err } diff --git a/packer/rpc/command.go b/packer/rpc/command.go index 18cd5667e..3e2b48b2f 100644 --- a/packer/rpc/command.go +++ b/packer/rpc/command.go @@ -66,7 +66,7 @@ func (c *CommandServer) Help(args *interface{}, reply *string) error { } func (c *CommandServer) Run(args *CommandRunArgs, reply *int) error { - client, err := rpc.Dial("tcp", args.RPCAddress) + client, err := rpcDial(args.RPCAddress) if err != nil { return err } diff --git a/packer/rpc/communicator.go b/packer/rpc/communicator.go index 21b507f0b..77e321153 100644 --- a/packer/rpc/communicator.go +++ b/packer/rpc/communicator.go @@ -177,7 +177,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface toClose := make([]net.Conn, 0) if args.StdinAddress != "" { - stdinC, err := net.Dial("tcp", args.StdinAddress) + stdinC, err := tcpDial(args.StdinAddress) if err != nil { return err } @@ -187,7 +187,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface } if args.StdoutAddress != "" { - stdoutC, err := net.Dial("tcp", args.StdoutAddress) + stdoutC, err := tcpDial(args.StdoutAddress) if err != nil { return err } @@ -197,7 +197,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface } if args.StderrAddress != "" { - stderrC, err := net.Dial("tcp", args.StderrAddress) + stderrC, err := tcpDial(args.StderrAddress) if err != nil { return err } @@ -208,7 +208,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface // Connect to the response address so we can write our result to it // when ready. - responseC, err := net.Dial("tcp", args.ResponseAddress) + responseC, err := tcpDial(args.ResponseAddress) if err != nil { return err } @@ -234,7 +234,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface } func (c *CommunicatorServer) Upload(args *CommunicatorUploadArgs, reply *interface{}) (err error) { - readerC, err := net.Dial("tcp", args.ReaderAddress) + readerC, err := tcpDial(args.ReaderAddress) if err != nil { return } @@ -250,7 +250,7 @@ func (c *CommunicatorServer) UploadDir(args *CommunicatorUploadDirArgs, reply *e } func (c *CommunicatorServer) Download(args *CommunicatorDownloadArgs, reply *interface{}) (err error) { - writerC, err := net.Dial("tcp", args.WriterAddress) + writerC, err := tcpDial(args.WriterAddress) if err != nil { return } diff --git a/packer/rpc/dial.go b/packer/rpc/dial.go new file mode 100644 index 000000000..10e2cad14 --- /dev/null +++ b/packer/rpc/dial.go @@ -0,0 +1,33 @@ +package rpc + +import ( + "net" + "net/rpc" +) + +// rpcDial makes a TCP connection to a remote RPC server and returns +// the client. This will set the connection up properly so that keep-alives +// are set and so on and should be used to make all RPC connections within +// this package. +func rpcDial(address string) (*rpc.Client, error) { + tcpConn, err := tcpDial(address) + if err != nil { + return nil, err + } + + // Create an RPC client around our connection + return rpc.NewClient(tcpConn), nil +} + +// tcpDial connects via TCP to the designated address. +func tcpDial(address string) (*net.TCPConn, error) { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, err + } + + // Set a keep-alive so that the connection stays alive even when idle + tcpConn := conn.(*net.TCPConn) + tcpConn.SetKeepAlive(true) + return tcpConn, nil +} diff --git a/packer/rpc/environment.go b/packer/rpc/environment.go index 8ebf709c0..36db72c56 100644 --- a/packer/rpc/environment.go +++ b/packer/rpc/environment.go @@ -28,7 +28,7 @@ func (e *Environment) Builder(name string) (b packer.Builder, err error) { return } - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { return } @@ -43,7 +43,7 @@ func (e *Environment) Cache() packer.Cache { panic(err) } - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { panic(err) } @@ -64,7 +64,7 @@ func (e *Environment) Hook(name string) (h packer.Hook, err error) { return } - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { return } @@ -80,7 +80,7 @@ func (e *Environment) PostProcessor(name string) (p packer.PostProcessor, err er return } - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { return } @@ -96,7 +96,7 @@ func (e *Environment) Provisioner(name string) (p packer.Provisioner, err error) return } - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { return } @@ -109,7 +109,7 @@ func (e *Environment) Ui() packer.Ui { var reply string e.client.Call("Environment.Ui", new(interface{}), &reply) - client, err := rpc.Dial("tcp", reply) + client, err := rpcDial(reply) if err != nil { panic(err) } diff --git a/packer/rpc/hook.go b/packer/rpc/hook.go index 687d991a7..223b96df2 100644 --- a/packer/rpc/hook.go +++ b/packer/rpc/hook.go @@ -46,7 +46,7 @@ func (h *hook) Cancel() { } func (h *HookServer) Run(args *HookRunArgs, reply *interface{}) error { - client, err := rpc.Dial("tcp", args.RPCAddress) + client, err := rpcDial(args.RPCAddress) if err != nil { return err } diff --git a/packer/rpc/post_processor.go b/packer/rpc/post_processor.go index fb43cb7d9..0a5eaefd3 100644 --- a/packer/rpc/post_processor.go +++ b/packer/rpc/post_processor.go @@ -57,7 +57,7 @@ func (p *postProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Art return nil, false, nil } - client, err := rpc.Dial("tcp", response.RPCAddress) + client, err := rpcDial(response.RPCAddress) if err != nil { return nil, false, err } @@ -75,7 +75,7 @@ func (p *PostProcessorServer) Configure(args *PostProcessorConfigureArgs, reply } func (p *PostProcessorServer) PostProcess(address string, reply *PostProcessorProcessResponse) error { - client, err := rpc.Dial("tcp", address) + client, err := rpcDial(address) if err != nil { return err } diff --git a/packer/rpc/provisioner.go b/packer/rpc/provisioner.go index 7d3ed1617..4cd329d6b 100644 --- a/packer/rpc/provisioner.go +++ b/packer/rpc/provisioner.go @@ -65,7 +65,7 @@ func (p *ProvisionerServer) Prepare(args *ProvisionerPrepareArgs, reply *error) } func (p *ProvisionerServer) Provision(args *ProvisionerProvisionArgs, reply *interface{}) error { - client, err := rpc.Dial("tcp", args.RPCAddress) + client, err := rpcDial(args.RPCAddress) if err != nil { return err } diff --git a/packer/template.go b/packer/template.go index 7c3dd0529..180b02c0f 100644 --- a/packer/template.go +++ b/packer/template.go @@ -48,6 +48,8 @@ type RawBuilderConfig struct { // configuration. It contains the type of the post processor as well as the // raw configuration that is handed to the post-processor for it to process. type RawPostProcessorConfig struct { + TemplateOnlyExcept `mapstructure:",squash"` + Type string KeepInputArtifact bool `mapstructure:"keep_input_artifact"` RawConfig map[string]interface{} @@ -57,6 +59,8 @@ type RawPostProcessorConfig struct { // It contains the type of the provisioner as well as the raw configuration // that is handed to the provisioner for it to process. type RawProvisionerConfig struct { + TemplateOnlyExcept `mapstructure:",squash"` + Type string Override map[string]interface{} @@ -120,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 @@ -189,32 +200,50 @@ func ParseTemplate(data []byte) (t *Template, err error) { continue } - t.PostProcessors[i] = make([]RawPostProcessorConfig, len(rawPP)) - configs := t.PostProcessors[i] + configs := make([]RawPostProcessorConfig, 0, len(rawPP)) for j, pp := range rawPP { - config := &configs[j] - if err := mapstructure.Decode(pp, config); err != nil { + var config RawPostProcessorConfig + if err := mapstructure.Decode(pp, &config); err != nil { if merr, ok := err.(*mapstructure.Error); ok { for _, err := range merr.Errors { - errors = append(errors, fmt.Errorf("Post-processor #%d.%d: %s", i+1, j+1, err)) + errors = append(errors, + fmt.Errorf("Post-processor #%d.%d: %s", i+1, j+1, err)) } } else { - errors = append(errors, fmt.Errorf("Post-processor %d.%d: %s", i+1, j+1, err)) + errors = append(errors, + fmt.Errorf("Post-processor %d.%d: %s", i+1, j+1, err)) } continue } if config.Type == "" { - errors = append(errors, fmt.Errorf("Post-processor %d.%d: missing 'type'", i+1, j+1)) + errors = append(errors, + fmt.Errorf("Post-processor %d.%d: missing 'type'", i+1, j+1)) continue } // Remove the input keep_input_artifact option + config.TemplateOnlyExcept.Prune(pp) delete(pp, "keep_input_artifact") + // Verify that the only settings are good + if errs := config.TemplateOnlyExcept.Validate(t.Builders); len(errs) > 0 { + for _, err := range errs { + errors = append(errors, + fmt.Errorf("Post-processor %d.%d: %s", i+1, j+1, err)) + } + + continue + } + config.RawConfig = pp + + // Add it to the list of configs + configs = append(configs, config) } + + t.PostProcessors[i] = configs } // Gather all the provisioners @@ -237,9 +266,8 @@ func ParseTemplate(data []byte) (t *Template, err error) { continue } - // The provisioners not only don't need or want the override settings - // (as they are processed as part of the preparation below), but will - // actively reject them as invalid configuration. + // Delete the keys that we used + raw.TemplateOnlyExcept.Prune(v) delete(v, "override") // Verify that the override keys exist... @@ -250,6 +278,14 @@ func ParseTemplate(data []byte) (t *Template, err error) { } } + // Verify that the only settings are good + if errs := raw.TemplateOnlyExcept.Validate(t.Builders); len(errs) > 0 { + for _, err := range errs { + errors = append(errors, + fmt.Errorf("provisioner %d: %s", i+1, err)) + } + } + raw.RawConfig = v } @@ -400,8 +436,12 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err // Prepare the post-processors postProcessors := make([][]coreBuildPostProcessor, 0, len(t.PostProcessors)) for _, rawPPs := range t.PostProcessors { - current := make([]coreBuildPostProcessor, len(rawPPs)) - for i, rawPP := range rawPPs { + current := make([]coreBuildPostProcessor, 0, len(rawPPs)) + for _, rawPP := range rawPPs { + if rawPP.TemplateOnlyExcept.Skip(name) { + continue + } + pp, err := components.PostProcessor(rawPP.Type) if err != nil { return nil, err @@ -411,12 +451,18 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err return nil, fmt.Errorf("PostProcessor type not found: %s", rawPP.Type) } - current[i] = coreBuildPostProcessor{ + current = append(current, coreBuildPostProcessor{ processor: pp, processorType: rawPP.Type, config: rawPP.RawConfig, keepInputArtifact: rawPP.KeepInputArtifact, - } + }) + } + + // If we have no post-processors in this chain, just continue. + // This can happen if the post-processors skip certain builds. + if len(current) == 0 { + continue } postProcessors = append(postProcessors, current) @@ -425,6 +471,10 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err // Prepare the provisioners provisioners := make([]coreBuildProvisioner, 0, len(t.Provisioners)) for _, rawProvisioner := range t.Provisioners { + if rawProvisioner.TemplateOnlyExcept.Skip(name) { + continue + } + var provisioner Provisioner provisioner, err = components.Provisioner(rawProvisioner.Type) if err != nil { @@ -471,3 +521,69 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err return } + +// TemplateOnlyExcept contains the logic required for "only" and "except" +// meta-parameters. +type TemplateOnlyExcept struct { + Only []string + Except []string +} + +// Prune will prune out the used values from the raw map. +func (t *TemplateOnlyExcept) Prune(raw map[string]interface{}) { + delete(raw, "except") + delete(raw, "only") +} + +// Skip tests if we should skip putting this item onto a build. +func (t *TemplateOnlyExcept) Skip(name string) bool { + if len(t.Only) > 0 { + onlyFound := false + for _, n := range t.Only { + if n == name { + onlyFound = true + break + } + } + + if !onlyFound { + // Skip this provisioner + return true + } + } + + // If the name is in the except list, then skip that + for _, n := range t.Except { + if n == name { + return true + } + } + + return false +} + +// Validates the only/except parameters. +func (t *TemplateOnlyExcept) Validate(b map[string]RawBuilderConfig) (e []error) { + if len(t.Only) > 0 && len(t.Except) > 0 { + e = append(e, + fmt.Errorf("Only one of 'only' or 'except' may be specified.")) + } + + if len(t.Only) > 0 { + for _, n := range t.Only { + if _, ok := b[n]; !ok { + e = append(e, + fmt.Errorf("'only' specified builder '%s' not found", n)) + } + } + } + + for _, n := range t.Except { + if _, ok := b[n]; !ok { + e = append(e, + fmt.Errorf("'except' specified builder '%s' not found", n)) + } + } + + return +} diff --git a/packer/template_test.go b/packer/template_test.go index 883933367..d457e7932 100644 --- a/packer/template_test.go +++ b/packer/template_test.go @@ -9,6 +9,33 @@ import ( "testing" ) +func testTemplateComponentFinder() *ComponentFinder { + builder := testBuilder() + pp := new(TestPostProcessor) + provisioner := &MockProvisioner{} + + builderMap := map[string]Builder{ + "test-builder": builder, + } + + ppMap := map[string]PostProcessor{ + "test-pp": pp, + } + + provisionerMap := map[string]Provisioner{ + "test-prov": provisioner, + } + + builderFactory := func(n string) (Builder, error) { return builderMap[n], nil } + ppFactory := func(n string) (PostProcessor, error) { return ppMap[n], nil } + provFactory := func(n string) (Provisioner, error) { return provisionerMap[n], nil } + return &ComponentFinder{ + Builder: builderFactory, + PostProcessor: ppFactory, + Provisioner: provFactory, + } +} + func TestParseTemplateFile_basic(t *testing.T) { data := ` { @@ -364,7 +391,8 @@ func TestParseTemplate_Variables(t *testing.T) { { "variables": { "foo": "bar", - "bar": null + "bar": null, + "baz": 27 }, "builders": [{"type": "something"}] @@ -376,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) } @@ -395,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) { @@ -663,6 +699,386 @@ func TestTemplate_Build(t *testing.T) { } } +func TestTemplateBuild_exceptOnlyPP(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "post-processors": [ + { + "type": "test-pp", + "except": ["test1"], + "only": ["test1"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_exceptOnlyProv(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "except": ["test1"], + "only": ["test1"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_exceptPPInvalid(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "post-processors": [ + { + "type": "test-pp", + "except": ["test5"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_exceptPP(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "post-processors": [ + { + "type": "test-pp", + "except": ["test1"] + } + ] + } + ` + + template, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Verify test1 has no post-processors + build, err := template.Build("test1", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild := build.(*coreBuild) + if len(cbuild.postProcessors) > 0 { + t.Fatal("should have no postProcessors") + } + + // Verify test2 has no post-processors + build, err = template.Build("test2", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild = build.(*coreBuild) + if len(cbuild.postProcessors) != 1 { + t.Fatalf("invalid: %d", len(cbuild.postProcessors)) + } +} + +func TestTemplateBuild_exceptProvInvalid(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "except": ["test5"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_exceptProv(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "except": ["test1"] + } + ] + } + ` + + template, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Verify test1 has no provisioners + build, err := template.Build("test1", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild := build.(*coreBuild) + if len(cbuild.provisioners) > 0 { + t.Fatal("should have no provisioners") + } + + // Verify test2 has no provisioners + build, err = template.Build("test2", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild = build.(*coreBuild) + if len(cbuild.provisioners) != 1 { + t.Fatalf("invalid: %d", len(cbuild.provisioners)) + } +} + +func TestTemplateBuild_onlyPPInvalid(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "post-processors": [ + { + "type": "test-pp", + "only": ["test5"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_onlyPP(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "post-processors": [ + { + "type": "test-pp", + "only": ["test2"] + } + ] + } + ` + + template, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Verify test1 has no post-processors + build, err := template.Build("test1", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild := build.(*coreBuild) + if len(cbuild.postProcessors) > 0 { + t.Fatal("should have no postProcessors") + } + + // Verify test2 has no post-processors + build, err = template.Build("test2", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild = build.(*coreBuild) + if len(cbuild.postProcessors) != 1 { + t.Fatalf("invalid: %d", len(cbuild.postProcessors)) + } +} + +func TestTemplateBuild_onlyProvInvalid(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "only": ["test5"] + } + ] + } + ` + + _, err := ParseTemplate([]byte(data)) + if err == nil { + t.Fatal("should have error") + } +} + +func TestTemplateBuild_onlyProv(t *testing.T) { + data := ` + { + "builders": [ + { + "name": "test1", + "type": "test-builder" + }, + { + "name": "test2", + "type": "test-builder" + } + ], + + "provisioners": [ + { + "type": "test-prov", + "only": ["test2"] + } + ] + } + ` + + template, err := ParseTemplate([]byte(data)) + if err != nil { + t.Fatalf("err: %s", err) + } + + // Verify test1 has no provisioners + build, err := template.Build("test1", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild := build.(*coreBuild) + if len(cbuild.provisioners) > 0 { + t.Fatal("should have no provisioners") + } + + // Verify test2 has no provisioners + build, err = template.Build("test2", testTemplateComponentFinder()) + if err != nil { + t.Fatalf("err: %s", err) + } + + cbuild = build.(*coreBuild) + if len(cbuild.provisioners) != 1 { + t.Fatalf("invalid: %d", len(cbuild.provisioners)) + } +} + func TestTemplate_Build_ProvisionerOverride(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) diff --git a/packer/version.go b/packer/version.go index 8f95a99ff..758985d14 100644 --- a/packer/version.go +++ b/packer/version.go @@ -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 diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index 13e39a763..91969e558 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -24,15 +24,12 @@ type Config struct { } type PostProcessor struct { - config Config - premade map[string]packer.PostProcessor - rawConfigs []interface{} + config Config + premade map[string]packer.PostProcessor + extraConfig map[string]interface{} } func (p *PostProcessor) Configure(raws ...interface{}) error { - // Store the raw configs for usage later - p.rawConfigs = raws - _, err := common.DecodeConfig(&p.config, raws...) if err != nil { return err @@ -45,10 +42,8 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { tpl.UserVars = p.config.PackerUserVars // Defaults - ppExtraConfig := make(map[string]interface{}) if p.config.OutputPath == "" { p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box" - ppExtraConfig["output"] = p.config.OutputPath } // Accumulate any errors @@ -58,10 +53,18 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { errs, fmt.Errorf("Error parsing output template: %s", err)) } - // Store the extra configuration for post-processors - p.rawConfigs = append(p.rawConfigs, ppExtraConfig) + // Store extra configuration we'll send to each post-processor type + p.extraConfig = make(map[string]interface{}) + p.extraConfig["output"] = p.config.OutputPath + p.extraConfig["packer_build_name"] = p.config.PackerBuildName + p.extraConfig["packer_builder_type"] = p.config.PackerBuilderType + p.extraConfig["packer_debug"] = p.config.PackerDebug + p.extraConfig["packer_force"] = p.config.PackerForce + p.extraConfig["packer_user_variables"] = p.config.PackerUserVars - // TODO(mitchellh): Properly handle multiple raw configs + // TODO(mitchellh): Properly handle multiple raw configs. This isn't + // very pressing at the moment because at the time of this comment + // only the first member of raws can contain the actual type-overrides. var mapConfig map[string]interface{} if err := mapstructure.Decode(raws[0], &mapConfig); err != nil { errs = packer.MultiErrorAppend(errs, @@ -71,18 +74,14 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { p.premade = make(map[string]packer.PostProcessor) for k, raw := range mapConfig { - pp := keyToPostProcessor(k) - if pp == nil { + pp, err := p.subPostProcessor(k, raw, p.extraConfig) + if err != nil { + errs = packer.MultiErrorAppend(errs, err) continue } - // Create the proper list of configurations - ppConfigs := make([]interface{}, 0, len(p.rawConfigs)+1) - copy(ppConfigs, p.rawConfigs) - ppConfigs = append(ppConfigs, raw) - - if err := pp.Configure(ppConfigs...); err != nil { - errs = packer.MultiErrorAppend(errs, err) + if pp == nil { + continue } p.premade[k] = pp @@ -106,13 +105,15 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac pp, ok := p.premade[ppName] if !ok { log.Printf("Premade post-processor for '%s' not found. Creating.", ppName) - pp = keyToPostProcessor(ppName) - if pp == nil { - return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName) + + var err error + pp, err = p.subPostProcessor(ppName, nil, p.extraConfig) + if err != nil { + return nil, false, err } - if err := pp.Configure(p.rawConfigs...); err != nil { - return nil, false, err + if pp == nil { + return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName) } } @@ -120,6 +121,21 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac return pp.PostProcess(ui, artifact) } +func (p *PostProcessor) subPostProcessor(key string, specific interface{}, extra map[string]interface{}) (packer.PostProcessor, error) { + pp := keyToPostProcessor(key) + if pp == nil { + return nil, nil + } + + if err := pp.Configure(extra, specific); err != nil { + return nil, err + } + + return pp, nil +} + +// keyToPostProcessor maps a configuration key to the actual post-processor +// it will be configuring. This returns a new instance of that post-processor. func keyToPostProcessor(key string) packer.PostProcessor { switch key { case "aws": diff --git a/provisioner/chef-solo/provisioner.go b/provisioner/chef-solo/provisioner.go index 04bb472b8..ddfc605fb 100644 --- a/provisioner/chef-solo/provisioner.go +++ b/provisioner/chef-solo/provisioner.go @@ -20,6 +20,8 @@ type Config struct { ConfigTemplate string `mapstructure:"config_template"` CookbookPaths []string `mapstructure:"cookbook_paths"` + RolesPath string `mapstructure:"roles_path"` + DataBagsPath string `mapstructure:"data_bags_path"` ExecuteCommand string `mapstructure:"execute_command"` InstallCommand string `mapstructure:"install_command"` RemoteCookbookPaths []string `mapstructure:"remote_cookbook_paths"` @@ -38,6 +40,14 @@ type Provisioner struct { type ConfigTemplate struct { CookbookPaths string + DataBagsPath string + RolesPath 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 } type ExecuteTemplate struct { @@ -83,6 +93,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { 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, } @@ -144,6 +156,24 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { } } + if p.config.RolesPath != "" { + pFileInfo, err := os.Stat(p.config.RolesPath) + + if err != nil || !pFileInfo.IsDir() { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Bad roles path '%s': %s", p.config.RolesPath, err)) + } + } + + if p.config.DataBagsPath != "" { + pFileInfo, err := os.Stat(p.config.DataBagsPath) + + if err != nil || !pFileInfo.IsDir() { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Bad data bags path '%s': %s", p.config.DataBagsPath, 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() @@ -180,7 +210,23 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { cookbookPaths = append(cookbookPaths, targetPath) } - configPath, err := p.createConfig(ui, comm, cookbookPaths) + rolesPath := "" + if p.config.RolesPath != "" { + 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) + } + } + + dataBagsPath := "" + if p.config.DataBagsPath != "" { + 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) if err != nil { return fmt.Errorf("Error creating Chef config file: %s", err) } @@ -217,7 +263,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) (string, error) { +func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, localCookbooks []string, rolesPath string, dataBagsPath string) (string, error) { ui.Message("Creating configuration file 'solo.rb'") cookbook_paths := make([]string, len(p.config.RemoteCookbookPaths)+len(localCookbooks)) @@ -248,7 +294,11 @@ func (p *Provisioner) createConfig(ui packer.Ui, comm packer.Communicator, local } configString, err := p.config.tpl.Process(tpl, &ConfigTemplate{ - CookbookPaths: strings.Join(cookbook_paths, ","), + CookbookPaths: strings.Join(cookbook_paths, ","), + RolesPath: rolesPath, + DataBagsPath: dataBagsPath, + HasRolesPath: rolesPath != "", + HasDataBagsPath: dataBagsPath != "", }) if err != nil { return "", err @@ -399,5 +449,11 @@ func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { } var DefaultConfigTemplate = ` -cookbook_path [{{.CookbookPaths}}] +cookbook_path [{{.CookbookPaths}}] +{{if .HasRolesPath}} +role_path "{{.RolesPath}}" +{{end}} +{{if .HasDataBagsPath}} +data_bag_path "{{.DataBagsPath}}" +{{end}} ` diff --git a/provisioner/chef-solo/provisioner_test.go b/provisioner/chef-solo/provisioner_test.go index dc8bea1e7..c18df246a 100644 --- a/provisioner/chef-solo/provisioner_test.go +++ b/provisioner/chef-solo/provisioner_test.go @@ -75,11 +75,25 @@ func TestProvisionerPrepare_cookbookPaths(t *testing.T) { t.Fatalf("err: %s", err) } + rolesPath, err := ioutil.TempDir("", "roles") + if err != nil { + t.Fatalf("err: %s", err) + } + + dataBagsPath, err := ioutil.TempDir("", "data_bags") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(path1) defer os.Remove(path2) + defer os.Remove(rolesPath) + defer os.Remove(dataBagsPath) config := testConfig() config["cookbook_paths"] = []string{path1, path2} + config["roles_path"] = rolesPath + config["data_bags_path"] = dataBagsPath err = p.Prepare(config) if err != nil { @@ -93,6 +107,58 @@ func TestProvisionerPrepare_cookbookPaths(t *testing.T) { if p.config.CookbookPaths[0] != path1 || p.config.CookbookPaths[1] != path2 { t.Fatalf("unexpected: %#v", p.config.CookbookPaths) } + + if p.config.RolesPath != rolesPath { + t.Fatalf("unexpected: %#v", p.config.RolesPath) + } + + if p.config.DataBagsPath != dataBagsPath { + t.Fatalf("unexpected: %#v", p.config.DataBagsPath) + } +} + +func TestProvisionerPrepare_dataBagsPath(t *testing.T) { + var p Provisioner + + dataBagsPath, err := ioutil.TempDir("", "data_bags") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(dataBagsPath) + + config := testConfig() + config["data_bags_path"] = dataBagsPath + + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.DataBagsPath != dataBagsPath { + t.Fatalf("unexpected: %#v", p.config.DataBagsPath) + } +} + +func TestProvisionerPrepare_rolesPath(t *testing.T) { + var p Provisioner + + rolesPath, err := ioutil.TempDir("", "roles") + if err != nil { + t.Fatalf("err: %s", err) + } + defer os.Remove(rolesPath) + + config := testConfig() + config["roles_path"] = rolesPath + + err = p.Prepare(config) + if err != nil { + t.Fatalf("err: %s", err) + } + + if p.config.RolesPath != rolesPath { + t.Fatalf("unexpected: %#v", p.config.RolesPath) + } } func TestProvisionerPrepare_json(t *testing.T) { diff --git a/provisioner/puppet-masterless/provisioner.go b/provisioner/puppet-masterless/provisioner.go index ff8b2112b..4552aef9a 100644 --- a/provisioner/puppet-masterless/provisioner.go +++ b/provisioner/puppet-masterless/provisioner.go @@ -82,7 +82,9 @@ func (p *Provisioner) Prepare(raws ...interface{}) error { // Templates templates := map[string]*string{ - "staging_dir": &p.config.StagingDir, + "hiera_config_path": &p.config.HieraConfigPath, + "manifest_file": &p.config.ManifestFile, + "staging_dir": &p.config.StagingDir, } for n, ptr := range templates { diff --git a/provisioner/shell/provisioner.go b/provisioner/shell/provisioner.go index e8a5f61a5..3cb75187d 100644 --- a/provisioner/shell/provisioner.go +++ b/provisioner/shell/provisioner.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/mitchellh/packer/common" "github.com/mitchellh/packer/packer" + "io" "io/ioutil" "log" "os" @@ -20,6 +21,10 @@ const DefaultRemotePath = "/tmp/script.sh" type config struct { common.PackerConfig `mapstructure:",squash"` + // If true, the script contains binary and line endings will not be + // converted from Windows to Unix-style. + Binary bool + // An inline script to execute. Multiple strings are all executed // in the context of a single shell. Inline []string @@ -259,6 +264,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { return err } + var r io.Reader = f + if !p.config.Binary { + r = &UnixReader{Reader: r} + } + if err := comm.Upload(p.config.RemotePath, f); err != nil { return fmt.Errorf("Error uploading script: %s", err) } diff --git a/provisioner/shell/unix_reader.go b/provisioner/shell/unix_reader.go new file mode 100644 index 000000000..5745dd291 --- /dev/null +++ b/provisioner/shell/unix_reader.go @@ -0,0 +1,88 @@ +package shell + +import ( + "bufio" + "bytes" + "io" + "sync" +) + +// UnixReader is a Reader implementation that automatically converts +// Windows line endings to Unix line endings. +type UnixReader struct { + Reader io.Reader + + buf []byte + once sync.Once + scanner *bufio.Scanner +} + +func (r *UnixReader) Read(p []byte) (n int, err error) { + // Create the buffered reader once + r.once.Do(func() { + r.scanner = bufio.NewScanner(r.Reader) + r.scanner.Split(scanUnixLine) + }) + + // If we have no data in our buffer, scan to the next token + if len(r.buf) == 0 { + if !r.scanner.Scan() { + err = r.scanner.Err() + if err == nil { + err = io.EOF + } + + return 0, err + } + + r.buf = r.scanner.Bytes() + } + + // Write out as much data as we can to the buffer, storing the rest + // for the next read. + n = len(p) + if n > len(r.buf) { + n = len(r.buf) + } + copy(p, r.buf) + r.buf = r.buf[n:] + + return +} + +// scanUnixLine is a bufio.Scanner SplitFunc. It tokenizes on lines, but +// 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 +} diff --git a/provisioner/shell/unix_reader_test.go b/provisioner/shell/unix_reader_test.go new file mode 100644 index 000000000..8dedf6300 --- /dev/null +++ b/provisioner/shell/unix_reader_test.go @@ -0,0 +1,33 @@ +package shell + +import ( + "bytes" + "io" + "testing" +) + +func TestUnixReader_impl(t *testing.T) { + var raw interface{} + raw = new(UnixReader) + if _, ok := raw.(io.Reader); !ok { + t.Fatal("should be reader") + } +} + +func TestUnixReader(t *testing.T) { + input := "one\r\ntwo\nthree\r\n" + expected := "one\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()) + } +} diff --git a/scripts/build.sh b/scripts/build.sh index 95ba90abf..8ba8d3d4f 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -26,6 +26,9 @@ if [ "$(go env GOOS)" = "windows" ]; then EXTENSION=".exe" fi +# Make sure that if we're killed, we kill all our subprocseses +trap "kill 0" SIGINT SIGTERM EXIT + # If we're building a race-enabled build, then set that up. if [ ! -z $PACKER_RACE ]; then echo -e "${OK_COLOR}--> Building with race detection enabled${NO_COLOR}" @@ -35,21 +38,57 @@ fi echo -e "${OK_COLOR}--> Installing dependencies to speed up builds...${NO_COLOR}" go get ./... +# This function waits for all background tasks to complete +waitAll() { + RESULT=0 + for job in `jobs -p`; do + wait $job + if [ $? -ne 0 ]; then + RESULT=1 + fi + done + + if [ $RESULT -ne 0 ]; then + exit $RESULT + fi +} + +waitSingle() { + if [ ! -z $PACKER_NO_BUILD_PARALLEL ]; then + waitAll + fi +} + +if [ -z $PACKER_NO_BUILD_PARALLEL ]; then + echo -e "${OK_COLOR}--> NOTE: Compilation of components " \ + "will be done in parallel.${NO_COLOR}" +fi + # Compile the main Packer app echo -e "${OK_COLOR}--> Compiling Packer${NO_COLOR}" +( go build \ ${PACKER_RACE} \ -ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \ -v \ -o bin/packer${EXTENSION} . +) & + +waitSingle # Go over each plugin and build it for PLUGIN in $(find ./plugin -mindepth 1 -maxdepth 1 -type d); do PLUGIN_NAME=$(basename ${PLUGIN}) echo -e "${OK_COLOR}--> Compiling Plugin: ${PLUGIN_NAME}${NO_COLOR}" + ( go build \ ${PACKER_RACE} \ -ldflags "-X github.com/mitchellh/packer/packer.GitCommit ${GIT_COMMIT}${GIT_DIRTY}" \ -v \ -o bin/packer-${PLUGIN_NAME}${EXTENSION} ${PLUGIN} + ) & + + waitSingle done + +waitAll diff --git a/website/Gemfile b/website/Gemfile index d270a5ae4..2b3a472d5 100644 --- a/website/Gemfile +++ b/website/Gemfile @@ -2,11 +2,11 @@ source 'https://rubygems.org' ruby '1.9.3' -gem "middleman", "~> 3.0.6" -gem "middleman-minify-html", "~> 3.0.0" +gem "middleman", "~> 3.1.5" +gem "middleman-minify-html", "~> 3.1.1" gem "rack-contrib", "~> 1.1.0" -gem "redcarpet", "~> 2.2.2" -gem "therubyracer", "~> 0.10.2" +gem "redcarpet", "~> 3.0.0" +gem "therubyracer", "~> 0.12.0" gem "thin", "~> 1.5.0" group :development do diff --git a/website/Gemfile.lock b/website/Gemfile.lock index d79220dde..a25bfa939 100644 --- a/website/Gemfile.lock +++ b/website/Gemfile.lock @@ -1,134 +1,109 @@ GEM remote: https://rubygems.org/ specs: - POpen4 (0.1.4) - Platform (>= 0.4.0) - open4 - Platform (0.4.0) - activesupport (3.2.9) - i18n (~> 0.6) + activesupport (3.2.14) + i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) - chunky_png (1.2.6) + chunky_png (1.2.8) coffee-script (2.2.0) coffee-script-source execjs - coffee-script-source (1.3.3) + coffee-script-source (1.6.3) compass (0.12.2) chunky_png (~> 1.2) fssm (>= 0.2.7) sass (~> 3.1) daemons (1.1.9) - eventmachine (1.0.0) + eventmachine (1.0.3) execjs (1.4.0) multi_json (~> 1.0) - ffi (1.2.0) - fssm (0.2.9) - haml (3.1.7) - highline (1.6.15) - hike (1.2.1) - htmlcompressor (0.0.3) - yui-compressor (~> 0.9.6) - http_router (0.10.2) - rack (>= 1.0.0) - url_mount (~> 0.2.1) - i18n (0.6.1) - libv8 (3.3.10.4) - listen (0.5.3) - maruku (0.6.1) - syntax (>= 1.0.0) - middleman (3.0.6) - middleman-core (= 3.0.6) - middleman-more (= 3.0.6) - middleman-sprockets (~> 3.0.2) - middleman-core (3.0.6) - activesupport (~> 3.2.6) - bundler (~> 1.1) - listen (~> 0.5.2) - rack (~> 1.4.1) - rack-test (~> 0.6.1) - rb-fsevent (~> 0.9.1) - rb-inotify (~> 0.8.8) - thor (~> 0.15.4) - tilt (~> 1.3.1) - middleman-minify-html (3.0.0) - htmlcompressor - middleman-core (~> 3.0.0) - middleman-more (3.0.6) + ffi (1.9.0) + fssm (0.2.10) + haml (4.0.3) + tilt + highline (1.6.19) + hike (1.2.3) + i18n (0.6.5) + kramdown (1.1.0) + libv8 (3.16.14.3) + listen (1.2.3) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + middleman (3.1.5) coffee-script (~> 2.2.0) - coffee-script-source (~> 1.3.3) compass (>= 0.12.2) execjs (~> 1.4.0) haml (>= 3.1.6) - i18n (~> 0.6.0) - maruku (~> 0.6.0) - middleman-core (= 3.0.6) - padrino-helpers (= 0.10.7) + kramdown (~> 1.1.0) + middleman-core (= 3.1.5) + middleman-more (= 3.1.5) + middleman-sprockets (>= 3.1.2) sass (>= 3.1.20) - uglifier (~> 1.2.6) - middleman-sprockets (3.0.4) - middleman-more (~> 3.0.1) - sprockets (~> 2.1, < 2.5) - sprockets-sass (~> 0.8.0) - multi_json (1.4.0) - open4 (1.3.0) - padrino-core (0.10.7) - activesupport (~> 3.2.0) - http_router (~> 0.10.2) - sinatra (~> 1.3.1) - thor (~> 0.15.2) - tilt (~> 1.3.0) - padrino-helpers (0.10.7) - i18n (~> 0.6) - padrino-core (= 0.10.7) - rack (1.4.1) + uglifier (~> 2.1.0) + middleman-core (3.1.5) + activesupport (~> 3.2.6) + bundler (~> 1.1) + i18n (~> 0.6.1) + listen (~> 1.2.2) + rack (>= 1.4.5) + rack-test (~> 0.6.1) + thor (>= 0.15.2, < 2.0) + tilt (~> 1.3.6) + middleman-minify-html (3.1.1) + middleman-core (~> 3.0) + middleman-more (3.1.5) + middleman-sprockets (3.1.4) + middleman-core (>= 3.0.14) + middleman-more (>= 3.0.14) + sprockets (~> 2.1) + sprockets-helpers (~> 1.0.0) + sprockets-sass (~> 1.0.0) + multi_json (1.8.0) + rack (1.5.2) rack-contrib (1.1.0) rack (>= 0.9.1) - rack-protection (1.2.0) - rack rack-test (0.6.2) rack (>= 1.0) - rb-fsevent (0.9.2) - rb-inotify (0.8.8) + rb-fsevent (0.9.3) + rb-inotify (0.9.2) ffi (>= 0.5.0) - redcarpet (2.2.2) - sass (3.2.3) - sinatra (1.3.3) - rack (~> 1.3, >= 1.3.6) - rack-protection (~> 1.2) - tilt (~> 1.3, >= 1.3.3) - sprockets (2.4.5) + rb-kqueue (0.2.0) + ffi (>= 0.5.0) + redcarpet (3.0.0) + ref (1.0.5) + sass (3.2.10) + sprockets (2.10.0) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-sass (0.8.0) + sprockets-helpers (1.0.1) + sprockets (~> 2.0) + sprockets-sass (1.0.1) sprockets (~> 2.0) tilt (~> 1.1) - syntax (1.0.0) - therubyracer (0.10.2) - libv8 (~> 3.3.10) - thin (1.5.0) + therubyracer (0.12.0) + libv8 (~> 3.16.14.0) + ref + thin (1.5.1) daemons (>= 1.0.9) eventmachine (>= 0.12.6) rack (>= 1.0.0) - thor (0.15.4) - tilt (1.3.3) - uglifier (1.2.7) + thor (0.18.1) + tilt (1.3.7) + uglifier (2.1.2) execjs (>= 0.3.0) - multi_json (~> 1.3) - url_mount (0.2.1) - rack - yui-compressor (0.9.6) - POpen4 (>= 0.1.4) + multi_json (~> 1.0, >= 1.0.2) PLATFORMS ruby DEPENDENCIES highline (~> 1.6.15) - middleman (~> 3.0.6) - middleman-minify-html (~> 3.0.0) + middleman (~> 3.1.5) + middleman-minify-html (~> 3.1.1) rack-contrib (~> 1.1.0) - redcarpet (~> 2.2.2) - therubyracer (~> 0.10.2) + redcarpet (~> 3.0.0) + therubyracer (~> 0.12.0) thin (~> 1.5.0) diff --git a/website/source/docs/builders/virtualbox.html.markdown b/website/source/docs/builders/virtualbox.html.markdown index 75e6b1943..b10cd8cb0 100644 --- a/website/source/docs/builders/virtualbox.html.markdown +++ b/website/source/docs/builders/virtualbox.html.markdown @@ -85,6 +85,10 @@ Optional: * `format` (string) - Either "ovf" or "ova", this specifies the output format of the exported virtual machine. This defaults to "ovf". +* `guest_additions_attach` (bool) - If this is true (defaults to "false"), + the guest additions ISO will be attached to the virtual machine as a CD + rather than uploaded as a raw ISO. + * `guest_additions_path` (string) - The path on the guest virtual machine where the VirtualBox guest additions ISO will be uploaded. By default this is "VBoxGuestAdditions.iso" which should upload into the login directory diff --git a/website/source/docs/provisioners/chef-solo.html.markdown b/website/source/docs/provisioners/chef-solo.html.markdown index 12f3c5724..0b1c2204b 100644 --- a/website/source/docs/provisioners/chef-solo.html.markdown +++ b/website/source/docs/provisioners/chef-solo.html.markdown @@ -44,6 +44,14 @@ configuration is actually required, but at least `run_list` is recommended. to the remote machine in the directory specified by the `staging_directory`. By default, this is empty. +* `roles_path` (string) - The path to the "roles" directory on your local filesystem. + These will be uploaded to the remote machine in the directory specified by the + `staging_directory`. By default, this is empty. + +* `data_bags_path` (string) - The path to the "data_bags" directory on your local filesystem. + These will be uploaded to the remote machine in the directory specified by the + `staging_directory`. By default, this is empty. + * `execute_command` (string) - The command used to execute Chef. This has various [configuration template variables](/docs/templates/configuration-templates.html) available. See below for more information. diff --git a/website/source/docs/provisioners/puppet-masterless.html.markdown b/website/source/docs/provisioners/puppet-masterless.html.markdown index 11dbc6a85..792718570 100644 --- a/website/source/docs/provisioners/puppet-masterless.html.markdown +++ b/website/source/docs/provisioners/puppet-masterless.html.markdown @@ -79,7 +79,7 @@ By default, Packer uses the following command (broken across multiple lines for readability) to execute Puppet: ``` -{{.FacterVars}}{{if .Sudo} sudo -E {{end}}puppet apply \ +{{.FacterVars}}{{if .Sudo}} sudo -E {{end}}puppet apply \ --verbose \ --modulepath='{{.ModulePath}}' \ {{if .HasHieraConfigPath}}--hiera_config='{{.HieraConfigPath}}' {{end}} \ diff --git a/website/source/docs/provisioners/shell.html.markdown b/website/source/docs/provisioners/shell.html.markdown index d3a895b3a..d47b59733 100644 --- a/website/source/docs/provisioners/shell.html.markdown +++ b/website/source/docs/provisioners/shell.html.markdown @@ -47,6 +47,10 @@ Exactly _one_ of the following is required: Optional parameters: +* `binary` (boolean) - If true, specifies that the script(s) are binary + files, and Packer should therefore not convert Windows line endings to + Unix line endings (if there are any). By default this is false. + * `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 @@ -98,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. diff --git a/website/source/docs/templates/post-processors.html.markdown b/website/source/docs/templates/post-processors.html.markdown index e4c1bfa8c..cda12e2a2 100644 --- a/website/source/docs/templates/post-processors.html.markdown +++ b/website/source/docs/templates/post-processors.html.markdown @@ -125,3 +125,28 @@ The answer is no, of course not. Packer is smart enough to figure out that at least one post-processor requested that the input be kept, so it will keep it around. + +## Run on Specific Builds + +You can use the `only` or `except` configurations to run a post-processor +only with specific builds. These two configurations do what you expect: +`only` will only run the post-processor on the specified builds and +`except` will run the post-processor on anything other than the specified +builds. + +An example of `only` being used is shown below, but the usage of `except` +is effectively the same. `only` and `except` can only be specified on "detailed" +configurations. If you have a sequence of post-processors to run, `only` +and `except` will only affect that single post-processor in the sequence. + +
+{
+  "type": "vagrant",
+  "only": ["virtualbox"]
+}
+
+ +The values within `only` or `except` are _build names_, not builder +types. If you recall, build names by default are just their builder type, +but if you specify a custom `name` parameter, then you should use that +as the value instead of the type. diff --git a/website/source/docs/templates/provisioners.html.markdown b/website/source/docs/templates/provisioners.html.markdown index f4263fb9b..eb49788e1 100644 --- a/website/source/docs/templates/provisioners.html.markdown +++ b/website/source/docs/templates/provisioners.html.markdown @@ -53,6 +53,30 @@ provisioner to run a local script within the machines: } +## Run on Specific Builds + +You can use the `only` or `except` configurations to run a provisioner +only with specific builds. These two configurations do what you expect: +`only` will only run the provisioner on the specified builds and +`except` will run the provisioner on anything other than the specified +builds. + +An example of `only` being used is shown below, but the usage of `except` +is effectively the same: + +
+{
+  "type": "shell",
+  "script": "script.sh",
+  "only": ["virtualbox"]
+}
+
+ +The values within `only` or `except` are _build names_, not builder +types. If you recall, build names by default are just their builder type, +but if you specify a custom `name` parameter, then you should use that +as the value instead of the type. + ## Build-Specific Overrides While the goal of Packer is to produce identical machine images, it diff --git a/website/source/docs/templates/user-variables.html.markdown b/website/source/docs/templates/user-variables.html.markdown index 8f19aadea..edce239ad 100644 --- a/website/source/docs/templates/user-variables.html.markdown +++ b/website/source/docs/templates/user-variables.html.markdown @@ -54,7 +54,7 @@ validation will fail. Using the variables is extremely easy. Variables are used by calling the user function in the form of {{user `variable`}}. -This function can be used in _any string_ within the template, in +This function can be used in _any value_ within the template, in builders, provisioners, _anything_. The user variable is available globally within the template.