legacy.go raw
1 // Package lwApi is a minimalist API client to LiquidWeb's (https://www.liquidweb.com) API:
2 //
3 // https://cart.liquidweb.com/storm/api/docs/v1
4 //
5 // https://cart.liquidweb.com/storm/api/docs/bleed
6 //
7 // As you might have guessed from the above API documentation links, there are API versions:
8 // "v1" and "bleed". As the name suggests, if you always want the latest features and abilities,
9 // use "bleed". If you want long term compatibility (at the cost of being a little further behind
10 // sometimes), use "v1".
11 package lwApi
12
13 import (
14 "bytes"
15 "crypto/tls"
16 "encoding/json"
17 "fmt"
18 "io/ioutil"
19 "net/http"
20 "sync"
21 "time"
22 )
23
24 // A LWAPIConfig holds the configuration details used to call
25 // the API with the client.
26 type LWAPIConfig struct {
27 Username *string
28 Password *string
29 Token *string
30 Url string
31 Timeout uint
32 Insecure bool
33 }
34
35 // A Client holds the packages *LWAPIConfig and *http.Client. To get a *Client, call New.
36 type Client struct {
37 Headers http.Header
38 config *LWAPIConfig
39 httpClient *http.Client
40 mutex sync.Mutex
41 }
42
43 // A LWAPIError is used to identify error responses when JSON unmarshalling json from a
44 // byte slice.
45 type LWAPIError struct {
46 ErrorMsg string `json:"error,omitempty"`
47 ErrorClass string `json:"error_class,omitempty"`
48 ErrorFullMsg string `json:"full_message,omitempty"`
49 }
50
51 // Given a LWAPIError, returns a string containing the ErrorClass and ErrorFullMsg.
52 func (e LWAPIError) Error() string {
53 return fmt.Sprintf("%v: %v", e.ErrorClass, e.ErrorFullMsg)
54 }
55
56 // Given a LWAPIError, returns boolean if ErrorClass was present or not. You can
57 // use this function to determine if a LWAPIRes response indicates an error or not.
58 func (e LWAPIError) HadError() bool {
59 return e.ErrorClass != ""
60 }
61
62 // LWAPIRes is a convenient interface used (for example) by CallInto to ensure a passed
63 // struct knows how to indicate whether or not it had an error.
64 type LWAPIRes interface {
65 Error() string
66 HadError() bool
67 }
68
69 // New takes a *LWAPIConfig, and gives you a *Client. If there's an error, it is returned.
70 // When using this package, this should be the first function you call. Below is an example
71 // that demonstrates creating the config and passing it to New.
72 //
73 // Example:
74 //
75 // username := "ExampleUsername"
76 // password := "ExamplePassword"
77 //
78 // config := lwApi.LWAPIConfig{
79 // Username: &username,
80 // Password: &password,
81 // Url: "api.liquidweb.com",
82 // }
83 // apiClient, newErr := lwApi.New(&config)
84 // if newErr != nil {
85 // panic(newErr)
86 // }
87 func New(config *LWAPIConfig) (*Client, error) {
88 if err := processConfig(config); err != nil {
89 return nil, err
90 }
91
92 httpClient := &http.Client{Timeout: time.Duration(time.Duration(config.Timeout) * time.Second)}
93
94 if config.Insecure {
95 tr := &http.Transport{
96 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
97 }
98 httpClient.Transport = tr
99 }
100
101 headers := make(http.Header)
102 client := Client{
103 config: config,
104 httpClient: httpClient,
105 Headers: headers,
106 mutex: sync.Mutex{},
107 }
108 return &client, nil
109 }
110
111 // Call takes a path, such as "network/zone/details" and a params structure.
112 // It is recommended that the params be a map[string]interface{}, but you can use
113 // anything that serializes to the right json structure.
114 // A `interface{}` and an error are returned, in typical go fasion.
115 //
116 // Example:
117 //
118 // args := map[string]interface{}{
119 // "uniq_id": "ABC123",
120 // }
121 // got, gotErr := apiClient.Call("bleed/asset/details", args)
122 // if gotErr != nil {
123 // panic(gotErr)
124 // }
125 func (client *Client) Call(method string, params interface{}) (interface{}, error) {
126 bsRb, err := client.CallRaw(method, params)
127 if err != nil {
128 return nil, err
129 }
130
131 var resp interface{}
132 resp, err = client.callRawRespToInterface(bsRb)
133
134 return resp, err
135 }
136
137 // CallInto is like call, but instead of returning an interface you pass it a
138 // struct which is filled, much like the json.Unmarshal function. The struct
139 // you pass must satisfy the LWAPIRes interface. If you embed the LWAPIError
140 // struct from this package into your struct, this will be taken care of for you.
141 //
142 // Example:
143 //
144 // type ZoneDetails struct {
145 // lwApi.LWAPIError
146 // AvlZone string `json:"availability_zone"`
147 // Desc string `json:"description"`
148 // GatewayDevs []string `json:"gateway_devices"`
149 // HvType string `json:"hv_type"`
150 // ID int `json:"id"`
151 // Legacy int `json:"legacy"`
152 // Name string `json:"name"`
153 // Status string `json:"status"`
154 // SourceHVs []string `json:"valid_source_hvs"`
155 // }
156 // var zone ZoneDetails
157 // err = apiClient.CallInto("network/zone/details", paramers, &zone)
158 // if err != nil {
159 // log.Fatal(err)
160 // }
161 // fmt.Printf("Got struct %#v\n", zone)
162 func (client *Client) CallInto(method string, params interface{}, into LWAPIRes) error {
163 bsRb, err := client.CallRaw(method, params)
164 if err != nil {
165 return err
166 }
167
168 err = json.Unmarshal(bsRb, into)
169 if err != nil {
170 return err
171 }
172
173 if into.HadError() {
174 // the LWAPIRes satisfies the Error interface, so we can just return it on
175 // error.
176 return into
177 }
178
179 return nil
180 }
181
182 // Similar to CallInto(), but populates an interface without needing to satisfy
183 // the LWAPIRes interface.
184 // Example:
185 //
186 // type ZoneDetails struct {
187 // AvlZone string `json:"availability_zone"`
188 // Desc string `json:"description"`
189 // GatewayDevs []string `json:"gateway_devices"`
190 // HvType string `json:"hv_type"`
191 // ID int `json:"id"`
192 // Legacy int `json:"legacy"`
193 // Name string `json:"name"`
194 // Status string `json:"status"`
195 // SourceHVs []string `json:"valid_source_hvs"`
196 // }
197 // var zone ZoneDetails
198 // err = apiClient.CallIntoInterface("network/zone/details", params, &zone)
199 func (client *Client) CallIntoInterface(method string, params interface{}, into interface{}) error {
200 bsRb, err := client.CallRaw(method, params)
201 if err != nil {
202 return err
203 }
204
205 if _, err = client.callRawRespToInterface(bsRb, &into); err != nil {
206 return err
207 }
208
209 return nil
210 }
211
212 // CallRaw is just like Call, except it returns the raw json as a byte slice. However, in contrast to
213 // Call, CallRaw does *not* check the API response for LiquidWeb specific exceptions as defined in
214 // the type LWAPIError. As such, if calling this function directly, you must check for LiquidWeb specific
215 // exceptions yourself.
216 //
217 // Example:
218 //
219 // args := map[string]interface{}{
220 // "uniq_id": "ABC123",
221 // }
222 // got, gotErr := apiClient.CallRaw("bleed/asset/details", args)
223 // if gotErr != nil {
224 // panic(gotErr)
225 // }
226 // // Check got now for LiquidWeb specific exceptions, as described above.
227 func (client *Client) CallRaw(method string, params interface{}) ([]byte, error) {
228 config := client.config
229 // api wants the "params" prefix key. Do it here so consumers dont have
230 // to do this everytime.
231 args := map[string]interface{}{
232 "params": params,
233 }
234 encodedArgs, encodeErr := json.Marshal(args)
235 if encodeErr != nil {
236 return nil, encodeErr
237 }
238 // formulate the HTTP POST request
239 url := fmt.Sprintf("%s/%s", config.Url, method)
240 req, reqErr := http.NewRequest("POST", url, bytes.NewReader(encodedArgs))
241 if reqErr != nil {
242 return nil, reqErr
243 }
244
245 // We need a unique copy of the headers map in each request struct, otherwise
246 // we can end up with a concurrent map access and a panic.
247 client.mutex.Lock()
248 for name, value := range client.Headers {
249 newvalue := make([]string, len(value))
250 copy(newvalue, value)
251 req.Header[name] = newvalue
252 }
253 client.mutex.Unlock()
254
255 if config.Token != nil {
256 // Oauth2 token
257 req.Header.Add("Authorization", "Bearer "+*config.Token)
258 } else if config.Username != nil && config.Password != nil {
259 // HTTP basic auth
260 req.SetBasicAuth(*config.Username, *config.Password)
261 } else {
262 return nil, fmt.Errorf("No valid credential provided")
263 }
264
265 // make the POST request
266 resp, doErr := client.httpClient.Do(req)
267 if doErr != nil {
268 return nil, doErr
269 }
270 defer resp.Body.Close()
271 if resp.StatusCode != 200 {
272 return nil, fmt.Errorf("Bad HTTP response code [%d] from [%s]", resp.StatusCode, url)
273 }
274 // read the response body into a byte slice
275 bsRb, readErr := ioutil.ReadAll(resp.Body)
276 if readErr != nil {
277 return nil, readErr
278 }
279
280 return bsRb, nil
281 }
282
283 /* private */
284
285 func processConfig(config *LWAPIConfig) error {
286 if config.Url == "" {
287 return fmt.Errorf("url is missing from config")
288 }
289 if config.Timeout == 0 {
290 config.Timeout = 20
291 }
292
293 if config.Token != nil {
294 // Oauth2 token
295 if *config.Token == "" {
296 return fmt.Errorf("Bearer token provided, but empty")
297 }
298 } else if config.Username != nil && config.Password != nil {
299 // HTTP basic auth
300 if *config.Username == "" {
301 return fmt.Errorf("provided username is empty")
302 }
303 if *config.Password == "" {
304 return fmt.Errorf("provided password is empty")
305 }
306 } else {
307 return fmt.Errorf("No valid credential provided")
308 }
309
310 return nil
311 }
312
313 func (client *Client) callRawRespToInterface(dataBytes []byte, into ...interface{}) (dataInter interface{}, err error) {
314 var raw map[string]interface{}
315 if err = json.Unmarshal(dataBytes, &raw); err == nil {
316 if errorClass, exists := raw["error_class"]; exists {
317 errorClassStr := fmt.Sprintf("%s", errorClass)
318 if errorClassStr != "" {
319 err = LWAPIError{
320 ErrorClass: errorClassStr,
321 ErrorFullMsg: fmt.Sprintf("%s", raw["full_message"]),
322 ErrorMsg: fmt.Sprintf("%s", raw["error"]),
323 }
324 return
325 }
326 }
327 } else {
328 return
329 }
330
331 if len(into) > 0 {
332 dataInter = &into[0]
333 }
334
335 err = json.Unmarshal(dataBytes, &dataInter)
336
337 return
338 }
339