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

builder/digitalocean: use names/slugs as well as IDs for image/region/si...
This commit is contained in:
Mitchell Hashimoto 2014-04-30 15:55:09 -07:00
commit 6f6d656267
8 changed files with 229 additions and 53 deletions

View File

@ -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"
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 {
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 {
} }
// 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
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) {
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
} }
} }
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 "", err 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 size '%v'", slug_or_name_or_id))
return Size{}, err
} }

View File

@ -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
} }

View File

@ -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 {

View File

@ -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 {
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) {
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
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
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,
} }

View File

@ -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) {
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) {
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) {
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) {
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) {
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) {
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)
} }
} }

View File

@ -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)

View File

@ -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
} }

View File

@ -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_id` (int) - The ID of the droplet size to use. This defaults to "66", * `size` (string) - The name (or slug) of the droplet size to use.
which is the 512MB droplet. 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 * `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.