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
|
||||
|
||||
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 {
|
||||
Id uint
|
||||
Name string
|
||||
Slug string
|
||||
Id uint `json:"id,omitempty"` //only in v1 api
|
||||
Slug string `json:"slug"` //presen in both api
|
||||
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 {
|
||||
|
@ -41,378 +18,51 @@ type RegionsResp struct {
|
|||
}
|
||||
|
||||
type Size struct {
|
||||
Id uint
|
||||
Name string
|
||||
Slug string
|
||||
Id uint `json:"id,omitempty"` //only in v1 api
|
||||
Name string `json:"name,omitempty"` //only in v1 api
|
||||
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 {
|
||||
Sizes []Size
|
||||
}
|
||||
|
||||
type DigitalOceanClient struct {
|
||||
// The http client for communicating
|
||||
client *http.Client
|
||||
|
||||
// Credentials
|
||||
ClientID string
|
||||
APIKey string
|
||||
|
||||
// The base URL of the API
|
||||
APIURL string
|
||||
type Image struct {
|
||||
Id uint `json:"id"` //presen in both api
|
||||
Name string `json:"name"` //presen in both api
|
||||
Slug string `json:"slug"` //presen in both api
|
||||
Distribution string `json:"distribution"` //presen in both api
|
||||
Public bool `json:"public,omitempty"` //only in v2 api
|
||||
Regions []string `json:"regions,omitempty"` //only in v2 api
|
||||
ActionIds []string `json:"action_ids,omitempty"` //only in v2 api
|
||||
CreatedAt string `json:"created_at,omitempty"` //only in v2 api
|
||||
}
|
||||
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string, url string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
},
|
||||
APIURL: url,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
}
|
||||
return c
|
||||
type ImagesResp struct {
|
||||
Images []Image
|
||||
}
|
||||
|
||||
// Creates an SSH Key and returns it's id
|
||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
params.Set("ssh_pub_key", pub)
|
||||
|
||||
body, err := NewRequest(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 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
|
||||
type DigitalOceanClient interface {
|
||||
CreateKey(string, string) (uint, error)
|
||||
DestroyKey(uint) error
|
||||
CreateDroplet(string, string, string, string, uint, bool) (uint, error)
|
||||
DestroyDroplet(uint) error
|
||||
PowerOffDroplet(uint) error
|
||||
ShutdownDroplet(uint) error
|
||||
CreateSnapshot(uint, string) error
|
||||
Images() ([]Image, error)
|
||||
DestroyImage(uint) error
|
||||
DropletStatus(uint) (string, string, error)
|
||||
Image(string) (Image, error)
|
||||
Regions() ([]Region, error)
|
||||
Region(string) (Region, error)
|
||||
Sizes() ([]Size, error)
|
||||
Size(string) (Size, error)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
// The client for making API calls
|
||||
client *DigitalOceanClient
|
||||
client DigitalOceanClient
|
||||
}
|
||||
|
||||
func (*Artifact) BuilderId() string {
|
||||
|
|
|
@ -40,6 +40,7 @@ type config struct {
|
|||
ClientID string `mapstructure:"client_id"`
|
||||
APIKey string `mapstructure:"api_key"`
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
APIToken string `mapstructure:"api_token"`
|
||||
RegionID uint `mapstructure:"region_id"`
|
||||
SizeID uint `mapstructure:"size_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")
|
||||
}
|
||||
|
||||
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.RegionID != 0 {
|
||||
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,
|
||||
"api_key": &b.config.APIKey,
|
||||
"api_url": &b.config.APIURL,
|
||||
"api_token": &b.config.APIToken,
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"droplet_name": &b.config.DropletName,
|
||||
"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.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("a client_id must be specified"))
|
||||
if b.config.APIToken == "" {
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
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 == "" {
|
||||
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)
|
||||
if err != nil {
|
||||
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) {
|
||||
var client DigitalOceanClient
|
||||
// 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
|
||||
state := new(multistep.BasicStateBag)
|
||||
|
|
|
@ -12,7 +12,7 @@ type stepCreateDroplet struct {
|
|||
}
|
||||
|
||||
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)
|
||||
c := state.Get("config").(config)
|
||||
sshKeyId := state.Get("ssh_key_id").(uint)
|
||||
|
@ -44,7 +44,7 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ type stepCreateSSHKey struct {
|
|||
}
|
||||
|
||||
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.Say("Creating temporary ssh key for droplet...")
|
||||
|
@ -71,15 +71,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(config)
|
||||
|
||||
ui.Say("Deleting temporary ssh key...")
|
||||
err := client.DestroyKey(s.keyId)
|
||||
|
||||
curlstr := fmt.Sprintf("curl '%v/ssh_keys/%v/destroy?client_id=%v&api_key=%v'",
|
||||
c.APIURL, s.keyId, c.ClientID, c.APIKey)
|
||||
curlstr := fmt.Sprintf("curl -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error cleaning up ssh key: %v", err.Error())
|
||||
|
|
|
@ -2,6 +2,7 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
@ -9,7 +10,7 @@ import (
|
|||
type stepDropletInfo struct{}
|
||||
|
||||
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)
|
||||
c := state.Get("config").(config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -2,15 +2,16 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepPowerOff struct{}
|
||||
|
||||
func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*DigitalOceanClient)
|
||||
client := state.Get("client").(DigitalOceanClient)
|
||||
c := state.Get("config").(config)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -2,16 +2,17 @@ package digitalocean
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type stepShutdown struct{}
|
||||
|
||||
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)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
||||
|
|
|
@ -3,15 +3,16 @@ package digitalocean
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
type stepSnapshot struct{}
|
||||
|
||||
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)
|
||||
c := state.Get("config").(config)
|
||||
dropletId := state.Get("droplet_id").(uint)
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
// waitForState simply blocks until the droplet is in
|
||||
// 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{})
|
||||
defer close(done)
|
||||
|
||||
|
|
Loading…
Reference in New Issue