Compare commits

..

34 Commits

Author SHA1 Message Date
Mitchell Hashimoto 1e61157c5c v0.2.3 2013-08-07 15:32:38 -07:00
Mitchell Hashimoto 7c4bd4f2d6 website: nitpick alternative installation methods 2013-08-07 15:32:09 -07:00
Mitchell Hashimoto 9c6cc829fa Merge pull request #244 from henrikhodne/hh-homebrew-docs-website
website: add documentation on installing through Homebrew
2013-08-07 15:01:57 -07:00
Mitchell Hashimoto 53b0223e20 Merge pull request #249 from rgarcia/config-decoding
provisioner/shell,file: use common for config decoding
2013-08-07 06:35:59 -07:00
Mark Peek 15e80316fe Update CHANGELOG for Amazon AMI tag support [GH-233] 2013-08-06 15:31:56 -07:00
Rafael Garcia 485d5b86d0 provisioner/file: state of the art config decoding 2013-08-06 15:30:49 -07:00
Rafael Garcia b0d5bff98f provisioner/shell: state of the art config decoding 2013-08-06 15:30:36 -07:00
Mark Peek 8c51316897 website: cleanup Amazon AMI tag documentation 2013-08-06 15:16:39 -07:00
Mark Peek 3bcfeafa15 builder/amazon/all: refactor ami tags [GH-233]
Refactor the EBS ami tag into a common step and add support
for instance-store ami tags.
/cc @jmassara
2013-08-06 15:05:03 -07:00
Mark Peek 3d461195e3 Merge pull request #233 from jmassara/tags
builder/amazon/ebs: Added tagging support for amazon/ebs AMIs
2013-08-06 15:01:32 -07:00
James Massara e6a1fc448a Updated tags configuration example 2013-08-06 07:41:00 -07:00
James Massara d1aefb38bb Simplied Tags configuration 2013-08-06 07:33:00 -07:00
Mitchell Hashimoto e0a66fbe30 Merge pull request #245 from extraordinaire/patch-1
builder/amazon/common: fix check for vpcid
2013-08-05 08:39:23 -07:00
Andrei Serdeliuc 80db9efef5 fix check for vpcid
check for VpcId was incorrectly checking if VPC is empty, instead of not empty.
2013-08-05 16:19:05 +01:00
James Massara 8853e7a17e Moved Tags configuration out of common and into ebs. 2013-08-03 20:09:14 -07:00
James Massara ad38604390 removed Tags definition from RunConfig 2013-08-03 17:53:53 -07:00
Henrik Hodne 64f4ee380d website: document Homebrew installation process [GH-227] 2013-08-03 16:44:30 -07:00
Mitchell Hashimoto 3bf88e2dd7 builder/amazon/common: sleep between API calls for SSHAddr 2013-08-03 16:38:21 -07:00
Mitchell Hashimoto c8508ade17 builder/amazon/common: be more gentle on AWS API while getting SSHAddr 2013-08-03 16:37:34 -07:00
Mitchell Hashimoto 4e8db89403 builder/amazon/all: use new SSHAddress method [GH-243] 2013-08-03 16:24:49 -07:00
Mitchell Hashimoto 1b8551d843 builder/amazon/common: refresh instance while connecting to SSH [GH-243] 2013-08-03 16:23:32 -07:00
Mitchell Hashimoto 46ec8f758b website: document {{.Vars}} in execute_command example [GH-241] 2013-08-03 13:40:31 -07:00
Mitchell Hashimoto 46e49b745e common: replace windows file URL backslash with forward slash
/cc @jasonberanek - Just adding this as well because I see this being
common as well.
2013-08-03 13:38:27 -07:00
Mitchell Hashimoto 4b29fab843 Update CHANGELOG 2013-08-03 13:35:19 -07:00
Mitchell Hashimoto 82e21622ef common: style 2013-08-03 13:34:48 -07:00
Mitchell Hashimoto e4dda36a1d Merge pull request #240 from jasonberanek/windows-paths-fix
common: Fix URL and Relative Windows file path support #235/#239
2013-08-03 13:32:51 -07:00
Jason A. Beranek 1216fc1cbb common/config,download: gofmt fixes 2013-08-02 16:06:06 -05:00
Jason A. Beranek 2b4735e825 common/config,download: fix URL and relative Windows local file path issues [GH-235] [GH-239] 2013-08-02 15:59:19 -05:00
James Massara 422e05d8b9 Updated with tags info 2013-08-01 21:28:25 -07:00
James Massara 4c273e33c5 removed amiId temp variable 2013-08-01 18:35:43 -07:00
James Massara b24fcbc800 Adds support for adding tags to the AMI 2013-08-01 16:38:22 -07:00
James Massara 852c10264b Adds support for adding tags to the AMI 2013-08-01 16:31:07 -07:00
Mitchell Hashimoto ef59ee41a8 rename builder/common to common since it is generally useful 2013-08-01 12:11:54 -07:00
Mitchell Hashimoto da7febbfb9 Update version for dev 2013-08-01 10:40:18 -07:00
33 changed files with 225 additions and 97 deletions
+13
View File
@@ -1,3 +1,16 @@
## 0.2.3 (August 7, 2013)
IMPROVEMENTS:
* builder/amazon/all: Added Amazon AMI tag support [GH-233]
BUG FIXES:
* core: Absolute/relative filepaths on Windows now work for iso_url
and other settings. [GH-240]
* builder/amazon/all: instance info is refreshed while waiting for SSH,
allowing Packer to see updated IP/DNS info. [GH-243]
## 0.2.2 (August 1, 2013)
FEATURES:
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"runtime"
+19 -2
View File
@@ -9,15 +9,30 @@ import (
"time"
)
// StateRefreshFunc is a function type used for StateChangeConf that is
// responsible for refreshing the item being watched for a state change.
//
// It returns three results. `result` is any object that will be returned
// as the final object after waiting for state change. This allows you to
// return the final updated object, for example an EC2 instance after refreshing
// it.
//
// `state` is the latest state of that object. And `err` is any error that
// may have happened while refreshing the state.
type StateRefreshFunc func() (result interface{}, state string, err error)
// StateChangeConf is the configuration struct used for `WaitForState`.
type StateChangeConf struct {
Conn *ec2.EC2
Pending []string
Refresh func() (interface{}, string, error)
Refresh StateRefreshFunc
StepState map[string]interface{}
Target string
}
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{}, string, error) {
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
@@ -29,6 +44,8 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{
}
}
// WaitForState watches an object and waits for it to achieve a certain
// state.
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
+27 -10
View File
@@ -6,23 +6,40 @@ import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/packer/communicator/ssh"
"time"
)
// SSHAddress returns a function that can be given to the SSH communicator
// for determining the SSH address based on the instance DNS name.
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
func SSHAddress(e *ec2.EC2, port int) func(map[string]interface{}) (string, error) {
return func(state map[string]interface{}) (string, error) {
var host string
instance := state["instance"].(*ec2.Instance)
if instance.DNSName != "" {
host = instance.DNSName
} else if instance.VpcId == "" {
host = instance.PrivateIpAddress
} else {
return "", errors.New("couldn't determine IP address for instance")
for j := 0; j < 2; j++ {
var host string
i := state["instance"].(*ec2.Instance)
if i.DNSName != "" {
host = i.DNSName
} else if i.VpcId != "" {
host = i.PrivateIpAddress
}
if host != "" {
return fmt.Sprintf("%s:%d", host, port), nil
}
r, err := e.Instances([]string{i.InstanceId}, ec2.NewFilter())
if err != nil {
return "", err
}
if len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
return "", fmt.Errorf("instance not found: %s", i.InstanceId)
}
state["instance"] = &r.Reservations[0].Instances[0]
time.Sleep(1 * time.Second)
}
return fmt.Sprintf("%s:%d", host, port), nil
return "", errors.New("couldn't determine IP address for instance")
}
}
+42
View File
@@ -0,0 +1,42 @@
package common
import (
"fmt"
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
)
type StepCreateTags struct {
Tags map[string]string
}
func (s *StepCreateTags) Run(state map[string]interface{}) multistep.StepAction {
ec2conn := state["ec2"].(*ec2.EC2)
ui := state["ui"].(packer.Ui)
amis := state["amis"].(map[string]string)
ami := amis[ec2conn.Region.Name]
if len(s.Tags) > 0 {
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
var ec2Tags []ec2.Tag
for key, value := range s.Tags {
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
}
_, err := ec2conn.CreateTags([]string{ami}, ec2Tags)
if err != nil {
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
state["error"] = err
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *StepCreateTags) Cleanup(state map[string]interface{}) {
// No cleanup...
}
+6 -2
View File
@@ -11,7 +11,7 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"text/template"
@@ -27,6 +27,9 @@ type config struct {
// Configuration of the resulting AMI
AMIName string `mapstructure:"ami_name"`
// Tags for the AMI
Tags map[string]string
}
type Builder struct {
@@ -101,13 +104,14 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
&common.StepProvision{},
&stepStopInstance{},
&stepCreateAMI{},
&awscommon.StepCreateTags{b.config.Tags},
}
// Run!
+4 -2
View File
@@ -8,7 +8,7 @@ import (
"github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep"
awscommon "github.com/mitchellh/packer/builder/amazon/common"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
@@ -33,6 +33,7 @@ type Config struct {
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
BundleVolCommand string `mapstructure:"bundle_vol_command"`
S3Bucket string `mapstructure:"s3_bucket"`
Tags map[string]string
X509CertPath string `mapstructure:"x509_cert_path"`
X509KeyPath string `mapstructure:"x509_key_path"`
X509UploadPath string `mapstructure:"x509_upload_path"`
@@ -167,7 +168,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SubnetId: b.config.SubnetId,
},
&common.StepConnectSSH{
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
SSHWaitTimeout: b.config.SSHTimeout(),
},
@@ -176,6 +177,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepBundleVolume{},
&StepUploadBundle{},
&StepRegisterAMI{},
&awscommon.StepCreateTags{b.config.Tags},
}
// Run!
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"os"
@@ -6,7 +6,7 @@ import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io"
"io/ioutil"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"math/rand"
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"log"
"time"
@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
)
@@ -73,6 +74,17 @@ func DownloadableURL(original string) (string, error) {
}
if url.Scheme == "file" {
// For Windows absolute file paths, remove leading / prior to processing
// since net/url turns "C:/" into "/C:/"
if runtime.GOOS == "windows" && url.Path[0] == '/' {
url.Path = url.Path[1:len(url.Path)]
// Also replace all backslashes with forwardslashes since Windows
// users are likely to do this but the URL should actually only
// contain forward slashes.
url.Path = strings.Replace(url.Path, `\`, `/`, -1)
}
if _, err := os.Stat(url.Path); err != nil {
return "", err
}
@@ -14,6 +14,7 @@ import (
"net/http"
"net/url"
"os"
"runtime"
)
// DownloadConfig is the configuration given to instantiate a new
@@ -107,6 +108,11 @@ func (d *DownloadClient) Get() (string, error) {
var finalPath string
if url.Scheme == "file" && !d.config.CopyFile {
finalPath = url.Path
// Remove forward slash on absolute Windows file URLs before processing
if runtime.GOOS == "windows" && finalPath[0] == '/' {
finalPath = finalPath[1:len(finalPath)]
}
} else {
finalPath = d.config.TargetPath
+1 -1
View File
@@ -10,7 +10,7 @@ import (
var GitCommit string
// The version of packer.
const Version = "0.2.2"
const Version = "0.2.3"
// 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
+8 -33
View File
@@ -3,11 +3,9 @@ package file
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"sort"
"strings"
)
type config struct {
@@ -23,49 +21,26 @@ type Provisioner struct {
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &p.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if _, err := os.Stat(p.config.Source); err != nil {
errs = append(errs,
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad source '%s': %s", p.config.Source, err))
}
if p.config.Destination == "" {
errs = append(errs, errors.New("Destination must be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Destination must be specified."))
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
+1 -1
View File
@@ -5,7 +5,7 @@ package saltmasterless
import (
"errors"
"fmt"
"github.com/mitchellh/packer/builder/common"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"os"
"path/filepath"
+14 -36
View File
@@ -7,12 +7,11 @@ import (
"bytes"
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"text/template"
)
@@ -61,37 +60,13 @@ type ExecuteCommandTemplate struct {
}
func (p *Provisioner) Prepare(raws ...interface{}) error {
var md mapstructure.Metadata
decoderConfig := &mapstructure.DecoderConfig{
Metadata: &md,
Result: &p.config,
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
md, err := common.DecodeConfig(&p.config, raws...)
if err != nil {
return err
}
for _, raw := range raws {
err := decoder.Decode(raw)
if err != nil {
return err
}
}
// Accumulate any errors
errs := make([]error, 0)
// Unused keys are errors
if len(md.Unused) > 0 {
sort.Strings(md.Unused)
for _, unused := range md.Unused {
if unused != "type" && !strings.HasPrefix(unused, "packer_") {
errs = append(
errs, fmt.Errorf("Unknown configuration key: %s", unused))
}
}
}
errs := common.CheckUnusedConfig(md)
if p.config.ExecuteCommand == "" {
p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
@@ -118,7 +93,8 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
if p.config.Script != "" && len(p.config.Scripts) > 0 {
errs = append(errs, errors.New("Only one of script or scripts can be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Only one of script or scripts can be specified."))
}
if p.config.Script != "" {
@@ -126,14 +102,17 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
}
if len(p.config.Scripts) == 0 && p.config.Inline == nil {
errs = append(errs, errors.New("Either a script file or inline script must be specified."))
errs = packer.MultiErrorAppend(errs,
errors.New("Either a script file or inline script must be specified."))
} else if len(p.config.Scripts) > 0 && p.config.Inline != nil {
errs = append(errs, errors.New("Only a script file or an inline script can be specified, not both."))
errs = packer.MultiErrorAppend(errs,
errors.New("Only a script file or an inline script can be specified, not both."))
}
for _, path := range p.config.Scripts {
if _, err := os.Stat(path); err != nil {
errs = append(errs, fmt.Errorf("Bad script '%s': %s", path, err))
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Bad script '%s': %s", path, err))
}
}
@@ -141,14 +120,13 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
for _, kv := range p.config.Vars {
vs := strings.Split(kv, "=")
if len(vs) != 2 || vs[0] == "" {
errs = append(
errs,
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Environment variable not in format 'key=value': %s", kv))
}
}
if len(errs) > 0 {
return &packer.MultiError{errs}
if errs != nil && len(errs.Errors) > 0 {
return errs
}
return nil
@@ -78,6 +78,8 @@ Optional:
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance.
* `tags` (object of key/value strings) - Tags applied to the AMI.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC.
@@ -94,7 +96,7 @@ Here is a basic example. It is completely valid except for the access keys:
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-quick-start {{.CreateTime}}"
"ami_name": "packer-quick-start {{.CreateTime}}",
}
</pre>
@@ -105,6 +107,28 @@ the section above for more information on what environmental variables Packer
will look for.
</div>
## Tag Example
Here is an example using the optional AMI tags. This will add the tags
"OS_Version" and "Release" to the finished AMI.
<pre class="prettyprint">
{
"type": "amazon-ebs",
"access_key": "YOUR KEY HERE",
"secret_key": "YOUR SECRET KEY HERE",
"region": "us-east-1",
"source_ami": "ami-de0d9eb7",
"instance_type": "t1.micro",
"ssh_username": "ubuntu",
"ami_name": "packer-quick-start {{.CreateTime}}",
"tags": {
"OS_Version": "Ubuntu",
"Release": "Latest"
}
}
</pre>
## AMI Name Variables
The AMI name specified by the `ami_name` configuration variable is actually
@@ -109,6 +109,8 @@ Optional:
* `subnet_id` (string) - If using VPC, the ID of the subnet, such as
"subnet-12345def", where Packer will launch the EC2 instance.
* `tags` (object of key/value strings) - Tags applied to the AMI.
* `vpc_id` (string) - If launching into a VPC subnet, Packer needs the
VPC ID in order to create a temporary security group within the VPC.
@@ -50,3 +50,22 @@ environmental variable was not setup properly. Please go back and ensure
that your PATH variable contains the directory which has Packer installed.
Otherwise, Packer is installed and you're ready to go!
## Alternative Installation Methods
Installation from binary packages is currently the only officially supported
installation method. The binary packages are guaranteed to be the latest
available version and match the proper checksums. However, in addition to
the official binaries, there are other unofficial 3rd party methods of
installation managed by the Packer community:
### Homebrew
If you're using OS X and [Homebrew](http://brew.sh), you can install Packer by
adding the `binary` tap. Remember that this is updated by a 3rd party, so
it may not be the latest available version.
```
$ brew tap homebrew/binary
$ brew install packer
```
@@ -78,11 +78,13 @@ and has the password "packer" for sudo usage, then you'll likely want to
change `execute_command` to be:
```
"echo 'packer' | sudo -S sh '{{ .Path }}'"
"echo 'packer' | {{ .Vars }} sudo -E -S sh '{{ .Path }}'"
```
The `-S` flag tells `sudo` to read the password from stdin, which in this
case is being piped in with the value of "packer".
case is being piped in with the value of "packer". The `-E` flag tells `sudo`
to preserve the environment, allowing our environmental variables to work
within the script.
By setting the `execute_command` to this, your script(s) can run with
root privileges without worrying about password prompts.
@@ -53,3 +53,18 @@ environment variable was not setup properly. Please go back and ensure
that your PATH variable contains the directory which has Packer installed.
Otherwise, Packer is installed and you're ready to go!
## Alternative Installation Methods
While the binary packages is the only official method of installation, there
are alternatives available.
### Homebrew
If you're using OS X and [Homebrew](http://brew.sh), you can install Packer by
adding the `binary` tap:
```
$ brew tap homebrew/binary
$ brew install packer
```