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 }