Commit 6f6d6562 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

Merge pull request #1089 from rasa/use-slugs-for-digitalocean

builder/digitalocean: use names/slugs as well as IDs for image/region/si...
parents 492a6d5a 2bcd9a30
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
) )
...@@ -22,6 +23,7 @@ const DIGITALOCEAN_API_URL = "https://api.digitalocean.com" ...@@ -22,6 +23,7 @@ const DIGITALOCEAN_API_URL = "https://api.digitalocean.com"
type Image struct { type Image struct {
Id uint Id uint
Name string Name string
Slug string
Distribution string Distribution string
} }
...@@ -32,12 +34,23 @@ type ImagesResp struct { ...@@ -32,12 +34,23 @@ type ImagesResp struct {
type Region struct { type Region struct {
Id uint Id uint
Name string Name string
Slug string
} }
type RegionsResp struct { type RegionsResp struct {
Regions []Region Regions []Region
} }
type Size struct {
Id uint
Name string
Slug string
}
type SizesResp struct {
Sizes []Size
}
type DigitalOceanClient struct { type DigitalOceanClient struct {
// The http client for communicating // The http client for communicating
client *http.Client client *http.Client
...@@ -90,12 +103,28 @@ func (d DigitalOceanClient) DestroyKey(id uint) error { ...@@ -90,12 +103,28 @@ func (d DigitalOceanClient) DestroyKey(id uint) error {
} }
// Creates a droplet and returns it's id // 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 := url.Values{}
params.Set("name", name) params.Set("name", name)
params.Set("size_id", fmt.Sprintf("%v", size))
params.Set("image_id", fmt.Sprintf("%v", image)) found_size, err := d.Size(size)
params.Set("region_id", fmt.Sprintf("%v", region)) 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("ssh_key_ids", fmt.Sprintf("%v", keyId))
params.Set("private_networking", fmt.Sprintf("%v", privateNetworking)) params.Set("private_networking", fmt.Sprintf("%v", privateNetworking))
...@@ -263,6 +292,38 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin ...@@ -263,6 +292,38 @@ func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[strin
return nil, lastErr 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. // Returns all available regions.
func (d DigitalOceanClient) Regions() ([]Region, error) { func (d DigitalOceanClient) Regions() ([]Region, error) {
resp, err := NewRequest(d, "regions", url.Values{}) resp, err := NewRequest(d, "regions", url.Values{})
...@@ -278,19 +339,81 @@ func (d DigitalOceanClient) Regions() ([]Region, error) { ...@@ -278,19 +339,81 @@ func (d DigitalOceanClient) Regions() ([]Region, error) {
return result.Regions, nil 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() regions, err := d.Regions()
if err != nil { if err != nil {
return "", err return Region{}, err
} }
for _, region := range regions { for _, region := range regions {
if region.Id == region_id { if strings.EqualFold(region.Slug, slug_or_name_or_id) {
return region.Name, nil return region, nil
}
}
for _, region := range regions {
if strings.EqualFold(region.Name, slug_or_name_or_id) {
return region, nil
}
}
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 region id %v", region_id)) err = errors.New(fmt.Sprintf("Unknown size '%v'", slug_or_name_or_id))
return "", err return Size{}, err
} }
...@@ -15,9 +15,6 @@ type Artifact struct { ...@@ -15,9 +15,6 @@ type Artifact struct {
// The name of the region // The name of the region
regionName string regionName string
// The ID of the region
regionId uint
// The client for making API calls // The client for making API calls
client *DigitalOceanClient client *DigitalOceanClient
} }
......
...@@ -14,7 +14,7 @@ func TestArtifact_Impl(t *testing.T) { ...@@ -14,7 +14,7 @@ func TestArtifact_Impl(t *testing.T) {
} }
func TestArtifactString(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'" expected := "A snapshot was created: 'packer-foobar' in region 'San Francisco'"
if a.String() != expected { if a.String() != expected {
......
...@@ -15,6 +15,18 @@ import ( ...@@ -15,6 +15,18 @@ import (
"time" "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 // The unique id for the builder
const BuilderId = "pearkes.digitalocean" const BuilderId = "pearkes.digitalocean"
...@@ -30,6 +42,10 @@ type config struct { ...@@ -30,6 +42,10 @@ type config struct {
SizeID uint `mapstructure:"size_id"` SizeID uint `mapstructure:"size_id"`
ImageID uint `mapstructure:"image_id"` ImageID uint `mapstructure:"image_id"`
Region string `mapstructure:"region"`
Size string `mapstructure:"size"`
Image string `mapstructure:"image"`
PrivateNetworking bool `mapstructure:"private_networking"` PrivateNetworking bool `mapstructure:"private_networking"`
SnapshotName string `mapstructure:"snapshot_name"` SnapshotName string `mapstructure:"snapshot_name"`
DropletName string `mapstructure:"droplet_name"` DropletName string `mapstructure:"droplet_name"`
...@@ -78,19 +94,28 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { ...@@ -78,19 +94,28 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID") b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID")
} }
if b.config.RegionID == 0 { if b.config.Region == "" {
// Default to Region "New York" if b.config.RegionID != 0 {
b.config.RegionID = 1 b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
} else {
b.config.Region = DefaultRegion
}
} }
if b.config.SizeID == 0 { if b.config.Size == "" {
// Default to 512mb, the smallest droplet size if b.config.SizeID != 0 {
b.config.SizeID = 66 b.config.Size = fmt.Sprintf("%v", b.config.SizeID)
} else {
b.config.Size = DefaultSize
}
} }
if b.config.ImageID == 0 { if b.config.Image == "" {
// Default to base image "Ubuntu 12.04.4 x64 (id: 3101045)" if b.config.ImageID != 0 {
b.config.ImageID = 3101045 b.config.Image = fmt.Sprintf("%v", b.config.ImageID)
} else {
b.config.Image = DefaultImage
}
} }
if b.config.SnapshotName == "" { if b.config.SnapshotName == "" {
...@@ -226,9 +251,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -226,9 +251,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, nil 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 { if err != nil {
return nil, err return nil, err
} }
...@@ -236,8 +270,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -236,8 +270,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
artifact := &Artifact{ artifact := &Artifact{
snapshotName: state.Get("snapshot_name").(string), snapshotName: state.Get("snapshot_name").(string),
snapshotId: state.Get("snapshot_image_id").(uint), snapshotId: state.Get("snapshot_image_id").(uint),
regionId: region_id, regionName: found_region.Name,
regionName: regionName,
client: client, client: client,
} }
......
...@@ -142,7 +142,7 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) { ...@@ -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 var b Builder
config := testConfig() config := testConfig()
...@@ -155,12 +155,15 @@ func TestBuilderPrepare_RegionID(t *testing.T) { ...@@ -155,12 +155,15 @@ func TestBuilderPrepare_RegionID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.RegionID != 1 { if b.config.Region != DefaultRegion {
t.Errorf("invalid: %d", b.config.RegionID) t.Errorf("found %s, expected %s", b.config.Region, DefaultRegion)
} }
expected := "sfo1"
// Test set // Test set
config["region_id"] = 2 config["region_id"] = 0
config["region"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
...@@ -170,12 +173,12 @@ func TestBuilderPrepare_RegionID(t *testing.T) { ...@@ -170,12 +173,12 @@ func TestBuilderPrepare_RegionID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.RegionID != 2 { if b.config.Region != expected {
t.Errorf("invalid: %d", b.config.RegionID) 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 var b Builder
config := testConfig() config := testConfig()
...@@ -188,12 +191,15 @@ func TestBuilderPrepare_SizeID(t *testing.T) { ...@@ -188,12 +191,15 @@ func TestBuilderPrepare_SizeID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SizeID != 66 { if b.config.Size != DefaultSize {
t.Errorf("invalid: %d", b.config.SizeID) t.Errorf("found %s, expected %s", b.config.Size, DefaultSize)
} }
expected := "1024mb"
// Test set // Test set
config["size_id"] = 67 config["size_id"] = 0
config["size"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
...@@ -203,12 +209,12 @@ func TestBuilderPrepare_SizeID(t *testing.T) { ...@@ -203,12 +209,12 @@ func TestBuilderPrepare_SizeID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SizeID != 67 { if b.config.Size != expected {
t.Errorf("invalid: %d", b.config.SizeID) 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 var b Builder
config := testConfig() config := testConfig()
...@@ -221,12 +227,15 @@ func TestBuilderPrepare_ImageID(t *testing.T) { ...@@ -221,12 +227,15 @@ func TestBuilderPrepare_ImageID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SizeID != 66 { if b.config.Image != DefaultImage {
t.Errorf("invalid: %d", b.config.SizeID) t.Errorf("found %s, expected %s", b.config.Image, DefaultImage)
} }
expected := "ubuntu-14-04-x64"
// Test set // Test set
config["size_id"] = 2 config["image_id"] = 0
config["image"] = expected
b = Builder{} b = Builder{}
warnings, err = b.Prepare(config) warnings, err = b.Prepare(config)
if len(warnings) > 0 { if len(warnings) > 0 {
...@@ -236,8 +245,8 @@ func TestBuilderPrepare_ImageID(t *testing.T) { ...@@ -236,8 +245,8 @@ func TestBuilderPrepare_ImageID(t *testing.T) {
t.Fatalf("should not have error: %s", err) t.Fatalf("should not have error: %s", err)
} }
if b.config.SizeID != 2 { if b.config.Image != expected {
t.Errorf("invalid: %d", b.config.SizeID) t.Errorf("found %s, expected %s", b.config.Image, expected)
} }
} }
......
...@@ -19,7 +19,7 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { ...@@ -19,7 +19,7 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
ui.Say("Creating droplet...") ui.Say("Creating droplet...")
// Create the droplet based on configuration // 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 { if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err) err := fmt.Errorf("Error creating droplet: %s", err)
......
...@@ -62,7 +62,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { ...@@ -62,7 +62,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
state.Put("snapshot_image_id", imageId) state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName) state.Put("snapshot_name", c.SnapshotName)
state.Put("region_id", c.RegionID) state.Put("region", c.Region)
return multistep.ActionContinue return multistep.ActionContinue
} }
......
...@@ -35,16 +35,30 @@ Required: ...@@ -35,16 +35,30 @@ Required:
Optional: 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 * `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", will be used to launch a new droplet and provision it.
which happens to be "Ubuntu 12.04.4 x64". 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, * `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 this is the region where the snapshot will be available.
"1", which is "New York 1". This setting is deprecated. Use `region` instead.
* `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 defaults to "66", * `size_id` (int) - The ID of the droplet size to use.
which is the 512MB droplet. This setting is deprecated. Use `size` instead.
* `private_networking` (bool) - Set to `true` to enable private networking * `private_networking` (bool) - Set to `true` to enable private networking
for the droplet being created. This defaults to `false`, or not enabled. for the droplet being created. This defaults to `false`, or not enabled.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment