378 lines
11 KiB
Go
378 lines
11 KiB
Go
|
package udnssdk
|
||
|
|
||
|
// udnssdk - a golang sdk for the ultradns REST service.
|
||
|
// 2015-07-03 - jmasseo@gmail.com
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
libraryVersion = "0.1"
|
||
|
// DefaultTestBaseURL returns the URL for UltraDNS's test restapi endpoint
|
||
|
DefaultTestBaseURL = "https://test-restapi.ultradns.com/"
|
||
|
// DefaultLiveBaseURL returns the URL for UltraDNS's production restapi endpoint
|
||
|
DefaultLiveBaseURL = "https://restapi.ultradns.com/"
|
||
|
|
||
|
userAgent = "udnssdk-go/" + libraryVersion
|
||
|
|
||
|
apiVersion = "v1"
|
||
|
)
|
||
|
|
||
|
// QueryInfo wraps a query request
|
||
|
type QueryInfo struct {
|
||
|
Q string `json:"q"`
|
||
|
Sort string `json:"sort"`
|
||
|
Reverse bool `json:"reverse"`
|
||
|
Limit int `json:"limit"`
|
||
|
}
|
||
|
|
||
|
// ResultInfo wraps the list metadata for an index response
|
||
|
type ResultInfo struct {
|
||
|
TotalCount int `json:"totalCount"`
|
||
|
Offset int `json:"offset"`
|
||
|
ReturnedCount int `json:"returnedCount"`
|
||
|
}
|
||
|
|
||
|
// Client wraps our general-purpose Service Client
|
||
|
type Client struct {
|
||
|
// This is our client structure.
|
||
|
HTTPClient *http.Client
|
||
|
|
||
|
// UltraDNS makes a call to an authorization API using your username and
|
||
|
// password, returning an 'Access Token' and a 'Refresh Token'.
|
||
|
// Our use case does not require the refresh token, but we should implement
|
||
|
// for completeness.
|
||
|
AccessToken string
|
||
|
RefreshToken string
|
||
|
Username string
|
||
|
Password string
|
||
|
BaseURL string
|
||
|
UserAgent string
|
||
|
|
||
|
// Accounts API
|
||
|
Accounts *AccountsService
|
||
|
// Probe Alerts API
|
||
|
Alerts *AlertsService
|
||
|
// Directional Pools API
|
||
|
DirectionalPools *DirectionalPoolsService
|
||
|
// Events API
|
||
|
Events *EventsService
|
||
|
// Notifications API
|
||
|
Notifications *NotificationsService
|
||
|
// Probes API
|
||
|
Probes *ProbesService
|
||
|
// Resource Record Sets API
|
||
|
RRSets *RRSetsService
|
||
|
// Tasks API
|
||
|
Tasks *TasksService
|
||
|
}
|
||
|
|
||
|
// NewClient returns a new ultradns API client.
|
||
|
func NewClient(username, password, BaseURL string) (*Client, error) {
|
||
|
accesstoken, refreshtoken, err := GetAuthTokens(username, password, BaseURL)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
c := &Client{
|
||
|
AccessToken: accesstoken,
|
||
|
RefreshToken: refreshtoken,
|
||
|
Username: username,
|
||
|
Password: password,
|
||
|
HTTPClient: &http.Client{},
|
||
|
BaseURL: BaseURL,
|
||
|
UserAgent: userAgent,
|
||
|
}
|
||
|
c.Accounts = &AccountsService{client: c}
|
||
|
c.Alerts = &AlertsService{client: c}
|
||
|
c.DirectionalPools = &DirectionalPoolsService{client: c}
|
||
|
c.Events = &EventsService{client: c}
|
||
|
c.Notifications = &NotificationsService{client: c}
|
||
|
c.Probes = &ProbesService{client: c}
|
||
|
c.RRSets = &RRSetsService{client: c}
|
||
|
c.Tasks = &TasksService{client: c}
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// newStubClient returns a new ultradns API client.
|
||
|
func newStubClient(username, password, BaseURL, accesstoken, refreshtoken string) (*Client, error) {
|
||
|
c := &Client{
|
||
|
AccessToken: accesstoken,
|
||
|
RefreshToken: refreshtoken,
|
||
|
Username: username,
|
||
|
Password: password,
|
||
|
HTTPClient: &http.Client{},
|
||
|
BaseURL: BaseURL,
|
||
|
UserAgent: userAgent,
|
||
|
}
|
||
|
c.Accounts = &AccountsService{client: c}
|
||
|
c.Alerts = &AlertsService{client: c}
|
||
|
c.DirectionalPools = &DirectionalPoolsService{client: c}
|
||
|
c.Events = &EventsService{client: c}
|
||
|
c.Notifications = &NotificationsService{client: c}
|
||
|
c.Probes = &ProbesService{client: c}
|
||
|
c.RRSets = &RRSetsService{client: c}
|
||
|
c.Tasks = &TasksService{client: c}
|
||
|
return c, nil
|
||
|
}
|
||
|
|
||
|
// NewAuthRequest creates an Authorization request to get an access and refresh token.
|
||
|
//
|
||
|
// {
|
||
|
// "tokenType":"Bearer",
|
||
|
// "refreshToken":"48472efcdce044c8850ee6a395c74a7872932c7112",
|
||
|
// "accessToken":"b91d037c75934fc89a9f43fe4a",
|
||
|
// "expiresIn":"3600",
|
||
|
// "expires_in":"3600"
|
||
|
// }
|
||
|
|
||
|
// AuthResponse wraps the response to an auth request
|
||
|
type AuthResponse struct {
|
||
|
TokenType string `json:"tokenType"`
|
||
|
AccessToken string `json:"accessToken"`
|
||
|
RefreshToken string `json:"refreshToken"`
|
||
|
ExpiresIn string `json:"expiresIn"`
|
||
|
}
|
||
|
|
||
|
// GetAuthTokens requests by username, password & base URL, returns the access-token & refresh-token, or a possible error
|
||
|
func GetAuthTokens(username, password, BaseURL string) (string, string, error) {
|
||
|
res, err := http.PostForm(fmt.Sprintf("%s/%s/authorization/token", BaseURL, apiVersion), url.Values{"grant_type": {"password"}, "username": {username}, "password": {password}})
|
||
|
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
|
||
|
//response := &Response{Response: res}
|
||
|
defer res.Body.Close()
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
err = CheckAuthResponse(res, body)
|
||
|
if err != nil {
|
||
|
return "", "", err
|
||
|
}
|
||
|
|
||
|
var authr AuthResponse
|
||
|
err = json.Unmarshal(body, &authr)
|
||
|
if err != nil {
|
||
|
return string(body), "JSON Decode Error", err
|
||
|
}
|
||
|
return authr.AccessToken, authr.RefreshToken, err
|
||
|
}
|
||
|
|
||
|
// NewRequest creates an API request.
|
||
|
// The path is expected to be a relative path and will be resolved
|
||
|
// according to the BaseURL of the Client. Paths should always be specified without a preceding slash.
|
||
|
func (c *Client) NewRequest(method, path string, payload interface{}) (*http.Request, error) {
|
||
|
url := c.BaseURL + fmt.Sprintf("%s/%s", apiVersion, path)
|
||
|
|
||
|
body := new(bytes.Buffer)
|
||
|
if payload != nil {
|
||
|
err := json.NewEncoder(body).Encode(payload)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
req, err := http.NewRequest(method, url, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
req.Header.Set("Content-Type", "application/json")
|
||
|
req.Header.Add("Accept", "application/json")
|
||
|
req.Header.Add("User-Agent", c.UserAgent)
|
||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.AccessToken))
|
||
|
req.Header.Add("Token", fmt.Sprintf("Bearer %s", c.AccessToken))
|
||
|
|
||
|
return req, nil
|
||
|
}
|
||
|
|
||
|
func (c *Client) get(path string, v interface{}) (*Response, error) {
|
||
|
return c.Do("GET", path, nil, v)
|
||
|
}
|
||
|
|
||
|
func (c *Client) post(path string, payload, v interface{}) (*Response, error) {
|
||
|
return c.Do("POST", path, payload, v)
|
||
|
}
|
||
|
|
||
|
func (c *Client) put(path string, payload, v interface{}) (*Response, error) {
|
||
|
return c.Do("PUT", path, payload, v)
|
||
|
}
|
||
|
|
||
|
func (c *Client) delete(path string, payload interface{}) (*Response, error) {
|
||
|
return c.Do("DELETE", path, payload, nil)
|
||
|
}
|
||
|
|
||
|
// Do sends an API request and returns the API response.
|
||
|
// The API response is JSON decoded and stored in the value pointed by v,
|
||
|
// or returned as an error if an API error has occurred.
|
||
|
// If v implements the io.Writer interface, the raw response body will be written to v,
|
||
|
// without attempting to decode it.
|
||
|
func (c *Client) Do(method, path string, payload, v interface{}) (*Response, error) {
|
||
|
req, err := c.NewRequest(method, path, payload)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
log.Printf("[DEBUG] HTTP Request: %+v\n", req)
|
||
|
res, err := c.HTTPClient.Do(req)
|
||
|
log.Printf("[DEBUG] HTTP Response: %+v\n", res)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer res.Body.Close()
|
||
|
origresponse := &Response{Response: res}
|
||
|
|
||
|
var nres *http.Response
|
||
|
nres = res
|
||
|
if res.StatusCode == 202 {
|
||
|
// This is a deferred task.
|
||
|
tid := TaskID(res.Header.Get("X-Task-Id"))
|
||
|
log.Printf("[DEBUG] Received Async Task %+v.. will retry...\n", tid)
|
||
|
// TODO: Sane Configuration for timeouts / retries
|
||
|
timeout := 5
|
||
|
waittime := 5 * time.Second
|
||
|
i := 0
|
||
|
breakmeout := false
|
||
|
for i < timeout || breakmeout {
|
||
|
myt, statusres, err := c.Tasks.Find(tid)
|
||
|
if err != nil {
|
||
|
return origresponse, err
|
||
|
}
|
||
|
log.Printf("[DEBUG] Task ID: %+v Retry: %d Status Code: %s\n", tid, i, myt.TaskStatusCode)
|
||
|
switch myt.TaskStatusCode {
|
||
|
case "COMPLETE":
|
||
|
// Yay
|
||
|
tres, err := c.Tasks.FindResultByTask(myt)
|
||
|
if err != nil {
|
||
|
return origresponse, err
|
||
|
}
|
||
|
nres = tres.Response
|
||
|
breakmeout = true
|
||
|
case "PENDING", "IN_PROCESS":
|
||
|
i = i + 1
|
||
|
time.Sleep(waittime)
|
||
|
continue
|
||
|
case "ERROR":
|
||
|
return statusres, err
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
response := &Response{Response: nres}
|
||
|
|
||
|
err = CheckResponse(nres)
|
||
|
if err != nil {
|
||
|
return response, err
|
||
|
}
|
||
|
|
||
|
if v != nil {
|
||
|
if w, ok := v.(io.Writer); ok {
|
||
|
io.Copy(w, res.Body)
|
||
|
} else {
|
||
|
err = json.NewDecoder(res.Body).Decode(v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return response, err
|
||
|
}
|
||
|
|
||
|
// A Response represents an API response.
|
||
|
type Response struct {
|
||
|
*http.Response
|
||
|
}
|
||
|
|
||
|
// ErrorResponse represents an error caused by an API request.
|
||
|
// Example:
|
||
|
// {"errorCode":60001,"errorMessage":"invalid_grant:Invalid username & password combination.","error":"invalid_grant","error_description":"60001: invalid_grant:Invalid username & password combination."}
|
||
|
type ErrorResponse struct {
|
||
|
Response *http.Response // HTTP response that caused this error
|
||
|
ErrorCode int `json:"errorCode"` // error code
|
||
|
ErrorMessage string `json:"errorMessage"` // human-readable message
|
||
|
ErrorStr string `json:"error"`
|
||
|
ErrorDescription string `json:"error_description"`
|
||
|
}
|
||
|
|
||
|
// ErrorResponseList wraps an HTTP response that has a list of errors
|
||
|
type ErrorResponseList struct {
|
||
|
Response *http.Response // HTTP response that caused this error
|
||
|
Responses []ErrorResponse
|
||
|
}
|
||
|
|
||
|
// Error implements the error interface.
|
||
|
func (r ErrorResponse) Error() string {
|
||
|
return fmt.Sprintf("%v %v: %d %d %v",
|
||
|
r.Response.Request.Method, r.Response.Request.URL,
|
||
|
r.Response.StatusCode, r.ErrorCode, r.ErrorMessage)
|
||
|
}
|
||
|
|
||
|
func (r ErrorResponseList) Error() string {
|
||
|
return fmt.Sprintf("%v %v: %d %d %v",
|
||
|
r.Response.Request.Method, r.Response.Request.URL,
|
||
|
r.Response.StatusCode, r.Responses[0].ErrorCode, r.Responses[0].ErrorMessage)
|
||
|
}
|
||
|
|
||
|
// CheckAuthResponse checks the API response for errors, and returns them if so
|
||
|
func CheckAuthResponse(r *http.Response, body []byte) error {
|
||
|
if code := r.StatusCode; 200 <= code && code <= 299 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Attempt marshaling to ErrorResponse
|
||
|
var er ErrorResponse
|
||
|
err := json.Unmarshal(body, &er)
|
||
|
if err == nil {
|
||
|
er.Response = r
|
||
|
return er
|
||
|
}
|
||
|
|
||
|
// Attempt marshaling to ErrorResponseList
|
||
|
var ers []ErrorResponse
|
||
|
err = json.Unmarshal(body, &ers)
|
||
|
if err == nil {
|
||
|
return &ErrorResponseList{Response: r, Responses: ers}
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("Response had non-successful status: %d, but could not extract error from body: %+v", r.StatusCode, body)
|
||
|
}
|
||
|
|
||
|
// CheckResponse checks the API response for errors, and returns them if present.
|
||
|
// A response is considered an error if the status code is different than 2xx. Specific requests
|
||
|
// may have additional requirements, but this is sufficient in most of the cases.
|
||
|
func CheckResponse(r *http.Response) error {
|
||
|
if code := r.StatusCode; 200 <= code && code <= 299 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
body, err := ioutil.ReadAll(r.Body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Attempt marshaling to ErrorResponse
|
||
|
var er ErrorResponse
|
||
|
err = json.Unmarshal(body, &er)
|
||
|
if err == nil {
|
||
|
er.Response = r
|
||
|
return er
|
||
|
}
|
||
|
|
||
|
// Attempt marshaling to ErrorResponseList
|
||
|
var ers []ErrorResponse
|
||
|
err = json.Unmarshal(body, &ers)
|
||
|
if err == nil {
|
||
|
return &ErrorResponseList{Response: r, Responses: ers}
|
||
|
}
|
||
|
|
||
|
return fmt.Errorf("Response had non-successful status: %d, but could not extract error from body: %+v", r.StatusCode, body)
|
||
|
}
|