govultr.go raw
1 // Package govultr contains the functionality to interact with the Vultr public
2 // HTTP REST API.
3 package govultr
4
5 import (
6 "bytes"
7 "context"
8 "encoding/json"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "net/http"
14 "net/url"
15 "strings"
16 "time"
17
18 "github.com/hashicorp/go-retryablehttp"
19 )
20
21 const (
22 version = "3.26.1"
23 defaultBase = "https://api.vultr.com"
24 userAgent = "govultr/" + version
25 rateLimit = 500 * time.Millisecond
26 retryLimit = 3
27 )
28
29 // RequestBody is used to create JSON bodies for one off calls
30 type RequestBody map[string]interface{}
31
32 // Client manages interaction with the Vultr API
33 type Client struct {
34 // Http Client used to interact with the Vultr API
35 client *retryablehttp.Client
36
37 // BASE URL for APIs
38 BaseURL *url.URL
39
40 // User Agent for the client
41 UserAgent string
42
43 // Services used to interact with the API
44 Account AccountService
45 Application ApplicationService
46 Backup BackupService
47 BareMetalServer BareMetalServerService
48 Billing BillingService
49 BlockStorage BlockStorageService
50 CDN CDNService
51 ContainerRegistry ContainerRegistryService
52 Database DatabaseService
53 Domain DomainService
54 DomainRecord DomainRecordService
55 FirewallGroup FirewallGroupService
56 FirewallRule FireWallRuleService
57 Instance InstanceService
58 ISO ISOService
59 Kubernetes KubernetesService
60 LoadBalancer LoadBalancerService
61 Logs LogsService
62 Marketplace MarketplaceService
63 ObjectStorage ObjectStorageService
64 OS OSService
65 Plan PlanService
66 Region RegionService
67 ReservedIP ReservedIPService
68 Inference InferenceService
69 Snapshot SnapshotService
70 SSHKey SSHKeyService
71 StartupScript StartupScriptService
72 SubAccount SubAccountService
73 User UserService
74 VirtualFileSystemStorage VirtualFileSystemStorageService
75 VPC VPCService
76 // Deprecated: VPC2 is no longer supported
77 VPC2 VPC2Service
78
79 // Optional function called after every successful request made to the Vultr API
80 onRequestCompleted RequestCompletionCallback
81 }
82
83 // RequestCompletionCallback defines the type of the request callback function
84 type RequestCompletionCallback func(*http.Request, *http.Response)
85
86 // NewClient returns a Vultr API Client
87 func NewClient(httpClient *http.Client) *Client {
88 if httpClient == nil {
89 httpClient = &http.Client{
90 Transport: &http.Transport{
91 DialContext: (&net.Dialer{
92 Timeout: 90 * time.Second,
93 KeepAlive: 90 * time.Second,
94 DualStack: true,
95 }).DialContext,
96 MaxIdleConns: 100,
97 IdleConnTimeout: 90 * time.Second,
98 TLSHandshakeTimeout: 30 * time.Second,
99 ExpectContinueTimeout: 1 * time.Second,
100 MaxIdleConnsPerHost: -1,
101 DisableKeepAlives: true,
102 },
103 Timeout: 60 * time.Second,
104 }
105 }
106
107 baseURL, _ := url.Parse(defaultBase)
108
109 client := &Client{
110 client: retryablehttp.NewClient(),
111 BaseURL: baseURL,
112 UserAgent: userAgent,
113 }
114
115 client.client.HTTPClient = httpClient
116 client.client.Logger = nil
117 client.client.ErrorHandler = client.vultrErrorHandler
118 client.SetRetryLimit(retryLimit)
119 client.SetRateLimit(rateLimit)
120
121 client.Account = &AccountServiceHandler{client}
122 client.Application = &ApplicationServiceHandler{client}
123 client.Backup = &BackupServiceHandler{client}
124 client.BareMetalServer = &BareMetalServerServiceHandler{client}
125 client.Billing = &BillingServiceHandler{client}
126 client.BlockStorage = &BlockStorageServiceHandler{client}
127 client.ContainerRegistry = &ContainerRegistryServiceHandler{client}
128 client.CDN = &CDNServiceHandler{client}
129 client.Database = &DatabaseServiceHandler{client}
130 client.Domain = &DomainServiceHandler{client}
131 client.DomainRecord = &DomainRecordsServiceHandler{client}
132 client.FirewallGroup = &FireWallGroupServiceHandler{client}
133 client.FirewallRule = &FireWallRuleServiceHandler{client}
134 client.Instance = &InstanceServiceHandler{client}
135 client.ISO = &ISOServiceHandler{client}
136 client.Kubernetes = &KubernetesHandler{client}
137 client.LoadBalancer = &LoadBalancerHandler{client}
138 client.Logs = &LogsServiceHandler{client}
139 client.Marketplace = &MarketplaceServiceHandler{client}
140 client.ObjectStorage = &ObjectStorageServiceHandler{client}
141 client.OS = &OSServiceHandler{client}
142 client.Plan = &PlanServiceHandler{client}
143 client.Region = &RegionServiceHandler{client}
144 client.ReservedIP = &ReservedIPServiceHandler{client}
145 client.Inference = &InferenceServiceHandler{client}
146 client.Snapshot = &SnapshotServiceHandler{client}
147 client.SSHKey = &SSHKeyServiceHandler{client}
148 client.StartupScript = &StartupScriptServiceHandler{client}
149 client.SubAccount = &SubAccountServiceHandler{client}
150 client.User = &UserServiceHandler{client}
151 client.VirtualFileSystemStorage = &VirtualFileSystemStorageServiceHandler{client}
152 client.VPC = &VPCServiceHandler{client}
153 client.VPC2 = &VPC2ServiceHandler{client}
154
155 return client
156 }
157
158 // NewRequest creates an API Request
159 func (c *Client) NewRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) {
160 resolvedURL, err := c.BaseURL.Parse(uri)
161 if err != nil {
162 return nil, err
163 }
164
165 buf := new(bytes.Buffer)
166 if body != nil {
167 if err2 := json.NewEncoder(buf).Encode(body); err2 != nil {
168 return nil, err2
169 }
170 }
171
172 req, err := http.NewRequestWithContext(ctx, method, resolvedURL.String(), buf)
173 if err != nil {
174 return nil, err
175 }
176
177 req.Header.Add("User-Agent", c.UserAgent)
178 req.Header.Add("Accept", "application/json")
179 req.Header.Add("Content-Type", "application/json")
180
181 return req, nil
182 }
183
184 // DoWithContext sends an API Request and returns back the response. The API response is checked to see if it was
185 // a successful call. A successful call is then checked to see if we need to unmarshal since some resources
186 // have their own implements of unmarshal.
187 func (c *Client) DoWithContext(ctx context.Context, r *http.Request, data interface{}) (*http.Response, error) {
188 rreq, err := retryablehttp.FromRequest(r)
189 if err != nil {
190 return nil, err
191 }
192
193 rreq = rreq.WithContext(ctx)
194
195 res, errDo := c.client.Do(rreq)
196
197 if c.onRequestCompleted != nil {
198 c.onRequestCompleted(r, res)
199 }
200
201 if errDo != nil {
202 return nil, errDo
203 }
204
205 defer func() {
206 if rerr := res.Body.Close(); err == nil {
207 err = rerr
208 }
209 }()
210
211 body, err := io.ReadAll(res.Body)
212 if err != nil {
213 return nil, err
214 }
215
216 res.Body = io.NopCloser(bytes.NewBuffer(body))
217
218 if res.StatusCode >= http.StatusOK && res.StatusCode <= http.StatusNoContent {
219 if data != nil {
220 if err := json.Unmarshal(body, data); err != nil {
221 return nil, err
222 }
223 }
224 return res, nil
225 }
226
227 return res, errors.New(string(body))
228 }
229
230 // SetBaseURL Overrides the default BaseUrl
231 func (c *Client) SetBaseURL(baseURL string) error {
232 updatedURL, err := url.Parse(baseURL)
233
234 if err != nil {
235 return err
236 }
237
238 c.BaseURL = updatedURL
239 return nil
240 }
241
242 // SetRateLimit Overrides the default rateLimit. For performance, exponential
243 // backoff is used with the minimum wait being 2/3rds the time provided.
244 func (c *Client) SetRateLimit(t time.Duration) {
245 c.client.RetryWaitMin = t / 3 * 2
246 c.client.RetryWaitMax = t
247 }
248
249 // SetUserAgent Overrides the default UserAgent
250 func (c *Client) SetUserAgent(ua string) {
251 c.UserAgent = ua
252 }
253
254 // OnRequestCompleted sets the API request completion callback
255 func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
256 c.onRequestCompleted = rc
257 }
258
259 // SetRetryLimit overrides the default RetryLimit
260 func (c *Client) SetRetryLimit(n int) {
261 c.client.RetryMax = n
262 }
263
264 func (c *Client) vultrErrorHandler(resp *http.Response, err error, numTries int) (*http.Response, error) {
265 if resp == nil {
266 if err != nil {
267 return nil, fmt.Errorf("gave up after %d attempts, last error : %s", numTries, err.Error())
268 }
269 return nil, fmt.Errorf("gave up after %d attempts, last error unavailable (resp == nil)", numTries)
270 }
271
272 buf, err := io.ReadAll(resp.Body)
273 if err != nil {
274 return nil, fmt.Errorf("gave up after %d attempts, last error unavailable (error reading response body: %v)", numTries, err)
275 }
276 return nil, fmt.Errorf("gave up after %d attempts, last error: %#v", numTries, strings.TrimSpace(string(buf)))
277 }
278
279 // BoolToBoolPtr helper function that returns a pointer from your bool value
280 func BoolToBoolPtr(value bool) *bool {
281 return &value
282 }
283
284 // StringToStringPtr helper function that returns a pointer from your string value
285 func StringToStringPtr(value string) *string {
286 return &value
287 }
288
289 // IntToIntPtr helper function that returns a pointer from your string value
290 func IntToIntPtr(value int) *int {
291 return &value
292 }
293