add v2 api support
Signed-off-by: Vasiliy Tolstov <v.tolstov@selfip.ru>
This commit is contained in:
parent
31876427e3
commit
6c6f3c24a5
|
@ -4,36 +4,13 @@
|
||||||
|
|
||||||
package digitalocean
|
package digitalocean
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
Id uint
|
|
||||||
Name string
|
|
||||||
Slug string
|
|
||||||
Distribution string
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImagesResp struct {
|
|
||||||
Images []Image
|
|
||||||
}
|
|
||||||
|
|
||||||
type Region struct {
|
type Region struct {
|
||||||
Id uint
|
Id uint `json:"id,omitempty"` //only in v1 api
|
||||||
Name string
|
Slug string `json:"slug"` //presen in both api
|
||||||
Slug string
|
Name string `json:"name"` //presen in both api
|
||||||
|
Sizes []string `json:"sizes,omitempty"` //only in v2 api
|
||||||
|
Available bool `json:"available,omitempty"` //only in v2 api
|
||||||
|
Features []string `json:"features,omitempty"` //only in v2 api
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegionsResp struct {
|
type RegionsResp struct {
|
||||||
|
@ -41,378 +18,51 @@ type RegionsResp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Size struct {
|
type Size struct {
|
||||||
Id uint
|
Id uint `json:"id,omitempty"` //only in v1 api
|
||||||
Name string
|
Name string `json:"name,omitempty"` //only in v1 api
|
||||||
Slug string
|
Slug string `json:"slug"` //presen in both api
|
||||||
|
Memory uint `json:"memory,omitempty"` //only in v2 api
|
||||||
|
VCPUS uint `json:"vcpus,omitempty"` //only in v2 api
|
||||||
|
Disk uint `json:"disk,omitempty"` //only in v2 api
|
||||||
|
Transfer uint `json:"transfer,omitempty"` //only in v2 api
|
||||||
|
PriceMonthly float64 `json:"price_monthly,omitempty"` //only in v2 api
|
||||||
|
PriceHourly float64 `json:"price_hourly,omitempty"` //only in v2 api
|
||||||
|
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||||
}
|
}
|
||||||
|
|
||||||
type SizesResp struct {
|
type SizesResp struct {
|
||||||
Sizes []Size
|
Sizes []Size
|
||||||
}
|
}
|
||||||
|
|
||||||
type DigitalOceanClient struct {
|
type Image struct {
|
||||||
// The http client for communicating
|
Id uint `json:"id"` //presen in both api
|
||||||
client *http.Client
|
Name string `json:"name"` //presen in both api
|
||||||
|
Slug string `json:"slug"` //presen in both api
|
||||||
// Credentials
|
Distribution string `json:"distribution"` //presen in both api
|
||||||
ClientID string
|
Public bool `json:"public,omitempty"` //only in v2 api
|
||||||
APIKey string
|
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||||
|
ActionIds []string `json:"action_ids,omitempty"` //only in v2 api
|
||||||
// The base URL of the API
|
CreatedAt string `json:"created_at,omitempty"` //only in v2 api
|
||||||
APIURL string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new client for communicating with DO
|
type ImagesResp struct {
|
||||||
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
|
Images []Image
|
||||||
c := &DigitalOceanClient{
|
|
||||||
client: &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
APIURL: url,
|
|
||||||
ClientID: client,
|
|
||||||
APIKey: key,
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an SSH Key and returns it's id
|
type DigitalOceanClient interface {
|
||||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
CreateKey(string, string) (uint, error)
|
||||||
params := url.Values{}
|
DestroyKey(uint) error
|
||||||
params.Set("name", name)
|
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
|
||||||
params.Set("ssh_pub_key", pub)
|
DestroyDroplet(uint) error
|
||||||
|
PowerOffDroplet(uint) error
|
||||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
ShutdownDroplet(uint) error
|
||||||
if err != nil {
|
CreateSnapshot(uint, string) error
|
||||||
return 0, err
|
Images() ([]Image, error)
|
||||||
}
|
DestroyImage(uint) error
|
||||||
|
DropletStatus(uint) (string, string, error)
|
||||||
// Read the SSH key's ID we just created
|
Image(string) (Image, error)
|
||||||
key := body["ssh_key"].(map[string]interface{})
|
Regions() ([]Region, error)
|
||||||
keyId := key["id"].(float64)
|
Region(string) (Region, error)
|
||||||
return uint(keyId), nil
|
Sizes() ([]Size, error)
|
||||||
}
|
Size(string) (Size, error)
|
||||||
|
|
||||||
// Destroys an SSH key
|
|
||||||
func (d DigitalOceanClient) DestroyKey(id uint) error {
|
|
||||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
|
||||||
_, err := NewRequest(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a droplet and returns it's id
|
|
||||||
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)
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
body, err := NewRequest(d, "droplets/new", params)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the Droplets ID
|
|
||||||
droplet := body["droplet"].(map[string]interface{})
|
|
||||||
dropletId := droplet["id"].(float64)
|
|
||||||
return uint(dropletId), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys a droplet
|
|
||||||
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
|
||||||
_, err := NewRequest(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Powers off a droplet
|
|
||||||
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
|
||||||
|
|
||||||
_, err := NewRequest(d, path, url.Values{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutsdown a droplet. This is a "soft" shutdown.
|
|
||||||
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/shutdown", id)
|
|
||||||
|
|
||||||
_, err := NewRequest(d, path, url.Values{})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creates a snaphot of a droplet by it's ID
|
|
||||||
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
|
||||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
|
||||||
|
|
||||||
params := url.Values{}
|
|
||||||
params.Set("name", name)
|
|
||||||
|
|
||||||
_, err := NewRequest(d, path, params)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns all available images.
|
|
||||||
func (d DigitalOceanClient) Images() ([]Image, error) {
|
|
||||||
resp, err := NewRequest(d, "images", url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result ImagesResp
|
|
||||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Images, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroys an image by its ID.
|
|
||||||
func (d DigitalOceanClient) DestroyImage(id uint) error {
|
|
||||||
path := fmt.Sprintf("images/%d/destroy", id)
|
|
||||||
_, err := NewRequest(d, path, url.Values{})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns DO's string representation of status "off" "new" "active" etc.
|
|
||||||
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
|
||||||
path := fmt.Sprintf("droplets/%v", id)
|
|
||||||
|
|
||||||
body, err := NewRequest(d, path, url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ip string
|
|
||||||
|
|
||||||
// Read the droplet's "status"
|
|
||||||
droplet := body["droplet"].(map[string]interface{})
|
|
||||||
status := droplet["status"].(string)
|
|
||||||
|
|
||||||
if droplet["ip_address"] != nil {
|
|
||||||
ip = droplet["ip_address"].(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ip, status, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sends an api request and returns a generic map[string]interface of
|
|
||||||
// the response.
|
|
||||||
func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
|
|
||||||
client := d.client
|
|
||||||
|
|
||||||
// Add the authentication parameters
|
|
||||||
params.Set("client_id", d.ClientID)
|
|
||||||
params.Set("api_key", d.APIKey)
|
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
|
|
||||||
|
|
||||||
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
|
||||||
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
|
||||||
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
|
||||||
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for attempts := 1; attempts < 10; attempts++ {
|
|
||||||
resp, err := client.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("response from digitalocean: %s", body)
|
|
||||||
|
|
||||||
var decodedResponse map[string]interface{}
|
|
||||||
err = json.Unmarshal(body, &decodedResponse)
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
|
||||||
resp.StatusCode, body))
|
|
||||||
return decodedResponse, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for errors sent by digitalocean
|
|
||||||
status := decodedResponse["status"].(string)
|
|
||||||
if status == "OK" {
|
|
||||||
return decodedResponse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if status == "ERROR" {
|
|
||||||
statusRaw, ok := decodedResponse["error_message"]
|
|
||||||
if ok {
|
|
||||||
status = statusRaw.(string)
|
|
||||||
} else {
|
|
||||||
status = fmt.Sprintf(
|
|
||||||
"Unknown error. Full response body: %s", body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
|
||||||
resp.StatusCode, status))
|
|
||||||
log.Println(lastErr)
|
|
||||||
if strings.Contains(status, "a pending event") {
|
|
||||||
// Retry, DigitalOcean sends these dumb "pending event"
|
|
||||||
// errors all the time.
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some other kind of error. Just return.
|
|
||||||
return decodedResponse, 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.
|
|
||||||
func (d DigitalOceanClient) Regions() ([]Region, error) {
|
|
||||||
resp, err := NewRequest(d, "regions", url.Values{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var result RegionsResp
|
|
||||||
if err := mapstructure.Decode(resp, &result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.Regions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d DigitalOceanClient) Region(slug_or_name_or_id string) (Region, error) {
|
|
||||||
regions, err := d.Regions()
|
|
||||||
if err != nil {
|
|
||||||
return Region{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, region := range regions {
|
|
||||||
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
|
||||||
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 size '%v'", slug_or_name_or_id))
|
|
||||||
|
|
||||||
return Size{}, err
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,382 @@
|
||||||
|
// All of the methods used to communicate with the digital_ocean API
|
||||||
|
// are here. Their API is on a path to V2, so just plain JSON is used
|
||||||
|
// in place of a proper client library for now.
|
||||||
|
|
||||||
|
package digitalocean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DigitalOceanClientV1 struct {
|
||||||
|
// The http client for communicating
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
// Credentials
|
||||||
|
ClientID string
|
||||||
|
APIKey string
|
||||||
|
// The base URL of the API
|
||||||
|
APIURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new client for communicating with DO
|
||||||
|
func DigitalOceanClientNewV1(client string, key string, url string) *DigitalOceanClientV1 {
|
||||||
|
c := &DigitalOceanClientV1{
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
APIURL: url,
|
||||||
|
ClientID: client,
|
||||||
|
APIKey: key,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an SSH Key and returns it's id
|
||||||
|
func (d DigitalOceanClientV1) CreateKey(name string, pub string) (uint, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("name", name)
|
||||||
|
params.Set("ssh_pub_key", pub)
|
||||||
|
|
||||||
|
body, err := NewRequestV1(d, "ssh_keys/new", params)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the SSH key's ID we just created
|
||||||
|
key := body["ssh_key"].(map[string]interface{})
|
||||||
|
keyId := key["id"].(float64)
|
||||||
|
return uint(keyId), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys an SSH key
|
||||||
|
func (d DigitalOceanClientV1) DestroyKey(id uint) error {
|
||||||
|
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
||||||
|
_, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a droplet and returns it's id
|
||||||
|
func (d DigitalOceanClientV1) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("name", name)
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
body, err := NewRequestV1(d, "droplets/new", params)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the Droplets ID
|
||||||
|
droplet := body["droplet"].(map[string]interface{})
|
||||||
|
dropletId := droplet["id"].(float64)
|
||||||
|
return uint(dropletId), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys a droplet
|
||||||
|
func (d DigitalOceanClientV1) DestroyDroplet(id uint) error {
|
||||||
|
path := fmt.Sprintf("droplets/%v/destroy", id)
|
||||||
|
_, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Powers off a droplet
|
||||||
|
func (d DigitalOceanClientV1) PowerOffDroplet(id uint) error {
|
||||||
|
path := fmt.Sprintf("droplets/%v/power_off", id)
|
||||||
|
_, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutsdown a droplet. This is a "soft" shutdown.
|
||||||
|
func (d DigitalOceanClientV1) ShutdownDroplet(id uint) error {
|
||||||
|
path := fmt.Sprintf("droplets/%v/shutdown", id)
|
||||||
|
_, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a snaphot of a droplet by it's ID
|
||||||
|
func (d DigitalOceanClientV1) CreateSnapshot(id uint, name string) error {
|
||||||
|
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("name", name)
|
||||||
|
|
||||||
|
_, err := NewRequestV1(d, path, params)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all available images.
|
||||||
|
func (d DigitalOceanClientV1) Images() ([]Image, error) {
|
||||||
|
resp, err := NewRequestV1(d, "images", url.Values{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ImagesResp
|
||||||
|
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys an image by its ID.
|
||||||
|
func (d DigitalOceanClientV1) DestroyImage(id uint) error {
|
||||||
|
path := fmt.Sprintf("images/%d/destroy", id)
|
||||||
|
_, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns DO's string representation of status "off" "new" "active" etc.
|
||||||
|
func (d DigitalOceanClientV1) DropletStatus(id uint) (string, string, error) {
|
||||||
|
path := fmt.Sprintf("droplets/%v", id)
|
||||||
|
|
||||||
|
body, err := NewRequestV1(d, path, url.Values{})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ip string
|
||||||
|
|
||||||
|
// Read the droplet's "status"
|
||||||
|
droplet := body["droplet"].(map[string]interface{})
|
||||||
|
status := droplet["status"].(string)
|
||||||
|
|
||||||
|
if droplet["ip_address"] != nil {
|
||||||
|
ip = droplet["ip_address"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an api request and returns a generic map[string]interface of
|
||||||
|
// the response.
|
||||||
|
func NewRequestV1(d DigitalOceanClientV1, path string, params url.Values) (map[string]interface{}, error) {
|
||||||
|
client := d.client
|
||||||
|
|
||||||
|
// Add the authentication parameters
|
||||||
|
params.Set("client_id", d.ClientID)
|
||||||
|
params.Set("api_key", d.APIKey)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/%s?%s", d.APIURL, path, params.Encode())
|
||||||
|
|
||||||
|
// Do some basic scrubbing so sensitive information doesn't appear in logs
|
||||||
|
scrubbedUrl := strings.Replace(url, d.ClientID, "CLIENT_ID", -1)
|
||||||
|
scrubbedUrl = strings.Replace(scrubbedUrl, d.APIKey, "API_KEY", -1)
|
||||||
|
log.Printf("sending new request to digitalocean: %s", scrubbedUrl)
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
for attempts := 1; attempts < 10; attempts++ {
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("response from digitalocean: %s", body)
|
||||||
|
|
||||||
|
var decodedResponse map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &decodedResponse)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New(fmt.Sprintf("Failed to decode JSON response (HTTP %v) from DigitalOcean: %s",
|
||||||
|
resp.StatusCode, body))
|
||||||
|
return decodedResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for errors sent by digitalocean
|
||||||
|
status := decodedResponse["status"].(string)
|
||||||
|
if status == "OK" {
|
||||||
|
return decodedResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == "ERROR" {
|
||||||
|
statusRaw, ok := decodedResponse["error_message"]
|
||||||
|
if ok {
|
||||||
|
status = statusRaw.(string)
|
||||||
|
} else {
|
||||||
|
status = fmt.Sprintf(
|
||||||
|
"Unknown error. Full response body: %s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = errors.New(fmt.Sprintf("Received error from DigitalOcean (%d): %s",
|
||||||
|
resp.StatusCode, status))
|
||||||
|
log.Println(lastErr)
|
||||||
|
if strings.Contains(status, "a pending event") {
|
||||||
|
// Retry, DigitalOcean sends these dumb "pending event"
|
||||||
|
// errors all the time.
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some other kind of error. Just return.
|
||||||
|
return decodedResponse, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DigitalOceanClientV1) 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 DigitalOceanClientV1) Regions() ([]Region, error) {
|
||||||
|
resp, err := NewRequestV1(d, "regions", url.Values{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result RegionsResp
|
||||||
|
if err := mapstructure.Decode(resp, &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Regions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DigitalOceanClientV1) Region(slug_or_name_or_id string) (Region, error) {
|
||||||
|
regions, err := d.Regions()
|
||||||
|
if err != nil {
|
||||||
|
return Region{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, region := range regions {
|
||||||
|
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
||||||
|
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 DigitalOceanClientV1) Sizes() ([]Size, error) {
|
||||||
|
resp, err := NewRequestV1(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 DigitalOceanClientV1) 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
|
||||||
|
}
|
|
@ -0,0 +1,448 @@
|
||||||
|
// are here. Their API is on a path to V2, so just plain JSON is used
|
||||||
|
// in place of a proper client library for now.
|
||||||
|
|
||||||
|
package digitalocean
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DigitalOceanClientV2 struct {
|
||||||
|
// The http client for communicating
|
||||||
|
client *http.Client
|
||||||
|
|
||||||
|
// Credentials
|
||||||
|
APIToken string
|
||||||
|
|
||||||
|
// The base URL of the API
|
||||||
|
APIURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new client for communicating with DO
|
||||||
|
func DigitalOceanClientNewV2(token string, url string) *DigitalOceanClientV2 {
|
||||||
|
c := &DigitalOceanClientV2{
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
APIURL: url,
|
||||||
|
APIToken: token,
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates an SSH Key and returns it's id
|
||||||
|
func (d DigitalOceanClientV2) CreateKey(name string, pub string) (uint, error) {
|
||||||
|
type KeyReq struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
}
|
||||||
|
type KeyRes struct {
|
||||||
|
SSHKey struct {
|
||||||
|
Id uint
|
||||||
|
Name string
|
||||||
|
Fingerprint string
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
} `json:"ssh_key"`
|
||||||
|
}
|
||||||
|
req := &KeyReq{Name: name, PublicKey: pub}
|
||||||
|
res := KeyRes{}
|
||||||
|
err := NewRequestV2(d, "v2/account/keys", "POST", req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.SSHKey.Id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys an SSH key
|
||||||
|
func (d DigitalOceanClientV2) DestroyKey(id uint) error {
|
||||||
|
path := fmt.Sprintf("v2/account/keys/%v", id)
|
||||||
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a droplet and returns it's id
|
||||||
|
func (d DigitalOceanClientV2) CreateDroplet(name string, size string, image string, region string, keyId uint, privateNetworking bool) (uint, error) {
|
||||||
|
type DropletReq struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
SSHKeys []string `json:"ssh_keys,omitempty"`
|
||||||
|
Backups bool `json:"backups,omitempty"`
|
||||||
|
IPv6 bool `json:"ipv6,omitempty"`
|
||||||
|
PrivateNetworking bool `json:"private_networking,omitempty"`
|
||||||
|
}
|
||||||
|
type DropletRes struct {
|
||||||
|
Droplet struct {
|
||||||
|
Id uint
|
||||||
|
Name string
|
||||||
|
Memory uint
|
||||||
|
VCPUS uint `json:"vcpus"`
|
||||||
|
Disk uint
|
||||||
|
Region Region
|
||||||
|
Image Image
|
||||||
|
Size Size
|
||||||
|
Locked bool
|
||||||
|
CreateAt string `json:"created_at"`
|
||||||
|
Status string
|
||||||
|
Networks struct {
|
||||||
|
V4 []struct {
|
||||||
|
IPAddr string `json:"ip_address"`
|
||||||
|
Netmask string
|
||||||
|
Gateway string
|
||||||
|
Type string
|
||||||
|
} `json:"v4,omitempty"`
|
||||||
|
V6 []struct {
|
||||||
|
IPAddr string `json:"ip_address"`
|
||||||
|
CIDR uint `json:"cidr"`
|
||||||
|
Gateway string
|
||||||
|
Type string
|
||||||
|
} `json:"v6,omitempty"`
|
||||||
|
}
|
||||||
|
Kernel struct {
|
||||||
|
Id uint
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
BackupIds []uint
|
||||||
|
SnapshotIds []uint
|
||||||
|
ActionIds []uint
|
||||||
|
Features []string `json:"features,omitempty"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req := &DropletReq{Name: name}
|
||||||
|
res := DropletRes{}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Size = found_size.Slug
|
||||||
|
req.Image = found_image.Slug
|
||||||
|
req.Region = found_region.Slug
|
||||||
|
req.SSHKeys = []string{fmt.Sprintf("%v", keyId)}
|
||||||
|
req.PrivateNetworking = privateNetworking
|
||||||
|
|
||||||
|
err = NewRequestV2(d, "v2/droplets", "POST", req, &res)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Droplet.Id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys a droplet
|
||||||
|
func (d DigitalOceanClientV2) DestroyDroplet(id uint) error {
|
||||||
|
path := fmt.Sprintf("v2/droplets/%v", id)
|
||||||
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Powers off a droplet
|
||||||
|
func (d DigitalOceanClientV2) PowerOffDroplet(id uint) error {
|
||||||
|
type ActionReq struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
type ActionRes struct {
|
||||||
|
}
|
||||||
|
req := &ActionReq{Type: "power_off"}
|
||||||
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
||||||
|
return NewRequestV2(d, path, "POST", req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutsdown a droplet. This is a "soft" shutdown.
|
||||||
|
func (d DigitalOceanClientV2) ShutdownDroplet(id uint) error {
|
||||||
|
type ActionReq struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
type ActionRes struct {
|
||||||
|
}
|
||||||
|
req := &ActionReq{Type: "shutdown"}
|
||||||
|
|
||||||
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
||||||
|
return NewRequestV2(d, path, "POST", req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a snaphot of a droplet by it's ID
|
||||||
|
func (d DigitalOceanClientV2) CreateSnapshot(id uint, name string) error {
|
||||||
|
type ActionReq struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
type ActionRes struct {
|
||||||
|
}
|
||||||
|
req := &ActionReq{Type: "snapshot", Name: name}
|
||||||
|
path := fmt.Sprintf("v2/droplets/%v/actions", id)
|
||||||
|
return NewRequestV2(d, path, "POST", req, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns all available images.
|
||||||
|
func (d DigitalOceanClientV2) Images() ([]Image, error) {
|
||||||
|
res := ImagesResp{}
|
||||||
|
|
||||||
|
err := NewRequestV2(d, "v2/images", "GET", nil, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroys an image by its ID.
|
||||||
|
func (d DigitalOceanClientV2) DestroyImage(id uint) error {
|
||||||
|
path := fmt.Sprintf("v2/images/%d", id)
|
||||||
|
return NewRequestV2(d, path, "DELETE", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns DO's string representation of status "off" "new" "active" etc.
|
||||||
|
func (d DigitalOceanClientV2) DropletStatus(id uint) (string, string, error) {
|
||||||
|
path := fmt.Sprintf("v2/droplets/%v", id)
|
||||||
|
type DropletRes struct {
|
||||||
|
Droplet struct {
|
||||||
|
Id uint
|
||||||
|
Name string
|
||||||
|
Memory uint
|
||||||
|
VCPUS uint `json:"vcpus"`
|
||||||
|
Disk uint
|
||||||
|
Region Region
|
||||||
|
Image Image
|
||||||
|
Size Size
|
||||||
|
Locked bool
|
||||||
|
CreateAt string `json:"created_at"`
|
||||||
|
Status string
|
||||||
|
Networks struct {
|
||||||
|
V4 []struct {
|
||||||
|
IPAddr string `json:"ip_address"`
|
||||||
|
Netmask string
|
||||||
|
Gateway string
|
||||||
|
Type string
|
||||||
|
} `json:"v4,omitempty"`
|
||||||
|
V6 []struct {
|
||||||
|
IPAddr string `json:"ip_address"`
|
||||||
|
CIDR uint `json:"cidr"`
|
||||||
|
Gateway string
|
||||||
|
Type string
|
||||||
|
} `json:"v6,omitempty"`
|
||||||
|
}
|
||||||
|
Kernel struct {
|
||||||
|
Id uint
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
BackupIds []uint
|
||||||
|
SnapshotIds []uint
|
||||||
|
ActionIds []uint
|
||||||
|
Features []string `json:"features,omitempty"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := DropletRes{}
|
||||||
|
err := NewRequestV2(d, path, "GET", nil, &res)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
var ip string
|
||||||
|
|
||||||
|
if len(res.Droplet.Networks.V4) > 0 {
|
||||||
|
ip = res.Droplet.Networks.V4[0].IPAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, res.Droplet.Status, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an api request and returns a generic map[string]interface of
|
||||||
|
// the response.
|
||||||
|
func NewRequestV2(d DigitalOceanClientV2, path string, method string, req interface{}, res interface{}) error {
|
||||||
|
var err error
|
||||||
|
var request *http.Request
|
||||||
|
|
||||||
|
client := d.client
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
// Add the authentication parameters
|
||||||
|
url := fmt.Sprintf("%s/%s", d.APIURL, path)
|
||||||
|
if req != nil {
|
||||||
|
enc := json.NewEncoder(buf)
|
||||||
|
enc.Encode(req)
|
||||||
|
defer buf.Reset()
|
||||||
|
request, err = http.NewRequest(method, url, buf)
|
||||||
|
} else {
|
||||||
|
request, err = http.NewRequest(method, url, nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Add the authentication parameters
|
||||||
|
request.Header.Add("Authorization", "Bearer "+d.APIToken)
|
||||||
|
|
||||||
|
log.Printf("sending new request to digitalocean: %s", url)
|
||||||
|
log.Printf("DDDD %+v\n", request)
|
||||||
|
resp, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if method == "DELETE" && resp.StatusCode == 204 {
|
||||||
|
if resp.Body != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Body == nil {
|
||||||
|
return errors.New("Request returned empty body")
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("response from digitalocean: %s", body)
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(fmt.Sprintf("Failed to decode JSON response %s (HTTP %v) from DigitalOcean: %s", err.Error(),
|
||||||
|
resp.StatusCode, body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DigitalOceanClientV2) 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 DigitalOceanClientV2) Regions() ([]Region, error) {
|
||||||
|
res := RegionsResp{}
|
||||||
|
err := NewRequestV2(d, "v2/regions", "GET", nil, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Regions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DigitalOceanClientV2) Region(slug_or_name_or_id string) (Region, error) {
|
||||||
|
regions, err := d.Regions()
|
||||||
|
if err != nil {
|
||||||
|
return Region{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, region := range regions {
|
||||||
|
if strings.EqualFold(region.Slug, slug_or_name_or_id) {
|
||||||
|
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 DigitalOceanClientV2) Sizes() ([]Size, error) {
|
||||||
|
res := SizesResp{}
|
||||||
|
err := NewRequestV2(d, "v2/sizes", "GET", nil, &res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Sizes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DigitalOceanClientV2) 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
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ type Artifact struct {
|
||||||
regionName string
|
regionName string
|
||||||
|
|
||||||
// The client for making API calls
|
// The client for making API calls
|
||||||
client *DigitalOceanClient
|
client DigitalOceanClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Artifact) BuilderId() string {
|
func (*Artifact) BuilderId() string {
|
||||||
|
|
|
@ -40,6 +40,7 @@ type config struct {
|
||||||
ClientID string `mapstructure:"client_id"`
|
ClientID string `mapstructure:"client_id"`
|
||||||
APIKey string `mapstructure:"api_key"`
|
APIKey string `mapstructure:"api_key"`
|
||||||
APIURL string `mapstructure:"api_url"`
|
APIURL string `mapstructure:"api_url"`
|
||||||
|
APIToken string `mapstructure:"api_token"`
|
||||||
RegionID uint `mapstructure:"region_id"`
|
RegionID uint `mapstructure:"region_id"`
|
||||||
SizeID uint `mapstructure:"size_id"`
|
SizeID uint `mapstructure:"size_id"`
|
||||||
ImageID uint `mapstructure:"image_id"`
|
ImageID uint `mapstructure:"image_id"`
|
||||||
|
@ -101,6 +102,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.config.APIToken == "" {
|
||||||
|
// Default to environment variable for api_token, if it exists
|
||||||
|
b.config.APIURL = os.Getenv("DIGITALOCEAN_API_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
if b.config.Region == "" {
|
if b.config.Region == "" {
|
||||||
if b.config.RegionID != 0 {
|
if b.config.RegionID != 0 {
|
||||||
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
b.config.Region = fmt.Sprintf("%v", b.config.RegionID)
|
||||||
|
@ -164,6 +170,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
"client_id": &b.config.ClientID,
|
"client_id": &b.config.ClientID,
|
||||||
"api_key": &b.config.APIKey,
|
"api_key": &b.config.APIKey,
|
||||||
"api_url": &b.config.APIURL,
|
"api_url": &b.config.APIURL,
|
||||||
|
"api_token": &b.config.APIToken,
|
||||||
"snapshot_name": &b.config.SnapshotName,
|
"snapshot_name": &b.config.SnapshotName,
|
||||||
"droplet_name": &b.config.DropletName,
|
"droplet_name": &b.config.DropletName,
|
||||||
"ssh_username": &b.config.SSHUsername,
|
"ssh_username": &b.config.SSHUsername,
|
||||||
|
@ -180,21 +187,23 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required configurations that will display errors if not set
|
if b.config.APIToken == "" {
|
||||||
if b.config.ClientID == "" {
|
// Required configurations that will display errors if not set
|
||||||
errs = packer.MultiErrorAppend(
|
if b.config.ClientID == "" {
|
||||||
errs, errors.New("a client_id must be specified"))
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("a client_id for v1 auth or api_token for v2 auth must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.APIKey == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("an api_key for v1 auth or api_token for v2 auth must be specified"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.APIURL == "" {
|
if b.config.APIURL == "" {
|
||||||
b.config.APIURL = "https://api.digitalocean.com"
|
b.config.APIURL = "https://api.digitalocean.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.config.APIKey == "" {
|
|
||||||
errs = packer.MultiErrorAppend(
|
|
||||||
errs, errors.New("an api_key must be specified"))
|
|
||||||
}
|
|
||||||
|
|
||||||
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = packer.MultiErrorAppend(
|
errs = packer.MultiErrorAppend(
|
||||||
|
@ -218,8 +227,13 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||||
|
var client DigitalOceanClient
|
||||||
// Initialize the DO API client
|
// Initialize the DO API client
|
||||||
client := DigitalOceanClient{}.New(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
if b.config.APIToken == "" {
|
||||||
|
client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL)
|
||||||
|
} else {
|
||||||
|
client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL)
|
||||||
|
}
|
||||||
|
|
||||||
// Set up the state
|
// Set up the state
|
||||||
state := new(multistep.BasicStateBag)
|
state := new(multistep.BasicStateBag)
|
||||||
|
|
|
@ -12,7 +12,7 @@ type stepCreateDroplet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
sshKeyId := state.Get("ssh_key_id").(uint)
|
sshKeyId := state.Get("ssh_key_id").(uint)
|
||||||
|
@ -44,7 +44,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ type stepCreateSSHKey struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
ui.Say("Creating temporary ssh key for droplet...")
|
ui.Say("Creating temporary ssh key for droplet...")
|
||||||
|
@ -71,15 +71,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
|
|
||||||
ui.Say("Deleting temporary ssh key...")
|
ui.Say("Deleting temporary ssh key...")
|
||||||
err := client.DestroyKey(s.keyId)
|
err := client.DestroyKey(s.keyId)
|
||||||
|
|
||||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
|
||||||
c.APIURL, s.keyId, c.ClientID, c.APIKey)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||||
|
|
|
@ -2,6 +2,7 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
type stepDropletInfo struct{}
|
type stepDropletInfo struct{}
|
||||||
|
|
||||||
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -2,15 +2,16 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepPowerOff struct{}
|
type stepPowerOff struct{}
|
||||||
|
|
||||||
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -2,16 +2,17 @@ package digitalocean
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mitchellh/multistep"
|
|
||||||
"github.com/mitchellh/packer/packer"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepShutdown struct{}
|
type stepShutdown struct{}
|
||||||
|
|
||||||
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,16 @@ package digitalocean
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/mitchellh/multistep"
|
"github.com/mitchellh/multistep"
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepSnapshot struct{}
|
type stepSnapshot struct{}
|
||||||
|
|
||||||
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
client := state.Get("client").(*DigitalOceanClient)
|
client := state.Get("client").(DigitalOceanClient)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
c := state.Get("config").(config)
|
c := state.Get("config").(config)
|
||||||
dropletId := state.Get("droplet_id").(uint)
|
dropletId := state.Get("droplet_id").(uint)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// waitForState simply blocks until the droplet is in
|
// waitForState simply blocks until the droplet is in
|
||||||
// a state we expect, while eventually timing out.
|
// a state we expect, while eventually timing out.
|
||||||
func waitForDropletState(desiredState string, dropletId uint, client *DigitalOceanClient, timeout time.Duration) error {
|
func waitForDropletState(desiredState string, dropletId uint, client DigitalOceanClient, timeout time.Duration) error {
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
defer close(done)
|
defer close(done)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue