330 lines
8.5 KiB
Go
Executable File
330 lines
8.5 KiB
Go
Executable File
package hcloud
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hetznercloud/hcloud-go/hcloud/schema"
|
|
)
|
|
|
|
// Endpoint is the base URL of the API.
|
|
const Endpoint = "https://api.hetzner.cloud/v1"
|
|
|
|
// UserAgent is the value for the library part of the User-Agent header
|
|
// that is sent with each request.
|
|
const UserAgent = "hcloud-go/" + Version
|
|
|
|
// A BackoffFunc returns the duration to wait before performing the
|
|
// next retry. The retries argument specifies how many retries have
|
|
// already been performed. When called for the first time, retries is 0.
|
|
type BackoffFunc func(retries int) time.Duration
|
|
|
|
// ConstantBackoff returns a BackoffFunc which backs off for
|
|
// constant duration d.
|
|
func ConstantBackoff(d time.Duration) BackoffFunc {
|
|
return func(_ int) time.Duration {
|
|
return d
|
|
}
|
|
}
|
|
|
|
// ExponentialBackoff returns a BackoffFunc which implements an exponential
|
|
// backoff using the formula: b^retries * d
|
|
func ExponentialBackoff(b float64, d time.Duration) BackoffFunc {
|
|
return func(retries int) time.Duration {
|
|
return time.Duration(math.Pow(b, float64(retries))) * d
|
|
}
|
|
}
|
|
|
|
// Client is a client for the Hetzner Cloud API.
|
|
type Client struct {
|
|
endpoint string
|
|
token string
|
|
pollInterval time.Duration
|
|
backoffFunc BackoffFunc
|
|
httpClient *http.Client
|
|
applicationName string
|
|
applicationVersion string
|
|
userAgent string
|
|
|
|
Action ActionClient
|
|
Datacenter DatacenterClient
|
|
FloatingIP FloatingIPClient
|
|
Image ImageClient
|
|
ISO ISOClient
|
|
Location LocationClient
|
|
Pricing PricingClient
|
|
Server ServerClient
|
|
ServerType ServerTypeClient
|
|
SSHKey SSHKeyClient
|
|
Volume VolumeClient
|
|
}
|
|
|
|
// A ClientOption is used to configure a Client.
|
|
type ClientOption func(*Client)
|
|
|
|
// WithEndpoint configures a Client to use the specified API endpoint.
|
|
func WithEndpoint(endpoint string) ClientOption {
|
|
return func(client *Client) {
|
|
client.endpoint = strings.TrimRight(endpoint, "/")
|
|
}
|
|
}
|
|
|
|
// WithToken configures a Client to use the specified token for authentication.
|
|
func WithToken(token string) ClientOption {
|
|
return func(client *Client) {
|
|
client.token = token
|
|
}
|
|
}
|
|
|
|
// WithPollInterval configures a Client to use the specified interval when polling
|
|
// from the API.
|
|
func WithPollInterval(pollInterval time.Duration) ClientOption {
|
|
return func(client *Client) {
|
|
client.pollInterval = pollInterval
|
|
}
|
|
}
|
|
|
|
// WithBackoffFunc configures a Client to use the specified backoff function.
|
|
func WithBackoffFunc(f BackoffFunc) ClientOption {
|
|
return func(client *Client) {
|
|
client.backoffFunc = f
|
|
}
|
|
}
|
|
|
|
// WithApplication configures a Client with the given application name and
|
|
// application version. The version may be blank. Programs are encouraged
|
|
// to at least set an application name.
|
|
func WithApplication(name, version string) ClientOption {
|
|
return func(client *Client) {
|
|
client.applicationName = name
|
|
client.applicationVersion = version
|
|
}
|
|
}
|
|
|
|
// NewClient creates a new client.
|
|
func NewClient(options ...ClientOption) *Client {
|
|
client := &Client{
|
|
endpoint: Endpoint,
|
|
httpClient: &http.Client{},
|
|
backoffFunc: ExponentialBackoff(2, 500*time.Millisecond),
|
|
pollInterval: 500 * time.Millisecond,
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(client)
|
|
}
|
|
|
|
client.buildUserAgent()
|
|
|
|
client.Action = ActionClient{client: client}
|
|
client.Datacenter = DatacenterClient{client: client}
|
|
client.FloatingIP = FloatingIPClient{client: client}
|
|
client.Image = ImageClient{client: client}
|
|
client.ISO = ISOClient{client: client}
|
|
client.Location = LocationClient{client: client}
|
|
client.Pricing = PricingClient{client: client}
|
|
client.Server = ServerClient{client: client}
|
|
client.ServerType = ServerTypeClient{client: client}
|
|
client.SSHKey = SSHKeyClient{client: client}
|
|
client.Volume = VolumeClient{client: client}
|
|
|
|
return client
|
|
}
|
|
|
|
// NewRequest creates an HTTP request against the API. The returned request
|
|
// is assigned with ctx and has all necessary headers set (auth, user agent, etc.).
|
|
func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) {
|
|
url := c.endpoint + path
|
|
req, err := http.NewRequest(method, url, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.token))
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req = req.WithContext(ctx)
|
|
return req, nil
|
|
}
|
|
|
|
// Do performs an HTTP request against the API.
|
|
func (c *Client) Do(r *http.Request, v interface{}) (*Response, error) {
|
|
var retries int
|
|
for {
|
|
resp, err := c.httpClient.Do(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response := &Response{Response: resp}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return response, err
|
|
}
|
|
resp.Body.Close()
|
|
resp.Body = ioutil.NopCloser(bytes.NewReader(body))
|
|
|
|
if err = response.readMeta(body); err != nil {
|
|
return response, fmt.Errorf("hcloud: error reading response meta data: %s", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 400 && resp.StatusCode <= 599 {
|
|
err = errorFromResponse(resp, body)
|
|
if err == nil {
|
|
err = fmt.Errorf("hcloud: server responded with status code %d", resp.StatusCode)
|
|
} else {
|
|
if err, ok := err.(Error); ok && err.Code == ErrorCodeRateLimitExceeded {
|
|
c.backoff(retries)
|
|
retries++
|
|
continue
|
|
}
|
|
}
|
|
return response, err
|
|
}
|
|
if v != nil {
|
|
if w, ok := v.(io.Writer); ok {
|
|
_, err = io.Copy(w, bytes.NewReader(body))
|
|
} else {
|
|
err = json.Unmarshal(body, v)
|
|
}
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
}
|
|
|
|
func (c *Client) backoff(retries int) {
|
|
time.Sleep(c.backoffFunc(retries))
|
|
}
|
|
|
|
func (c *Client) all(f func(int) (*Response, error)) (*Response, error) {
|
|
var (
|
|
page = 1
|
|
)
|
|
for {
|
|
resp, err := f(page)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 {
|
|
return resp, nil
|
|
}
|
|
page = resp.Meta.Pagination.NextPage
|
|
}
|
|
}
|
|
|
|
func (c *Client) buildUserAgent() {
|
|
switch {
|
|
case c.applicationName != "" && c.applicationVersion != "":
|
|
c.userAgent = c.applicationName + "/" + c.applicationVersion + " " + UserAgent
|
|
case c.applicationName != "" && c.applicationVersion == "":
|
|
c.userAgent = c.applicationName + " " + UserAgent
|
|
default:
|
|
c.userAgent = UserAgent
|
|
}
|
|
}
|
|
|
|
func errorFromResponse(resp *http.Response, body []byte) error {
|
|
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
|
|
return nil
|
|
}
|
|
|
|
var respBody schema.ErrorResponse
|
|
if err := json.Unmarshal(body, &respBody); err != nil {
|
|
return nil
|
|
}
|
|
if respBody.Error.Code == "" && respBody.Error.Message == "" {
|
|
return nil
|
|
}
|
|
return ErrorFromSchema(respBody.Error)
|
|
}
|
|
|
|
// Response represents a response from the API. It embeds http.Response.
|
|
type Response struct {
|
|
*http.Response
|
|
Meta Meta
|
|
}
|
|
|
|
func (r *Response) readMeta(body []byte) error {
|
|
if h := r.Header.Get("RateLimit-Limit"); h != "" {
|
|
r.Meta.Ratelimit.Limit, _ = strconv.Atoi(h)
|
|
}
|
|
if h := r.Header.Get("RateLimit-Remaining"); h != "" {
|
|
r.Meta.Ratelimit.Remaining, _ = strconv.Atoi(h)
|
|
}
|
|
if h := r.Header.Get("RateLimit-Reset"); h != "" {
|
|
if ts, err := strconv.ParseInt(h, 10, 64); err == nil {
|
|
r.Meta.Ratelimit.Reset = time.Unix(ts, 0)
|
|
}
|
|
}
|
|
|
|
if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
|
|
var s schema.MetaResponse
|
|
if err := json.Unmarshal(body, &s); err != nil {
|
|
return err
|
|
}
|
|
if s.Meta.Pagination != nil {
|
|
p := PaginationFromSchema(*s.Meta.Pagination)
|
|
r.Meta.Pagination = &p
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Meta represents meta information included in an API response.
|
|
type Meta struct {
|
|
Pagination *Pagination
|
|
Ratelimit Ratelimit
|
|
}
|
|
|
|
// Pagination represents pagination meta information.
|
|
type Pagination struct {
|
|
Page int
|
|
PerPage int
|
|
PreviousPage int
|
|
NextPage int
|
|
LastPage int
|
|
TotalEntries int
|
|
}
|
|
|
|
// Ratelimit represents ratelimit information.
|
|
type Ratelimit struct {
|
|
Limit int
|
|
Remaining int
|
|
Reset time.Time
|
|
}
|
|
|
|
// ListOpts specifies options for listing resources.
|
|
type ListOpts struct {
|
|
Page int // Page (starting at 1)
|
|
PerPage int // Items per page (0 means default)
|
|
LabelSelector string // Label selector for filtering by labels
|
|
}
|
|
|
|
func valuesForListOpts(opts ListOpts) url.Values {
|
|
vals := url.Values{}
|
|
if opts.Page > 0 {
|
|
vals.Add("page", strconv.Itoa(opts.Page))
|
|
}
|
|
if opts.PerPage > 0 {
|
|
vals.Add("per_page", strconv.Itoa(opts.PerPage))
|
|
}
|
|
if len(opts.LabelSelector) > 0 {
|
|
vals.Add("label_selector", opts.LabelSelector)
|
|
}
|
|
return vals
|
|
}
|