From 6c6f3c24a561dd64734562e5ae794bad9b1788f6 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Thu, 4 Sep 2014 01:37:33 +0400 Subject: [PATCH] add v2 api support Signed-off-by: Vasiliy Tolstov --- builder/digitalocean/api.go | 436 ++----------------- builder/digitalocean/api_v1.go | 382 +++++++++++++++++ builder/digitalocean/api_v2.go | 448 ++++++++++++++++++++ builder/digitalocean/artifact.go | 2 +- builder/digitalocean/builder.go | 34 +- builder/digitalocean/step_create_droplet.go | 4 +- builder/digitalocean/step_create_ssh_key.go | 7 +- builder/digitalocean/step_droplet_info.go | 3 +- builder/digitalocean/step_power_off.go | 5 +- builder/digitalocean/step_shutdown.go | 7 +- builder/digitalocean/step_snapshot.go | 5 +- builder/digitalocean/wait.go | 2 +- 12 files changed, 916 insertions(+), 419 deletions(-) create mode 100644 builder/digitalocean/api_v1.go create mode 100644 builder/digitalocean/api_v2.go diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go index 86798633f..d2b5f9f61 100644 --- a/builder/digitalocean/api.go +++ b/builder/digitalocean/api.go @@ -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) } diff --git a/builder/digitalocean/api_v1.go b/builder/digitalocean/api_v1.go new file mode 100644 index 000000000..23746d11f --- /dev/null +++ b/builder/digitalocean/api_v1.go @@ -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 +} diff --git a/builder/digitalocean/api_v2.go b/builder/digitalocean/api_v2.go new file mode 100644 index 000000000..3edf56f3e --- /dev/null +++ b/builder/digitalocean/api_v2.go @@ -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 +} diff --git a/builder/digitalocean/artifact.go b/builder/digitalocean/artifact.go index ebabdd41c..527713e94 100644 --- a/builder/digitalocean/artifact.go +++ b/builder/digitalocean/artifact.go @@ -16,7 +16,7 @@ type Artifact struct { regionName string // The client for making API calls - client *DigitalOceanClient + client DigitalOceanClient } func (*Artifact) BuilderId() string { diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 73a72252d..c894aa63d 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -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) diff --git a/builder/digitalocean/step_create_droplet.go b/builder/digitalocean/step_create_droplet.go index a00cecac9..85164abaf 100644 --- a/builder/digitalocean/step_create_droplet.go +++ b/builder/digitalocean/step_create_droplet.go @@ -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) diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go index cecb4f8d5..78bb474c1 100644 --- a/builder/digitalocean/step_create_ssh_key.go +++ b/builder/digitalocean/step_create_ssh_key.go @@ -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()) diff --git a/builder/digitalocean/step_droplet_info.go b/builder/digitalocean/step_droplet_info.go index b9350c531..ea08599ce 100644 --- a/builder/digitalocean/step_droplet_info.go +++ b/builder/digitalocean/step_droplet_info.go @@ -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) diff --git a/builder/digitalocean/step_power_off.go b/builder/digitalocean/step_power_off.go index 9aa5e30b8..efead2d8a 100644 --- a/builder/digitalocean/step_power_off.go +++ b/builder/digitalocean/step_power_off.go @@ -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) diff --git a/builder/digitalocean/step_shutdown.go b/builder/digitalocean/step_shutdown.go index fc36dbad4..06a2ae9f5 100644 --- a/builder/digitalocean/step_shutdown.go +++ b/builder/digitalocean/step_shutdown.go @@ -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) diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index 1b29dc1ef..4fdf20bc1 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -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) diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index a0104c60d..e5b1dee90 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -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)