From 2cad46aa1fb16ffe1d7bd176050c578004bd10fb Mon Sep 17 00:00:00 2001 From: Ross Smith II Date: Sat, 5 Oct 2013 19:29:02 -0700 Subject: [PATCH] post-processor/vagrant: Adds vagrant support for digitalocean Conflicts: post-processor/vagrant/post-processor.go --- builder/digitalocean/api.go | 43 +++++- builder/digitalocean/artifact.go | 13 +- builder/digitalocean/builder.go | 9 ++ builder/digitalocean/step_snapshot.go | 1 + builder/digitalocean/wait.go | 2 +- post-processor/vagrant/digitalocean.go | 151 ++++++++++++++++++++ post-processor/vagrant/digitalocean_test.go | 14 ++ post-processor/vagrant/post-processor.go | 3 + 8 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 post-processor/vagrant/digitalocean.go create mode 100644 post-processor/vagrant/digitalocean_test.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index b22c1bb4d..769caf926 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -29,6 +29,15 @@ type ImagesResp struct { Images []Image } +type Region struct { + Id uint + Name string +} + +type RegionsResp struct { + Regions []Region +} + type DigitalOceanClient struct { // The http client for communicating client *http.Client @@ -227,7 +236,7 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin } if status == "ERROR" { - statusRaw, ok := decodedResponse["message"] + statusRaw, ok := decodedResponse["error_message"] if ok { status = statusRaw.(string) } else { @@ -252,3 +261,35 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin return nil, lastErr } + +// Returns all available regions. +func (d DigitalOceanClient) Regions() ([]Region, error) { + resp, err := NewRequest(d, "regions", url.Values{}) + if err != nil { + return nil, err + } + + var result RegionsResp + if err := mapstructure.Decode(resp, &result); err != nil { + return nil, err + } + + return result.Regions, nil +} + +func (d DigitalOceanClient) RegionName(region_id uint) (string, error) { + regions, err := d.Regions() + if err != nil { + return "", err + } + + for _, region := range regions { + if region.Id == region_id { + return region.Name, nil + } + } + + err = errors.New(fmt.Sprintf("Unknown region id %v", region_id)) + + return "", err +} diff --git a/builder/digitalocean/artifact.go b/builder/digitalocean/artifact.go index 292068c8f..06f09fe40 100644 --- a/builder/digitalocean/artifact.go +++ b/builder/digitalocean/artifact.go @@ -12,6 +12,12 @@ type Artifact struct { // The ID of the image snapshotId uint + // The name of the region + regionName string + + // The ID of the region + regionId uint + // The client for making API calls client *DigitalOceanClient } @@ -26,14 +32,15 @@ func (*Artifact) Files() []string { } func (a *Artifact) Id() string { - return a.snapshotName + // mimicing the aws builder + return fmt.Sprintf("%s:%s", a.regionName, a.snapshotName) } func (a *Artifact) String() string { - return fmt.Sprintf("A snapshot was created: %v", a.snapshotName) + return fmt.Sprintf("A snapshot was created: '%v' in region '%v'", a.snapshotName, a.regionName) } func (a *Artifact) Destroy() error { - log.Printf("Destroying image: %d", a.snapshotId) + log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) return a.client.DestroyImage(a.snapshotId) } diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 83e5e79d5..7e97630c6 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -225,9 +225,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, nil } + region_id := state.Get("region_id").(uint) + + regionName, err := client.RegionName(region_id) + if err != nil { + return nil, err + } + artifact := &Artifact{ snapshotName: state.Get("snapshot_name").(string), snapshotId: state.Get("snapshot_image_id").(uint), + regionId: region_id, + regionName: regionName, client: client, } diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index b5a531df3..b8ae66062 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -62,6 +62,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { state.Put("snapshot_image_id", imageId) state.Put("snapshot_name", c.SnapshotName) + state.Put("region_id", c.RegionID) return multistep.ActionContinue } diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index ba2b03cb0..a0104c60d 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -44,7 +44,7 @@ func waitForDropletState(desiredState string, dropletId uint, client *DigitalOce } }() - log.Printf("Waiting for up to %d seconds for droplet to become %s", timeout, desiredState) + log.Printf("Waiting for up to %d seconds for droplet to become %s", timeout/time.Second, desiredState) select { case err := <-result: return err diff --git a/post-processor/vagrant/digitalocean.go b/post-processor/vagrant/digitalocean.go new file mode 100644 index 000000000..5c04b1210 --- /dev/null +++ b/post-processor/vagrant/digitalocean.go @@ -0,0 +1,151 @@ +package vagrant + +import ( + "fmt" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/packer" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +type DigitalOceanBoxConfig struct { + common.PackerConfig `mapstructure:",squash"` + + OutputPath string `mapstructure:"output"` + VagrantfileTemplate string `mapstructure:"vagrantfile_template"` + + tpl *packer.ConfigTemplate +} + +type DigitalOceanVagrantfileTemplate struct { + Image string "" + Region string "" +} + +type DigitalOceanBoxPostProcessor struct { + config DigitalOceanBoxConfig +} + +func (p *DigitalOceanBoxPostProcessor) Configure(rDigitalOcean ...interface{}) error { + md, err := common.DecodeConfig(&p.config, rDigitalOcean...) + if err != nil { + return err + } + + p.config.tpl, err = packer.NewConfigTemplate() + if err != nil { + return err + } + p.config.tpl.UserVars = p.config.PackerUserVars + + // Accumulate any errors + errs := common.CheckUnusedConfig(md) + + validates := map[string]*string{ + "output": &p.config.OutputPath, + "vagrantfile_template": &p.config.VagrantfileTemplate, + } + + for n, ptr := range validates { + if err := p.config.tpl.Validate(*ptr); err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Error parsing %s: %s", n, err)) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + + return nil +} + +func (p *DigitalOceanBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { + // Determine the image and region... + tplData := &DigitalOceanVagrantfileTemplate{} + + parts := strings.Split(artifact.Id(), ":") + if len(parts) != 2 { + return nil, false, fmt.Errorf("Poorly formatted artifact ID: %s", artifact.Id()) + } + + tplData.Region = parts[0] + tplData.Image = parts[1] + + // Compile the output path + outputPath, err := p.config.tpl.Process(p.config.OutputPath, &OutputPathTemplate{ + ArtifactId: artifact.Id(), + BuildName: p.config.PackerBuildName, + Provider: "digitalocean", + }) + if err != nil { + return nil, false, err + } + + // Create a temporary directory for us to build the contents of the box in + dir, err := ioutil.TempDir("", "packer") + if err != nil { + return nil, false, err + } + defer os.RemoveAll(dir) + + // Create the Vagrantfile from the template + vf, err := os.Create(filepath.Join(dir, "Vagrantfile")) + if err != nil { + return nil, false, err + } + defer vf.Close() + + vagrantfileContents := defaultDigitalOceanVagrantfile + if p.config.VagrantfileTemplate != "" { + log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate) + f, err := os.Open(p.config.VagrantfileTemplate) + if err != nil { + err = fmt.Errorf("error opening vagrantfile template: %s", err) + return nil, false, err + } + defer f.Close() + + contents, err := ioutil.ReadAll(f) + if err != nil { + err = fmt.Errorf("error reading vagrantfile template: %s", err) + return nil, false, err + } + + vagrantfileContents = string(contents) + } + + vagrantfileContents, err = p.config.tpl.Process(vagrantfileContents, tplData) + if err != nil { + return nil, false, fmt.Errorf("Error writing Vagrantfile: %s", err) + } + vf.Write([]byte(vagrantfileContents)) + vf.Close() + + // Create the metadata + metadata := map[string]string{"provider": "digital_ocean"} + if err := WriteMetadata(dir, metadata); err != nil { + return nil, false, err + } + + // Compress the directory to the given output path + if err := DirToBox(outputPath, dir, ui); err != nil { + err = fmt.Errorf("error creating box: %s", err) + return nil, false, err + } + + return NewArtifact("DigitalOcean", outputPath), true, nil +} + +var defaultDigitalOceanVagrantfile = ` +Vagrant.configure("2") do |config| + config.vm.provider :digital_ocean do |digital_ocean| + digital_ocean.image = "{{ .Image }}" + digital_ocean.region = "{{ .Region }}" + end +end + +` diff --git a/post-processor/vagrant/digitalocean_test.go b/post-processor/vagrant/digitalocean_test.go new file mode 100644 index 000000000..44451c6a4 --- /dev/null +++ b/post-processor/vagrant/digitalocean_test.go @@ -0,0 +1,14 @@ +package vagrant + +import ( + "github.com/mitchellh/packer/packer" + "testing" +) + +func TestDigitalOceanBoxPostProcessor_ImplementsPostProcessor(t *testing.T) { + var raw interface{} + raw = &DigitalOceanBoxPostProcessor{} + if _, ok := raw.(packer.PostProcessor); !ok { + t.Fatalf("Digitalocean PostProcessor should be a PostProcessor") + } +} diff --git a/post-processor/vagrant/post-processor.go b/post-processor/vagrant/post-processor.go index 8aa44d9fb..75d99215a 100644 --- a/post-processor/vagrant/post-processor.go +++ b/post-processor/vagrant/post-processor.go @@ -16,6 +16,7 @@ var builtins = map[string]string{ "mitchellh.amazon.instance": "aws", "mitchellh.virtualbox": "virtualbox", "mitchellh.vmware": "vmware", + "pearkes.digitalocean": "digitalocean", } type Config struct { @@ -141,6 +142,8 @@ func keyToPostProcessor(key string) packer.PostProcessor { switch key { case "aws": return new(AWSBoxPostProcessor) + case "digitalocean": + return new(DigitalOceanBoxPostProcessor) case "virtualbox": return new(VBoxBoxPostProcessor) case "vmware":