client.go raw

   1  package internal
   2  
   3  import (
   4  	"bytes"
   5  	"context"
   6  	"encoding/json"
   7  	"errors"
   8  	"fmt"
   9  	"io"
  10  	"net/http"
  11  	"net/url"
  12  	"time"
  13  
  14  	"github.com/go-acme/lego/v4/providers/dns/internal/errutils"
  15  )
  16  
  17  const defaultServer = "console.ves.volterra.io"
  18  
  19  const authorizationHeader = "Authorization"
  20  
  21  // Client the F5 XC API client.
  22  type Client struct {
  23  	apiToken string
  24  
  25  	baseURL    *url.URL
  26  	HTTPClient *http.Client
  27  }
  28  
  29  // NewClient creates a new Client.
  30  func NewClient(apiToken, tenantName, server string) (*Client, error) {
  31  	if apiToken == "" {
  32  		return nil, errors.New("credentials missing")
  33  	}
  34  
  35  	baseURL, err := createBaseURL(tenantName, server)
  36  	if err != nil {
  37  		return nil, err
  38  	}
  39  
  40  	return &Client{
  41  		apiToken:   apiToken,
  42  		baseURL:    baseURL,
  43  		HTTPClient: &http.Client{Timeout: 10 * time.Second},
  44  	}, nil
  45  }
  46  
  47  // CreateRRSet creates RRSet.
  48  // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Create
  49  func (c *Client) CreateRRSet(ctx context.Context, dnsZoneName, groupName string, rrSet RRSet) (*APIRRSet, error) {
  50  	endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName)
  51  
  52  	req, err := newJSONRequest(ctx, http.MethodPost, endpoint, APIRRSet{
  53  		DNSZoneName: dnsZoneName,
  54  		GroupName:   groupName,
  55  		RRSet:       rrSet,
  56  	})
  57  	if err != nil {
  58  		return nil, err
  59  	}
  60  
  61  	result := &APIRRSet{}
  62  
  63  	err = c.do(req, result)
  64  	if err != nil {
  65  		return nil, err
  66  	}
  67  
  68  	return result, nil
  69  }
  70  
  71  // GetRRSet gets RRSets.
  72  // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Get
  73  func (c *Client) GetRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {
  74  	endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
  75  
  76  	req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
  77  	if err != nil {
  78  		return nil, err
  79  	}
  80  
  81  	result := &APIRRSet{}
  82  
  83  	err = c.do(req, result)
  84  	if err != nil {
  85  		usce := &APIError{}
  86  		if errors.As(err, &usce) && usce.StatusCode == http.StatusNotFound {
  87  			return nil, nil
  88  		}
  89  
  90  		return nil, err
  91  	}
  92  
  93  	return result, nil
  94  }
  95  
  96  // DeleteRRSet deletes RRSet.
  97  // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Delete
  98  func (c *Client) DeleteRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string) (*APIRRSet, error) {
  99  	endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
 100  
 101  	req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
 102  	if err != nil {
 103  		return nil, err
 104  	}
 105  
 106  	result := &APIRRSet{}
 107  
 108  	err = c.do(req, result)
 109  	if err != nil {
 110  		return nil, err
 111  	}
 112  
 113  	return result, nil
 114  }
 115  
 116  // ReplaceRRSet replaces RRSet.
 117  // https://docs.cloud.f5.com/docs-v2/api/dns-zone-rrset#operation/ves.io.schema.dns_zone.rrset.CustomAPI.Replace
 118  func (c *Client) ReplaceRRSet(ctx context.Context, dnsZoneName, groupName, recordName, recordType string, rrSet RRSet) (*APIRRSet, error) {
 119  	endpoint := c.baseURL.JoinPath("api", "config", "dns", "namespaces", "system", "dns_zones", dnsZoneName, "rrsets", groupName, recordName, recordType)
 120  
 121  	req, err := newJSONRequest(ctx, http.MethodPut, endpoint, APIRRSet{
 122  		DNSZoneName: dnsZoneName,
 123  		GroupName:   groupName,
 124  		RRSet:       rrSet,
 125  		Type:        recordType,
 126  	})
 127  	if err != nil {
 128  		return nil, err
 129  	}
 130  
 131  	result := &APIRRSet{}
 132  
 133  	err = c.do(req, result)
 134  	if err != nil {
 135  		return nil, err
 136  	}
 137  
 138  	return result, nil
 139  }
 140  
 141  func (c *Client) do(req *http.Request, result any) error {
 142  	req.Header.Set(authorizationHeader, "APIToken "+c.apiToken)
 143  
 144  	resp, err := c.HTTPClient.Do(req)
 145  	if err != nil {
 146  		return errutils.NewHTTPDoError(req, err)
 147  	}
 148  
 149  	defer func() { _ = resp.Body.Close() }()
 150  
 151  	if resp.StatusCode/100 != 2 {
 152  		return parseError(req, resp)
 153  	}
 154  
 155  	if result == nil {
 156  		return nil
 157  	}
 158  
 159  	raw, err := io.ReadAll(resp.Body)
 160  	if err != nil {
 161  		return errutils.NewReadResponseError(req, resp.StatusCode, err)
 162  	}
 163  
 164  	err = json.Unmarshal(raw, result)
 165  	if err != nil {
 166  		return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
 167  	}
 168  
 169  	return nil
 170  }
 171  
 172  func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
 173  	buf := new(bytes.Buffer)
 174  
 175  	if payload != nil {
 176  		err := json.NewEncoder(buf).Encode(payload)
 177  		if err != nil {
 178  			return nil, fmt.Errorf("failed to create request JSON body: %w", err)
 179  		}
 180  	}
 181  
 182  	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
 183  	if err != nil {
 184  		return nil, fmt.Errorf("unable to create request: %w", err)
 185  	}
 186  
 187  	req.Header.Set("Accept", "application/json")
 188  
 189  	if payload != nil {
 190  		req.Header.Set("Content-Type", "application/json")
 191  	}
 192  
 193  	return req, nil
 194  }
 195  
 196  func parseError(req *http.Request, resp *http.Response) error {
 197  	raw, _ := io.ReadAll(resp.Body)
 198  
 199  	apiErr := APIError{StatusCode: resp.StatusCode}
 200  
 201  	err := json.Unmarshal(raw, &apiErr)
 202  	if err != nil {
 203  		return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw)
 204  	}
 205  
 206  	return &apiErr
 207  }
 208  
 209  func createBaseURL(tenant, server string) (*url.URL, error) {
 210  	if tenant == "" {
 211  		return nil, errors.New("missing tenant name")
 212  	}
 213  
 214  	if server == "" {
 215  		server = defaultServer
 216  	}
 217  
 218  	baseURL, err := url.Parse(fmt.Sprintf("https://%s.%s", tenant, server))
 219  	if err != nil {
 220  		return nil, fmt.Errorf("parse base URL: %w", err)
 221  	}
 222  
 223  	return baseURL, nil
 224  }
 225