diff --git a/builder/digitalocean/api.go b/builder/digitalocean/api.go deleted file mode 100644 index 87339ffc9..000000000 --- a/builder/digitalocean/api.go +++ /dev/null @@ -1,76 +0,0 @@ -// 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 - -type Region struct { - Slug string `json:"slug"` - Name string `json:"name"` - - // v1 only - Id uint `json:"id,omitempty"` - - // v2 only - Sizes []string `json:"sizes,omitempty"` - Available bool `json:"available,omitempty"` - Features []string `json:"features,omitempty"` -} - -type RegionsResp struct { - Regions []Region -} - -type Size struct { - Slug string `json:"slug"` - - // v1 only - Id uint `json:"id,omitempty"` - Name string `json:"name,omitempty"` - - // v2 only - Memory uint `json:"memory,omitempty"` - VCPUS uint `json:"vcpus,omitempty"` - Disk uint `json:"disk,omitempty"` - Transfer float64 `json:"transfer,omitempty"` - PriceMonthly float64 `json:"price_monthly,omitempty"` - PriceHourly float64 `json:"price_hourly,omitempty"` -} - -type SizesResp struct { - Sizes []Size -} - -type Image struct { - Id uint `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - Distribution string `json:"distribution"` - - // v2 only - Public bool `json:"public,omitempty"` - ActionIds []string `json:"action_ids,omitempty"` - CreatedAt string `json:"created_at,omitempty"` -} - -type ImagesResp struct { - Images []Image -} - -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 deleted file mode 100644 index 23746d11f..000000000 --- a/builder/digitalocean/api_v1.go +++ /dev/null @@ -1,382 +0,0 @@ -// 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 deleted file mode 100644 index 46454a9f8..000000000 --- a/builder/digitalocean/api_v2.go +++ /dev/null @@ -1,462 +0,0 @@ -// 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) - } - - if found_image.Slug == "" { - req.Image = strconv.Itoa(int(found_image.Id)) - } else { - req.Image = found_image.Slug - } - - req.Size = found_size.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?per_page=200", "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 - - for _, n := range res.Droplet.Networks.V4 { - if n.Type == "public" { - ip = n.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) - request.Header.Add("Content-Type", "application/json") - } else { - request, err = http.NewRequest(method, url, nil) - } - if err != nil { - return err - } - - // Add the authentication parameters - request.Header.Add("Authorization", "Bearer "+d.APIToken) - if buf != nil { - log.Printf("sending new request to digitalocean: %s buffer: %s", url, buf) - } else { - log.Printf("sending new request to digitalocean: %s", url) - } - 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)) - } - switch resp.StatusCode { - case 403, 401, 429, 422, 404, 503, 500: - return errors.New(fmt.Sprintf("digitalocean request error: %+v", res)) - } - 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?per_page=200", "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?per_page=200", "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 d1d878193..3b6a05e53 100644 --- a/builder/digitalocean/artifact.go +++ b/builder/digitalocean/artifact.go @@ -4,6 +4,8 @@ import ( "fmt" "log" "strconv" + + "github.com/digitalocean/godo" ) type Artifact struct { @@ -11,13 +13,13 @@ type Artifact struct { snapshotName string // The ID of the image - snapshotId uint + snapshotId int // The name of the region regionName string // The client for making API calls - client DigitalOceanClient + client *godo.Client } func (*Artifact) BuilderId() string { @@ -43,5 +45,6 @@ func (a *Artifact) State(name string) interface{} { func (a *Artifact) Destroy() error { log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName) - return a.client.DestroyImage(a.snapshotId) + _, err := a.client.Images.Delete(a.snapshotId) + return err } diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go index 3292bea10..3ba7074a2 100644 --- a/builder/digitalocean/builder.go +++ b/builder/digitalocean/builder.go @@ -4,18 +4,14 @@ package digitalocean import ( - "errors" - "fmt" "log" - "os" "time" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/common" - "github.com/mitchellh/packer/common/uuid" - "github.com/mitchellh/packer/helper/config" "github.com/mitchellh/packer/packer" - "github.com/mitchellh/packer/template/interpolate" + "golang.org/x/oauth2" ) // see https://api.digitalocean.com/images/?client_id=[client_id]&api_key=[api_key] @@ -33,179 +29,25 @@ const DefaultSize = "512mb" // The unique id for the builder const BuilderId = "pearkes.digitalocean" -// Configuration tells the builder the credentials -// to use while communicating with DO and describes the image -// you are creating -type Config struct { - common.PackerConfig `mapstructure:",squash"` - - 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"` - - Region string `mapstructure:"region"` - Size string `mapstructure:"size"` - Image string `mapstructure:"image"` - - PrivateNetworking bool `mapstructure:"private_networking"` - SnapshotName string `mapstructure:"snapshot_name"` - DropletName string `mapstructure:"droplet_name"` - SSHUsername string `mapstructure:"ssh_username"` - SSHPort uint `mapstructure:"ssh_port"` - - RawSSHTimeout string `mapstructure:"ssh_timeout"` - RawStateTimeout string `mapstructure:"state_timeout"` - - // These are unexported since they're set by other fields - // being set. - sshTimeout time.Duration - stateTimeout time.Duration - - ctx *interpolate.Context -} - type Builder struct { config Config runner multistep.Runner } func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { - err := config.Decode(&b.config, &config.DecodeOpts{ - Interpolate: true, - }, raws...) - if err != nil { - return nil, err + c, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs } + b.config = *c - // Optional configuration with defaults - if b.config.APIKey == "" { - // Default to environment variable for api_key, if it exists - b.config.APIKey = os.Getenv("DIGITALOCEAN_API_KEY") - } - - if b.config.ClientID == "" { - // Default to environment variable for client_id, if it exists - b.config.ClientID = os.Getenv("DIGITALOCEAN_CLIENT_ID") - } - - if b.config.APIURL == "" { - // Default to environment variable for api_url, if it exists - b.config.APIURL = os.Getenv("DIGITALOCEAN_API_URL") - } - - if b.config.APIToken == "" { - // Default to environment variable for api_token, if it exists - b.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN") - } - - if b.config.Region == "" { - if b.config.RegionID != 0 { - b.config.Region = fmt.Sprintf("%v", b.config.RegionID) - } else { - b.config.Region = DefaultRegion - } - } - - if b.config.Size == "" { - if b.config.SizeID != 0 { - b.config.Size = fmt.Sprintf("%v", b.config.SizeID) - } else { - b.config.Size = DefaultSize - } - } - - if b.config.Image == "" { - if b.config.ImageID != 0 { - b.config.Image = fmt.Sprintf("%v", b.config.ImageID) - } else { - b.config.Image = DefaultImage - } - } - - if b.config.SnapshotName == "" { - // Default to packer-{{ unix timestamp (utc) }} - b.config.SnapshotName = "packer-{{timestamp}}" - } - - if b.config.DropletName == "" { - // Default to packer-[time-ordered-uuid] - b.config.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) - } - - if b.config.SSHUsername == "" { - // Default to "root". You can override this if your - // SourceImage has a different user account then the DO default - b.config.SSHUsername = "root" - } - - if b.config.SSHPort == 0 { - // Default to port 22 per DO default - b.config.SSHPort = 22 - } - - if b.config.RawSSHTimeout == "" { - // Default to 1 minute timeouts - b.config.RawSSHTimeout = "1m" - } - - if b.config.RawStateTimeout == "" { - // Default to 6 minute timeouts waiting for - // desired state. i.e waiting for droplet to become active - b.config.RawStateTimeout = "6m" - } - - var errs *packer.MultiError - 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("a 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" - } - - sshTimeout, err := time.ParseDuration(b.config.RawSSHTimeout) - if err != nil { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) - } - b.config.sshTimeout = sshTimeout - - stateTimeout, err := time.ParseDuration(b.config.RawStateTimeout) - if err != nil { - errs = packer.MultiErrorAppend( - errs, fmt.Errorf("Failed parsing state_timeout: %s", err)) - } - b.config.stateTimeout = stateTimeout - - if errs != nil && len(errs.Errors) > 0 { - return nil, errs - } - - common.ScrubConfig(b.config, b.config.ClientID, b.config.APIKey) return nil, nil } func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { - var client DigitalOceanClient - // Initialize the DO API client - if b.config.APIToken == "" { - client = DigitalOceanClientNewV1(b.config.ClientID, b.config.APIKey, b.config.APIURL) - } else { - client = DigitalOceanClientNewV2(b.config.APIToken, b.config.APIURL) - } + client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{ + AccessToken: b.config.APIToken, + })) // Set up the state state := new(multistep.BasicStateBag) @@ -252,26 +94,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe return nil, nil } - sregion := state.Get("region") - - var region string - - if sregion != nil { - region = sregion.(string) - } else { - region = fmt.Sprintf("%v", state.Get("region_id").(uint)) - } - - found_region, err := client.Region(region) - - if err != nil { - return nil, err - } - artifact := &Artifact{ snapshotName: state.Get("snapshot_name").(string), - snapshotId: state.Get("snapshot_image_id").(uint), - regionName: found_region.Name, + snapshotId: state.Get("snapshot_image_id").(int), + regionName: state.Get("region").(string), client: client, } diff --git a/builder/digitalocean/builder_acc_test.go b/builder/digitalocean/builder_acc_test.go new file mode 100644 index 000000000..20e56b924 --- /dev/null +++ b/builder/digitalocean/builder_acc_test.go @@ -0,0 +1,30 @@ +package digitalocean + +import ( + "os" + "testing" + + builderT "github.com/mitchellh/packer/helper/builder/testing" +) + +func TestBuilderAcc_basic(t *testing.T) { + builderT.Test(t, builderT.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Builder: &Builder{}, + Template: testBuilderAccBasic, + }) +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" { + t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests") + } +} + +const testBuilderAccBasic = ` +{ + "builders": [{ + "type": "test" + }] +} +` diff --git a/builder/digitalocean/builder_test.go b/builder/digitalocean/builder_test.go index bd3bb1d21..878431691 100644 --- a/builder/digitalocean/builder_test.go +++ b/builder/digitalocean/builder_test.go @@ -1,22 +1,15 @@ package digitalocean import ( - "github.com/mitchellh/packer/packer" - "os" "strconv" "testing" -) -func init() { - // Clear out the credential env vars - os.Setenv("DIGITALOCEAN_API_KEY", "") - os.Setenv("DIGITALOCEAN_CLIENT_ID", "") -} + "github.com/mitchellh/packer/packer" +) func testConfig() map[string]interface{} { return map[string]interface{}{ - "client_id": "foo", - "api_key": "bar", + "api_token": "bar", } } @@ -43,90 +36,6 @@ func TestBuilder_Prepare_BadType(t *testing.T) { } } -func TestBuilderPrepare_APIKey(t *testing.T) { - var b Builder - config := testConfig() - - // Test good - config["api_key"] = "foo" - warnings, err := b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - if b.config.APIKey != "foo" { - t.Errorf("access key invalid: %s", b.config.APIKey) - } - - // Test bad - delete(config, "api_key") - b = Builder{} - warnings, err = b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err == nil { - t.Fatal("should have error") - } - - // Test env variable - delete(config, "api_key") - os.Setenv("DIGITALOCEAN_API_KEY", "foo") - defer os.Setenv("DIGITALOCEAN_API_KEY", "") - warnings, err = b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - -func TestBuilderPrepare_ClientID(t *testing.T) { - var b Builder - config := testConfig() - - // Test good - config["client_id"] = "foo" - warnings, err := b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } - - if b.config.ClientID != "foo" { - t.Errorf("invalid: %s", b.config.ClientID) - } - - // Test bad - delete(config, "client_id") - b = Builder{} - warnings, err = b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err == nil { - t.Fatal("should have error") - } - - // Test env variable - delete(config, "client_id") - os.Setenv("DIGITALOCEAN_CLIENT_ID", "foo") - defer os.Setenv("DIGITALOCEAN_CLIENT_ID", "") - warnings, err = b.Prepare(config) - if len(warnings) > 0 { - t.Fatalf("bad: %#v", warnings) - } - if err != nil { - t.Fatalf("should not have error: %s", err) - } -} - func TestBuilderPrepare_InvalidKey(t *testing.T) { var b Builder config := testConfig() @@ -162,7 +71,6 @@ func TestBuilderPrepare_Region(t *testing.T) { expected := "sfo1" // Test set - config["region_id"] = 0 config["region"] = expected b = Builder{} warnings, err = b.Prepare(config) @@ -198,7 +106,6 @@ func TestBuilderPrepare_Size(t *testing.T) { expected := "1024mb" // Test set - config["size_id"] = 0 config["size"] = expected b = Builder{} warnings, err = b.Prepare(config) @@ -234,7 +141,6 @@ func TestBuilderPrepare_Image(t *testing.T) { expected := "ubuntu-14-04-x64" // Test set - config["image_id"] = 0 config["image"] = expected b = Builder{} warnings, err = b.Prepare(config) diff --git a/builder/digitalocean/config.go b/builder/digitalocean/config.go new file mode 100644 index 000000000..5defe89db --- /dev/null +++ b/builder/digitalocean/config.go @@ -0,0 +1,137 @@ +package digitalocean + +import ( + "errors" + "fmt" + "os" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/mitchellh/packer/common" + "github.com/mitchellh/packer/common/uuid" + "github.com/mitchellh/packer/helper/config" + "github.com/mitchellh/packer/packer" + "github.com/mitchellh/packer/template/interpolate" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + + APIToken string `mapstructure:"api_token"` + + Region string `mapstructure:"region"` + Size string `mapstructure:"size"` + Image string `mapstructure:"image"` + + PrivateNetworking bool `mapstructure:"private_networking"` + SnapshotName string `mapstructure:"snapshot_name"` + DropletName string `mapstructure:"droplet_name"` + SSHUsername string `mapstructure:"ssh_username"` + SSHPort uint `mapstructure:"ssh_port"` + + RawSSHTimeout string `mapstructure:"ssh_timeout"` + RawStateTimeout string `mapstructure:"state_timeout"` + + // These are unexported since they're set by other fields + // being set. + sshTimeout time.Duration + stateTimeout time.Duration + + ctx *interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + var c Config + + var md mapstructure.Metadata + err := config.Decode(&c, &config.DecodeOpts{ + Metadata: &md, + Interpolate: true, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "run_command", + }, + }, + }, raws...) + if err != nil { + return nil, nil, err + } + + // Defaults + if c.APIToken == "" { + // Default to environment variable for api_token, if it exists + c.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN") + } + + if c.Region == "" { + c.Region = DefaultRegion + } + + if c.Size == "" { + c.Size = DefaultSize + } + + if c.Image == "" { + c.Image = DefaultImage + } + + if c.SnapshotName == "" { + // Default to packer-{{ unix timestamp (utc) }} + c.SnapshotName = "packer-{{timestamp}}" + } + + if c.DropletName == "" { + // Default to packer-[time-ordered-uuid] + c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + } + + if c.SSHUsername == "" { + // Default to "root". You can override this if your + // SourceImage has a different user account then the DO default + c.SSHUsername = "root" + } + + if c.SSHPort == 0 { + // Default to port 22 per DO default + c.SSHPort = 22 + } + + if c.RawSSHTimeout == "" { + // Default to 1 minute timeouts + c.RawSSHTimeout = "1m" + } + + if c.RawStateTimeout == "" { + // Default to 6 minute timeouts waiting for + // desired state. i.e waiting for droplet to become active + c.RawStateTimeout = "6m" + } + + var errs *packer.MultiError + if c.APIToken == "" { + // Required configurations that will display errors if not set + errs = packer.MultiErrorAppend( + errs, errors.New("api_token for auth must be specified")) + } + + sshTimeout, err := time.ParseDuration(c.RawSSHTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err)) + } + c.sshTimeout = sshTimeout + + stateTimeout, err := time.ParseDuration(c.RawStateTimeout) + if err != nil { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("Failed parsing state_timeout: %s", err)) + } + c.stateTimeout = stateTimeout + + if errs != nil && len(errs.Errors) > 0 { + return nil, nil, errs + } + + common.ScrubConfig(c, c.APIToken) + return &c, nil, nil +} diff --git a/builder/digitalocean/step_create_droplet.go b/builder/digitalocean/step_create_droplet.go index afb3e5814..40ac8f0e9 100644 --- a/builder/digitalocean/step_create_droplet.go +++ b/builder/digitalocean/step_create_droplet.go @@ -3,25 +3,35 @@ package digitalocean import ( "fmt" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) type stepCreateDroplet struct { - dropletId uint + dropletId int } func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) c := state.Get("config").(Config) - sshKeyId := state.Get("ssh_key_id").(uint) - - ui.Say("Creating droplet...") + sshKeyId := state.Get("ssh_key_id").(int) // Create the droplet based on configuration - dropletId, err := client.CreateDroplet(c.DropletName, c.Size, c.Image, c.Region, sshKeyId, c.PrivateNetworking) - + ui.Say("Creating droplet...") + droplet, _, err := client.Droplets.Create(&godo.DropletCreateRequest{ + Name: c.DropletName, + Region: c.Region, + Size: c.Size, + Image: godo.DropletCreateImage{ + Slug: c.Image, + }, + SSHKeys: []godo.DropletCreateSSHKey{ + godo.DropletCreateSSHKey{ID: int(sshKeyId)}, + }, + PrivateNetworking: c.PrivateNetworking, + }) if err != nil { err := fmt.Errorf("Error creating droplet: %s", err) state.Put("error", err) @@ -30,10 +40,10 @@ func (s *stepCreateDroplet) Run(state multistep.StateBag) multistep.StepAction { } // We use this in cleanup - s.dropletId = dropletId + s.dropletId = droplet.ID // Store the droplet id for later - state.Put("droplet_id", dropletId) + state.Put("droplet_id", droplet.ID) return multistep.ActionContinue } @@ -44,19 +54,14 @@ func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) { return } - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) - c := state.Get("config").(Config) // Destroy the droplet we just created ui.Say("Destroying droplet...") - - err := client.DestroyDroplet(s.dropletId) + _, err := client.Droplets.Delete(s.dropletId) if err != nil { - curlstr := fmt.Sprintf("curl '%v/droplets/%v/destroy?client_id=%v&api_key=%v'", - c.APIURL, s.dropletId, c.ClientID, c.APIKey) - ui.Error(fmt.Sprintf( - "Error destroying droplet. Please destroy it manually: %v", curlstr)) + "Error destroying droplet. Please destroy it manually: %s", err)) } } diff --git a/builder/digitalocean/step_create_ssh_key.go b/builder/digitalocean/step_create_ssh_key.go index db1ad9c16..fa0940c23 100644 --- a/builder/digitalocean/step_create_ssh_key.go +++ b/builder/digitalocean/step_create_ssh_key.go @@ -9,17 +9,18 @@ import ( "log" "code.google.com/p/gosshold/ssh" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/common/uuid" "github.com/mitchellh/packer/packer" ) type stepCreateSSHKey struct { - keyId uint + keyId int } func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) ui.Say("Creating temporary ssh key for droplet...") @@ -46,7 +47,10 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) // Create the key! - keyId, err := client.CreateKey(name, pub_sshformat) + key, _, err := client.Keys.Create(&godo.KeyCreateRequest{ + Name: name, + PublicKey: pub_sshformat, + }) if err != nil { err := fmt.Errorf("Error creating temporary SSH key: %s", err) state.Put("error", err) @@ -55,12 +59,12 @@ func (s *stepCreateSSHKey) Run(state multistep.StateBag) multistep.StepAction { } // We use this to check cleanup - s.keyId = keyId + s.keyId = key.ID log.Printf("temporary ssh key name: %s", name) // Remember some state for the future - state.Put("ssh_key_id", keyId) + state.Put("ssh_key_id", key.ID) return multistep.ActionContinue } @@ -71,18 +75,14 @@ func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) { return } - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) 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 -H 'Authorization: Bearer #TOKEN#' -X DELETE '%v/v2/account/keys/%v'", c.APIURL, s.keyId) - + _, err := client.Keys.DeleteByID(s.keyId) if err != nil { - log.Printf("Error cleaning up ssh key: %v", err.Error()) + log.Printf("Error cleaning up ssh key: %s", err) ui.Error(fmt.Sprintf( - "Error cleaning up ssh key. Please delete the key manually: %v", curlstr)) + "Error cleaning up ssh key. Please delete the key manually: %s", err)) } } diff --git a/builder/digitalocean/step_droplet_info.go b/builder/digitalocean/step_droplet_info.go index 8e9b69927..5fbcb7141 100644 --- a/builder/digitalocean/step_droplet_info.go +++ b/builder/digitalocean/step_droplet_info.go @@ -3,6 +3,7 @@ package digitalocean import ( "fmt" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) @@ -10,10 +11,10 @@ import ( type stepDropletInfo struct{} func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) c := state.Get("config").(Config) - dropletId := state.Get("droplet_id").(uint) + dropletId := state.Get("droplet_id").(int) ui.Say("Waiting for droplet to become active...") @@ -26,16 +27,25 @@ func (s *stepDropletInfo) Run(state multistep.StateBag) multistep.StepAction { } // Set the IP on the state for later - ip, _, err := client.DropletStatus(dropletId) + droplet, _, err := client.Droplets.Get(dropletId) if err != nil { - err := fmt.Errorf("Error retrieving droplet ID: %s", err) + err := fmt.Errorf("Error retrieving droplet: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - state.Put("droplet_ip", ip) + // Verify we have an IPv4 address + invalid := droplet.Networks == nil || + len(droplet.Networks.V4) == 0 + if invalid { + err := fmt.Errorf("IPv4 address not found for droplet!") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + state.Put("droplet_ip", droplet.Networks.V4[0].IPAddress) return multistep.ActionContinue } diff --git a/builder/digitalocean/step_power_off.go b/builder/digitalocean/step_power_off.go index d6ef49a22..3d547e8c2 100644 --- a/builder/digitalocean/step_power_off.go +++ b/builder/digitalocean/step_power_off.go @@ -4,6 +4,7 @@ import ( "fmt" "log" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) @@ -11,12 +12,12 @@ import ( type stepPowerOff struct{} func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) c := state.Get("config").(Config) ui := state.Get("ui").(packer.Ui) - dropletId := state.Get("droplet_id").(uint) + dropletId := state.Get("droplet_id").(int) - _, status, err := client.DropletStatus(dropletId) + droplet, _, err := client.Droplets.Get(dropletId) if err != nil { err := fmt.Errorf("Error checking droplet state: %s", err) state.Put("error", err) @@ -24,14 +25,14 @@ func (s *stepPowerOff) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - if status == "off" { + if droplet.Status == "off" { // Droplet is already off, don't do anything return multistep.ActionContinue } // Pull the plug on the Droplet ui.Say("Forcefully shutting down Droplet...") - err = client.PowerOffDroplet(dropletId) + _, _, err = client.DropletActions.PowerOff(dropletId) if err != nil { err := fmt.Errorf("Error powering off droplet: %s", err) state.Put("error", err) diff --git a/builder/digitalocean/step_shutdown.go b/builder/digitalocean/step_shutdown.go index 06a2ae9f5..602f3e690 100644 --- a/builder/digitalocean/step_shutdown.go +++ b/builder/digitalocean/step_shutdown.go @@ -5,6 +5,7 @@ import ( "log" "time" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) @@ -12,16 +13,16 @@ import ( type stepShutdown struct{} func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) - dropletId := state.Get("droplet_id").(uint) + dropletId := state.Get("droplet_id").(int) // Gracefully power off the droplet. We have to retry this a number // of times because sometimes it says it completed when it actually // did absolutely nothing (*ALAKAZAM!* magic!). We give up after // a pretty arbitrary amount of time. ui.Say("Gracefully shutting down droplet...") - err := client.ShutdownDroplet(dropletId) + _, _, err := client.DropletActions.Shutdown(dropletId) if err != nil { // If we get an error the first time, actually report it err := fmt.Errorf("Error shutting down droplet: %s", err) @@ -48,7 +49,7 @@ func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction { for attempts := 2; attempts > 0; attempts++ { log.Printf("ShutdownDroplet attempt #%d...", attempts) - err := client.ShutdownDroplet(dropletId) + _, _, err := client.DropletActions.Shutdown(dropletId) if err != nil { log.Printf("Shutdown retry error: %s", err) } diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go index 1903c1a34..cfda7af20 100644 --- a/builder/digitalocean/step_snapshot.go +++ b/builder/digitalocean/step_snapshot.go @@ -5,6 +5,7 @@ import ( "fmt" "log" + "github.com/digitalocean/godo" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" ) @@ -12,13 +13,13 @@ import ( type stepSnapshot struct{} func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { - client := state.Get("client").(DigitalOceanClient) + client := state.Get("client").(*godo.Client) ui := state.Get("ui").(packer.Ui) c := state.Get("config").(Config) - dropletId := state.Get("droplet_id").(uint) + dropletId := state.Get("droplet_id").(int) ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName)) - err := client.CreateSnapshot(dropletId, c.SnapshotName) + _, _, err := client.DropletActions.Snapshot(dropletId, c.SnapshotName) if err != nil { err := fmt.Errorf("Error creating snapshot: %s", err) state.Put("error", err) @@ -36,7 +37,7 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { } log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName) - images, err := client.Images() + images, _, err := client.Images.ListUser(&godo.ListOptions{PerPage: 200}) if err != nil { err := fmt.Errorf("Error looking up snapshot ID: %s", err) state.Put("error", err) @@ -44,10 +45,10 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { return multistep.ActionHalt } - var imageId uint + var imageId int for _, image := range images { if image.Name == c.SnapshotName { - imageId = image.Id + imageId = image.ID break } } @@ -60,7 +61,6 @@ func (s *stepSnapshot) Run(state multistep.StateBag) multistep.StepAction { } log.Printf("Snapshot image ID: %d", imageId) - state.Put("snapshot_image_id", imageId) state.Put("snapshot_name", c.SnapshotName) state.Put("region", c.Region) diff --git a/builder/digitalocean/token_source.go b/builder/digitalocean/token_source.go new file mode 100644 index 000000000..eab5a084b --- /dev/null +++ b/builder/digitalocean/token_source.go @@ -0,0 +1,15 @@ +package digitalocean + +import ( + "golang.org/x/oauth2" +) + +type apiTokenSource struct { + AccessToken string +} + +func (t *apiTokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: t.AccessToken, + }, nil +} diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go index e5b1dee90..3d299d433 100644 --- a/builder/digitalocean/wait.go +++ b/builder/digitalocean/wait.go @@ -4,11 +4,15 @@ import ( "fmt" "log" "time" + + "github.com/digitalocean/godo" ) // 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 int, + client *godo.Client, timeout time.Duration) error { done := make(chan struct{}) defer close(done) @@ -19,13 +23,13 @@ func waitForDropletState(desiredState string, dropletId uint, client DigitalOcea attempts += 1 log.Printf("Checking droplet status... (attempt: %d)", attempts) - _, status, err := client.DropletStatus(dropletId) + droplet, _, err := client.Droplets.Get(dropletId) if err != nil { result <- err return } - if status == desiredState { + if droplet.Status == desiredState { result <- nil return } diff --git a/website/source/docs/builders/digitalocean.html.markdown b/website/source/docs/builders/digitalocean.html.markdown index 5ffe1c668..28254b19c 100644 --- a/website/source/docs/builders/digitalocean.html.markdown +++ b/website/source/docs/builders/digitalocean.html.markdown @@ -24,31 +24,13 @@ There are many configuration options available for the builder. They are segmented below into two categories: required and optional parameters. Within each category, the available configuration keys are alphabetized. -### Required v1 api: +### Required: -* `api_key` (string) - The API key to use to access your account. You can - retrieve this on the "API" page visible after logging into your account - on DigitalOcean. - If not specified, Packer will use the environment variable - `DIGITALOCEAN_API_KEY`, if set. - -* `client_id` (string) - The client ID to use to access your account. You can - find this on the "API" page visible after logging into your account on - DigitalOcean. - If not specified, Packer will use the environment variable - `DIGITALOCEAN_CLIENT_ID`, if set. - -### Required v2 api: - -* `api_token` (string) - The client TOKEN to use to access your account. If it - specified, then use v2 api (current), if not then used old (v1) deprecated api. - Also it can be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set. +* `api_token` (string) - The client TOKEN to use to access your account. + It can also be specified via environment variable `DIGITALOCEAN_API_TOKEN`, if set. ### Optional: -* `api_url` (string) - API endpoint, by default use https://api.digitalocean.com - Also it can be specified via environment variable `DIGITALOCEAN_API_URL`, if set. - * `droplet_name` (string) - The name assigned to the droplet. DigitalOcean sets the hostname of the machine to this value. @@ -57,10 +39,6 @@ each category, the available configuration keys are alphabetized. defaults to 'ubuntu-12-04-x64' which is the slug for "Ubuntu 12.04.4 x64". See https://developers.digitalocean.com/documentation/v2/#list-all-images for details on how to get a list of the the accepted image names/slugs. -* `image_id` (integer) - The ID of the base image to use. This is the image that - will be used to launch a new droplet and provision it. - This setting is deprecated. Use `image` instead. - * `private_networking` (boolean) - Set to `true` to enable private networking for the droplet being created. This defaults to `false`, or not enabled. @@ -69,17 +47,10 @@ each category, the available configuration keys are alphabetized. This defaults to "nyc3", which is the slug for "New York 3". See https://developers.digitalocean.com/documentation/v2/#list-all-regions for the accepted region names/slugs. -* `region_id` (integer) - The ID of the region to launch the droplet in. Consequently, - this is the region where the snapshot will be available. - This setting is deprecated. Use `region` instead. - * `size` (string) - The name (or slug) of the droplet size to use. This defaults to "512mb", which is the slug for "512MB". See https://developers.digitalocean.com/documentation/v2/#list-all-sizes for the accepted size names/slugs. -* `size_id` (integer) - The ID of the droplet size to use. - This setting is deprecated. Use `size` instead. - * `snapshot_name` (string) - The name of the resulting snapshot that will appear in your account. This must be unique. To help make this unique, use a function like `timestamp` (see @@ -107,20 +78,6 @@ own access tokens: ```javascript { "type": "digitalocean", - "client_id": "YOUR CLIENT ID", - "api_key": "YOUR API KEY" + "api_token": "YOUR API KEY" } ``` - -## Finding Image, Region, and Size IDs - -Unfortunately, finding a list of available values for `image_id`, `region_id`, -and `size_id` is not easy at the moment. Basically, it has to be done through -the [DigitalOcean API](https://www.digitalocean.com/api_access) using the -`/images`, `/regions`, and `/sizes` endpoints. You can use `curl` for this -or request it in your browser. - -If you're comfortable installing RubyGems, [Tugboat](https://github.com/pearkes/tugboat) -is a fantastic DigitalOcean command-line client that has commands to -find the available images, regions, and sizes. For example, to see all the -global images, you can run `tugboat images --global`.