client.go raw

   1  /*
   2  Package internal Civo API client.
   3  
   4  Because the dependencies on k8s, the official client cannot be used.
   5  - https://github.com/civo/civogo/blob/v0.2.99/go.mod -> k8s.io/client-go
   6  - https://github.com/civo/civogo/blob/v0.3.34/go.mod -> k8s.io/api
   7  - https://github.com/civo/civogo/blob/v0.3.38/go.mod -> k8s.io/api + k8s.io/apimachinery
   8  - Current version -> https://github.com/civo/civogo/blob/v0.6.1/go.mod
   9  */
  10  package internal
  11  
  12  import (
  13  	"bytes"
  14  	"context"
  15  	"encoding/json"
  16  	"fmt"
  17  	"io"
  18  	"net/http"
  19  	"net/url"
  20  	"time"
  21  
  22  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  23  	"github.com/go-acme/lego/v4/providers/dns/internal/useragent"
  24  	"golang.org/x/oauth2"
  25  )
  26  
  27  const defaultBaseURL = "https://api.civo.com/v2"
  28  
  29  // Client the Civo API client.
  30  type Client struct {
  31  	region string
  32  
  33  	BaseURL    *url.URL
  34  	HTTPClient *http.Client
  35  }
  36  
  37  // NewClient creates a new Client.
  38  func NewClient(hc *http.Client, region string) (*Client, error) {
  39  	baseURL, _ := url.Parse(defaultBaseURL)
  40  
  41  	if hc == nil {
  42  		hc = &http.Client{Timeout: 10 * time.Second}
  43  	}
  44  
  45  	return &Client{
  46  		region:     region,
  47  		BaseURL:    baseURL,
  48  		HTTPClient: hc,
  49  	}, nil
  50  }
  51  
  52  // ListDomains a list of all domain names within the account.
  53  // https://www.civo.com/api/dns#list-domain-names
  54  func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
  55  	endpoint := c.BaseURL.JoinPath("dns")
  56  
  57  	req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  58  	if err != nil {
  59  		return nil, err
  60  	}
  61  
  62  	var result []Domain
  63  
  64  	err = c.do(req, &result)
  65  	if err != nil {
  66  		return nil, err
  67  	}
  68  
  69  	return result, nil
  70  }
  71  
  72  // ListDNSRecords a list of all DNS records in the specified domain.
  73  // https://www.civo.com/api/dns#list-dns-records
  74  func (c *Client) ListDNSRecords(ctx context.Context, domainID string) ([]Record, error) {
  75  	endpoint := c.BaseURL.JoinPath("dns", domainID, "records")
  76  
  77  	req, err := c.newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  78  	if err != nil {
  79  		return nil, err
  80  	}
  81  
  82  	var result []Record
  83  
  84  	err = c.do(req, &result)
  85  	if err != nil {
  86  		return nil, err
  87  	}
  88  
  89  	return result, nil
  90  }
  91  
  92  // CreateDNSRecord creates DNS records for a specific domain.
  93  // https://www.civo.com/api/dns#create-a-new-dns-record
  94  func (c *Client) CreateDNSRecord(ctx context.Context, domainID string, record Record) (*Record, error) {
  95  	endpoint := c.BaseURL.JoinPath("dns", domainID, "records")
  96  
  97  	req, err := c.newJSONRequest(ctx, http.MethodPost, endpoint, record)
  98  	if err != nil {
  99  		return nil, err
 100  	}
 101  
 102  	var result Record
 103  
 104  	err = c.do(req, &result)
 105  	if err != nil {
 106  		return nil, err
 107  	}
 108  
 109  	return &result, nil
 110  }
 111  
 112  // DeleteDNSRecord remove a DNS record from a domain.
 113  // https://www.civo.com/api/dns#deleting-a-dns-record
 114  func (c *Client) DeleteDNSRecord(ctx context.Context, record Record) error {
 115  	endpoint := c.BaseURL.JoinPath("dns", record.DomainID, "records", record.ID)
 116  
 117  	req, err := c.newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 118  	if err != nil {
 119  		return err
 120  	}
 121  
 122  	return c.do(req, nil)
 123  }
 124  
 125  func (c *Client) do(req *http.Request, result any) error {
 126  	resp, err := c.HTTPClient.Do(req)
 127  	if err != nil {
 128  		return errutils.NewHTTPDoError(req, err)
 129  	}
 130  
 131  	defer func() { _ = resp.Body.Close() }()
 132  
 133  	if resp.StatusCode/100 != 2 {
 134  		return parseError(req, resp)
 135  	}
 136  
 137  	if result == nil {
 138  		return nil
 139  	}
 140  
 141  	raw, err := io.ReadAll(resp.Body)
 142  	if err != nil {
 143  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 144  	}
 145  
 146  	err = json.Unmarshal(raw, result)
 147  	if err != nil {
 148  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 149  	}
 150  
 151  	return nil
 152  }
 153  
 154  func (c *Client) newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 155  	buf := new(bytes.Buffer)
 156  
 157  	if payload != nil {
 158  		err := json.NewEncoder(buf).Encode(payload)
 159  		if err != nil {
 160  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 161  		}
 162  	}
 163  
 164  	if method == http.MethodGet || method == http.MethodDelete {
 165  		query := endpoint.Query()
 166  		query.Set("region", c.region)
 167  
 168  		endpoint.RawQuery = query.Encode()
 169  	}
 170  
 171  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 172  	if err != nil {
 173  		return nil, fmt.Errorf("unable to create request: %w", err)
 174  	}
 175  
 176  	req.Header.Set("Accept", "application/json")
 177  
 178  	if payload != nil {
 179  		req.Header.Set("Content-Type", "application/json")
 180  	}
 181  
 182  	useragent.SetHeader(req.Header)
 183  
 184  	return req, nil
 185  }
 186  
 187  func parseError(req *http.Request, resp *http.Response) error {
 188  	raw, _ := io.ReadAll(resp.Body)
 189  
 190  	var errAPI APIError
 191  
 192  	err := json.Unmarshal(raw, &errAPI)
 193  	if err != nil {
 194  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 195  	}
 196  
 197  	return &errAPI
 198  }
 199  
 200  // OAuthStaticAccessToken Authorization header.
 201  // https://www.civo.com/api#authentication
 202  func OAuthStaticAccessToken(client *http.Client, accessToken string) *http.Client {
 203  	if client == nil {
 204  		client = &http.Client{Timeout: 5 * time.Second}
 205  	}
 206  
 207  	client.Transport = &oauth2.Transport{
 208  		Source: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}),
 209  		Base:   client.Transport,
 210  	}
 211  
 212  	return client
 213  }
 214