builder/digitalOcean: use names/slugs as well as IDs for image/region/size
This commit is contained in:
parent
0e4fa69662
commit
2bcd9a304e
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue