From 2bcd9a304e1ad53808cd2cbd422ea8605143e001 Mon Sep 17 00:00:00 2001 From: Ross Smith II Date: Tue, 29 Apr 2014 20:33:31 -0700 Subject: [PATCH] builder/digitalOcean: use names/slugs as well as IDs for image/region/size --- builder/digitalocean/api.go | 143 ++++++++++++++++-- builder/digitalocean/artifact.go | 3 - builder/digitalocean/artifact_test.go | 2 +- builder/digitalocean/builder.go | 59 ++++++-- builder/digitalocean/builder_test.go | 45 +++--- builder/digitalocean/step_create_droplet.go | 2 +- builder/digitalocean/step_snapshot.go | 2 +- .../docs/builders/digitalocean.html.markdown | 26 +++- 8 files changed, 229 insertions(+), 53 deletions(-) diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index 7aca1c14f..e9525c0bf 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -13,6 +13,7 @@ import ( "log" "net/http" "net/url" + "strconv" "strings" "time" ) @@ -22,6 +23,7 @@ const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" type Image struct { Id uint Name string + Slug string Distribution string } @@ -32,12 +34,23 @@ type ImagesResp struct { type Region struct { Id uint Name string + Slug string } type RegionsResp struct { Regions []Region } +type Size struct { + Id uint + Name string + Slug string +} + +type SizesResp struct { + Sizes []Size +} + type DigitalOceanClient struct { // The http client for communicating client *http.Client @@ -90,12 +103,28 @@ func (d DigitalOceanClient) DestroyKey(id uint) error { } // Creates a droplet and returns it's id -func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint, privateNetworking bool) (uint, error) { +func (d DigitalOceanClient) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) { params := url.Values{} params.Set("name", name) - params.Set("size_id", fmt.Sprintf("%v", size)) - params.Set("image_id", fmt.Sprintf("%v", image)) - params.Set("region_id", fmt.Sprintf("%v", region)) + + found_size, err := d.Size(size) + if err != nil { + return 0, fmt.Errorf("Invalid size or lookup failure: '%s': %s", size, err) + } + + found_image, err := d.Image(image) + if err != nil { + return 0, fmt.Errorf("Invalid image or lookup failure: '%s': %s", image, err) + } + + found_region, err := d.Region(region) + if err != nil { + return 0, fmt.Errorf("Invalid region or lookup failure: '%s': %s", region, err) + } + + params.Set("size_slug", found_size.Slug) + params.Set("image_slug", found_image.Slug) + params.Set("region_slug", found_region.Slug) params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId)) params.Set("private_networking", fmt.Sprintf("%v", privateNetworking)) @@ -263,6 +292,38 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin return nil, lastErr } +func (d DigitalOceanClient) Image(slug_or_name_or_id string) (Image, error) { + images, err := d.Images() + if err != nil { + return Image{}, err + } + + for _, image := range images { + if strings.EqualFold(image.Slug, slug_or_name_or_id) { + return image, nil + } + } + + for _, image := range images { + if strings.EqualFold(image.Name, slug_or_name_or_id) { + return image, nil + } + } + + for _, image := range images { + id, err := strconv.Atoi(slug_or_name_or_id) + if err == nil { + if image.Id == uint(id) { + return image, nil + } + } + } + + err = errors.New(fmt.Sprintf("Unknown image '%v'", slug_or_name_or_id)) + + return Image{}, err +} + // Returns all available regions. func (d DigitalOceanClient) Regions() ([]Region, error) { resp, err := NewRequest(d, "regions", url.Values{}) @@ -278,19 +339,81 @@ func (d DigitalOceanClient) Regions() ([]Region, error) { return result.Regions, nil } -func (d DigitalOceanClient) RegionName(region_id uint) (string, error) { +func (d DigitalOceanClient) Region(slug_or_name_or_id string) (Region, error) { regions, err := d.Regions() if err != nil { - return "", err + return Region{}, err } for _, region := range regions { - if region.Id == region_id { - return region.Name, nil + if strings.EqualFold(region.Slug, slug_or_name_or_id) { + return region, nil } } - err = errors.New(fmt.Sprintf("Unknown region id %v", region_id)) + for _, region := range regions { + if strings.EqualFold(region.Name, slug_or_name_or_id) { + return region, nil + } + } - return "", err + for _, region := range regions { + id, err := strconv.Atoi(slug_or_name_or_id) + if err == nil { + if region.Id == uint(id) { + return region, nil + } + } + } + + err = errors.New(fmt.Sprintf("Unknown region '%v'", slug_or_name_or_id)) + + return Region{}, err +} + +// Returns all available sizes. +func (d DigitalOceanClient) Sizes() ([]Size, error) { + resp, err := NewRequest(d, "sizes", url.Values{}) + if err != nil { + return nil, err + } + + var result SizesResp + if err := mapstructure.Decode(resp, &result); err != nil { + return nil, err + } + + return result.Sizes, nil +} + +func (d DigitalOceanClient) Size(slug_or_name_or_id string) (Size, error) { + sizes, err := d.Sizes() + if err != nil { + return Size{}, err + } + + for _, size := range sizes { + if strings.EqualFold(size.Slug, slug_or_name_or_id) { + return size, nil + } + } + + for _, size := range sizes { + if strings.EqualFold(size.Name, slug_or_name_or_id) { + return size, nil + } + } + + for _, size := range sizes { + id, err := strconv.Atoi(slug_or_name_or_id) + if err == nil { + if size.Id == uint(id) { + return size, nil + } + } + } + + err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id)) + + return Size{}, err } diff --git a/builder/digitalocean/artifact.go b/builder/digitalocean/artifact.go index 06f09fe40..ebabdd41c 100644 --- a/builder/digitalocean/artifact.go +++ b/builder/digitalocean/artifact.go @@ -15,9 +15,6 @@ type Artifact struct { // The name of the region regionName string - // The ID of the region - regionId uint - // The client for making API calls client *DigitalOceanClient } diff --git a/builder/digitalocean/artifact_test.go b/builder/digitalocean/artifact_test.go index 0b4427eae..83681b3fa 100644 --- a/builder/digitalocean/artifact_test.go +++ b/builder/digitalocean/artifact_test.go @@ -14,7 +14,7 @@ func TestArtifact_Impl(t *testing.T) { } func TestArtifactString(t *testing.T) { - a := &Artifact{"packer-foobar", 42, "San Francisco", 3, nil} + a := &Artifact{"packer-foobar", 42, "San Francisco", nil} expected := "A snapshot was created: 'packer-foobar' in region 'San Francisco'" if a.String() != expected { diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index ef90aab85..d07bb0e52 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -15,6 +15,18 @@ import ( "time" ) +// see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key] +// name="Ubuntu 12.04.4 x64", id=3101045, +const DefaultImage = "ubuntu-12-04-x64" + +// see https://api.digitalocean.com/regions/?client_id=[client_id]&api_key=[api_key] +// name="New York", id=1 +const DefaultRegion = "nyc1" + +// see https://api.digitalocean.com/sizes/?client_id=[client_id]&api_key=[api_key] +// name="512MB", id=66 (the smallest droplet size) +const DefaultSize = "512mb" + // The unique id for the builder const BuilderId = "pearkes.digitalocean" @@ -30,6 +42,10 @@ type config struct { SizeID uint `mapstructure:"size_id"` ImageID uint `mapstructure:"image_id"` + Region string `mapstructure:"region"` + Size string `mapstructure:"size"` + Image string `mapstructure:"image"` + PrivateNetworking bool `mapstructure:"private_networking"` SnapshotName string `mapstructure:"snapshot_name"` DropletName string `mapstructure:"droplet_name"` @@ -78,19 +94,28 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID") } - if b.config.RegionID == 0 { - // Default to Region "New York" - b.config.RegionID = 1 + if b.config.Region == "" { + if b.config.RegionID != 0 { + b.config.Region = fmt.Sprintf("%v", b.config.RegionID) + } else { + b.config.Region = DefaultRegion + } } - if b.config.SizeID == 0 { - // Default to 512mb, the smallest droplet size - b.config.SizeID = 66 + if b.config.Size == "" { + if b.config.SizeID != 0 { + b.config.Size = fmt.Sprintf("%v", b.config.SizeID) + } else { + b.config.Size = DefaultSize + } } - if b.config.ImageID == 0 { - // Default to base image "Ubuntu 12.04.4 x64 (id: 3101045)" - b.config.ImageID = 3101045 + if b.config.Image == "" { + if b.config.ImageID != 0 { + b.config.Image = fmt.Sprintf("%v", b.config.ImageID) + } else { + b.config.Image = DefaultImage + } } if b.config.SnapshotName == "" { @@ -226,9 +251,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) + sregion := state.Get("region") + + var region string + + if sregion != nil { + region = sregion.(string) + } else { + region = fmt.Sprintf("%v", state.Get("region_id").(uint)) + } + + found_region, err := client.Region(region) - regionName, err := client.RegionName(region_id) if err != nil { return nil, err } @@ -236,8 +270,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe artifact := &Artifact{ snapshotName: state.Get("snapshot_name").(string), snapshotId: state.Get("snapshot_image_id").(uint), - regionId: region_id, - regionName: regionName, + regionName: found_region.Name, client: client, } diff --git a/builder/digitalocean/builder_test.go b/builder/digitalocean/builder_test.go index 751cd1301..d6857bbe6 100644 --- a/builder/digitalocean/builder_test.go +++ b/builder/digitalocean/builder_test.go @@ -142,7 +142,7 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) { } } -func TestBuilderPrepare_RegionID(t *testing.T) { +func TestBuilderPrepare_Region(t *testing.T) { var b Builder config := testConfig() @@ -155,12 +155,15 @@ func TestBuilderPrepare_RegionID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.RegionID != 1 { - t.Errorf("invalid: %d", b.config.RegionID) + if b.config.Region != DefaultRegion { + t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion) } + expected := "sfo1" + // Test set - config["region_id"] = 2 + config["region_id"] = 0 + config["region"] = expected b = Builder{} warnings, err = b.Prepare(config) if len(warnings) > 0 { @@ -170,12 +173,12 @@ func TestBuilderPrepare_RegionID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.RegionID != 2 { - t.Errorf("invalid: %d", b.config.RegionID) + if b.config.Region != expected { + t.Errorf("found %s, expected %s", b.config.Region, expected) } } -func TestBuilderPrepare_SizeID(t *testing.T) { +func TestBuilderPrepare_Size(t *testing.T) { var b Builder config := testConfig() @@ -188,12 +191,15 @@ func TestBuilderPrepare_SizeID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.SizeID != 66 { - t.Errorf("invalid: %d", b.config.SizeID) + if b.config.Size != DefaultSize { + t.Errorf("found %s, expected %s", b.config.Size, DefaultSize) } + expected := "1024mb" + // Test set - config["size_id"] = 67 + config["size_id"] = 0 + config["size"] = expected b = Builder{} warnings, err = b.Prepare(config) if len(warnings) > 0 { @@ -203,12 +209,12 @@ func TestBuilderPrepare_SizeID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.SizeID != 67 { - t.Errorf("invalid: %d", b.config.SizeID) + if b.config.Size != expected { + t.Errorf("found %s, expected %s", b.config.Size, expected) } } -func TestBuilderPrepare_ImageID(t *testing.T) { +func TestBuilderPrepare_Image(t *testing.T) { var b Builder config := testConfig() @@ -221,12 +227,15 @@ func TestBuilderPrepare_ImageID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.SizeID != 66 { - t.Errorf("invalid: %d", b.config.SizeID) + if b.config.Image != DefaultImage { + t.Errorf("found %s, expected %s", b.config.Image, DefaultImage) } + expected := "ubuntu-14-04-x64" + // Test set - config["size_id"] = 2 + config["image_id"] = 0 + config["image"] = expected b = Builder{} warnings, err = b.Prepare(config) if len(warnings) > 0 { @@ -236,8 +245,8 @@ func TestBuilderPrepare_ImageID(t *testing.T) { t.Fatalf("should not have error: %s", err) } - if b.config.SizeID != 2 { - t.Errorf("invalid: %d", b.config.SizeID) + if b.config.Image != expected { + t.Errorf("found %s, expected %s", b.config.Image, expected) } } diff --git a/builder/digitalocean/step_create_droplet.go b/builder/digitalocean/step_create_droplet.go index f46e7fd3c..79a235413 100644 --- a/builder/digitalocean/step_create_droplet.go +++ b/builder/digitalocean/step_create_droplet.go @@ -19,7 +19,7 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { ui.Say("Creating droplet...") // Create the droplet based on configuration - dropletId, err := client.CreateDroplet(c.DropletName, c.SizeID, c.ImageID, c.RegionID, sshKeyId, c.PrivateNetworking) + dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking) if err != nil { err := fmt.Errorf("Error creating droplet: %s", err) diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index b8ae66062..1b29dc1ef 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -62,7 +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) + state.Put("region", c.Region) return multistep.ActionContinue } diff --git a/website/source/docs/builders/digitalocean.html.markdown b/website/source/docs/builders/digitalocean.html.markdown index e5139db27..c317842a0 100644 --- a/website/source/docs/builders/digitalocean.html.markdown +++ b/website/source/docs/builders/digitalocean.html.markdown @@ -35,16 +35,30 @@ Required: Optional: +* `image` (string) - The name (or slug) of the base image to use. This is the + image that will be used to launch a new droplet and provision it. This + defaults to 'ubuntu-12-04-x64' which is the slug for "Ubuntu 12.04.4 x64". + See https://developers.digitalocean.com/images/ for the accepted image names/slugs. + * `image_id` (int) - The ID of the base image to use. This is the image that - will be used to launch a new droplet and provision it. Defaults to "3101045", - which happens to be "Ubuntu 12.04.4 x64". + will be used to launch a new droplet and provision it. + This setting is deprecated. Use `image` instead. + +* `region` (string) - The name (or slug) of the region to launch the droplet in. + Consequently, this is the region where the snapshot will be available. + This defaults to "nyc1", which the slug for "New York 1". + See https://developers.digitalocean.com/regions/ for the accepted region names/slugs. * `region_id` (int) - The ID of the region to launch the droplet in. Consequently, - this is the region where the snapshot will be available. This defaults to - "1", which is "New York 1". + this is the region where the snapshot will be available. + This setting is deprecated. Use `region` instead. -* `size_id` (int) - The ID of the droplet size to use. This defaults to "66", - which is the 512MB droplet. +* `size` (string) - The name (or slug) of the droplet size to use. + This defaults to "512mb", which is the slug for "512MB". + See https://developers.digitalocean.com/sizes/ for the accepted size names/slugs. + +* `size_id` (int) - The ID of the droplet size to use. + This setting is deprecated. Use `size` instead. * `private_networking` (bool) - Set to `true` to enable private networking for the droplet being created. This defaults to `false`, or not enabled.