From f4c9f9608548b370791f62cea5b930f686052d01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 26 Jun 2013 18:55:11 -0700 Subject: [PATCH] post-processor/vagrant: Can make AWS boxes! --- post-processor/vagrant/artifact.go | 40 +++++++++++++ post-processor/vagrant/artifact_test.go | 14 +++++ post-processor/vagrant/aws.go | 76 +++++++++++++++++++++++- post-processor/vagrant/aws_test.go | 1 - post-processor/vagrant/post-processor.go | 2 +- post-processor/vagrant/util.go | 75 +++++++++++++++++++++++ 6 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 post-processor/vagrant/artifact.go create mode 100644 post-processor/vagrant/artifact_test.go create mode 100644 post-processor/vagrant/util.go diff --git a/post-processor/vagrant/artifact.go b/post-processor/vagrant/artifact.go new file mode 100644 index 000000000..ecd36e740 --- /dev/null +++ b/post-processor/vagrant/artifact.go @@ -0,0 +1,40 @@ +package vagrant + +import ( + "fmt" + "os" +) + +const BuilderId = "mitchellh.post-processor.vagrant" + +type Artifact struct { + Path string + Provider string +} + +func NewArtifact(provider, path string) *Artifact { + return &Artifact{ + Path: path, + Provider: provider, + } +} + +func (*Artifact) BuilderId() string { + return BuilderId +} + +func (a *Artifact) Files() []string { + return []string{a.Path} +} + +func (a *Artifact) Id() string { + return "" +} + +func (a *Artifact) String() string { + return fmt.Sprintf("'%s' provider box: %s", a.Provider, a.Path) +} + +func (a *Artifact) Destroy() error { + return os.Remove(a.Path) +} diff --git a/post-processor/vagrant/artifact_test.go b/post-processor/vagrant/artifact_test.go new file mode 100644 index 000000000..5c711dad2 --- /dev/null +++ b/post-processor/vagrant/artifact_test.go @@ -0,0 +1,14 @@ +package vagrant + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestArtifact_ImplementsArtifact(t *testing.T) { + var raw interface{} + raw = &Artifact{} + if _, ok := raw.(packer.Artifact); !ok { + t.Fatalf("Artifact should be a Artifact") + } +} diff --git a/post-processor/vagrant/aws.go b/post-processor/vagrant/aws.go index 02de6b53b..1c01d475e 100644 --- a/post-processor/vagrant/aws.go +++ b/post-processor/vagrant/aws.go @@ -1,16 +1,90 @@ package vagrant import ( + "fmt" + "github.com/mitchellh/mapstructure" "github.com/mitchellh/packer/packer" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/template" ) +type AWSBoxConfig struct { + OutputPath string `mapstructure:"output"` +} + +type AWSVagrantfileTemplate struct { + Images map[string]string +} + type AWSBoxPostProcessor struct { + config AWSBoxConfig } func (p *AWSBoxPostProcessor) Configure(raw interface{}) error { + err := mapstructure.Decode(raw, &p.config) + if err != nil { + return err + } + return nil } func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, error) { - return nil, nil + // Determine the regions... + tplData := &AWSVagrantfileTemplate{ + Images: make(map[string]string), + } + + for _, regions := range strings.Split(artifact.Id(), ",") { + parts := strings.Split(regions, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id()) + } + + tplData.Images[parts[0]] = parts[1] + } + + // Create a temporary directory for us to build the contents of the box in + dir, err := ioutil.TempDir("", "packer") + if err != nil { + return nil, err + } + defer os.RemoveAll(dir) + + // Create the Vagrantfile from the template + vf, err := os.Create(filepath.Join(dir, "Vagrantfile")) + if err != nil { + return nil, err + } + defer vf.Close() + + t := template.Must(template.New("vagrantfile").Parse(defaultVagrantfile)) + t.Execute(vf, tplData) + vf.Close() + + // Create the metadata + metadata := map[string]string{"provider": "aws"} + if err := WriteMetadata(dir, metadata); err != nil { + return nil, err + } + + // Compress the directory to the given output path + if err := DirToBox(p.config.OutputPath, dir); err != nil { + return nil, err + } + + return NewArtifact("aws", p.config.OutputPath), nil } + +var defaultVagrantfile = ` +Vagrant.configure("2") do |config| + config.vm.provider "aws" do |aws| + {{ range $region, $ami := .Images }} + aws.region_config "{{ $region }}", ami: "{{ $ami }}" + {{ end }} + end +end +` diff --git a/post-processor/vagrant/aws_test.go b/post-processor/vagrant/aws_test.go index 318a48213..d7d5ce696 100644 --- a/post-processor/vagrant/aws_test.go +++ b/post-processor/vagrant/aws_test.go @@ -12,4 +12,3 @@ func TestPostProcessor_ImplementsPostProcessor(t *testing.T) { t.Fatalf("AWS PostProcessor should be a PostProcessor") } } - diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index df78753c0..2bcddb8fd 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -11,7 +11,7 @@ var builtins = map[string]string{ "mitchellh.amazonebs": "aws", } -type Config struct {} +type Config struct{} type PostProcessor struct { config Config diff --git a/post-processor/vagrant/util.go b/post-processor/vagrant/util.go new file mode 100644 index 000000000..8f18a6221 --- /dev/null +++ b/post-processor/vagrant/util.go @@ -0,0 +1,75 @@ +package vagrant + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "io" + "os" + "path/filepath" +) + +// DirToBox takes the directory and compresses it into a Vagrant-compatible +// box. This function does not perform checks to verify that dir is +// actually a proper box. This is an expected precondition. +func DirToBox(dst, dir string) error { + dstF, err := os.Create(dst) + if err != nil { + return err + } + defer dstF.Close() + + gzipWriter := gzip.NewWriter(dstF) + defer gzipWriter.Close() + + tarWriter := tar.NewWriter(gzipWriter) + defer tarWriter.Close() + + // This is the walk func that tars each of the files in the dir + tarWalk := func(path string, info os.FileInfo, prevErr error) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + header, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + // We have to set the Name explicitly because it is supposed to + // be a relative path to the root. Otherwise, the tar ends up + // being a bunch of files in the root, even if they're actually + // nested in a dir in the original "dir" param. + header.Name, err = filepath.Rel(dir, path) + if err != nil { + return err + } + + if err := tarWriter.WriteHeader(header); err != nil { + return err + } + + if _, err := io.Copy(tarWriter, f); err != nil { + return err + } + + return nil + } + + // Tar.gz everything up + return filepath.Walk(dir, tarWalk) +} + +// WriteMetadata writes the "metadata.json" file for a Vagrant box. +func WriteMetadata(dir string, contents interface{}) error { + f, err := os.Create(filepath.Join(dir, "metadata.json")) + if err != nil { + return err + } + defer f.Close() + + enc := json.NewEncoder(f) + return enc.Encode(contents) +}