builder/digitalOcean: use names/slugs as well as IDs for image/region/size

This commit is contained in:
Ross Smith II 2014-04-29 20:33:31 -07:00
parent 0e4fa69662
commit 2bcd9a304e
8 changed files with 229 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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