sigs.k8s.io/external-dns@v0.14.1/provider/godaddy/client.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package godaddy
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"math/rand"
    27  	"net/http"
    28  	"strconv"
    29  	"time"
    30  
    31  	"golang.org/x/time/rate"
    32  
    33  	"sigs.k8s.io/external-dns/pkg/apis/externaldns"
    34  )
    35  
    36  // DefaultTimeout api requests after 180s
    37  const DefaultTimeout = 180 * time.Second
    38  
    39  // Errors
    40  var (
    41  	ErrAPIDown = errors.New("godaddy: the GoDaddy API is down")
    42  )
    43  
    44  // APIError error
    45  type APIError struct {
    46  	Code    string
    47  	Message string
    48  }
    49  
    50  func (err *APIError) Error() string {
    51  	return fmt.Sprintf("Error %s: %q", err.Code, err.Message)
    52  }
    53  
    54  // Logger is the interface that should be implemented for loggers that wish to
    55  // log HTTP requests and HTTP responses.
    56  type Logger interface {
    57  	// LogRequest logs an HTTP request.
    58  	LogRequest(*http.Request)
    59  
    60  	// LogResponse logs an HTTP response.
    61  	LogResponse(*http.Response)
    62  }
    63  
    64  // Client represents a client to call the GoDaddy API
    65  type Client struct {
    66  	// APIKey holds the Application key
    67  	APIKey string
    68  
    69  	// APISecret holds the Application secret key
    70  	APISecret string
    71  
    72  	// API endpoint
    73  	APIEndPoint string
    74  
    75  	// Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default.
    76  	Client *http.Client
    77  
    78  	// GoDaddy limits to 60 requests per minute
    79  	Ratelimiter *rate.Limiter
    80  
    81  	// Logger is used to log HTTP requests and responses.
    82  	Logger Logger
    83  
    84  	Timeout time.Duration
    85  }
    86  
    87  // GDErrorField describe the error reason
    88  type GDErrorField struct {
    89  	Code        string `json:"code,omitempty"`
    90  	Message     string `json:"message,omitempty"`
    91  	Path        string `json:"path,omitempty"`
    92  	PathRelated string `json:"pathRelated,omitempty"`
    93  }
    94  
    95  // GDErrorResponse is the body response when an API call fails
    96  type GDErrorResponse struct {
    97  	Code    string         `json:"code"`
    98  	Fields  []GDErrorField `json:"fields,omitempty"`
    99  	Message string         `json:"message,omitempty"`
   100  }
   101  
   102  func (r GDErrorResponse) String() string {
   103  	if b, err := json.Marshal(r); err == nil {
   104  		return string(b)
   105  	}
   106  
   107  	return "<error>"
   108  }
   109  
   110  // NewClient represents a new client to call the API
   111  func NewClient(useOTE bool, apiKey, apiSecret string) (*Client, error) {
   112  	var endpoint string
   113  
   114  	if useOTE {
   115  		endpoint = "https://api.ote-godaddy.com"
   116  	} else {
   117  		endpoint = "https://api.godaddy.com"
   118  	}
   119  
   120  	client := Client{
   121  		APIKey:      apiKey,
   122  		APISecret:   apiSecret,
   123  		APIEndPoint: endpoint,
   124  		Client:      &http.Client{},
   125  		// Add one token every second
   126  		Ratelimiter: rate.NewLimiter(rate.Every(time.Second), 60),
   127  		Timeout:     DefaultTimeout,
   128  	}
   129  
   130  	// Get and check the configuration
   131  	if err := client.validate(); err != nil {
   132  		return nil, err
   133  	}
   134  	return &client, nil
   135  }
   136  
   137  //
   138  // Common request wrappers
   139  //
   140  
   141  // Get is a wrapper for the GET method
   142  func (c *Client) Get(url string, resType interface{}) error {
   143  	return c.CallAPI("GET", url, nil, resType, true)
   144  }
   145  
   146  // Patch is a wrapper for the PATCH method
   147  func (c *Client) Patch(url string, reqBody, resType interface{}) error {
   148  	return c.CallAPI("PATCH", url, reqBody, resType, true)
   149  }
   150  
   151  // Post is a wrapper for the POST method
   152  func (c *Client) Post(url string, reqBody, resType interface{}) error {
   153  	return c.CallAPI("POST", url, reqBody, resType, true)
   154  }
   155  
   156  // Put is a wrapper for the PUT method
   157  func (c *Client) Put(url string, reqBody, resType interface{}) error {
   158  	return c.CallAPI("PUT", url, reqBody, resType, true)
   159  }
   160  
   161  // Delete is a wrapper for the DELETE method
   162  func (c *Client) Delete(url string, resType interface{}) error {
   163  	return c.CallAPI("DELETE", url, nil, resType, true)
   164  }
   165  
   166  // GetWithContext is a wrapper for the GET method
   167  func (c *Client) GetWithContext(ctx context.Context, url string, resType interface{}) error {
   168  	return c.CallAPIWithContext(ctx, "GET", url, nil, resType, true)
   169  }
   170  
   171  // PatchWithContext is a wrapper for the PATCH method
   172  func (c *Client) PatchWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
   173  	return c.CallAPIWithContext(ctx, "PATCH", url, reqBody, resType, true)
   174  }
   175  
   176  // PostWithContext is a wrapper for the POST method
   177  func (c *Client) PostWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
   178  	return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, true)
   179  }
   180  
   181  // PutWithContext is a wrapper for the PUT method
   182  func (c *Client) PutWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
   183  	return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, true)
   184  }
   185  
   186  // DeleteWithContext is a wrapper for the DELETE method
   187  func (c *Client) DeleteWithContext(ctx context.Context, url string, resType interface{}) error {
   188  	return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, true)
   189  }
   190  
   191  // NewRequest returns a new HTTP request
   192  func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) {
   193  	var body []byte
   194  	var err error
   195  
   196  	if reqBody != nil {
   197  		body, err = json.Marshal(reqBody)
   198  		if err != nil {
   199  			return nil, err
   200  		}
   201  	}
   202  
   203  	target := fmt.Sprintf("%s%s", c.APIEndPoint, path)
   204  	req, err := http.NewRequest(method, target, bytes.NewReader(body))
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	// Inject headers
   210  	if body != nil {
   211  		req.Header.Set("Content-Type", "application/json;charset=utf-8")
   212  	}
   213  	req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", c.APIKey, c.APISecret))
   214  	req.Header.Set("Accept", "application/json")
   215  	req.Header.Set("User-Agent", "ExternalDNS/"+externaldns.Version)
   216  
   217  	// Send the request with requested timeout
   218  	c.Client.Timeout = c.Timeout
   219  
   220  	return req, nil
   221  }
   222  
   223  // Do sends an HTTP request and returns an HTTP response
   224  func (c *Client) Do(req *http.Request) (*http.Response, error) {
   225  	if c.Logger != nil {
   226  		c.Logger.LogRequest(req)
   227  	}
   228  
   229  	c.Ratelimiter.Wait(req.Context())
   230  	resp, err := c.Client.Do(req)
   231  	// In case of several clients behind NAT we still can hit rate limit
   232  	for i := 1; i < 3 && err == nil && resp.StatusCode == 429; i++ {
   233  		retryAfter, _ := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 0)
   234  
   235  		jitter := rand.Int63n(retryAfter)
   236  		retryAfterSec := retryAfter + jitter/2
   237  
   238  		sleepTime := time.Duration(retryAfterSec) * time.Second
   239  		time.Sleep(sleepTime)
   240  
   241  		c.Ratelimiter.Wait(req.Context())
   242  		resp, err = c.Client.Do(req)
   243  	}
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	if c.Logger != nil {
   248  		c.Logger.LogResponse(resp)
   249  	}
   250  	return resp, nil
   251  }
   252  
   253  // CallAPI is the lowest level call helper. If needAuth is true,
   254  // inject authentication headers and sign the request.
   255  //
   256  // Request signature is a sha1 hash on following fields, joined by '+':
   257  // - applicationSecret (from Client instance)
   258  // - consumerKey (from Client instance)
   259  // - capitalized method (from arguments)
   260  // - full request url, including any query string argument
   261  // - full serialized request body
   262  // - server current time (takes time delta into account)
   263  //
   264  // Call will automatically assemble the target url from the endpoint
   265  // configured in the client instance and the path argument. If the reqBody
   266  // argument is not nil, it will also serialize it as json and inject
   267  // the required Content-Type header.
   268  //
   269  // If everything went fine, unmarshall response into resType and return nil
   270  // otherwise, return the error
   271  func (c *Client) CallAPI(method, path string, reqBody, resType interface{}, needAuth bool) error {
   272  	return c.CallAPIWithContext(context.Background(), method, path, reqBody, resType, needAuth)
   273  }
   274  
   275  // CallAPIWithContext is the lowest level call helper. If needAuth is true,
   276  // inject authentication headers and sign the request.
   277  //
   278  // Request signature is a sha1 hash on following fields, joined by '+':
   279  // - applicationSecret (from Client instance)
   280  // - consumerKey (from Client instance)
   281  // - capitalized method (from arguments)
   282  // - full request url, including any query string argument
   283  // - full serialized request body
   284  // - server current time (takes time delta into account)
   285  //
   286  // # Context is used by http.Client to handle context cancelation
   287  //
   288  // Call will automatically assemble the target url from the endpoint
   289  // configured in the client instance and the path argument. If the reqBody
   290  // argument is not nil, it will also serialize it as json and inject
   291  // the required Content-Type header.
   292  //
   293  // If everything went fine, unmarshall response into resType and return nil
   294  // otherwise, return the error
   295  func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, reqBody, resType interface{}, needAuth bool) error {
   296  	req, err := c.NewRequest(method, path, reqBody, needAuth)
   297  	if err != nil {
   298  		return err
   299  	}
   300  	req = req.WithContext(ctx)
   301  	response, err := c.Do(req)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	return c.UnmarshalResponse(response, resType)
   306  }
   307  
   308  // UnmarshalResponse checks the response and unmarshals it into the response
   309  // type if needed Helper function, called from CallAPI
   310  func (c *Client) UnmarshalResponse(response *http.Response, resType interface{}) error {
   311  	// Read all the response body
   312  	defer response.Body.Close()
   313  	body, err := io.ReadAll(response.Body)
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	// < 200 && >= 300 : API error
   319  	if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
   320  		apiError := &APIError{
   321  			Code: fmt.Sprintf("HTTPStatus: %d", response.StatusCode),
   322  		}
   323  
   324  		if err = json.Unmarshal(body, apiError); err != nil {
   325  			return err
   326  		}
   327  
   328  		return apiError
   329  	}
   330  
   331  	// Nothing to unmarshal
   332  	if len(body) == 0 || resType == nil {
   333  		return nil
   334  	}
   335  
   336  	return json.Unmarshal(body, &resType)
   337  }
   338  
   339  func (c *Client) validate() error {
   340  	var response interface{}
   341  
   342  	if err := c.Get(domainsURI, response); err != nil {
   343  		return err
   344  	}
   345  
   346  	return nil
   347  }