435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
307 lines
8.2 KiB
Go
307 lines
8.2 KiB
Go
package packngo
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
packetTokenEnvVar = "PACKET_AUTH_TOKEN"
|
|
libraryVersion = "0.1.0"
|
|
baseURL = "https://api.packet.net/"
|
|
userAgent = "packngo/" + libraryVersion
|
|
mediaType = "application/json"
|
|
debugEnvVar = "PACKNGO_DEBUG"
|
|
|
|
headerRateLimit = "X-RateLimit-Limit"
|
|
headerRateRemaining = "X-RateLimit-Remaining"
|
|
headerRateReset = "X-RateLimit-Reset"
|
|
)
|
|
|
|
// ListOptions specifies optional global API parameters
|
|
type ListOptions struct {
|
|
// for paginated result sets, page of results to retrieve
|
|
Page int `url:"page,omitempty"`
|
|
|
|
// for paginated result sets, the number of results to return per page
|
|
PerPage int `url:"per_page,omitempty"`
|
|
|
|
// specify which resources you want to return as collections instead of references
|
|
Includes string
|
|
}
|
|
|
|
func (l *ListOptions) createURL() (url string) {
|
|
if l.Includes != "" {
|
|
url += fmt.Sprintf("include=%s", l.Includes)
|
|
}
|
|
|
|
if l.Page != 0 {
|
|
if url != "" {
|
|
url += "&"
|
|
}
|
|
url += fmt.Sprintf("page=%d", l.Page)
|
|
}
|
|
|
|
if l.PerPage != 0 {
|
|
if url != "" {
|
|
url += "&"
|
|
}
|
|
url += fmt.Sprintf("per_page=%d", l.PerPage)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// meta contains pagination information
|
|
type meta struct {
|
|
Self *Href `json:"self"`
|
|
First *Href `json:"first"`
|
|
Last *Href `json:"last"`
|
|
Previous *Href `json:"previous,omitempty"`
|
|
Next *Href `json:"next,omitempty"`
|
|
Total int `json:"total"`
|
|
CurrentPageNum int `json:"current_page"`
|
|
LastPageNum int `json:"last_page"`
|
|
}
|
|
|
|
// Response is the http response from api calls
|
|
type Response struct {
|
|
*http.Response
|
|
Rate
|
|
}
|
|
|
|
// Href is an API link
|
|
type Href struct {
|
|
Href string `json:"href"`
|
|
}
|
|
|
|
func (r *Response) populateRate() {
|
|
// parse the rate limit headers and populate Response.Rate
|
|
if limit := r.Header.Get(headerRateLimit); limit != "" {
|
|
r.Rate.RequestLimit, _ = strconv.Atoi(limit)
|
|
}
|
|
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
|
|
r.Rate.RequestsRemaining, _ = strconv.Atoi(remaining)
|
|
}
|
|
if reset := r.Header.Get(headerRateReset); reset != "" {
|
|
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
|
|
r.Rate.Reset = Timestamp{time.Unix(v, 0)}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ErrorResponse is the http response used on errors
|
|
type ErrorResponse struct {
|
|
Response *http.Response
|
|
Errors []string `json:"errors"`
|
|
SingleError string `json:"error"`
|
|
}
|
|
|
|
func (r *ErrorResponse) Error() string {
|
|
return fmt.Sprintf("%v %v: %d %v %v",
|
|
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(r.Errors, ", "), r.SingleError)
|
|
}
|
|
|
|
// Client is the base API Client
|
|
type Client struct {
|
|
client *http.Client
|
|
debug bool
|
|
|
|
BaseURL *url.URL
|
|
|
|
UserAgent string
|
|
ConsumerToken string
|
|
APIKey string
|
|
|
|
RateLimit Rate
|
|
|
|
// Packet Api Objects
|
|
Plans PlanService
|
|
Users UserService
|
|
Emails EmailService
|
|
SSHKeys SSHKeyService
|
|
Devices DeviceService
|
|
Projects ProjectService
|
|
Facilities FacilityService
|
|
OperatingSystems OSService
|
|
DeviceIPs DeviceIPService
|
|
DevicePorts DevicePortService
|
|
ProjectIPs ProjectIPService
|
|
ProjectVirtualNetworks ProjectVirtualNetworkService
|
|
Volumes VolumeService
|
|
VolumeAttachments VolumeAttachmentService
|
|
SpotMarket SpotMarketService
|
|
Organizations OrganizationService
|
|
}
|
|
|
|
// NewRequest inits a new http request with the proper headers
|
|
func (c *Client) NewRequest(method, path string, body interface{}) (*http.Request, error) {
|
|
// relative path to append to the endpoint url, no leading slash please
|
|
rel, err := url.Parse(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
u := c.BaseURL.ResolveReference(rel)
|
|
|
|
// json encode the request body, if any
|
|
buf := new(bytes.Buffer)
|
|
if body != nil {
|
|
err := json.NewEncoder(buf).Encode(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest(method, u.String(), buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Close = true
|
|
|
|
req.Header.Add("X-Auth-Token", c.APIKey)
|
|
req.Header.Add("X-Consumer-Token", c.ConsumerToken)
|
|
|
|
req.Header.Add("Content-Type", mediaType)
|
|
req.Header.Add("Accept", mediaType)
|
|
req.Header.Add("User-Agent", userAgent)
|
|
return req, nil
|
|
}
|
|
|
|
// Do executes the http request
|
|
func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
response := Response{Response: resp}
|
|
response.populateRate()
|
|
if c.debug {
|
|
o, _ := httputil.DumpResponse(response.Response, true)
|
|
log.Printf("\n=======[RESPONSE]============\n%s\n\n", string(o))
|
|
}
|
|
c.RateLimit = response.Rate
|
|
|
|
err = checkResponse(resp)
|
|
// if the response is an error, return the ErrorResponse
|
|
if err != nil {
|
|
return &response, err
|
|
}
|
|
|
|
if v != nil {
|
|
// if v implements the io.Writer interface, return the raw response
|
|
if w, ok := v.(io.Writer); ok {
|
|
io.Copy(w, resp.Body)
|
|
} else {
|
|
err = json.NewDecoder(resp.Body).Decode(v)
|
|
if err != nil {
|
|
return &response, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return &response, err
|
|
}
|
|
|
|
// DoRequest is a convenience method, it calls NewRequest followed by Do
|
|
// v is the interface to unmarshal the response JSON into
|
|
func (c *Client) DoRequest(method, path string, body, v interface{}) (*Response, error) {
|
|
req, err := c.NewRequest(method, path, body)
|
|
if c.debug {
|
|
o, _ := httputil.DumpRequestOut(req, true)
|
|
log.Printf("\n=======[REQUEST]=============\n%s\n", string(o))
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.Do(req, v)
|
|
}
|
|
|
|
func NewClient() (*Client, error) {
|
|
apiToken := os.Getenv(packetTokenEnvVar)
|
|
if apiToken == "" {
|
|
return nil, fmt.Errorf("you must export %s.", packetTokenEnvVar)
|
|
}
|
|
c := NewClientWithAuth("packngo lib", apiToken, nil)
|
|
return c, nil
|
|
|
|
}
|
|
|
|
// NewClientWithAuth initializes and returns a Client, use this to get an API Client to operate on
|
|
// N.B.: Packet's API certificate requires Go 1.5+ to successfully parse. If you are using
|
|
// an older version of Go, pass in a custom http.Client with a custom TLS configuration
|
|
// that sets "InsecureSkipVerify" to "true"
|
|
func NewClientWithAuth(consumerToken string, apiKey string, httpClient *http.Client) *Client {
|
|
client, _ := NewClientWithBaseURL(consumerToken, apiKey, httpClient, baseURL)
|
|
return client
|
|
}
|
|
|
|
// NewClientWithBaseURL returns a Client pointing to nonstandard API URL, e.g.
|
|
// for mocking the remote API
|
|
func NewClientWithBaseURL(consumerToken string, apiKey string, httpClient *http.Client, apiBaseURL string) (*Client, error) {
|
|
if httpClient == nil {
|
|
// Don't fall back on http.DefaultClient as it's not nice to adjust state
|
|
// implicitly. If the client wants to use http.DefaultClient, they can
|
|
// pass it in explicitly.
|
|
httpClient = &http.Client{}
|
|
}
|
|
|
|
u, err := url.Parse(apiBaseURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := &Client{client: httpClient, BaseURL: u, UserAgent: userAgent, ConsumerToken: consumerToken, APIKey: apiKey}
|
|
c.debug = os.Getenv(debugEnvVar) != ""
|
|
c.Plans = &PlanServiceOp{client: c}
|
|
c.Organizations = &OrganizationServiceOp{client: c}
|
|
c.Users = &UserServiceOp{client: c}
|
|
c.Emails = &EmailServiceOp{client: c}
|
|
c.SSHKeys = &SSHKeyServiceOp{client: c}
|
|
c.Devices = &DeviceServiceOp{client: c}
|
|
c.Projects = &ProjectServiceOp{client: c}
|
|
c.Facilities = &FacilityServiceOp{client: c}
|
|
c.OperatingSystems = &OSServiceOp{client: c}
|
|
c.DeviceIPs = &DeviceIPServiceOp{client: c}
|
|
c.DevicePorts = &DevicePortServiceOp{client: c}
|
|
c.ProjectVirtualNetworks = &ProjectVirtualNetworkServiceOp{client: c}
|
|
c.ProjectIPs = &ProjectIPServiceOp{client: c}
|
|
c.Volumes = &VolumeServiceOp{client: c}
|
|
c.VolumeAttachments = &VolumeAttachmentServiceOp{client: c}
|
|
c.SpotMarket = &SpotMarketServiceOp{client: c}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func checkResponse(r *http.Response) error {
|
|
// return if http status code is within 200 range
|
|
if c := r.StatusCode; c >= 200 && c <= 299 {
|
|
// response is good, return
|
|
return nil
|
|
}
|
|
|
|
errorResponse := &ErrorResponse{Response: r}
|
|
data, err := ioutil.ReadAll(r.Body)
|
|
// if the response has a body, populate the message in errorResponse
|
|
if err == nil && len(data) > 0 {
|
|
json.Unmarshal(data, errorResponse)
|
|
}
|
|
|
|
return errorResponse
|
|
}
|