1package gsclient
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"io/ioutil"
10	"net"
11	"net/http"
12	"strconv"
13	"time"
14)
15
16//gsRequest gridscale's custom gsRequest struct
17type gsRequest struct {
18	uri                 string
19	method              string
20	body                interface{}
21	skipCheckingRequest bool
22}
23
24//CreateResponse common struct of a response for creation
25type CreateResponse struct {
26	//UUID of the object being created
27	ObjectUUID string `json:"object_uuid"`
28
29	//UUID of the request
30	RequestUUID string `json:"request_uuid"`
31}
32
33//RequestStatus status of a request
34type RequestStatus map[string]RequestStatusProperties
35
36//RequestStatusProperties JSON struct of properties of a request's status
37type RequestStatusProperties struct {
38	Status     string `json:"status"`
39	Message    string `json:"message"`
40	CreateTime GSTime `json:"create_time"`
41}
42
43//RequestError error of a request
44type RequestError struct {
45	Title       string `json:"title"`
46	Description string `json:"description"`
47	StatusCode  int
48	RequestUUID string
49}
50
51//Error just returns error as string
52func (r RequestError) Error() string {
53	message := r.Description
54	if message == "" {
55		message = "no error message received from server"
56	}
57	errorMessageFormat := "Status code: %v. Error: %s. Request UUID: %s. "
58	if r.StatusCode >= 500 {
59		errorMessageFormat += "Please report this error along with the request UUID."
60	}
61	return fmt.Sprintf(errorMessageFormat, r.StatusCode, message, r.RequestUUID)
62}
63
64const (
65	requestUUIDHeaderParam           = "X-Request-Id"
66	requestRateLimitResetHeaderParam = "ratelimit-reset"
67)
68
69//This function takes the client and a struct and then adds the result to the given struct if possible
70func (r *gsRequest) execute(ctx context.Context, c Client, output interface{}) error {
71
72	//Prepare http request (including HTTP headers preparation, etc.)
73	httpReq, err := r.prepareHTTPRequest(ctx, c.cfg)
74	logger.Debugf("Request body: %v", httpReq.Body)
75	logger.Debugf("Request headers: %v", httpReq.Header)
76
77	//Execute the request (including retrying when needed)
78	requestUUID, responseBodyBytes, err := r.retryHTTPRequest(ctx, c.HttpClient(), httpReq, c.MaxNumberOfRetries(), c.DelayInterval())
79	if err != nil {
80		return err
81	}
82
83	//if output is set
84	if output != nil {
85		//Unmarshal body bytes to the given struct
86		err = json.Unmarshal(responseBodyBytes, output)
87		if err != nil {
88			logger.Errorf("Error while marshaling JSON: %v", err)
89			return err
90		}
91	}
92
93	//If the client is synchronous, and the request does not skip
94	//checking a request, wait until the request completes
95	if c.Synchronous() && !r.skipCheckingRequest {
96		return c.waitForRequestCompleted(ctx, requestUUID)
97	}
98	return nil
99}
100
101//prepareHTTPRequest prepares a http request
102func (r *gsRequest) prepareHTTPRequest(ctx context.Context, cfg *Config) (*http.Request, error) {
103	url := cfg.apiURL + r.uri
104	logger.Debugf("Preparing %v request sent to URL: %v", r.method, url)
105
106	//Convert the body of the request to json
107	jsonBody := new(bytes.Buffer)
108	if r.body != nil {
109		err := json.NewEncoder(jsonBody).Encode(r.body)
110		if err != nil {
111			return nil, err
112		}
113	}
114
115	//Add authentication headers and content type
116	request, err := http.NewRequest(r.method, url, jsonBody)
117	if err != nil {
118		return nil, err
119	}
120	request = request.WithContext(ctx)
121	request.Header.Set("User-Agent", cfg.userAgent)
122	request.Header.Set("X-Auth-UserID", cfg.userUUID)
123	request.Header.Set("X-Auth-Token", cfg.apiToken)
124	request.Header.Set("Content-Type", bodyType)
125
126	//Set headers based on a given list of custom headers
127	//Use Header.Set() instead of Header.Add() because we want to
128	//override the headers' values if they are already set.
129	for k, v := range cfg.httpHeaders {
130		request.Header.Set(k, v)
131	}
132
133	return request, nil
134}
135
136//retryHTTPRequest runs & retries a HTTP request
137//returns UUID (string), response body ([]byte), error
138func (r *gsRequest) retryHTTPRequest(
139	ctx context.Context,
140	httpClient *http.Client,
141	httpReq *http.Request,
142	maxNoOfRetries int,
143	delayInterval time.Duration,
144) (string, []byte, error) {
145	//Init request UUID variable
146	var requestUUID string
147	//Init empty response body
148	var responseBodyBytes []byte
149	//
150	err := retryWithLimitedNumOfRetries(func() (bool, error) {
151		//execute the request
152		resp, err := httpClient.Do(httpReq)
153		if err != nil {
154			//If the error is caused by expired context, return context error and no need to retry
155			if ctx.Err() != nil {
156				return false, ctx.Err()
157			}
158
159			if err, ok := err.(net.Error); ok {
160				// exclude retry request with none GET method (write operations) in case of a request timeout or a context error
161				if err.Timeout() && r.method != http.MethodGet {
162					return false, err
163				}
164				logger.Debugf("Retrying request due to network error %v", err)
165				return true, err
166			}
167			logger.Errorf("Error while executing the request: %v", err)
168			//stop retrying (false) and return error
169			return false, err
170		}
171		//Close body to prevent resource leak
172		defer resp.Body.Close()
173
174		statusCode := resp.StatusCode
175		requestUUID = resp.Header.Get(requestUUIDHeaderParam)
176		responseBodyBytes, err = ioutil.ReadAll(resp.Body)
177		if err != nil {
178			logger.Errorf("Error while reading the response's body: %v", err)
179			//stop retrying (false) and return error
180			return false, err
181		}
182
183		logger.Debugf("Status code: %v. Request UUID: %v. Headers: %v", statusCode, requestUUID, resp.Header)
184
185		//If the status code is an error code
186		if statusCode >= 300 {
187			var errorMessage RequestError //error messages have a different structure, so they are read with a different struct
188			errorMessage.StatusCode = statusCode
189			errorMessage.RequestUUID = requestUUID
190			json.Unmarshal(responseBodyBytes, &errorMessage)
191
192			//If the error is retryable
193			if isErrorHTTPCodeRetryable(statusCode) {
194				//If status code is 429, that means we reach the rate limit
195				if statusCode == 429 {
196					//Get the time that the rate limit will be reset
197					rateLimitResetTimestamp := resp.Header.Get(requestRateLimitResetHeaderParam)
198					//Get the delay time
199					delayMs, err := getDelayTimeInMsFromTimestampStr(rateLimitResetTimestamp)
200					if err != nil {
201						return false, err
202					}
203					//Delay the retry until the rate limit is reset
204					logger.Debugf("Delay request for %d ms: %v method sent to url %v with body %v", delayMs, r.method, httpReq.URL.RequestURI(), r.body)
205					time.Sleep(time.Duration(delayMs) * time.Millisecond)
206				}
207				logger.Debugf("Retrying request: %v method sent to url %v with body %v", r.method, httpReq.URL.RequestURI(), r.body)
208				return true, errorMessage
209			}
210
211			//If the error is not retryable
212			logger.Errorf(
213				"Error message: %v. Title: %v. Code: %v. Request UUID: %v.",
214				errorMessage.Description,
215				errorMessage.Title,
216				errorMessage.StatusCode,
217				errorMessage.RequestUUID,
218			)
219			//stop retrying (false) and return custom error
220			return false, errorMessage
221
222		}
223		logger.Debugf("Response body: %v", string(responseBodyBytes))
224		//stop retrying (false) as no more errors
225		return false, nil
226	}, maxNoOfRetries, delayInterval)
227	//No need to return when the context is already expired.
228	select {
229	case <-ctx.Done():
230		return "", nil, ctx.Err()
231	default:
232	}
233	return requestUUID, responseBodyBytes, err
234}
235
236func isErrorHTTPCodeRetryable(statusCode int) bool {
237	//if internal server error (>=500)
238	//OR object is in status that does not allow the request (424)
239	//OR we reach the rate limit (429), retry
240	if statusCode >= 500 || statusCode == 424 || statusCode == 429 {
241		return true
242	}
243	//stop retrying (false) and return custom error
244	return false
245}
246
247//getDelayTimeInMsFromTimestampStr takes a unix timestamp (ms) string
248//and returns the amount of ms to delay the next HTTP request retry
249//return error if the input string is not a valid unix timestamp(ms)
250func getDelayTimeInMsFromTimestampStr(timestamp string) (int64, error) {
251	if timestamp == "" {
252		return 0, errors.New("timestamp is empty")
253	}
254	//convert timestamp from string to int
255	timestampInt, err := strconv.ParseInt(timestamp, 10, 64)
256	if err != nil {
257		return 0, err
258	}
259	currentTimestampMs := time.Now().UnixNano() / 1000000
260	return timestampInt - currentTimestampMs, nil
261}
262